js-confuser-vm 0.1.0 → 0.1.2
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.
- package/README.md +281 -147
- package/dist/build-runtime.js +41 -15
- package/dist/compiler.js +714 -265
- package/dist/disassembler.js +367 -0
- package/dist/index.js +7 -2
- package/dist/runtime.js +160 -119
- package/dist/template.js +163 -42
- package/dist/transforms/bytecode/aliasedOpcodes.js +4 -1
- package/dist/transforms/bytecode/concealConstants.js +2 -2
- package/dist/transforms/bytecode/controlFlowFlattening.js +569 -0
- package/dist/transforms/bytecode/dispatcher.js +15 -111
- package/dist/transforms/bytecode/macroOpcodes.js +2 -2
- package/{src/transforms/bytecode/resolveContants.ts → dist/transforms/bytecode/resolveConstants.js} +30 -56
- package/dist/transforms/bytecode/resolveRegisters.js +23 -4
- package/dist/transforms/bytecode/selfModifying.js +88 -21
- package/dist/transforms/bytecode/semanticOpcodes.js +162 -0
- package/dist/transforms/bytecode/specializedOpcodes.js +23 -12
- package/dist/transforms/bytecode/stringConcealing.js +288 -0
- package/dist/transforms/runtime/classObfuscation.js +43 -0
- package/dist/transforms/runtime/handlerTable.js +91 -0
- package/dist/transforms/runtime/semanticOpcodes.js +35 -0
- package/dist/transforms/runtime/specializedOpcodes.js +11 -5
- package/dist/types.js +1 -1
- package/dist/utils/ast-utils.js +75 -0
- package/dist/utils/op-utils.js +1 -2
- package/dist/utils/pass-utils.js +100 -0
- package/dist/utils/profile-utils.js +3 -0
- package/package.json +8 -1
- package/.gitmodules +0 -4
- package/.prettierignore +0 -1
- package/CHANGELOG.md +0 -335
- package/babel-plugin-inline-runtime.cjs +0 -34
- package/babel.config.json +0 -23
- package/index.ts +0 -38
- package/jest-strip-types.js +0 -10
- package/jest.config.js +0 -52
- package/src/build-runtime.ts +0 -78
- package/src/compiler.ts +0 -2593
- package/src/index.ts +0 -14
- package/src/minify.ts +0 -21
- package/src/options.ts +0 -18
- package/src/runtime.ts +0 -923
- package/src/template.ts +0 -141
- package/src/transforms/bytecode/aliasedOpcodes.ts +0 -148
- package/src/transforms/bytecode/concealConstants.ts +0 -52
- package/src/transforms/bytecode/dispatcher.ts +0 -398
- package/src/transforms/bytecode/macroOpcodes.ts +0 -193
- package/src/transforms/bytecode/microOpcodes.ts +0 -291
- package/src/transforms/bytecode/resolveLabels.ts +0 -112
- package/src/transforms/bytecode/resolveRegisters.ts +0 -221
- package/src/transforms/bytecode/selfModifying.ts +0 -121
- package/src/transforms/bytecode/specializedOpcodes.ts +0 -153
- package/src/transforms/runtime/aliasedOpcodes.ts +0 -191
- package/src/transforms/runtime/internalVariables.ts +0 -270
- package/src/transforms/runtime/macroOpcodes.ts +0 -138
- package/src/transforms/runtime/microOpcodes.ts +0 -93
- package/src/transforms/runtime/minify.ts +0 -1
- package/src/transforms/runtime/shuffleOpcodes.ts +0 -24
- package/src/transforms/runtime/specializedOpcodes.ts +0 -156
- package/src/types.ts +0 -93
- package/src/utils/op-utils.ts +0 -48
- package/src/utils/random-utils.ts +0 -31
- package/tsconfig.json +0 -12
package/src/template.ts
DELETED
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
// Template
|
|
2
|
-
// Compiles a JS code snippet into raw IR bytecode that can be spliced into the
|
|
3
|
-
// parent compiler's bytecode stream at any point before resolveRegisters /
|
|
4
|
-
// resolveLabels run.
|
|
5
|
-
//
|
|
6
|
-
// ── Usage ─────────────────────────────────────────────────────────────────────
|
|
7
|
-
//
|
|
8
|
-
// const tmpl = new Template(`
|
|
9
|
-
// function {name}(x, y) {
|
|
10
|
-
// return x + y;
|
|
11
|
-
// }
|
|
12
|
-
// `);
|
|
13
|
-
//
|
|
14
|
-
// const bc = tmpl.compile({ name: "myHelper" }, parentCompiler);
|
|
15
|
-
// result.push(...bc);
|
|
16
|
-
//
|
|
17
|
-
// ── How it works ──────────────────────────────────────────────────────────────
|
|
18
|
-
//
|
|
19
|
-
// 1. {name} placeholders are replaced with the caller-supplied string values.
|
|
20
|
-
// 2. A fresh child Compiler is created, inheriting the parent's OP table so
|
|
21
|
-
// opcode numbers match exactly (including randomizeOpcodes mappings).
|
|
22
|
-
// 3. The child compiles the snippet to raw IR (no passes, no label/register
|
|
23
|
-
// resolution).
|
|
24
|
-
// 4. Post-processing makes the child's bytecode compatible with the parent:
|
|
25
|
-
//
|
|
26
|
-
// Labels — every label string is renamed via parentCompiler._makeLabel()
|
|
27
|
-
// so names never collide with existing or future labels.
|
|
28
|
-
//
|
|
29
|
-
// FnIds — the child's main scope (fnDescriptors[0]) is mapped to
|
|
30
|
-
// targetFnId (default 0). Any inner functions (closures
|
|
31
|
-
// declared inside the template) are appended to
|
|
32
|
-
// parentCompiler.fnDescriptors with fresh indices.
|
|
33
|
-
//
|
|
34
|
-
// 5. The main function's entry defineLabel is stripped from the output — it is
|
|
35
|
-
// a synthetic wrapper added by _compileMain and is not part of the injected
|
|
36
|
-
// code. All other instructions (including the implicit RETURN at the end of
|
|
37
|
-
// the main scope and any inner-function blocks) are returned as-is so the
|
|
38
|
-
// caller can append them wherever appropriate.
|
|
39
|
-
//
|
|
40
|
-
// ── Limitations (MVP) ─────────────────────────────────────────────────────────
|
|
41
|
-
// • Variables are plain string/number interpolation only — no AST-node
|
|
42
|
-
// substitution.
|
|
43
|
-
// • Templates that reference upvalue-captured registers from the call site are
|
|
44
|
-
// not supported (inner functions closing over template-local variables work).
|
|
45
|
-
// • Opcodes with no JS equivalent (JUMP_REG, BXOR used as decode, etc.) cannot
|
|
46
|
-
// be expressed in a template; write those instruction arrays manually.
|
|
47
|
-
|
|
48
|
-
import { Compiler } from "./compiler.ts";
|
|
49
|
-
import { DEFAULT_OPTIONS } from "./options.ts";
|
|
50
|
-
import type { Bytecode, Instruction } from "./types.ts";
|
|
51
|
-
|
|
52
|
-
export class Template {
|
|
53
|
-
private readonly _source: string;
|
|
54
|
-
|
|
55
|
-
constructor(source: string) {
|
|
56
|
-
this._source = source;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// ── String interpolation ──────────────────────────────────────────────────
|
|
60
|
-
private _interpolate(variables: Record<string, string | number>): string {
|
|
61
|
-
return this._source.replace(/\{(\w+)\}/g, (match, name) => {
|
|
62
|
-
if (!(name in variables)) {
|
|
63
|
-
throw new Error(`Template: missing variable {${name}}`);
|
|
64
|
-
}
|
|
65
|
-
return String(variables[name]);
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// ── Main entry point ───────────────────────────────────────────────────────
|
|
70
|
-
/**
|
|
71
|
-
* Compile the template and return the inner (non-main) function descriptors
|
|
72
|
-
* and their bytecode blocks, ready to splice into the parent compiler's
|
|
73
|
-
* instruction stream.
|
|
74
|
-
*
|
|
75
|
-
* The template source should declare one or more named functions. The
|
|
76
|
-
* top-level ("main") scope of the template is discarded — it exists only as
|
|
77
|
-
* a syntactic wrapper so that function declarations parse correctly.
|
|
78
|
-
*
|
|
79
|
-
* Each inner function is registered in parentCompiler.fnDescriptors with a
|
|
80
|
-
* fresh fnIdx, and its bytecode block (defineLabel + body instructions) is
|
|
81
|
-
* returned so the caller can append it to the parent bytecode stream at the
|
|
82
|
-
* desired location (typically at the end, after all function bodies).
|
|
83
|
-
*
|
|
84
|
-
* @param variables Substitution map for {name} placeholders.
|
|
85
|
-
* @param parentCompiler The Compiler whose OP table, label counter, and
|
|
86
|
-
* fnDescriptors are shared.
|
|
87
|
-
*
|
|
88
|
-
* @returns
|
|
89
|
-
* functions — ordered list of inner FnDescriptors (index 0 = first named
|
|
90
|
-
* function in the template source). Use .entryLabel and
|
|
91
|
-
* ._fnIdx to build MAKE_CLOSURE operands.
|
|
92
|
-
* bytecode — IR bytecode blocks for all inner functions, ready to splice
|
|
93
|
-
* after the parent's function bodies. Does NOT include the
|
|
94
|
-
* template's main-scope instructions.
|
|
95
|
-
*/
|
|
96
|
-
compile(
|
|
97
|
-
variables: Record<string, string | number>,
|
|
98
|
-
parentCompiler: Compiler,
|
|
99
|
-
): { functions: any[]; bytecode: Bytecode } {
|
|
100
|
-
// ── 1. Interpolate ────────────────────────────────────────────────────
|
|
101
|
-
const code = this._interpolate(variables);
|
|
102
|
-
|
|
103
|
-
// ── 2. Create child compiler, inherit parent's OP table ───────────────
|
|
104
|
-
// randomizeOpcodes is disabled — we copy the parent's already-randomized
|
|
105
|
-
// mapping directly so all opcode numbers are identical.
|
|
106
|
-
const child = new Compiler({ ...DEFAULT_OPTIONS, randomizeOpcodes: false });
|
|
107
|
-
child.OP = { ...parentCompiler.OP };
|
|
108
|
-
child.OP_NAME = { ...parentCompiler.OP_NAME };
|
|
109
|
-
child.JUMP_OPS = new Set(parentCompiler.JUMP_OPS);
|
|
110
|
-
|
|
111
|
-
child._makeLabel = parentCompiler._makeLabel.bind(parentCompiler);
|
|
112
|
-
|
|
113
|
-
// Record how many descriptors the parent already has so we can find the
|
|
114
|
-
// child's main (index = startIdx) and inner functions (startIdx+1 …).
|
|
115
|
-
const startIdx = parentCompiler.fnDescriptors.length;
|
|
116
|
-
child.fnDescriptors = parentCompiler.fnDescriptors; // share — inner functions auto-register
|
|
117
|
-
|
|
118
|
-
// ── 3. Compile to raw IR (no passes) ──────────────────────────────────
|
|
119
|
-
child.compile(code);
|
|
120
|
-
|
|
121
|
-
// parentCompiler.fnDescriptors[startIdx] → child's main (discard)
|
|
122
|
-
// parentCompiler.fnDescriptors[startIdx+1…] → inner helper functions
|
|
123
|
-
const innerDescs = parentCompiler.fnDescriptors.slice(startIdx + 1);
|
|
124
|
-
|
|
125
|
-
// Build bytecode blocks for inner functions only.
|
|
126
|
-
// child.bytecode was assembled by _compileMain from ALL fnDescriptors
|
|
127
|
-
// starting at startIdx. We rebuild it here from the inner descs only.
|
|
128
|
-
const innerBytecode: Bytecode = [];
|
|
129
|
-
for (const desc of innerDescs) {
|
|
130
|
-
innerBytecode.push([
|
|
131
|
-
null,
|
|
132
|
-
{ type: "defineLabel", label: desc.entryLabel },
|
|
133
|
-
] as Instruction);
|
|
134
|
-
for (const instr of (desc as any).bytecode as Bytecode) {
|
|
135
|
-
innerBytecode.push(instr);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
return { functions: innerDescs, bytecode: innerBytecode };
|
|
140
|
-
}
|
|
141
|
-
}
|
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
import type { Bytecode, InstrOperand, Instruction } from "../../types.ts";
|
|
2
|
-
import { Compiler, SOURCE_NODE_SYM } from "../../compiler.ts";
|
|
3
|
-
import { nextFreeSlot } from "../../utils/op-utils.ts";
|
|
4
|
-
import { shuffle } from "../../utils/random-utils.ts";
|
|
5
|
-
|
|
6
|
-
// Opcodes that must not be aliased.
|
|
7
|
-
// Variable-length operand opcodes cannot be statically aliased since the
|
|
8
|
-
// number of this._operand() calls varies at runtime.
|
|
9
|
-
// Infrastructure opcodes (PATCH, TRY_SETUP, TRY_END, DEBUGGER) are excluded
|
|
10
|
-
// because aliasing them would interfere with self-modifying bytecode and
|
|
11
|
-
// exception-handling machinery.
|
|
12
|
-
const DISALLOWED_OP_NAMES = new Set([
|
|
13
|
-
"MAKE_CLOSURE",
|
|
14
|
-
"BUILD_ARRAY",
|
|
15
|
-
"BUILD_OBJECT",
|
|
16
|
-
"CALL",
|
|
17
|
-
"CALL_METHOD",
|
|
18
|
-
"NEW",
|
|
19
|
-
"PATCH",
|
|
20
|
-
"TRY_SETUP",
|
|
21
|
-
"TRY_END",
|
|
22
|
-
"DEBUGGER",
|
|
23
|
-
]);
|
|
24
|
-
|
|
25
|
-
// Creates aliased opcodes: duplicate handlers for commonly-used opcodes,
|
|
26
|
-
// optionally with a permuted operand read order in the bytecode stream.
|
|
27
|
-
//
|
|
28
|
-
// For each aliased op, we record an `order` permutation of length `arity`.
|
|
29
|
-
// order[i] = j means: bytecode slot i holds what was originally operand j.
|
|
30
|
-
//
|
|
31
|
-
// Example: LOAD_GLOBAL [dst, nameIdx] with order=[1,0]:
|
|
32
|
-
// Bytecode stores: [ALIAS_OP, nameIdx, dst]
|
|
33
|
-
// Handler reads: _unsortedOperands = [nameIdx, dst]
|
|
34
|
-
// _operands = [_unsortedOperands[1], _unsortedOperands[0]]
|
|
35
|
-
// = [dst, nameIdx] ← original order restored
|
|
36
|
-
//
|
|
37
|
-
// Runs LAST among bytecode transforms (after selfModifying), before resolveLabels.
|
|
38
|
-
export function aliasedOpcodes(
|
|
39
|
-
bc: Bytecode,
|
|
40
|
-
compiler: Compiler,
|
|
41
|
-
): { bytecode: Bytecode } {
|
|
42
|
-
// Build a map of base opcode value → name, excluding disallowed ops
|
|
43
|
-
const baseOpValueToName = new Map<number, string>();
|
|
44
|
-
for (const [name, val] of Object.entries(compiler.OP)) {
|
|
45
|
-
if (DISALLOWED_OP_NAMES.has(name)) continue;
|
|
46
|
-
baseOpValueToName.set(val as number, name);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// ── Step 1: count frequency and determine arity for each eligible base opcode ─
|
|
50
|
-
// We scan the actual post-transform bytecode so frequency reflects what's
|
|
51
|
-
// really left (specialized/macro ops already consumed their share).
|
|
52
|
-
const opStats = new Map<number, { freq: number; arity: number | null }>();
|
|
53
|
-
|
|
54
|
-
for (const instr of bc) {
|
|
55
|
-
const op = instr[0];
|
|
56
|
-
if (op === null || !baseOpValueToName.has(op)) continue;
|
|
57
|
-
|
|
58
|
-
const arity = instr.length - 1;
|
|
59
|
-
if (arity < 1) continue; // 0-operand opcodes have nothing to permute
|
|
60
|
-
|
|
61
|
-
const existing = opStats.get(op);
|
|
62
|
-
if (!existing) {
|
|
63
|
-
opStats.set(op, { freq: 1, arity });
|
|
64
|
-
} else {
|
|
65
|
-
if (existing.arity !== arity) {
|
|
66
|
-
// Inconsistent arity → variable-length; skip
|
|
67
|
-
existing.arity = null;
|
|
68
|
-
}
|
|
69
|
-
existing.freq++;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// ── Step 2: sort by frequency descending, keep only consistent-arity ops ────
|
|
74
|
-
const candidates = Array.from(opStats.entries())
|
|
75
|
-
.filter(([, s]) => s.arity !== null)
|
|
76
|
-
.sort(([, a], [, b]) => b.freq - a.freq);
|
|
77
|
-
|
|
78
|
-
if (candidates.length === 0) return { bytecode: bc };
|
|
79
|
-
|
|
80
|
-
// ── Step 3: assign free slots, build order permutations ─────────────────────
|
|
81
|
-
// aliasMap: originalOp → aliasOp (only the winning alias per original op)
|
|
82
|
-
const aliasMap = new Map<number, number>();
|
|
83
|
-
const aliasedOps: Compiler["ALIASED_OPS"] = {};
|
|
84
|
-
|
|
85
|
-
for (const [originalOp, stats] of candidates) {
|
|
86
|
-
const aliasOp = nextFreeSlot(compiler);
|
|
87
|
-
if (aliasOp === -1) break;
|
|
88
|
-
|
|
89
|
-
const arity = stats.arity!;
|
|
90
|
-
|
|
91
|
-
// Build a permutation of [0 .. arity-1].
|
|
92
|
-
// For arity >= 2: shuffle until we get a non-identity permutation so the
|
|
93
|
-
// operand order is actually different (makes the alias more confusing).
|
|
94
|
-
// For arity == 1: only one permutation exists ([0]); still useful as a clone.
|
|
95
|
-
let order: number[];
|
|
96
|
-
if (arity >= 2) {
|
|
97
|
-
const identity = Array.from({ length: arity }, (_, i) => i);
|
|
98
|
-
let attempts = 0;
|
|
99
|
-
do {
|
|
100
|
-
order = shuffle([...identity]);
|
|
101
|
-
attempts++;
|
|
102
|
-
} while (attempts < 20 && order.every((v, i) => v === i));
|
|
103
|
-
} else {
|
|
104
|
-
order = [0];
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
aliasMap.set(originalOp, aliasOp);
|
|
108
|
-
aliasedOps[aliasOp] = { originalOp, order };
|
|
109
|
-
|
|
110
|
-
const originalName = compiler.OP_NAME[originalOp] ?? `OP_${originalOp}`;
|
|
111
|
-
compiler.OP_NAME[aliasOp] = `ALIAS_${originalName}_${order.join("_")}`;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
compiler.ALIASED_OPS = aliasedOps;
|
|
115
|
-
|
|
116
|
-
if (aliasMap.size === 0) return { bytecode: bc };
|
|
117
|
-
|
|
118
|
-
// ── Step 4: rewrite bytecode ─────────────────────────────────────────────────
|
|
119
|
-
const result: Bytecode = [];
|
|
120
|
-
|
|
121
|
-
for (const instr of bc) {
|
|
122
|
-
const op = instr[0];
|
|
123
|
-
if (op === null || !aliasMap.has(op)) {
|
|
124
|
-
result.push(instr);
|
|
125
|
-
continue;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const aliasOp = aliasMap.get(op)!;
|
|
129
|
-
const { order } = aliasedOps[aliasOp];
|
|
130
|
-
const originalOperands = instr.slice(1) as InstrOperand[];
|
|
131
|
-
|
|
132
|
-
// Guard: if arity changed (shouldn't happen after the consistency check),
|
|
133
|
-
// fall back to the original instruction.
|
|
134
|
-
if (originalOperands.length !== order.length) {
|
|
135
|
-
result.push(instr);
|
|
136
|
-
continue;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// Rearrange operands: new slot i receives original operand order[i].
|
|
140
|
-
const newOperands = order.map((i) => originalOperands[i]);
|
|
141
|
-
|
|
142
|
-
const newInstr: Instruction = [aliasOp, ...newOperands];
|
|
143
|
-
(newInstr as any)[SOURCE_NODE_SYM] = (instr as any)[SOURCE_NODE_SYM];
|
|
144
|
-
result.push(newInstr);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
return { bytecode: result };
|
|
148
|
-
}
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import { Compiler } from "../../compiler.ts";
|
|
2
|
-
import type * as b from "../../types.ts";
|
|
3
|
-
|
|
4
|
-
export function concealConstants(
|
|
5
|
-
bytecode: b.Bytecode,
|
|
6
|
-
compiler: Compiler,
|
|
7
|
-
): {
|
|
8
|
-
bytecode: b.Bytecode;
|
|
9
|
-
} {
|
|
10
|
-
const newBytecode: b.Bytecode = [];
|
|
11
|
-
|
|
12
|
-
for (const instr of bytecode) {
|
|
13
|
-
const [op, ...operands] = instr;
|
|
14
|
-
|
|
15
|
-
const hasContant = operands.some(
|
|
16
|
-
(o) =>
|
|
17
|
-
o !== undefined &&
|
|
18
|
-
o !== null &&
|
|
19
|
-
typeof o === "object" &&
|
|
20
|
-
(o as any).type === "constant",
|
|
21
|
-
);
|
|
22
|
-
|
|
23
|
-
if (!hasContant) {
|
|
24
|
-
newBytecode.push(instr);
|
|
25
|
-
continue;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const newOperands = [];
|
|
29
|
-
for (const operand of operands) {
|
|
30
|
-
if ((operand as any)?.type === "constant") {
|
|
31
|
-
const tsOperand = operand as any;
|
|
32
|
-
newOperands.push(operand);
|
|
33
|
-
newOperands.push({
|
|
34
|
-
type: "constant",
|
|
35
|
-
value: tsOperand.value,
|
|
36
|
-
key: true,
|
|
37
|
-
});
|
|
38
|
-
} else {
|
|
39
|
-
newOperands.push(operand);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
instr.length = 0;
|
|
44
|
-
instr.push(op, ...newOperands);
|
|
45
|
-
|
|
46
|
-
newBytecode.push(instr);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
return {
|
|
50
|
-
bytecode: newBytecode,
|
|
51
|
-
};
|
|
52
|
-
}
|