js-confuser-vm 0.0.4 → 0.0.6
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/CHANGELOG.md +58 -3
- package/README.MD +186 -107
- package/dist/build-runtime.js +59 -0
- package/dist/compiler.js +1777 -0
- package/dist/index.js +10 -0
- package/dist/minify.js +18 -0
- package/dist/options.js +1 -0
- package/dist/runtime.js +826 -0
- package/dist/transforms/bytecode/aliasedOpcodes.js +140 -0
- package/dist/transforms/bytecode/concealConstants.js +31 -0
- package/dist/transforms/bytecode/macroOpcodes.js +164 -0
- package/dist/transforms/bytecode/resolveContants.js +106 -0
- package/dist/transforms/bytecode/resolveLabels.js +80 -0
- package/dist/transforms/bytecode/selfModifying.js +108 -0
- package/dist/transforms/bytecode/specializedOpcodes.js +113 -0
- package/dist/transforms/runtime/aliasedOpcodes.js +134 -0
- package/dist/transforms/runtime/macroOpcodes.js +88 -0
- package/dist/transforms/runtime/minify.js +1 -0
- package/dist/transforms/runtime/shuffleOpcodes.js +20 -0
- package/dist/transforms/runtime/specializedOpcodes.js +107 -0
- package/{src/transforms/utils/op-utils.ts → dist/transforms/utils/op-utils.js} +25 -26
- package/dist/transforms/utils/random-utils.js +27 -0
- package/dist/types.js +15 -0
- package/dist/utils/op-utils.js +29 -0
- package/dist/utils/random-utils.js +27 -0
- package/dist/utilts.js +3 -0
- package/index.ts +10 -8
- package/jest.config.js +10 -0
- package/package.json +3 -4
- package/src/build-runtime.ts +7 -1
- package/src/compiler.ts +2395 -2069
- package/src/options.ts +2 -0
- package/src/runtime.ts +838 -771
- package/src/transforms/bytecode/aliasedOpcodes.ts +158 -0
- package/src/transforms/bytecode/concealConstants.ts +52 -0
- package/src/transforms/bytecode/macroOpcodes.ts +32 -15
- package/src/transforms/bytecode/resolveContants.ts +87 -16
- package/src/transforms/bytecode/selfModifying.ts +3 -3
- package/src/transforms/bytecode/specializedOpcodes.ts +58 -29
- package/src/transforms/runtime/aliasedOpcodes.ts +191 -0
- package/src/transforms/runtime/shuffleOpcodes.ts +1 -1
- package/src/transforms/runtime/specializedOpcodes.ts +39 -24
- package/src/utils/op-utils.ts +33 -0
- /package/src/{transforms/utils → utils}/random-utils.ts +0 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { SOURCE_NODE_SYM } from "../../compiler.js";
|
|
2
|
+
import { getInstructionSize, nextFreeSlot, U16_MAX } from "../../utils/op-utils.js";
|
|
3
|
+
|
|
4
|
+
// Creates specialized opcodes for the most frequent (OPCODE + single_integer_operand) pairs.
|
|
5
|
+
// Example: [OP.LOAD_CONST, 1] becomes [SPECIALIZED_LOAD_CONST_1].
|
|
6
|
+
// Only instructions with *exactly one numeric operand* are considered.
|
|
7
|
+
// MAKE_CLOSURE and other N-sized instructions cannot be specialized
|
|
8
|
+
// Runs after selfModifying but before resolveLabels (operands stay plain numbers).
|
|
9
|
+
export function specializedOpcodes(bc, compiler) {
|
|
10
|
+
const disallowedOps = new Set([compiler.OP.MAKE_CLOSURE, compiler.OP.BUILD_ARRAY, compiler.OP.BUILD_OBJECT, compiler.OP.CALL, compiler.OP.CALL_METHOD, compiler.OP.NEW]);
|
|
11
|
+
|
|
12
|
+
// ── Collect used opcodes exactly as specified ─────────────────────────────
|
|
13
|
+
const usedOpcodes = new Set(Object.keys(compiler.OP_NAME).map(k => parseInt(k, 10)).filter(v => !isNaN(v)));
|
|
14
|
+
if (usedOpcodes.size > U16_MAX) return {
|
|
15
|
+
bytecode: bc
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// ── Step 1: count frequency of eligible (op, operand) pairs ───────────────
|
|
19
|
+
const freqMap = new Map();
|
|
20
|
+
for (const instr of bc) {
|
|
21
|
+
const op = instr[0];
|
|
22
|
+
if (op === null || disallowedOps.has(op)) continue;
|
|
23
|
+
|
|
24
|
+
// Only supports between 1-6 operands
|
|
25
|
+
const operandCount = getInstructionSize(instr) - 1;
|
|
26
|
+
if (operandCount < 1 || operandCount > 6) continue;
|
|
27
|
+
const operands = instr.slice(1);
|
|
28
|
+
const operandsKey = JSON.stringify(operands);
|
|
29
|
+
const key = `${op},${operandsKey}`;
|
|
30
|
+
const entry = freqMap.get(key);
|
|
31
|
+
if (entry) {
|
|
32
|
+
entry.occurences++;
|
|
33
|
+
} else {
|
|
34
|
+
freqMap.set(key, {
|
|
35
|
+
op,
|
|
36
|
+
operands,
|
|
37
|
+
operandsKey,
|
|
38
|
+
occurences: 1
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ── Step 2: keep combinations that appear >= 2 times, sort by frequency ───
|
|
44
|
+
const candidates = Array.from(freqMap.values()).filter(e => e.occurences >= 1).sort((a, b) => b.occurences - a.occurences);
|
|
45
|
+
if (candidates.length === 0) return {
|
|
46
|
+
bytecode: bc
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// ── Step 3: assign free opcode slots to the best candidates ───────────────
|
|
50
|
+
const sigToSpecial = new Map();
|
|
51
|
+
const specializedOps = {};
|
|
52
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
53
|
+
const specialOp = nextFreeSlot(usedOpcodes);
|
|
54
|
+
if (specialOp === -1) break;
|
|
55
|
+
const {
|
|
56
|
+
op: originalOp,
|
|
57
|
+
operands,
|
|
58
|
+
operandsKey
|
|
59
|
+
} = candidates[i];
|
|
60
|
+
const key = `${originalOp},${operandsKey}`;
|
|
61
|
+
sigToSpecial.set(key, specialOp);
|
|
62
|
+
specializedOps[specialOp] = {
|
|
63
|
+
originalOp,
|
|
64
|
+
operands
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// Register a human-readable name for disassembly / debugging
|
|
68
|
+
const originalName = compiler.OP_NAME[originalOp] ?? `OP_${originalOp}`;
|
|
69
|
+
compiler.OP_NAME[specialOp] = `${originalName}_${JSON.stringify(operandsKey)}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Store mapping so the interpreter knows how to dispatch the specialized op
|
|
73
|
+
compiler.SPECIALIZED_OPS = specializedOps;
|
|
74
|
+
|
|
75
|
+
// ── Step 4: replace matching instructions with the new single-byte opcode ─
|
|
76
|
+
const result = [];
|
|
77
|
+
for (const instr of bc) {
|
|
78
|
+
const op = instr[0];
|
|
79
|
+
// Only consider instructions with one or more operands
|
|
80
|
+
if (op === null || instr.length <= 1 || op === compiler.OP.MAKE_CLOSURE) {
|
|
81
|
+
result.push(instr);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const operands = instr.slice(1);
|
|
85
|
+
const operandsKey = JSON.stringify(operands);
|
|
86
|
+
const key = `${op},${operandsKey}`;
|
|
87
|
+
const specialOpCode = sigToSpecial.get(key);
|
|
88
|
+
if (!specialOpCode) {
|
|
89
|
+
result.push(instr);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const newOperands = operands.map(operand => {
|
|
93
|
+
const operandAsObject = typeof operand === "object" && operand ? operand : {
|
|
94
|
+
type: "number",
|
|
95
|
+
value: operand,
|
|
96
|
+
resolvedValue: operand
|
|
97
|
+
};
|
|
98
|
+
const newOperand = {
|
|
99
|
+
...operandAsObject,
|
|
100
|
+
placeholder: true
|
|
101
|
+
};
|
|
102
|
+
return newOperand;
|
|
103
|
+
});
|
|
104
|
+
const newInstr = [specialOpCode, ...newOperands];
|
|
105
|
+
|
|
106
|
+
// Preserve source-node information for error reporting
|
|
107
|
+
newInstr[SOURCE_NODE_SYM] = instr[SOURCE_NODE_SYM];
|
|
108
|
+
result.push(newInstr);
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
bytecode: result
|
|
112
|
+
};
|
|
113
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import * as t from "@babel/types";
|
|
2
|
+
import traverseImport from "@babel/traverse";
|
|
3
|
+
import { ok } from "assert";
|
|
4
|
+
const traverse = traverseImport.default || traverseImport;
|
|
5
|
+
|
|
6
|
+
// Extract the real statement list from a SwitchCase consequent.
|
|
7
|
+
function extractCaseBody(switchCase) {
|
|
8
|
+
let stmts;
|
|
9
|
+
if (switchCase.consequent.length === 1 && t.isBlockStatement(switchCase.consequent[0])) {
|
|
10
|
+
stmts = switchCase.consequent[0].body;
|
|
11
|
+
} else {
|
|
12
|
+
stmts = switchCase.consequent;
|
|
13
|
+
}
|
|
14
|
+
return stmts.filter(s => !t.isBreakStatement(s) && !t.isEmptyStatement(s));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Replace every `this._operand()` call in bodyStmts with `_operands[i]`
|
|
18
|
+
// where i is the call's sequential index (0-based).
|
|
19
|
+
// Returns the number of replacements performed.
|
|
20
|
+
function replaceOperandCalls(bodyStmts) {
|
|
21
|
+
let replaced = 0;
|
|
22
|
+
traverse(t.blockStatement(bodyStmts), {
|
|
23
|
+
noScope: true,
|
|
24
|
+
CallExpression(path) {
|
|
25
|
+
const callee = path.node.callee;
|
|
26
|
+
const isMethodCall = methodName => {
|
|
27
|
+
return t.isMemberExpression(callee) && t.isThisExpression(callee.object) && t.isIdentifier(callee.property, {
|
|
28
|
+
name: methodName
|
|
29
|
+
}) && path.node.arguments.length === 0;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// Replace with _operands[i]
|
|
33
|
+
const createOperandAccess = () => {
|
|
34
|
+
return t.memberExpression(t.identifier("_operands"), t.numericLiteral(replaced++), true // computed
|
|
35
|
+
);
|
|
36
|
+
};
|
|
37
|
+
if (isMethodCall("_operand")) {
|
|
38
|
+
path.replaceWith(createOperandAccess());
|
|
39
|
+
}
|
|
40
|
+
if (isMethodCall("_constant")) {
|
|
41
|
+
path.node.arguments = [createOperandAccess(), createOperandAccess()];
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
return replaced;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Appends a generated switch case for every entry in compiler.ALIASED_OPS.
|
|
49
|
+
// Each alias case:
|
|
50
|
+
// 1. Reads all operands eagerly into `_unsortedOperands` (in the shuffled
|
|
51
|
+
// bytecode order) via sequential this._operand() calls.
|
|
52
|
+
// 2. Restores the original operand order into `_operands` using the INVERSE
|
|
53
|
+
// of the stored `order` permutation:
|
|
54
|
+
// inverseOrder[order[i]] = i
|
|
55
|
+
// _operands[j] = _unsortedOperands[inverseOrder[j]]
|
|
56
|
+
// This is necessary because the bytecode stored originalOperands[order[i]]
|
|
57
|
+
// at slot i, so recovering originalOperands[j] requires the inverse lookup.
|
|
58
|
+
// 3. Executes a clone of the original handler body where every
|
|
59
|
+
// this._operand() has been replaced by the corresponding `_operands[i]`.
|
|
60
|
+
//
|
|
61
|
+
// Must run AFTER applyMacroOpcodes / applySpecializedOpcodes (so original
|
|
62
|
+
// cases already exist) but BEFORE applyShuffleOpcodes (so the new alias
|
|
63
|
+
// cases are also shuffled into the handler order).
|
|
64
|
+
export function applyAliasedOpcodes(ast, compiler) {
|
|
65
|
+
if (!compiler.ALIASED_OPS || Object.keys(compiler.ALIASED_OPS).length === 0) return;
|
|
66
|
+
let switchStatement = null;
|
|
67
|
+
traverse(ast, {
|
|
68
|
+
SwitchStatement(path) {
|
|
69
|
+
if (path.node.leadingComments?.some(c => c.value.includes("@SWITCH"))) {
|
|
70
|
+
switchStatement = path.node;
|
|
71
|
+
path.stop();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
ok(switchStatement, "Could not find @SWITCH statement for aliased opcodes");
|
|
76
|
+
|
|
77
|
+
// Build opName → SwitchCase map from existing OP.xxx case tests.
|
|
78
|
+
const nameToCaseMap = new Map();
|
|
79
|
+
for (const sc of switchStatement.cases) {
|
|
80
|
+
const test = sc.test;
|
|
81
|
+
if (test && t.isMemberExpression(test) && t.isIdentifier(test.object, {
|
|
82
|
+
name: "OP"
|
|
83
|
+
}) && t.isIdentifier(test.property)) {
|
|
84
|
+
nameToCaseMap.set(test.property.name, sc);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
for (const [aliasOpStr, info] of Object.entries(compiler.ALIASED_OPS)) {
|
|
88
|
+
const aliasOpCode = Number(aliasOpStr);
|
|
89
|
+
const {
|
|
90
|
+
originalOp,
|
|
91
|
+
order
|
|
92
|
+
} = info;
|
|
93
|
+
const arity = order.length;
|
|
94
|
+
const originalName = compiler.OP_NAME[originalOp];
|
|
95
|
+
if (!originalName) continue;
|
|
96
|
+
const originalCase = nameToCaseMap.get(originalName);
|
|
97
|
+
if (!originalCase) continue;
|
|
98
|
+
|
|
99
|
+
// Clone the original handler body (deep clone so we don't mutate the source)
|
|
100
|
+
const bodyStmts = extractCaseBody(originalCase).map(s => t.cloneNode(s, true));
|
|
101
|
+
|
|
102
|
+
// Replace this._operand() calls with _operands[i]
|
|
103
|
+
const replaced = replaceOperandCalls(bodyStmts);
|
|
104
|
+
|
|
105
|
+
// If the handler has a different number of _operand() calls than our
|
|
106
|
+
// recorded arity, skip this alias (variable-operand handler guard).
|
|
107
|
+
if (replaced !== arity) continue;
|
|
108
|
+
|
|
109
|
+
// Build: var _unsortedOperands = [this._operand(), this._operand(), ...]
|
|
110
|
+
// Reads operands in the NEW (shuffled) bytecode order.
|
|
111
|
+
const unsortedInit = t.variableDeclaration("let", [t.variableDeclarator(t.identifier("_unsortedOperands"), t.arrayExpression(Array.from({
|
|
112
|
+
length: arity
|
|
113
|
+
}, () => t.callExpression(t.memberExpression(t.thisExpression(), t.identifier("_operand")), []))))]);
|
|
114
|
+
|
|
115
|
+
// The inverse permutation maps original position j → unsorted index i,
|
|
116
|
+
// because the bytecode stored originalOperands[order[i]] at slot i.
|
|
117
|
+
// inverseOrder[j] = i means: original operand j lives at _unsortedOperands[i]
|
|
118
|
+
const inverseOrder = new Array(arity);
|
|
119
|
+
for (let i = 0; i < arity; i++) {
|
|
120
|
+
inverseOrder[order[i]] = i;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Build: var _operands = [_unsortedOperands[inverseOrder[0]], ...]
|
|
124
|
+
// Restores the original operand order expected by the handler body.
|
|
125
|
+
const operandsInit = t.variableDeclaration("let", [t.variableDeclarator(t.identifier("_operands"), t.arrayExpression(inverseOrder.map(idx => t.memberExpression(t.identifier("_unsortedOperands"), t.numericLiteral(idx), true // computed
|
|
126
|
+
))))]);
|
|
127
|
+
const allStmts = [unsortedInit, operandsInit, ...bodyStmts];
|
|
128
|
+
|
|
129
|
+
// Add a leading comment for readability in non-minified output
|
|
130
|
+
t.addComment(allStmts[0], "leading", ` ${compiler.OP_NAME[aliasOpCode]} (order: [${order.join(",")}])`, true);
|
|
131
|
+
allStmts.push(t.breakStatement());
|
|
132
|
+
switchStatement.cases.push(t.switchCase(t.numericLiteral(aliasOpCode), [t.blockStatement(allStmts)]));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import * as t from "@babel/types";
|
|
2
|
+
import traverseImport from "@babel/traverse";
|
|
3
|
+
import { ok } from "assert";
|
|
4
|
+
const traverse = traverseImport.default || traverseImport;
|
|
5
|
+
|
|
6
|
+
// Extract the real statement list from a SwitchCase consequent, normalising
|
|
7
|
+
// the two forms that appear in the runtime:
|
|
8
|
+
// • A single wrapping BlockStatement → use its .body
|
|
9
|
+
// • Statements listed directly → use as-is
|
|
10
|
+
// In both cases trailing BreakStatement / EmptyStatement are filtered out.
|
|
11
|
+
function extractCaseBody(switchCase) {
|
|
12
|
+
let stmts;
|
|
13
|
+
if (switchCase.consequent.length === 1 && t.isBlockStatement(switchCase.consequent[0])) {
|
|
14
|
+
stmts = switchCase.consequent[0].body;
|
|
15
|
+
} else {
|
|
16
|
+
stmts = switchCase.consequent;
|
|
17
|
+
}
|
|
18
|
+
return stmts.filter(s => !t.isBreakStatement(s) && !t.isEmptyStatement(s));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Append a generated switch case for every entry in compiler.MACRO_OPS.
|
|
22
|
+
// Each case inlines the constituent case bodies directly — no operand stack,
|
|
23
|
+
// no substitution needed. Because every opcode handler now reads its own
|
|
24
|
+
// operands via this._operand(), those calls naturally consume the inline
|
|
25
|
+
// operands that macroOpcodes.ts embedded on the macro instruction.
|
|
26
|
+
// Must be called BEFORE applyShuffleOpcodes so the new cases get shuffled.
|
|
27
|
+
export function applyMacroOpcodes(ast, compiler) {
|
|
28
|
+
let switchStatement = null;
|
|
29
|
+
traverse(ast, {
|
|
30
|
+
SwitchStatement(path) {
|
|
31
|
+
if (path.node.leadingComments?.some(c => c.value.includes("@SWITCH"))) {
|
|
32
|
+
switchStatement = path.node;
|
|
33
|
+
path.stop();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
ok(switchStatement, "Could not find @SWITCH statement for macro opcodes");
|
|
38
|
+
|
|
39
|
+
// Build a map opName → SwitchCase from the existing OP.xxx case tests.
|
|
40
|
+
const nameToCaseMap = new Map();
|
|
41
|
+
for (const sc of switchStatement.cases) {
|
|
42
|
+
const test = sc.test;
|
|
43
|
+
if (test && t.isMemberExpression(test) && t.isIdentifier(test.object, {
|
|
44
|
+
name: "OP"
|
|
45
|
+
}) && t.isIdentifier(test.property)) {
|
|
46
|
+
nameToCaseMap.set(test.property.name, sc);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
for (const [macroOpStr, constituentOps] of Object.entries(compiler.MACRO_OPS)) {
|
|
50
|
+
const macroOpCode = Number(macroOpStr);
|
|
51
|
+
const N = constituentOps.length;
|
|
52
|
+
|
|
53
|
+
// Resolve each constituent op value → case node via OP_NAME lookup.
|
|
54
|
+
const constituentCases = [];
|
|
55
|
+
let allFound = true;
|
|
56
|
+
for (const opVal of constituentOps) {
|
|
57
|
+
const opName = compiler.OP_NAME[opVal];
|
|
58
|
+
if (!opName) {
|
|
59
|
+
allFound = false;
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
const found = nameToCaseMap.get(opName);
|
|
63
|
+
if (!found) {
|
|
64
|
+
allFound = false;
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
constituentCases.push(found);
|
|
68
|
+
}
|
|
69
|
+
if (!allFound) continue;
|
|
70
|
+
const opNames = constituentOps.map(v => compiler.OP_NAME[v] ?? `OP_${v}`);
|
|
71
|
+
|
|
72
|
+
// ── Build the macro case body ──────────────────────────────────────────
|
|
73
|
+
// Clone and inline each sub-instruction's case body directly.
|
|
74
|
+
// No operand substitution needed: each body already calls this._operand()
|
|
75
|
+
// to read its own operands, which will consume the inline operands that
|
|
76
|
+
// macroOpcodes.ts embedded on the macro instruction in order.
|
|
77
|
+
const bodyStmts = [];
|
|
78
|
+
for (let i = 0; i < N; i++) {
|
|
79
|
+
const subStmts = extractCaseBody(constituentCases[i]).map(s => t.cloneNode(s, true));
|
|
80
|
+
if (subStmts.length > 0) {
|
|
81
|
+
t.addComment(subStmts[0], "leading", ` ${opNames[i]}`, true);
|
|
82
|
+
bodyStmts.push(...subStmts);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
bodyStmts.push(t.breakStatement());
|
|
86
|
+
switchStatement.cases.push(t.switchCase(t.numericLiteral(macroOpCode), [t.blockStatement(bodyStmts)]));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { minify as applyMinify } from "../../minify.js";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import traverseImport from "@babel/traverse";
|
|
2
|
+
import { ok } from "assert";
|
|
3
|
+
import { shuffle } from "../../utils/random-utils.js";
|
|
4
|
+
const traverse = traverseImport.default || traverseImport;
|
|
5
|
+
|
|
6
|
+
// Randomly reorder the switch cases inside the @SWITCH statement so the
|
|
7
|
+
// opcode handler order varies per build.
|
|
8
|
+
export function applyShuffleOpcodes(ast) {
|
|
9
|
+
let switchStatement = null;
|
|
10
|
+
traverse(ast, {
|
|
11
|
+
SwitchStatement(path) {
|
|
12
|
+
if (path.node.leadingComments?.some(c => c.value.includes("@SWITCH"))) {
|
|
13
|
+
switchStatement = path.node;
|
|
14
|
+
path.stop();
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
ok(switchStatement, "Could not find opcode handlers switch statement");
|
|
19
|
+
switchStatement.cases = shuffle(switchStatement.cases);
|
|
20
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import * as t from "@babel/types";
|
|
2
|
+
import traverseImport from "@babel/traverse";
|
|
3
|
+
import { ok } from "assert";
|
|
4
|
+
const traverse = traverseImport.default || traverseImport;
|
|
5
|
+
|
|
6
|
+
// Extract the real statement list from a SwitchCase consequent (identical to the
|
|
7
|
+
// helper used by applyMacroOpcodes so the two files stay in sync).
|
|
8
|
+
function extractCaseBody(switchCase) {
|
|
9
|
+
let stmts;
|
|
10
|
+
if (switchCase.consequent.length === 1 && t.isBlockStatement(switchCase.consequent[0])) {
|
|
11
|
+
stmts = switchCase.consequent[0].body;
|
|
12
|
+
} else {
|
|
13
|
+
stmts = switchCase.consequent;
|
|
14
|
+
}
|
|
15
|
+
return stmts.filter(s => !t.isBreakStatement(s) && !t.isEmptyStatement(s));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Inline a fixed numeric operand in place of every `this._operand()` call.
|
|
19
|
+
// Because specialized opcodes are only created for instructions that have
|
|
20
|
+
// *exactly one* numeric operand, every `_operand()` call inside the original
|
|
21
|
+
// handler is replaced by the constant value that was baked into the opcode.
|
|
22
|
+
function inlineFixedOperands(bodyStmts, resolvedValues) {
|
|
23
|
+
// Wrap the statements in a temporary BlockStatement so traverse has a root.
|
|
24
|
+
// The replacement mutates the original statement objects in place.
|
|
25
|
+
var replaced = 0;
|
|
26
|
+
traverse(t.blockStatement(bodyStmts), {
|
|
27
|
+
noScope: true,
|
|
28
|
+
CallExpression(path) {
|
|
29
|
+
const callee = path.node.callee;
|
|
30
|
+
const isMethodCall = methodName => {
|
|
31
|
+
return t.isMemberExpression(callee) && t.isThisExpression(callee.object) && t.isIdentifier(callee.property, {
|
|
32
|
+
name: methodName
|
|
33
|
+
}) && path.node.arguments.length === 0;
|
|
34
|
+
};
|
|
35
|
+
if (isMethodCall("_operand")) {
|
|
36
|
+
path.replaceWith(t.numericLiteral(resolvedValues[replaced++]));
|
|
37
|
+
}
|
|
38
|
+
if (isMethodCall("_constant")) {
|
|
39
|
+
path.node.arguments = [t.numericLiteral(resolvedValues[replaced++]), t.numericLiteral(resolvedValues[replaced++])];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
ok(replaced === resolvedValues.length, `Expected to replace ${resolvedValues.length} operands, but replaced ${replaced}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Append a generated switch case for every entry in compiler.SPECIALIZED_OPS.
|
|
47
|
+
// Each case is a clone of the original opcode’s handler with `this._operand()`
|
|
48
|
+
// replaced by the constant integer that was captured at compile time.
|
|
49
|
+
// Must be called AFTER applyMacroOpcodes (so the original cases exist) but
|
|
50
|
+
// BEFORE applyShuffleOpcodes so the new specialized cases also get shuffled.
|
|
51
|
+
export function applySpecializedOpcodes(ast, bytecode, compiler) {
|
|
52
|
+
let switchStatement = null;
|
|
53
|
+
traverse(ast, {
|
|
54
|
+
SwitchStatement(path) {
|
|
55
|
+
if (path.node.leadingComments?.some(c => c.value.includes("@SWITCH"))) {
|
|
56
|
+
switchStatement = path.node;
|
|
57
|
+
path.stop();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
ok(switchStatement, "Could not find @SWITCH statement for specialized opcodes");
|
|
62
|
+
|
|
63
|
+
// Build a map opName → SwitchCase from the existing OP.xxx case tests.
|
|
64
|
+
const nameToCaseMap = new Map();
|
|
65
|
+
for (const sc of switchStatement.cases) {
|
|
66
|
+
const test = sc.test;
|
|
67
|
+
if (test && t.isMemberExpression(test) && t.isIdentifier(test.object, {
|
|
68
|
+
name: "OP"
|
|
69
|
+
}) && t.isIdentifier(test.property)) {
|
|
70
|
+
nameToCaseMap.set(test.property.name, sc);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (!compiler.SPECIALIZED_OPS) return;
|
|
74
|
+
for (const [specialOpStr, info] of Object.entries(compiler.SPECIALIZED_OPS)) {
|
|
75
|
+
const specialOpCode = Number(specialOpStr);
|
|
76
|
+
const {
|
|
77
|
+
originalOp,
|
|
78
|
+
operands
|
|
79
|
+
} = info;
|
|
80
|
+
const newName = compiler.OP_NAME[specialOpCode];
|
|
81
|
+
const originalName = compiler.OP_NAME[originalOp];
|
|
82
|
+
if (!originalName) continue;
|
|
83
|
+
const originalCase = nameToCaseMap.get(originalName);
|
|
84
|
+
if (!originalCase) continue;
|
|
85
|
+
|
|
86
|
+
// Clone the original handler body
|
|
87
|
+
const bodyStmts = extractCaseBody(originalCase).map(s => t.cloneNode(s, true));
|
|
88
|
+
const placedOperands = info.resolvedOperands;
|
|
89
|
+
ok(placedOperands, `Could not find operand for original opcode ${newName}`);
|
|
90
|
+
const resolvedValues = placedOperands.map(placedOperand => {
|
|
91
|
+
return placedOperand?.resolvedValue ?? placedOperand;
|
|
92
|
+
});
|
|
93
|
+
ok(!resolvedValues.find(v => typeof v !== "number"), "Expected a numeric operand value");
|
|
94
|
+
|
|
95
|
+
// Replace this._operand() with the baked-in constant
|
|
96
|
+
inlineFixedOperands(bodyStmts, resolvedValues);
|
|
97
|
+
|
|
98
|
+
// Add a leading comment so the generated source stays readable
|
|
99
|
+
if (bodyStmts.length > 0) {
|
|
100
|
+
t.addComment(bodyStmts[0], "leading", ` ${compiler.OP_NAME[specialOpCode]} (specialized)`, true);
|
|
101
|
+
}
|
|
102
|
+
bodyStmts.push(t.breakStatement());
|
|
103
|
+
|
|
104
|
+
// Insert the new specialized case into the big switch
|
|
105
|
+
switchStatement.cases.push(t.switchCase(t.numericLiteral(specialOpCode), [t.blockStatement(bodyStmts)]));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -1,26 +1,25 @@
|
|
|
1
|
-
import { getRandomInt } from "./random-utils.
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
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(usedOpcodes) {
|
|
6
|
+
if (usedOpcodes.size > U16_MAX) return -1;
|
|
7
|
+
let attempts = 0;
|
|
8
|
+
while (attempts++ < 512) {
|
|
9
|
+
const candidate = getRandomInt(0, U16_MAX);
|
|
10
|
+
if (!usedOpcodes.has(candidate)) {
|
|
11
|
+
usedOpcodes.add(candidate);
|
|
12
|
+
return candidate;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
// Fallback: linear scan from a random start
|
|
16
|
+
const start = getRandomInt(0, U16_MAX);
|
|
17
|
+
for (let i = 0; i <= U16_MAX; i++) {
|
|
18
|
+
const v = start + i & U16_MAX;
|
|
19
|
+
if (!usedOpcodes.has(v)) {
|
|
20
|
+
usedOpcodes.add(v);
|
|
21
|
+
return v;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return -1;
|
|
25
|
+
}
|
|
@@ -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/dist/types.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Bytecode supports both real instructions and IR pseudo-instructions
|
|
2
|
+
// Real instruction: [OP.ADD, 5] or multi-operand: [OP.MAKE_CLOSURE, labelRef, 2, 3, 0]
|
|
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
|
+
// Instructions may carry any number of operands; the flat output serializes
|
|
8
|
+
// each operand as a separate u16 slot in the bytecode array.
|
|
9
|
+
|
|
10
|
+
export function constantOperand(value) {
|
|
11
|
+
return {
|
|
12
|
+
type: "constant",
|
|
13
|
+
value: value
|
|
14
|
+
};
|
|
15
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
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(usedOpcodes) {
|
|
6
|
+
if (usedOpcodes.size > U16_MAX) return -1;
|
|
7
|
+
let attempts = 0;
|
|
8
|
+
while (attempts++ < 512) {
|
|
9
|
+
const candidate = getRandomInt(0, U16_MAX);
|
|
10
|
+
if (!usedOpcodes.has(candidate)) {
|
|
11
|
+
usedOpcodes.add(candidate);
|
|
12
|
+
return candidate;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
// Fallback: linear scan from a random start
|
|
16
|
+
const start = getRandomInt(0, U16_MAX);
|
|
17
|
+
for (let i = 0; i <= U16_MAX; i++) {
|
|
18
|
+
const v = start + i & U16_MAX;
|
|
19
|
+
if (!usedOpcodes.has(v)) {
|
|
20
|
+
usedOpcodes.add(v);
|
|
21
|
+
return v;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return -1;
|
|
25
|
+
}
|
|
26
|
+
export function getInstructionSize(instr) {
|
|
27
|
+
const size = instr.filter(op => op?.placeholder !== true).length;
|
|
28
|
+
return size;
|
|
29
|
+
}
|
|
@@ -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/dist/utilts.js
ADDED
package/index.ts
CHANGED
|
@@ -9,14 +9,16 @@ async function main() {
|
|
|
9
9
|
|
|
10
10
|
const { code: output } = await JsConfuserVM.obfuscate(sourceCode, {
|
|
11
11
|
target: "browser", // or "node"
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
+
aliasedOpcodes: true, // create duplicate opcodes for commonly used opcodes?
|
|
19
|
+
timingChecks: true, // add timing checks to detect debuggers?
|
|
20
|
+
minify: true, // pass final output through Google Closure Compiler? (Renames VM class properties)
|
|
21
|
+
concealConstants: true,
|
|
20
22
|
});
|
|
21
23
|
|
|
22
24
|
writeFileSync("output.original.js", orginalOutput, "utf-8");
|