js-confuser-vm 0.0.5 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/CHANGELOG.md +112 -2
  2. package/README.MD +249 -106
  3. package/dist/build-runtime.js +22 -3
  4. package/dist/compiler.js +864 -801
  5. package/dist/runtime.js +414 -333
  6. package/dist/transforms/bytecode/aliasedOpcodes.js +134 -0
  7. package/dist/transforms/bytecode/concealConstants.js +31 -0
  8. package/dist/transforms/bytecode/macroOpcodes.js +37 -23
  9. package/dist/transforms/bytecode/microOpcodes.js +236 -0
  10. package/dist/transforms/bytecode/resolveContants.js +69 -12
  11. package/dist/transforms/bytecode/resolveLabels.js +5 -3
  12. package/dist/transforms/bytecode/selfModifying.js +3 -2
  13. package/dist/transforms/bytecode/specializedOpcodes.js +54 -39
  14. package/dist/transforms/runtime/aliasedOpcodes.js +134 -0
  15. package/dist/transforms/runtime/internalVariables.js +202 -0
  16. package/dist/transforms/runtime/macroOpcodes.js +30 -18
  17. package/dist/transforms/runtime/microOpcodes.js +76 -0
  18. package/dist/transforms/runtime/shuffleOpcodes.js +1 -1
  19. package/dist/transforms/runtime/specializedOpcodes.js +36 -29
  20. package/dist/utils/op-utils.js +36 -0
  21. package/dist/utils/random-utils.js +27 -0
  22. package/index.ts +11 -8
  23. package/jest.config.js +12 -0
  24. package/package.json +1 -1
  25. package/src/build-runtime.ts +25 -4
  26. package/src/compiler.ts +2482 -2069
  27. package/src/options.ts +3 -0
  28. package/src/runtime.ts +842 -771
  29. package/src/transforms/bytecode/aliasedOpcodes.ts +148 -0
  30. package/src/transforms/bytecode/concealConstants.ts +52 -0
  31. package/src/transforms/bytecode/macroOpcodes.ts +49 -33
  32. package/src/transforms/bytecode/microOpcodes.ts +291 -0
  33. package/src/transforms/bytecode/resolveContants.ts +82 -18
  34. package/src/transforms/bytecode/resolveLabels.ts +5 -4
  35. package/src/transforms/bytecode/selfModifying.ts +3 -3
  36. package/src/transforms/bytecode/specializedOpcodes.ts +85 -46
  37. package/src/transforms/runtime/aliasedOpcodes.ts +191 -0
  38. package/src/transforms/runtime/internalVariables.ts +270 -0
  39. package/src/transforms/runtime/macroOpcodes.ts +47 -20
  40. package/src/transforms/runtime/microOpcodes.ts +93 -0
  41. package/src/transforms/runtime/shuffleOpcodes.ts +1 -1
  42. package/src/transforms/runtime/specializedOpcodes.ts +56 -46
  43. package/src/types.ts +1 -1
  44. package/src/utils/op-utils.ts +46 -0
  45. package/src/transforms/utils/op-utils.ts +0 -26
  46. package/src/utilts.ts +0 -3
  47. /package/src/{transforms/utils → utils}/random-utils.ts +0 -0
@@ -1,6 +1,7 @@
1
1
  import * as t from "@babel/types";
2
2
  import traverseImport from "@babel/traverse";
3
3
  import { ok } from "assert";
4
+ import { getOpcodeToCaseMap } from "./macroOpcodes.js";
4
5
  const traverse = traverseImport.default || traverseImport;
5
6
 
6
7
  // Extract the real statement list from a SwitchCase consequent (identical to the
