js-confuser-vm 0.0.1 → 0.0.3

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.
@@ -0,0 +1,56 @@
1
+ import { generate } from "@babel/generator";
2
+ import { parse } from "@babel/parser";
3
+ import traverseImport from "@babel/traverse";
4
+ import { ok } from "assert";
5
+ import { shuffle } from "./random.js";
6
+ import { minify } from "./minify.js";
7
+ const traverse = traverseImport.default || traverseImport;
8
+ export async function obfuscateRuntime(runtime, options) {
9
+ let ast;
10
+ try {
11
+ ast = parse(runtime, {
12
+ sourceType: "unambiguous"
13
+ });
14
+ } catch (error) {
15
+ throw new Error("VM-Runtime final parsing failed", {
16
+ cause: error
17
+ });
18
+ }
19
+
20
+ // shuffle order of opcode handlers
21
+
22
+ if (options.shuffleOpcodes) {
23
+ let switchStatement = null;
24
+ traverse(ast, {
25
+ SwitchStatement(path) {
26
+ if (path.node.leadingComments?.some(comment => comment.value.includes("@SWITCH"))) {
27
+ switchStatement = path.node;
28
+ path.stop();
29
+ }
30
+ }
31
+ });
32
+ ok(switchStatement, "Could not find opcode handlers switch statement");
33
+
34
+ // simply shuffle the order of the cases
35
+
36
+ switchStatement.cases = shuffle(switchStatement.cases);
37
+ }
38
+ let generated;
39
+ try {
40
+ generated = generate(ast).code;
41
+ } catch (error) {
42
+ throw new Error("VM-Runtime final generation failed", {
43
+ cause: error
44
+ });
45
+ }
46
+ if (options.minify) {
47
+ try {
48
+ generated = await minify(generated);
49
+ } catch (error) {
50
+ throw new Error("VM-Runtime final minification failed", {
51
+ cause: error
52
+ });
53
+ }
54
+ }
55
+ return generated;
56
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Breaks functions into DAGs (Directed Acyclic Graphs)
3
+ *
4
+ * - 1. Break bytecode into chunks
5
+ * - 2. Shuffle chunks but remember their original position
6
+ * - 3. Create an effectively Switch statement inside a While loop, each case is a chunk, and the while loops exits on the last transition.
7
+ *
8
+ * The Switch statement:
9
+ *
10
+ * - 1. The state variable controls which case will run next
11
+ * - 2. At the end of each case, the state variable is updated to the next block of code.
12
+ * - 3. The while loop continues until the the state variable is the end state.
13
+ */
14
+ export async function controlFlowFlattening(bytecode) {
15
+ // break bytecode into basic blocks
16
+ // 1. read bytecode and track the current label from the IR-instruction "defineLabel"
17
+ // 2. track any potential jumps inside this block using the IR-instruction operand "label"
18
+ // at this stage in the passing process, may still use these IR-instruction labels for jumps, meaning no effort is required to maintain absolute PCs
19
+ // create a bare CFF implementation of a simple switch dispatch loop effectively
20
+ // This CFF implementation should only apply to "easy jumps" such as conditional jump (if-statement)
21
+ // the complex and specific jumps for for..in shouldn't get flattened
22
+ }
@@ -0,0 +1,33 @@
1
+ import { SOURCE_NODE_SYM } from "../compiler.js";
2
+
3
+ // Resolve all {type:"constant", value} operands to integer indices into the
4
+ // constants pool. Returns both the resolved bytecode and the constants array
5
+ // so the Serializer can use it for comment generation and output.
6
+ export function resolveConstants(bc) {
7
+ const constants = [];
8
+ const constantsMap = new Map();
9
+ function intern(value) {
10
+ let idx = constantsMap.get(value);
11
+ if (typeof idx !== "number") {
12
+ idx = constants.length;
13
+ constantsMap.set(value, idx);
14
+ constants.push(value);
15
+ }
16
+ return idx;
17
+ }
18
+ const resolved = [];
19
+ for (const instr of bc) {
20
+ const [op, operand] = instr;
21
+ if (operand !== undefined && operand !== null && typeof operand === "object" && operand.type === "constant") {
22
+ const newInstr = [op, intern(operand.value)];
23
+ newInstr[SOURCE_NODE_SYM] = instr[SOURCE_NODE_SYM];
24
+ resolved.push(newInstr);
25
+ } else {
26
+ resolved.push(instr);
27
+ }
28
+ }
29
+ return {
30
+ bytecode: resolved,
31
+ constants
32
+ };
33
+ }
@@ -0,0 +1,59 @@
1
+ // --- Label IR ---
2
+ // During compilation, jump targets are symbolic labels instead of hard-coded
3
+ // PC numbers. Two IR "pseudo operands" carry the label information:
4
+ //
5
+ // defineLabel operand : [null, {type:"defineLabel", label:"FN_ENTRY_1"}]
6
+ // Marks a position in the bytecode array.
7
+ // resolveLabels() strips these out entirely.
8
+ //
9
+ // label ref operand : [OP.JUMP, {type:"label", label:"FN_ENTRY_1"}]
10
+ // Used as the operand of any jump instruction. resolveLabels() replaces
11
+ // it with the integer PC that the corresponding defineLabel resolves to.
12
+
13
+ // Resolve symbolic labels to absolute PC indices within a bytecode array.
14
+ // defineLabel pseudo-instructions are stripped; label-ref operands become ints.
15
+ // Mutates `bc` in place so callers holding a reference see the resolved result.
16
+ export function resolveLabels(bc, compiler) {
17
+ // Pass 1 – walk the array and record each label's real PC, counting only
18
+ // real instructions (defineLabel pseudo-ops don't occupy a PC slot).
19
+ const labelToPc = new Map();
20
+ let realPc = 0;
21
+ for (const instr of bc) {
22
+ const op = instr[0];
23
+ const operand = instr[1];
24
+ if (op === null && operand !== null && typeof operand === "object" && operand.type === "defineLabel") {
25
+ labelToPc.set(operand.label, realPc);
26
+ } else {
27
+ realPc++;
28
+ }
29
+ }
30
+
31
+ // Pass 2 – build the resolved instruction list.
32
+ const resolved = [];
33
+ for (const instr of bc) {
34
+ const op = instr[0];
35
+ const operand = instr[1];
36
+
37
+ // Strip defineLabel pseudo-ops.
38
+ if (op === null && typeof operand === "object" && operand?.type === "defineLabel") {
39
+ continue;
40
+ }
41
+
42
+ // Replace label-ref operands with integer PCs.
43
+ if (operand !== undefined && operand !== null && typeof operand === "object" && operand.type === "label") {
44
+ const pc = labelToPc.get(operand.label);
45
+ if (pc === undefined) throw new Error(`Undefined label: ${operand.label}`);
46
+ resolved.push([op, pc + (operand.offset ?? 0)]);
47
+ } else {
48
+ resolved.push(instr);
49
+ }
50
+ }
51
+
52
+ // Patch each function descriptor's startPc now that labels are resolved.
53
+ for (const desc of compiler.fnDescriptors) {
54
+ desc.startPc = labelToPc.get(desc.startLabel);
55
+ }
56
+ return {
57
+ bytecode: resolved
58
+ };
59
+ }
@@ -0,0 +1,107 @@
1
+ export function selfModifying(bc, compiler) {
2
+ // Walk the bytecode looking for "defineLabel" pseudo-ops, which start basic
3
+ // blocks. For each block we collect the body (instructions between the label
4
+ // and the next label/jump terminator), move it to the end of the bytecode
5
+ // under a fresh "patch_LXX" label, and replace it in-place with:
6
+ //
7
+ // defineLabel ("originalLabel") ← kept as-is (pseudo-op)
8
+ // LOAD_INT { label: patch_LXX, offset: N } ← push slice-end PC
9
+ // LOAD_INT { label: patch_LXX } ← push slice-start PC
10
+ // PATCH { label: originalLabel, offset: 3 } ← destPc = L+3
11
+ // LOAD_INT 0 × N ← N placeholder instructions
12
+ //
13
+ // PATCH pops (start, end) from the stack and copies bytecode[start..end) to
14
+ // bytecode[destPc..]. Since destPc = L+3 = first placeholder, the body is
15
+ // written exactly over the placeholder region on the first call. Subsequent
16
+ // calls are idempotent (same bytes written again). Execution falls through
17
+ // from PATCH into the freshly-patched body at L+3, then continues naturally
18
+ // to whatever terminator (JUMP/RETURN) follows at L+3+N.
19
+
20
+ const {
21
+ OP,
22
+ JUMP_OPS
23
+ } = compiler;
24
+ const result = [];
25
+ const appended = [];
26
+ let patchCount = 0;
27
+ let i = 0;
28
+ while (i < bc.length) {
29
+ const instr = bc[i];
30
+ const [op, operand] = instr;
31
+
32
+ // Detect a defineLabel pseudo-op — start of a new basic block.
33
+ if (op === null && operand !== null && typeof operand === "object" && operand.type === "defineLabel") {
34
+ const originalLabel = operand.label;
35
+ result.push(instr); // keep the defineLabel marker
36
+ i++;
37
+
38
+ // Collect body: everything after the label until the next terminator.
39
+ let j = i;
40
+ while (j < bc.length) {
41
+ const [nextOp, nextOperand] = bc[j];
42
+
43
+ // Another defineLabel = boundary of the next block.
44
+ if (nextOp === null && typeof nextOperand === "object" && nextOperand?.type === "defineLabel") {
45
+ break;
46
+ }
47
+
48
+ // Jump instructions, RETURN, and DATA (function header words) all
49
+ // terminate the body without being included in it.
50
+ if (nextOp !== null && (JUMP_OPS.has(nextOp) || nextOp === OP.RETURN || nextOp === OP.DATA)) {
51
+ break;
52
+ }
53
+ j++;
54
+ }
55
+ const body = bc.slice(i, j);
56
+ const N = body.length;
57
+ if (N === 0) {
58
+ // Nothing to transform — label is immediately followed by a terminator.
59
+ continue;
60
+ }
61
+ const patchLabel = `patch_${originalLabel}_${patchCount++}`;
62
+
63
+ // ── Stub (3 real instructions) ──────────────────────────────────────
64
+ // LOAD_INT pushes the end-index of the body slice (patchLabel_pc + N).
65
+ // LOAD_INT pushes the start-index (patchLabel_pc).
66
+ // Stack before PATCH: [end (bottom), start (top)].
67
+ // PATCH: slice(pop()=start, pop()=end) copies the body to destPc = L+3.
68
+ result.push([OP.LOAD_INT, {
69
+ type: "label",
70
+ label: patchLabel,
71
+ offset: N
72
+ }]);
73
+ result.push([OP.LOAD_INT, {
74
+ type: "label",
75
+ label: patchLabel
76
+ }]);
77
+ result.push([OP.PATCH, {
78
+ type: "label",
79
+ label: originalLabel,
80
+ offset: 3
81
+ }]);
82
+
83
+ // ── Placeholders (N instructions) ───────────────────────────────────
84
+ // These are overwritten by PATCH on the first execution. They never
85
+ // execute as LOAD_INT 0 in a correct run.
86
+ for (let p = 0; p < N; p++) {
87
+ result.push([OP.LOAD_INT, 0]);
88
+ }
89
+
90
+ // ── Append real body at end ─────────────────────────────────────────
91
+ appended.push([null, {
92
+ type: "defineLabel",
93
+ label: patchLabel
94
+ }]);
95
+ for (const bodyInstr of body) {
96
+ appended.push(bodyInstr);
97
+ }
98
+ i = j; // skip over the original body in the input array
99
+ continue;
100
+ }
101
+ result.push(instr);
102
+ i++;
103
+ }
104
+ return {
105
+ bytecode: [...result, ...appended]
106
+ };
107
+ }
package/dist/types.js ADDED
@@ -0,0 +1,13 @@
1
+ // Bytecode supports both real instructions and IR pseudo-instructions
2
+ // Real instruction: [OP.ADD, 5]
3
+ // IR instruction: [null, { type: "defineLabel", label: "FN_ENTRY_1" }]
4
+
5
+ // IR instructions are used to hold symbolic information during compilation
6
+ // All "null" instructions are dropped before assembly time
7
+
8
+ export function constantOperand(value) {
9
+ return {
10
+ type: "constant",
11
+ value: value
12
+ };
13
+ }
package/dist/utilts.js ADDED
@@ -0,0 +1,3 @@
1
+ export function escapeRegex(s) {
2
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3
+ }
package/index.ts CHANGED
@@ -1,17 +1,22 @@
1
- import { virtualize } from "./src/index.js";
1
+ import JsConfuserVM from "./src/index.ts";
2
2
  import { readFileSync, writeFileSync } from "fs";
3
3
 
4
- // Compile and write the output to a file
5
- const sourceCode = readFileSync("input.js", "utf-8");
6
- const { code: output } = virtualize(sourceCode);
4
+ async function main() {
5
+ // Compile and write the output to a file
6
+ const sourceCode = readFileSync("input.js", "utf-8");
7
+ const { code: output } = await JsConfuserVM.obfuscate(sourceCode, {
8
+ selfModifying: true,
9
+ });
7
10
 
8
- writeFileSync("output.js", output, "utf-8");
9
- console.log(output);
11
+ writeFileSync("output.js", output, "utf-8");
10
12
 
11
- // Eval the code like our test suite does
12
- var window = { TEST_OUTPUT: null };
13
- eval(output);
14
- console.log(window.TEST_OUTPUT);
13
+ // Eval the code like our test suite does
14
+ var window = { TEST_OUTPUT: null };
15
+ eval(output);
16
+ console.log(window.TEST_OUTPUT);
15
17
 
16
- // Minify using Google Closure Compiler (optional)
17
- import("./minify.js");
18
+ // Minify using Google Closure Compiler (optional)
19
+ // import("./minify.js");
20
+ }
21
+
22
+ main();
package/jest.config.js CHANGED
@@ -1,7 +1,28 @@
1
- export default {
2
- extensionsToTreatAsEsm: [".ts"],
3
- moduleFileExtensions: ["ts", "js", "json"],
4
- transform: {
5
- "\\.ts$": "./jest-strip-types.js",
1
+ const OPTIONS_MATRIX = [
2
+ { displayName: "default", VM_OPTIONS: {} },
3
+ { displayName: "randomizeOpcodes", VM_OPTIONS: { randomizeOpcodes: true } },
4
+ { displayName: "shuffleOpcodes", VM_OPTIONS: { shuffleOpcodes: true } },
5
+ { displayName: "encodeBytecode", VM_OPTIONS: { encodeBytecode: true } },
6
+ { displayName: "selfModifying", VM_OPTIONS: { selfModifying: true } },
7
+ { displayName: "timingChecks", VM_OPTIONS: { timingChecks: true } },
8
+ {
9
+ displayName: "all",
10
+ VM_OPTIONS: {
11
+ randomizeOpcodes: true,
12
+ shuffleOpcodes: true,
13
+ encodeBytecode: true,
14
+ selfModifying: true,
15
+ timingChecks: true,
16
+ },
6
17
  },
18
+ ];
19
+
20
+ export default {
21
+ projects: OPTIONS_MATRIX.map(({ displayName, VM_OPTIONS }) => ({
22
+ displayName,
23
+ extensionsToTreatAsEsm: [".ts"],
24
+ moduleFileExtensions: ["ts", "js", "json"],
25
+ transform: { "\\.ts$": "./jest-strip-types.js" },
26
+ globals: { VM_OPTIONS },
27
+ })),
7
28
  };
package/package.json CHANGED
@@ -1,12 +1,14 @@
1
1
  {
2
2
  "name": "js-confuser-vm",
3
- "version": "0.0.1",
4
- "main": "src/index.ts",
3
+ "version": "0.0.3",
4
+ "main": "dist/index.js",
5
5
  "scripts": {
6
+ "build": "babel src --out-dir dist --extensions '.ts,.js'",
6
7
  "index": "NODE_OPTIONS=\"--disable-warning=ExperimentalWarning\" node index.ts",
7
- "test": "NODE_OPTIONS=\"--experimental-vm-modules --experimental-strip-types --disable-warning=ExperimentalWarning\" jest --coverage --coverageReporters=html",
8
+ "test": "NODE_OPTIONS=\"--experimental-vm-modules --experimental-strip-types --disable-warning=ExperimentalWarning\" jest --coverage --coverageReporters=html --selectProjects default",
9
+ "test-all": "NODE_OPTIONS=\"--experimental-vm-modules --experimental-strip-types --disable-warning=ExperimentalWarning\" jest --coverage --coverageReporters=html",
8
10
  "test262": "NODE_OPTIONS=\"--experimental-vm-modules --experimental-strip-types --disable-warning=ExperimentalWarning\" node test262-scripts/run-test262.ts",
9
- "prepublishOnly": "npm run test"
11
+ "prepublishOnly": "npm run build && npm run test"
10
12
  },
11
13
  "type": "module",
12
14
  "keywords": [
@@ -28,14 +30,19 @@
28
30
  "@babel/traverse": "^7.29.0",
29
31
  "@babel/types": "^7.29.0",
30
32
  "google-closure-compiler": "^20260216.0.0",
31
- "js-confuser": "^2.0.0",
32
33
  "json5": "^2.2.3"
33
34
  },
34
35
  "devDependencies": {
36
+ "@babel/cli": "^7.28.6",
37
+ "@babel/core": "^7.29.0",
38
+ "@babel/preset-env": "^7.29.0",
39
+ "@babel/preset-typescript": "^7.28.5",
35
40
  "@types/node": "^25.3.0",
41
+ "babel-plugin-module-resolver": "^5.0.2",
42
+ "babel-plugin-replace-import-extension": "^1.1.5",
36
43
  "jest": "^30.2.0"
37
44
  },
38
45
  "engines": {
39
- "node": ">=24.0.0"
46
+ "node": ">=18.0.0"
40
47
  }
41
48
  }