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,191 @@
|
|
|
1
|
+
import * as t from "@babel/types";
|
|
2
|
+
import traverseImport from "@babel/traverse";
|
|
3
|
+
import { ok } from "assert";
|
|
4
|
+
import { Compiler } from "../../compiler.ts";
|
|
5
|
+
|
|
6
|
+
const traverse = (traverseImport.default ||
|
|
7
|
+
traverseImport) as typeof traverseImport.default;
|
|
8
|
+
|
|
9
|
+
// Extract the real statement list from a SwitchCase consequent.
|
|
10
|
+
function extractCaseBody(switchCase: t.SwitchCase): t.Statement[] {
|
|
11
|
+
let stmts: t.Statement[];
|
|
12
|
+
if (
|
|
13
|
+
switchCase.consequent.length === 1 &&
|
|
14
|
+
t.isBlockStatement(switchCase.consequent[0])
|
|
15
|
+
) {
|
|
16
|
+
stmts = (switchCase.consequent[0] as t.BlockStatement).body;
|
|
17
|
+
} else {
|
|
18
|
+
stmts = switchCase.consequent as t.Statement[];
|
|
19
|
+
}
|
|
20
|
+
return stmts.filter((s) => !t.isBreakStatement(s) && !t.isEmptyStatement(s));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Replace every `this._operand()` call in bodyStmts with `_operands[i]`
|
|
24
|
+
// where i is the call's sequential index (0-based).
|
|
25
|
+
// Returns the number of replacements performed.
|
|
26
|
+
function replaceOperandCalls(bodyStmts: t.Statement[]): number {
|
|
27
|
+
let replaced = 0;
|
|
28
|
+
|
|
29
|
+
traverse(t.blockStatement(bodyStmts), {
|
|
30
|
+
noScope: true,
|
|
31
|
+
CallExpression(path) {
|
|
32
|
+
const callee = path.node.callee;
|
|
33
|
+
|
|
34
|
+
const isMethodCall = (methodName) => {
|
|
35
|
+
return (
|
|
36
|
+
t.isMemberExpression(callee) &&
|
|
37
|
+
t.isThisExpression(callee.object) &&
|
|
38
|
+
t.isIdentifier(callee.property, { name: methodName }) &&
|
|
39
|
+
path.node.arguments.length === 0
|
|
40
|
+
);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// Replace with _operands[i]
|
|
44
|
+
const createOperandAccess = () => {
|
|
45
|
+
return t.memberExpression(
|
|
46
|
+
t.identifier("_operands"),
|
|
47
|
+
t.numericLiteral(replaced++),
|
|
48
|
+
true, // computed
|
|
49
|
+
);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
if (isMethodCall("_operand")) {
|
|
53
|
+
path.replaceWith(createOperandAccess());
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (isMethodCall("_constant")) {
|
|
57
|
+
path.node.arguments = [createOperandAccess(), createOperandAccess()];
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
return replaced;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Appends a generated switch case for every entry in compiler.ALIASED_OPS.
|
|
66
|
+
// Each alias case:
|
|
67
|
+
// 1. Reads all operands eagerly into `_unsortedOperands` (in the shuffled
|
|
68
|
+
// bytecode order) via sequential this._operand() calls.
|
|
69
|
+
// 2. Restores the original operand order into `_operands` using the INVERSE
|
|
70
|
+
// of the stored `order` permutation:
|
|
71
|
+
// inverseOrder[order[i]] = i
|
|
72
|
+
// _operands[j] = _unsortedOperands[inverseOrder[j]]
|
|
73
|
+
// This is necessary because the bytecode stored originalOperands[order[i]]
|
|
74
|
+
// at slot i, so recovering originalOperands[j] requires the inverse lookup.
|
|
75
|
+
// 3. Executes a clone of the original handler body where every
|
|
76
|
+
// this._operand() has been replaced by the corresponding `_operands[i]`.
|
|
77
|
+
//
|
|
78
|
+
// Must run AFTER applyMacroOpcodes / applySpecializedOpcodes (so original
|
|
79
|
+
// cases already exist) but BEFORE applyShuffleOpcodes (so the new alias
|
|
80
|
+
// cases are also shuffled into the handler order).
|
|
81
|
+
export function applyAliasedOpcodes(ast: t.File, compiler: Compiler): void {
|
|
82
|
+
if (!compiler.ALIASED_OPS || Object.keys(compiler.ALIASED_OPS).length === 0)
|
|
83
|
+
return;
|
|
84
|
+
|
|
85
|
+
let switchStatement: t.SwitchStatement | null = null;
|
|
86
|
+
traverse(ast, {
|
|
87
|
+
SwitchStatement(path) {
|
|
88
|
+
if (path.node.leadingComments?.some((c) => c.value.includes("@SWITCH"))) {
|
|
89
|
+
switchStatement = path.node;
|
|
90
|
+
path.stop();
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
ok(switchStatement, "Could not find @SWITCH statement for aliased opcodes");
|
|
96
|
+
|
|
97
|
+
// Build opName → SwitchCase map from existing OP.xxx case tests.
|
|
98
|
+
const nameToCaseMap = new Map<string, t.SwitchCase>();
|
|
99
|
+
for (const sc of (switchStatement as t.SwitchStatement).cases) {
|
|
100
|
+
const test = sc.test;
|
|
101
|
+
if (
|
|
102
|
+
test &&
|
|
103
|
+
t.isMemberExpression(test) &&
|
|
104
|
+
t.isIdentifier(test.object, { name: "OP" }) &&
|
|
105
|
+
t.isIdentifier(test.property)
|
|
106
|
+
) {
|
|
107
|
+
nameToCaseMap.set((test.property as t.Identifier).name, sc);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
for (const [aliasOpStr, info] of Object.entries(compiler.ALIASED_OPS)) {
|
|
112
|
+
const aliasOpCode = Number(aliasOpStr);
|
|
113
|
+
const { originalOp, order } = info;
|
|
114
|
+
const arity = order.length;
|
|
115
|
+
|
|
116
|
+
const originalName = compiler.OP_NAME[originalOp];
|
|
117
|
+
if (!originalName) continue;
|
|
118
|
+
|
|
119
|
+
const originalCase = nameToCaseMap.get(originalName);
|
|
120
|
+
if (!originalCase) continue;
|
|
121
|
+
|
|
122
|
+
// Clone the original handler body (deep clone so we don't mutate the source)
|
|
123
|
+
const bodyStmts: t.Statement[] = extractCaseBody(originalCase).map(
|
|
124
|
+
(s) => t.cloneNode(s, true) as t.Statement,
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
// Replace this._operand() calls with _operands[i]
|
|
128
|
+
const replaced = replaceOperandCalls(bodyStmts);
|
|
129
|
+
|
|
130
|
+
// If the handler has a different number of _operand() calls than our
|
|
131
|
+
// recorded arity, skip this alias (variable-operand handler guard).
|
|
132
|
+
if (replaced !== arity) continue;
|
|
133
|
+
|
|
134
|
+
// Build: var _unsortedOperands = [this._operand(), this._operand(), ...]
|
|
135
|
+
// Reads operands in the NEW (shuffled) bytecode order.
|
|
136
|
+
const unsortedInit = t.variableDeclaration("let", [
|
|
137
|
+
t.variableDeclarator(
|
|
138
|
+
t.identifier("_unsortedOperands"),
|
|
139
|
+
t.arrayExpression(
|
|
140
|
+
Array.from({ length: arity }, () =>
|
|
141
|
+
t.callExpression(
|
|
142
|
+
t.memberExpression(t.thisExpression(), t.identifier("_operand")),
|
|
143
|
+
[],
|
|
144
|
+
),
|
|
145
|
+
),
|
|
146
|
+
),
|
|
147
|
+
),
|
|
148
|
+
]);
|
|
149
|
+
|
|
150
|
+
// The inverse permutation maps original position j → unsorted index i,
|
|
151
|
+
// because the bytecode stored originalOperands[order[i]] at slot i.
|
|
152
|
+
// inverseOrder[j] = i means: original operand j lives at _unsortedOperands[i]
|
|
153
|
+
const inverseOrder = new Array<number>(arity);
|
|
154
|
+
for (let i = 0; i < arity; i++) {
|
|
155
|
+
inverseOrder[order[i]] = i;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Build: var _operands = [_unsortedOperands[inverseOrder[0]], ...]
|
|
159
|
+
// Restores the original operand order expected by the handler body.
|
|
160
|
+
const operandsInit = t.variableDeclaration("let", [
|
|
161
|
+
t.variableDeclarator(
|
|
162
|
+
t.identifier("_operands"),
|
|
163
|
+
t.arrayExpression(
|
|
164
|
+
inverseOrder.map((idx) =>
|
|
165
|
+
t.memberExpression(
|
|
166
|
+
t.identifier("_unsortedOperands"),
|
|
167
|
+
t.numericLiteral(idx),
|
|
168
|
+
true, // computed
|
|
169
|
+
),
|
|
170
|
+
),
|
|
171
|
+
),
|
|
172
|
+
),
|
|
173
|
+
]);
|
|
174
|
+
|
|
175
|
+
const allStmts: t.Statement[] = [unsortedInit, operandsInit, ...bodyStmts];
|
|
176
|
+
|
|
177
|
+
// Add a leading comment for readability in non-minified output
|
|
178
|
+
t.addComment(
|
|
179
|
+
allStmts[0],
|
|
180
|
+
"leading",
|
|
181
|
+
` ${compiler.OP_NAME[aliasOpCode]} (order: [${order.join(",")}])`,
|
|
182
|
+
true,
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
allStmts.push(t.breakStatement());
|
|
186
|
+
|
|
187
|
+
(switchStatement as t.SwitchStatement).cases.push(
|
|
188
|
+
t.switchCase(t.numericLiteral(aliasOpCode), [t.blockStatement(allStmts)]),
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
@@ -1,7 +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 { shuffle } from "
|
|
4
|
+
import { shuffle } from "../../utils/random-utils.ts";
|
|
5
5
|
const traverse = (traverseImport.default ||
|
|
6
6
|
traverseImport) as typeof traverseImport.default;
|
|
7
7
|
|
|
@@ -26,26 +26,45 @@ function extractCaseBody(switchCase: t.SwitchCase): t.Statement[] {
|
|
|
26
26
|
// Because specialized opcodes are only created for instructions that have
|
|
27
27
|
// *exactly one* numeric operand, every `_operand()` call inside the original
|
|
28
28
|
// handler is replaced by the constant value that was baked into the opcode.
|
|
29
|
-
function
|
|
29
|
+
function inlineFixedOperands(
|
|
30
30
|
bodyStmts: t.Statement[],
|
|
31
|
-
|
|
31
|
+
resolvedValues: number[],
|
|
32
32
|
): void {
|
|
33
33
|
// Wrap the statements in a temporary BlockStatement so traverse has a root.
|
|
34
34
|
// The replacement mutates the original statement objects in place.
|
|
35
|
+
var replaced = 0;
|
|
36
|
+
|
|
35
37
|
traverse(t.blockStatement(bodyStmts), {
|
|
36
38
|
noScope: true,
|
|
37
39
|
CallExpression(path) {
|
|
38
40
|
const callee = path.node.callee;
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
41
|
+
|
|
42
|
+
const isMethodCall = (methodName) => {
|
|
43
|
+
return (
|
|
44
|
+
t.isMemberExpression(callee) &&
|
|
45
|
+
t.isThisExpression(callee.object) &&
|
|
46
|
+
t.isIdentifier(callee.property, { name: methodName }) &&
|
|
47
|
+
path.node.arguments.length === 0
|
|
48
|
+
);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
if (isMethodCall("_operand")) {
|
|
52
|
+
path.replaceWith(t.numericLiteral(resolvedValues[replaced++]));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (isMethodCall("_constant")) {
|
|
56
|
+
path.node.arguments = [
|
|
57
|
+
t.numericLiteral(resolvedValues[replaced++]),
|
|
58
|
+
t.numericLiteral(resolvedValues[replaced++]),
|
|
59
|
+
];
|
|
46
60
|
}
|
|
47
61
|
},
|
|
48
62
|
});
|
|
63
|
+
|
|
64
|
+
ok(
|
|
65
|
+
replaced === resolvedValues.length,
|
|
66
|
+
`Expected to replace ${resolvedValues.length} operands, but replaced ${replaced}`,
|
|
67
|
+
);
|
|
49
68
|
}
|
|
50
69
|
|
|
51
70
|
// Append a generated switch case for every entry in compiler.SPECIALIZED_OPS.
|
|
@@ -91,10 +110,7 @@ export function applySpecializedOpcodes(
|
|
|
91
110
|
|
|
92
111
|
for (const [specialOpStr, info] of Object.entries(compiler.SPECIALIZED_OPS)) {
|
|
93
112
|
const specialOpCode = Number(specialOpStr);
|
|
94
|
-
const { originalOp,
|
|
95
|
-
originalOp: number;
|
|
96
|
-
operand: number;
|
|
97
|
-
};
|
|
113
|
+
const { originalOp, operands } = info;
|
|
98
114
|
|
|
99
115
|
const newName = compiler.OP_NAME[specialOpCode];
|
|
100
116
|
const originalName = compiler.OP_NAME[originalOp];
|
|
@@ -108,21 +124,20 @@ export function applySpecializedOpcodes(
|
|
|
108
124
|
(s) => t.cloneNode(s, true) as t.Statement,
|
|
109
125
|
);
|
|
110
126
|
|
|
111
|
-
const
|
|
127
|
+
const placedOperands = info.resolvedOperands;
|
|
128
|
+
ok(placedOperands, `Could not find operand for original opcode ${newName}`);
|
|
129
|
+
|
|
130
|
+
const resolvedValues = placedOperands.map((placedOperand) => {
|
|
131
|
+
return (placedOperand as any)?.resolvedValue ?? placedOperand;
|
|
132
|
+
});
|
|
133
|
+
|
|
112
134
|
ok(
|
|
113
|
-
|
|
114
|
-
|
|
135
|
+
!resolvedValues.find((v) => typeof v !== "number"),
|
|
136
|
+
"Expected a numeric operand value",
|
|
115
137
|
);
|
|
116
138
|
|
|
117
|
-
const resolvedValue =
|
|
118
|
-
(placedOperand as any)?.resolvedValue ?? placedOperand;
|
|
119
|
-
if (typeof resolvedValue !== "number") {
|
|
120
|
-
console.error(resolvedValue);
|
|
121
|
-
}
|
|
122
|
-
ok(typeof resolvedValue === "number", "Expected a numeric operand value");
|
|
123
|
-
|
|
124
139
|
// Replace this._operand() with the baked-in constant
|
|
125
|
-
|
|
140
|
+
inlineFixedOperands(bodyStmts, resolvedValues);
|
|
126
141
|
|
|
127
142
|
// Add a leading comment so the generated source stays readable
|
|
128
143
|
if (bodyStmts.length > 0) {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { getRandomInt } from "./random-utils.ts";
|
|
2
|
+
import * as b from "../types.ts";
|
|
3
|
+
|
|
4
|
+
export const U16_MAX = 0xffff; // bytecode operands are u16
|
|
5
|
+
|
|
6
|
+
/** Returns the next free opcode slot, or -1 when the space is exhausted. */
|
|
7
|
+
export function nextFreeSlot(usedOpcodes: Set<number>): number {
|
|
8
|
+
if (usedOpcodes.size > U16_MAX) return -1;
|
|
9
|
+
let attempts = 0;
|
|
10
|
+
while (attempts++ < 512) {
|
|
11
|
+
const candidate = getRandomInt(0, U16_MAX);
|
|
12
|
+
if (!usedOpcodes.has(candidate)) {
|
|
13
|
+
usedOpcodes.add(candidate);
|
|
14
|
+
return candidate;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
// Fallback: linear scan from a random start
|
|
18
|
+
const start = getRandomInt(0, U16_MAX);
|
|
19
|
+
for (let i = 0; i <= U16_MAX; i++) {
|
|
20
|
+
const v = (start + i) & U16_MAX;
|
|
21
|
+
if (!usedOpcodes.has(v)) {
|
|
22
|
+
usedOpcodes.add(v);
|
|
23
|
+
return v;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return -1;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function getInstructionSize(instr: b.Instruction): number {
|
|
30
|
+
const size = instr.filter((op) => (op as any)?.placeholder !== true).length;
|
|
31
|
+
|
|
32
|
+
return size;
|
|
33
|
+
}
|
|
File without changes
|