@@ -19,20 +20,33 @@ function extractCaseBody(switchCase) {
19
20
  // Because specialized opcodes are only created for instructions that have
20
21
  // *exactly one* numeric operand, every `_operand()` call inside the original
21
22
  // handler is replaced by the constant value that was baked into the opcode.
22
- function inlineFixedOperand(bodyStmts, resolvedValue) {
23
+ function inlineFixedOperands(bodyStmts, resolvedValues) {
23
24
  // Wrap the statements in a temporary BlockStatement so traverse has a root.
24
25
  // The replacement mutates the original statement objects in place.
26
+ var replaced = 0;
27
+ function consumeOperand() {
28
+ const resolvedValue = resolvedValues[replaced++];
29
+ ok(typeof resolvedValue === "number", `Expected a numeric operand value, got ${resolvedValue}`);
30
+ return t.numericLiteral(resolvedValue);
31
+ }
25
32
  traverse(t.blockStatement(bodyStmts), {
26
33
  noScope: true,
27
34
  CallExpression(path) {
28
35
  const callee = path.node.callee;
29
- if (t.isMemberExpression(callee) && t.isThisExpression(callee.object) && t.isIdentifier(callee.property, {
30
- name: "_operand"
31
- }) && path.node.arguments.length === 0) {
32
- path.replaceWith(t.numericLiteral(resolvedValue));
36
+ const isMethodCall = methodName => {
37
+ return t.isMemberExpression(callee) && t.isThisExpression(callee.object) && t.isIdentifier(callee.property, {
38
+ name: methodName
39
+ }) && path.node.arguments.length === 0;
40
+ };
41
+ if (isMethodCall("_operand")) {
42
+ path.replaceWith(consumeOperand());
43
+ }
44
+ if (isMethodCall("_constant")) {
45
+ path.node.arguments = [consumeOperand(), consumeOperand()];
33
46
  }
34
47
  }
35
48
  });
49
+ ok(replaced === resolvedValues.length, `Expected to replace ${resolvedValues.length} operands, but replaced ${replaced}`);
36
50
  }
37
51
 
38
52
  // Append a generated switch case for every entry in compiler.SPECIALIZED_OPS.
@@ -40,7 +54,7 @@ function inlineFixedOperand(bodyStmts, resolvedValue) {
40
54
  // replaced by the constant integer that was captured at compile time.
41
55
  // Must be called AFTER applyMacroOpcodes (so the original cases exist) but
42
56
  // BEFORE applyShuffleOpcodes so the new specialized cases also get shuffled.
43
- export function applySpecializedOpcodes(ast, bytecode, compiler) {
57
+ export function applySpecializedOpcodes(ast, compiler) {
44
58
  let switchStatement = null;
45
59
  traverse(ast, {
46
60
  SwitchStatement(path) {
@@ -53,42 +67,35 @@ export function applySpecializedOpcodes(ast, bytecode, compiler) {
53
67
  ok(switchStatement, "Could not find @SWITCH statement for specialized opcodes");
54
68
 
55
69
  // Build a map opName → SwitchCase from the existing OP.xxx case tests.
56
- const nameToCaseMap = new Map();
57
- for (const sc of switchStatement.cases) {
58
- const test = sc.test;
59
- if (test && t.isMemberExpression(test) && t.isIdentifier(test.object, {
60
- name: "OP"
61
- }) && t.isIdentifier(test.property)) {
62
- nameToCaseMap.set(test.property.name, sc);
63
- }
64
- }
70
+ const opcodeToCaseMap = getOpcodeToCaseMap(switchStatement, compiler);
65
71
  if (!compiler.SPECIALIZED_OPS) return;
66
72
  for (const [specialOpStr, info] of Object.entries(compiler.SPECIALIZED_OPS)) {
67
73
  const specialOpCode = Number(specialOpStr);
68
74
  const {
69
75
  originalOp,
70
- operand
76
+ operands
71
77
  } = info;
72
- const newName = compiler.OP_NAME[specialOpCode];
78
+ let newName = compiler.OP_NAME[specialOpCode];
73
79
  const originalName = compiler.OP_NAME[originalOp];
74
- if (!originalName) continue;
75
- const originalCase = nameToCaseMap.get(originalName);
76
- if (!originalCase) continue;
80
+ const originalCase = opcodeToCaseMap.get(originalOp);
81
+ ok(originalCase, `Could not find original case for opcode ${originalName} (${originalOp})`);
77
82
 
78
83
  // Clone the original handler body
79
84
  const bodyStmts = extractCaseBody(originalCase).map(s => t.cloneNode(s, true));
80
- const placedOperand = info.resolvedOperand || {
81
- resolvedValue: 1337
82
- };
83
- ok(placedOperand !== undefined, `Could not find operand for original opcode ${newName}`);
84
- const resolvedValue = placedOperand?.resolvedValue ?? placedOperand;
85
- if (typeof resolvedValue !== "number") {
86
- console.error(resolvedValue);
85
+ const placedOperands = info.operands;
86
+ ok(placedOperands, `Could not find operand for original opcode ${newName}`);
87
+ const resolvedValues = placedOperands.map(placedOperand => {
88
+ return placedOperand?.resolvedValue ?? placedOperand;
89
+ });
90
+ if (resolvedValues.find(v => typeof v !== "number")) {
91
+ console.error(info);
92
+ throw new Error("Expected all resolved operand values to be numbers");
87
93
  }
88
- ok(typeof resolvedValue === "number", "Expected a numeric operand value");
94
+ newName = `${originalName}_${resolvedValues.join("_")}`;
95
+ compiler.OP_NAME[specialOpCode] = newName;
89
96
 
90
97
  // Replace this._operand() with the baked-in constant
91
- inlineFixedOperand(bodyStmts, resolvedValue);
98
+ inlineFixedOperands(bodyStmts, resolvedValues);
92
99
 
93
100
  // Add a leading comment so the generated source stays readable
94
101
  if (bodyStmts.length > 0) {
@@ -0,0 +1,36 @@
1
+ import { getRandomInt } from "./random-utils.js";
2
+ export const U16_MAX = 0xffff; // bytecode operands are u16
3
+
4
+ /** Returns the next free opcode slot, or -1 when the space is exhausted. */
5
+ export function nextFreeSlot(compiler) {
6
+ // ── Collect used opcodes exactly as specified ─────────────────────────────
7
+ const usedOpcodes = new Set(Object.keys(compiler.OP_NAME).map(k => parseInt(k, 10)).filter(v => !isNaN(v)));
8
+ if (usedOpcodes.size > U16_MAX) return -1;
9
+
10
+ // Random opcode
11
+ if (compiler.options.randomizeOpcodes) {
12
+ let attempts = 0;
13
+ while (attempts++ < 512) {
14
+ const candidate = getRandomInt(0, U16_MAX);
15
+ if (!usedOpcodes.has(candidate)) {
16
+ usedOpcodes.add(candidate);
17
+ return candidate;
18
+ }
19
+ }
20
+ }
21
+
22
+ // Fallback: linear scan from a random start
23
+ const start = Object.keys(compiler.OP_NAME).length;
24
+ for (let i = 0; i <= U16_MAX; i++) {
25
+ const v = start + i & U16_MAX;
26
+ if (!usedOpcodes.has(v)) {
27
+ usedOpcodes.add(v);
28
+ return v;
29
+ }
30
+ }
31
+ return -1;
32
+ }
33
+ export function getInstructionSize(instr) {
34
+ const size = instr.filter(op => op?.placeholder !== true).length;
35
+ return size;
36
+ }
@@ -0,0 +1,27 @@
1
+ import { ok } from "assert";
2
+ export function getPlaceholder() {
3
+ return Math.random().toString(36).substring(2, 15);
4
+ }
5
+ export function choice(elements) {
6
+ ok(elements.length > 0, "choice() called on empty sequence");
7
+ return elements[Math.floor(Math.random() * elements.length)];
8
+ }
9
+ export function getRandom() {
10
+ return Math.random();
11
+ }
12
+ export function getRandomInt(min, max) {
13
+ ok(min <= max, "min must be <= max");
14
+ return Math.floor(Math.random() * (max - min + 1)) + min;
15
+ }
16
+
17
+ /**
18
+ * Shuffles an array in-place using the Fisher-Yates algorithm.
19
+ * @param array - The array to shuffle (mutated)
20
+ */
21
+ export function shuffle(array) {
22
+ for (let i = array.length - 1; i > 0; i--) {
23
+ const j = Math.floor(Math.random() * (i + 1));
24
+ [array[i], array[j]] = [array[j], array[i]];
25
+ }
26
+ return array;
27
+ }
package/index.ts CHANGED
@@ -9,14 +9,17 @@ async function main() {
9
9
 
10
10
  const { code: output } = await JsConfuserVM.obfuscate(sourceCode, {
11
11
  target: "browser", // or "node"
12
- // randomizeOpcodes: true, // randomize the opcode numbers?
13
- // shuffleOpcodes: true, // shuffle order of opcode handlers in the runtime?
14
- // encodeBytecode: true, // encode bytecode? when off, comments for instructions are added
15
- // selfModifying: true, // do self-modifying bytecode for function bodies?
16
- // macroOpcodes: true, // create combined opcodes for repeated instruction sequences?
17
- // specializedOpcodes: true, // create specialized opcodes for commonly used opcode+operand pairs?
18
- // timingChecks: true, // add timing checks to detect debuggers?
19
- // minify: true, // pass final output through Google Closure Compiler? (
12
+ randomizeOpcodes: true, // randomize the opcode numbers?
13
+ shuffleOpcodes: true, // shuffle order of opcode handlers in the runtime?
14
+ encodeBytecode: true, // encode the bytecode array?
15
+ concealConstants: true, // conceal strings and integers in the constant pool?
16
+ selfModifying: true, // do self-modifying bytecode for function bodies?
17
+ macroOpcodes: true, // create combined opcodes for repeated instruction sequences?
18
+ microOpcodes: true, // break opcodes into sub-opcodes?
19
+ specializedOpcodes: true, // create specialized opcodes for commonly used opcode+operand pairs?
20
+ aliasedOpcodes: true, // create duplicate opcodes for commonly used opcodes?
21
+ timingChecks: true, // add timing checks to detect debuggers?
22
+ minify: true, // pass final output through Google Closure Compiler? (Renames VM class properties)
20
23
  });
21
24
 
22
25
  writeFileSync("output.original.js", orginalOutput, "utf-8");
package/jest.config.js CHANGED
@@ -6,10 +6,19 @@ const OPTIONS_MATRIX = [
6
6
  { displayName: "selfModifying", VM_OPTIONS: { selfModifying: true } },
7
7
  { displayName: "timingChecks", VM_OPTIONS: { timingChecks: true } },
8
8
  { displayName: "macroOpcodes", VM_OPTIONS: { macroOpcodes: true } },
9
+ { displayName: "microOpcodes", VM_OPTIONS: { microOpcodes: true } },
9
10
  {
10
11
  displayName: "specializedOpcodes",
11
12
  VM_OPTIONS: { specializedOpcodes: true },
12
13
  },
14
+ {
15
+ displayName: "aliasedOpcodes",
16
+ VM_OPTIONS: { aliasedOpcodes: true },
17
+ },
18
+ {
19
+ displayName: "concealConstants",
20
+ VM_OPTIONS: { concealConstants: true },
21
+ },
13
22
  {
14
23
  displayName: "all",
15
24
  VM_OPTIONS: {
@@ -19,7 +28,10 @@ const OPTIONS_MATRIX = [
19
28
  selfModifying: true,
20
29
  timingChecks: true,
21
30
  macroOpcodes: true,
31
+ microOpcodes: true,
22
32
  specializedOpcodes: true,
33
+ aliasedOpcodes: true,
34
+ concealConstants: true,
23
35
  },
24
36
  },
25
37
  ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "js-confuser-vm",
3
- "version": "0.0.5",
3
+ "version": "0.0.7",
4
4
  "main": "dist/index.js",
5
5
  "scripts": {
6
6
  "build": "babel src --out-dir dist --extensions \".ts,.js\"",
@@ -1,19 +1,23 @@
1
1
  import { generate } from "@babel/generator";
2
- import { parse, type ParseResult } from "@babel/parser";
2
+ import { parse } from "@babel/parser";
3
3
  import type * as t from "@babel/types";
4
4
  import type { Options } from "./options.ts";
5
5
  import { applyMacroOpcodes } from "./transforms/runtime/macroOpcodes.ts";
6
+ import { applyMicroOpcodes } from "./transforms/runtime/microOpcodes.ts";
7
+ import { applyInteralVariablesToRuntime } from "./transforms/runtime/internalVariables.ts";
6
8
  import { applyShuffleOpcodes } from "./transforms/runtime/shuffleOpcodes.ts";
7
9
  import { applyMinify } from "./transforms/runtime/minify.ts";
8
10
  import { Compiler } from "./compiler.ts";
9
11
  import { applySpecializedOpcodes } from "./transforms/runtime/specializedOpcodes.ts";
12
+ import { applyAliasedOpcodes } from "./transforms/runtime/aliasedOpcodes.ts";
10
13
  import type * as b from "./types.ts";
11
14
 
12
15
  export async function obfuscateRuntime(
13
16
  runtime: string,
14
17
  bytecode: b.Bytecode,
15
18
  options: Options,
16
- compiler?: Compiler,
19
+ compiler: Compiler,
20
+ generateBytecodeComment,
17
21
  ) {
18
22
  let ast: t.File;
19
23
  try {
@@ -22,9 +26,18 @@ export async function obfuscateRuntime(
22
26
  throw new Error("VM-Runtime final parsing failed", { cause: error });
23
27
  }
24
28
 
25
- // Macro opcode cases must be applied BEFORE shuffleOpcodes
29
+ // Specialized opcode cases must be applied BEFORE shuffleOpcodes
26
30
  if (options.specializedOpcodes) {
27
- applySpecializedOpcodes(ast, bytecode, compiler);
31
+ applySpecializedOpcodes(ast, compiler);
32
+ }
33
+
34
+ if (options.microOpcodes) {
35
+ applyInteralVariablesToRuntime(ast, compiler);
36
+ }
37
+
38
+ // Micro opcode cases must be applied BEFORE shuffleOpcodes
39
+ if (options.microOpcodes && Object.keys(compiler.MICRO_OPS).length > 0) {
40
+ applyMicroOpcodes(ast, compiler);
28
41
  }
29
42
 
30
43
  // Macro opcode cases must be applied BEFORE shuffleOpcodes
@@ -32,6 +45,11 @@ export async function obfuscateRuntime(
32
45
  applyMacroOpcodes(ast, compiler);
33
46
  }
34
47
 
48
+ // Aliased opcode cases must be applied BEFORE shuffleOpcodes
49
+ if (options.aliasedOpcodes) {
50
+ applyAliasedOpcodes(ast, compiler);
51
+ }
52
+
35
53
  // Shuffle opcode handle order
36
54
  if (options.shuffleOpcodes) {
37
55
  applyShuffleOpcodes(ast);
@@ -44,6 +62,9 @@ export async function obfuscateRuntime(
44
62
  throw new Error("VM-Runtime final generation failed", { cause: error });
45
63
  }
46
64
 
65
+ // Add comment here for more accurate opcode names
66
+ generated = generateBytecodeComment() + "\n" + generated;
67
+
47
68
  // Minify code?
48
69
  if (options.minify) {
49
70
  try {