js-confuser-vm 0.0.6 → 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.
- package/CHANGELOG.md +55 -0
- package/README.MD +101 -37
- package/dist/build-runtime.js +15 -2
- package/dist/compiler.js +98 -51
- package/dist/runtime.js +5 -1
- package/dist/transforms/bytecode/aliasedOpcodes.js +2 -8
- package/dist/transforms/bytecode/macroOpcodes.js +21 -19
- package/dist/transforms/bytecode/microOpcodes.js +236 -0
- package/dist/transforms/bytecode/resolveContants.js +5 -11
- package/dist/transforms/bytecode/resolveLabels.js +5 -3
- package/dist/transforms/bytecode/specializedOpcodes.js +21 -16
- package/dist/transforms/runtime/internalVariables.js +202 -0
- package/dist/transforms/runtime/macroOpcodes.js +30 -18
- package/dist/transforms/runtime/microOpcodes.js +76 -0
- package/dist/transforms/runtime/specializedOpcodes.js +20 -18
- package/dist/utils/op-utils.js +15 -8
- package/index.ts +3 -2
- package/jest.config.js +2 -0
- package/package.json +1 -1
- package/src/build-runtime.ts +18 -3
- package/src/compiler.ts +152 -65
- package/src/options.ts +1 -0
- package/src/runtime.ts +5 -1
- package/src/transforms/bytecode/aliasedOpcodes.ts +2 -12
- package/src/transforms/bytecode/macroOpcodes.ts +28 -29
- package/src/transforms/bytecode/microOpcodes.ts +291 -0
- package/src/transforms/bytecode/resolveContants.ts +6 -13
- package/src/transforms/bytecode/resolveLabels.ts +5 -4
- package/src/transforms/bytecode/specializedOpcodes.ts +38 -28
- package/src/transforms/runtime/internalVariables.ts +270 -0
- package/src/transforms/runtime/macroOpcodes.ts +47 -20
- package/src/transforms/runtime/microOpcodes.ts +93 -0
- package/src/transforms/runtime/specializedOpcodes.ts +27 -32
- package/src/types.ts +1 -1
- package/src/utils/op-utils.ts +21 -8
- package/src/utilts.ts +0 -3
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import * as t from "@babel/types";
|
|
2
|
+
import traverseImport from "@babel/traverse";
|
|
3
|
+
import { ok } from "assert";
|
|
4
|
+
import { applyInternalVariablesToSwitchCase } from "./internalVariables.js";
|
|
5
|
+
const traverse = traverseImport.default || traverseImport;
|
|
6
|
+
|
|
7
|
+
// Extract the real statement list from a SwitchCase consequent.
|
|
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
|
+
// Append a generated switch case for every entry in compiler.MICRO_OPS.
|
|
19
|
+
// applyInteralVariablesToRuntime must run before this so that the source
|
|
20
|
+
// case bodies are already using this._internals[index] instead of local vars.
|
|
21
|
+
// Must be called BEFORE applyShuffleOpcodes so the new cases get shuffled.
|
|
22
|
+
export function applyMicroOpcodes(ast, compiler) {
|
|
23
|
+
if (!compiler.MICRO_OPS || Object.keys(compiler.MICRO_OPS).length === 0) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
let switchStatement = null;
|
|
27
|
+
traverse(ast, {
|
|
28
|
+
SwitchStatement(path) {
|
|
29
|
+
if (path.node.leadingComments?.some(c => c.value.includes("@SWITCH"))) {
|
|
30
|
+
switchStatement = path.node;
|
|
31
|
+
path.stop();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
ok(switchStatement, "Could not find @SWITCH statement for micro opcodes");
|
|
36
|
+
|
|
37
|
+
// Build opName → SwitchCase from existing cases.
|
|
38
|
+
const nameToCaseMap = new Map();
|
|
39
|
+
for (const sc of switchStatement.cases) {
|
|
40
|
+
const test = sc.test;
|
|
41
|
+
if (test && t.isMemberExpression(test) && t.isIdentifier(test.object, {
|
|
42
|
+
name: "OP"
|
|
43
|
+
}) && t.isIdentifier(test.property)) {
|
|
44
|
+
nameToCaseMap.set(test.property.name, sc);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
for (const [microOpStr, info] of Object.entries(compiler.MICRO_OPS)) {
|
|
48
|
+
const microOpCode = Number(microOpStr);
|
|
49
|
+
const {
|
|
50
|
+
originalOp,
|
|
51
|
+
stmtIndex
|
|
52
|
+
} = info;
|
|
53
|
+
const originalName = compiler.OP_NAME[originalOp];
|
|
54
|
+
if (!originalName) continue;
|
|
55
|
+
const originalCase = nameToCaseMap.get(originalName);
|
|
56
|
+
if (!originalCase) continue;
|
|
57
|
+
|
|
58
|
+
// Extract and clone all non-break statements from the original case body.
|
|
59
|
+
const allStmts = extractCaseBody(originalCase);
|
|
60
|
+
if (stmtIndex >= allStmts.length) continue;
|
|
61
|
+
const rawStmt = t.cloneNode(allStmts[stmtIndex], true);
|
|
62
|
+
const newCase = t.switchCase(t.numericLiteral(microOpCode), [t.blockStatement([rawStmt, t.breakStatement()])]);
|
|
63
|
+
|
|
64
|
+
// Apply internal-variable substitution — this may replace rawStmt in the
|
|
65
|
+
// block body (var decl → assignment), so add the comment afterwards on
|
|
66
|
+
// whatever the first statement of the block actually is.
|
|
67
|
+
applyInternalVariablesToSwitchCase(newCase, compiler, microOpCode);
|
|
68
|
+
const blockBody = newCase.consequent[0].body;
|
|
69
|
+
const firstStmt = blockBody[0];
|
|
70
|
+
if (firstStmt) {
|
|
71
|
+
const microName = compiler.OP_NAME[microOpCode] ?? `MICRO_${microOpCode}`;
|
|
72
|
+
t.addComment(firstStmt, "leading", ` ${microName}`, true);
|
|
73
|
+
}
|
|
74
|
+
switchStatement.cases.push(newCase);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -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
|
|
@@ -23,6 +24,11 @@ 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.
|
|
25
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
|
+
}
|
|
26
32
|
traverse(t.blockStatement(bodyStmts), {
|
|
27
33
|
noScope: true,
|
|
28
34
|
CallExpression(path) {
|
|
@@ -33,10 +39,10 @@ function inlineFixedOperands(bodyStmts, resolvedValues) {
|
|
|
33
39
|
}) && path.node.arguments.length === 0;
|
|
34
40
|
};
|
|
35
41
|
if (isMethodCall("_operand")) {
|
|
36
|
-
path.replaceWith(
|
|
42
|
+
path.replaceWith(consumeOperand());
|
|
37
43
|
}
|
|
38
44
|
if (isMethodCall("_constant")) {
|
|
39
|
-
path.node.arguments = [
|
|
45
|
+
path.node.arguments = [consumeOperand(), consumeOperand()];
|
|
40
46
|
}
|
|
41
47
|
}
|
|
42
48
|
});
|
|
@@ -48,7 +54,7 @@ function inlineFixedOperands(bodyStmts, resolvedValues) {
|
|
|
48
54
|
// replaced by the constant integer that was captured at compile time.
|
|
49
55
|
// Must be called AFTER applyMacroOpcodes (so the original cases exist) but
|
|
50
56
|
// BEFORE applyShuffleOpcodes so the new specialized cases also get shuffled.
|
|
51
|
-
export function applySpecializedOpcodes(ast,
|
|
57
|
+
export function applySpecializedOpcodes(ast, compiler) {
|
|
52
58
|
let switchStatement = null;
|
|
53
59
|
traverse(ast, {
|
|
54
60
|
SwitchStatement(path) {
|
|
@@ -61,15 +67,7 @@ export function applySpecializedOpcodes(ast, bytecode, compiler) {
|
|
|
61
67
|
ok(switchStatement, "Could not find @SWITCH statement for specialized opcodes");
|
|
62
68
|
|
|
63
69
|
// Build a map opName → SwitchCase from the existing OP.xxx case tests.
|
|
64
|
-
const
|
|
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
|
-
}
|
|
70
|
+
const opcodeToCaseMap = getOpcodeToCaseMap(switchStatement, compiler);
|
|
73
71
|
if (!compiler.SPECIALIZED_OPS) return;
|
|
74
72
|
for (const [specialOpStr, info] of Object.entries(compiler.SPECIALIZED_OPS)) {
|
|
75
73
|
const specialOpCode = Number(specialOpStr);
|
|
@@ -77,20 +75,24 @@ export function applySpecializedOpcodes(ast, bytecode, compiler) {
|
|
|
77
75
|
originalOp,
|
|
78
76
|
operands
|
|
79
77
|
} = info;
|
|
80
|
-
|
|
78
|
+
let newName = compiler.OP_NAME[specialOpCode];
|
|
81
79
|
const originalName = compiler.OP_NAME[originalOp];
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
if (!originalCase) continue;
|
|
80
|
+
const originalCase = opcodeToCaseMap.get(originalOp);
|
|
81
|
+
ok(originalCase, `Could not find original case for opcode ${originalName} (${originalOp})`);
|
|
85
82
|
|
|
86
83
|
// Clone the original handler body
|
|
87
84
|
const bodyStmts = extractCaseBody(originalCase).map(s => t.cloneNode(s, true));
|
|
88
|
-
const placedOperands = info.
|
|
85
|
+
const placedOperands = info.operands;
|
|
89
86
|
ok(placedOperands, `Could not find operand for original opcode ${newName}`);
|
|
90
87
|
const resolvedValues = placedOperands.map(placedOperand => {
|
|
91
88
|
return placedOperand?.resolvedValue ?? placedOperand;
|
|
92
89
|
});
|
|
93
|
-
|
|
90
|
+
if (resolvedValues.find(v => typeof v !== "number")) {
|
|
91
|
+
console.error(info);
|
|
92
|
+
throw new Error("Expected all resolved operand values to be numbers");
|
|
93
|
+
}
|
|
94
|
+
newName = `${originalName}_${resolvedValues.join("_")}`;
|
|
95
|
+
compiler.OP_NAME[specialOpCode] = newName;
|
|
94
96
|
|
|
95
97
|
// Replace this._operand() with the baked-in constant
|
|
96
98
|
inlineFixedOperands(bodyStmts, resolvedValues);
|
package/dist/utils/op-utils.js
CHANGED
|
@@ -2,18 +2,25 @@ import { getRandomInt } from "./random-utils.js";
|
|
|
2
2
|
export const U16_MAX = 0xffff; // bytecode operands are u16
|
|
3
3
|
|
|
4
4
|
/** Returns the next free opcode slot, or -1 when the space is exhausted. */
|
|
5
|
-
export function nextFreeSlot(
|
|
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)));
|
|
6
8
|
if (usedOpcodes.size > U16_MAX) return -1;
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
+
}
|
|
13
19
|
}
|
|
14
20
|
}
|
|
21
|
+
|
|
15
22
|
// Fallback: linear scan from a random start
|
|
16
|
-
const start =
|
|
23
|
+
const start = Object.keys(compiler.OP_NAME).length;
|
|
17
24
|
for (let i = 0; i <= U16_MAX; i++) {
|
|
18
25
|
const v = start + i & U16_MAX;
|
|
19
26
|
if (!usedOpcodes.has(v)) {
|
package/index.ts
CHANGED
|
@@ -11,14 +11,15 @@ async function main() {
|
|
|
11
11
|
target: "browser", // or "node"
|
|
12
12
|
randomizeOpcodes: true, // randomize the opcode numbers?
|
|
13
13
|
shuffleOpcodes: true, // shuffle order of opcode handlers in the runtime?
|
|
14
|
-
encodeBytecode: true, // encode bytecode?
|
|
14
|
+
encodeBytecode: true, // encode the bytecode array?
|
|
15
|
+
concealConstants: true, // conceal strings and integers in the constant pool?
|
|
15
16
|
selfModifying: true, // do self-modifying bytecode for function bodies?
|
|
16
17
|
macroOpcodes: true, // create combined opcodes for repeated instruction sequences?
|
|
18
|
+
microOpcodes: true, // break opcodes into sub-opcodes?
|
|
17
19
|
specializedOpcodes: true, // create specialized opcodes for commonly used opcode+operand pairs?
|
|
18
20
|
aliasedOpcodes: true, // create duplicate opcodes for commonly used opcodes?
|
|
19
21
|
timingChecks: true, // add timing checks to detect debuggers?
|
|
20
22
|
minify: true, // pass final output through Google Closure Compiler? (Renames VM class properties)
|
|
21
|
-
concealConstants: true,
|
|
22
23
|
});
|
|
23
24
|
|
|
24
25
|
writeFileSync("output.original.js", orginalOutput, "utf-8");
|
package/jest.config.js
CHANGED
|
@@ -6,6 +6,7 @@ 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 },
|
|
@@ -27,6 +28,7 @@ const OPTIONS_MATRIX = [
|
|
|
27
28
|
selfModifying: true,
|
|
28
29
|
timingChecks: true,
|
|
29
30
|
macroOpcodes: true,
|
|
31
|
+
microOpcodes: true,
|
|
30
32
|
specializedOpcodes: true,
|
|
31
33
|
aliasedOpcodes: true,
|
|
32
34
|
concealConstants: true,
|
package/package.json
CHANGED
package/src/build-runtime.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { generate } from "@babel/generator";
|
|
2
|
-
import { parse
|
|
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";
|
|
@@ -14,7 +16,8 @@ export async function obfuscateRuntime(
|
|
|
14
16
|
runtime: string,
|
|
15
17
|
bytecode: b.Bytecode,
|
|
16
18
|
options: Options,
|
|
17
|
-
compiler
|
|
19
|
+
compiler: Compiler,
|
|
20
|
+
generateBytecodeComment,
|
|
18
21
|
) {
|
|
19
22
|
let ast: t.File;
|
|
20
23
|
try {
|
|
@@ -25,7 +28,16 @@ export async function obfuscateRuntime(
|
|
|
25
28
|
|
|
26
29
|
// Specialized opcode cases must be applied BEFORE shuffleOpcodes
|
|
27
30
|
if (options.specializedOpcodes) {
|
|
28
|
-
applySpecializedOpcodes(ast,
|
|
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);
|
|
29
41
|
}
|
|
30
42
|
|
|
31
43
|
// Macro opcode cases must be applied BEFORE shuffleOpcodes
|
|
@@ -50,6 +62,9 @@ export async function obfuscateRuntime(
|
|
|
50
62
|
throw new Error("VM-Runtime final generation failed", { cause: error });
|
|
51
63
|
}
|
|
52
64
|
|
|
65
|
+
// Add comment here for more accurate opcode names
|
|
66
|
+
generated = generateBytecodeComment() + "\n" + generated;
|
|
67
|
+
|
|
53
68
|
// Minify code?
|
|
54
69
|
if (options.minify) {
|
|
55
70
|
try {
|
package/src/compiler.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
+
import * as t from "@babel/types";
|
|
2
|
+
import * as b from "./types.ts";
|
|
1
3
|
import { parse } from "@babel/parser";
|
|
2
4
|
import traverseImport from "@babel/traverse";
|
|
3
5
|
import { generate } from "@babel/generator";
|
|
4
|
-
|
|
5
|
-
import { readFileSync } from "fs";
|
|
6
6
|
import { join } from "path";
|
|
7
|
+
import { readFileSync } from "fs";
|
|
7
8
|
import { stripTypeScriptTypes } from "module";
|
|
8
|
-
import * as t from "@babel/types";
|
|
9
9
|
import { ok } from "assert";
|
|
10
10
|
import { obfuscateRuntime } from "./build-runtime.ts";
|
|
11
11
|
import { DEFAULT_OPTIONS, type Options } from "./options.ts";
|
|
@@ -13,7 +13,7 @@ import { resolveLabels } from "./transforms/bytecode/resolveLabels.ts";
|
|
|
13
13
|
import { resolveConstants } from "./transforms/bytecode/resolveContants.ts";
|
|
14
14
|
import { selfModifying } from "./transforms/bytecode/selfModifying.ts";
|
|
15
15
|
import { macroOpcodes } from "./transforms/bytecode/macroOpcodes.ts";
|
|
16
|
-
import
|
|
16
|
+
import { microOpcodes } from "./transforms/bytecode/microOpcodes.ts";
|
|
17
17
|
import { specializedOpcodes } from "./transforms/bytecode/specializedOpcodes.ts";
|
|
18
18
|
import { aliasedOpcodes } from "./transforms/bytecode/aliasedOpcodes.ts";
|
|
19
19
|
import { getRandomInt } from "./utils/random-utils.ts";
|
|
@@ -264,10 +264,21 @@ export class Compiler {
|
|
|
264
264
|
{
|
|
265
265
|
originalOp: number;
|
|
266
266
|
operands: b.InstrOperand[];
|
|
267
|
-
resolvedOperands?: b.InstrOperand[];
|
|
268
267
|
}
|
|
269
268
|
>;
|
|
270
269
|
ALIASED_OPS: Record<number, { originalOp: number; order: number[] }>;
|
|
270
|
+
MICRO_OPS: Record<
|
|
271
|
+
number,
|
|
272
|
+
{ originalOp: number; stmtIndex: number; irOperandCount: number }
|
|
273
|
+
>;
|
|
274
|
+
|
|
275
|
+
/** Internal variable slot registry.
|
|
276
|
+
* globally: shared name→index pool (written on first sight; reused by non-random mode or by 50% chance in random mode).
|
|
277
|
+
* opcodes: per-opcode source-of-truth — all assignment lookups are read/written here. */
|
|
278
|
+
_internals: {
|
|
279
|
+
globally: Map<string, number>;
|
|
280
|
+
opcodes: Map<number, Map<string, number>>;
|
|
281
|
+
};
|
|
271
282
|
|
|
272
283
|
OP_NAME: Record<number, string>;
|
|
273
284
|
JUMP_OPS: Set<number>;
|
|
@@ -293,8 +304,10 @@ export class Compiler {
|
|
|
293
304
|
|
|
294
305
|
this.serializer = new Serializer(this);
|
|
295
306
|
this.MACRO_OPS = {};
|
|
307
|
+
this.MICRO_OPS = {};
|
|
296
308
|
this.SPECIALIZED_OPS = {};
|
|
297
309
|
this.ALIASED_OPS = {};
|
|
310
|
+
this._internals = { globally: new Map(), opcodes: new Map() };
|
|
298
311
|
|
|
299
312
|
this.OP = { ...OP_ORIGINAL };
|
|
300
313
|
|
|
@@ -455,14 +468,6 @@ export class Compiler {
|
|
|
455
468
|
}
|
|
456
469
|
|
|
457
470
|
compileAST(ast: t.File) {
|
|
458
|
-
traverse(ast, {
|
|
459
|
-
FunctionDeclaration: (path) => {
|
|
460
|
-
if (path.parent.type !== "Program") return;
|
|
461
|
-
this._compileFunctionDecl(path.node);
|
|
462
|
-
path.skip();
|
|
463
|
-
},
|
|
464
|
-
});
|
|
465
|
-
|
|
466
471
|
this._compileMain(ast.program.body);
|
|
467
472
|
return this.bytecode;
|
|
468
473
|
}
|
|
@@ -1291,14 +1296,10 @@ export class Compiler {
|
|
|
1291
1296
|
|
|
1292
1297
|
case "TryStatement": {
|
|
1293
1298
|
if (node.finalizer) {
|
|
1294
|
-
throw new Error(
|
|
1295
|
-
"try..finally is not supported. Use a helper function instead",
|
|
1296
|
-
);
|
|
1299
|
+
throw new Error("try..finally is not supported");
|
|
1297
1300
|
}
|
|
1298
1301
|
if (!node.handler) {
|
|
1299
|
-
throw new Error(
|
|
1300
|
-
"try without catch is not supported (requires finally).",
|
|
1301
|
-
);
|
|
1302
|
+
throw new Error("try without catch is not supported");
|
|
1302
1303
|
}
|
|
1303
1304
|
|
|
1304
1305
|
const catchLabel = this._makeLabel("catch");
|
|
@@ -1538,6 +1539,45 @@ export class Compiler {
|
|
|
1538
1539
|
return reg_result;
|
|
1539
1540
|
}
|
|
1540
1541
|
|
|
1542
|
+
case "TemplateLiteral": {
|
|
1543
|
+
const n = node as t.TemplateLiteral;
|
|
1544
|
+
// Fold: quasi[0] + expr[0] + quasi[1] + ... + quasi[last]
|
|
1545
|
+
let acc = ctx.allocReg();
|
|
1546
|
+
this.emit(
|
|
1547
|
+
bc,
|
|
1548
|
+
[
|
|
1549
|
+
this.OP.LOAD_CONST,
|
|
1550
|
+
acc,
|
|
1551
|
+
b.constantOperand(n.quasis[0].value.cooked ?? ""),
|
|
1552
|
+
],
|
|
1553
|
+
node,
|
|
1554
|
+
);
|
|
1555
|
+
for (let i = 0; i < n.expressions.length; i++) {
|
|
1556
|
+
const exprReg = this._compileExpr(
|
|
1557
|
+
n.expressions[i] as t.Expression,
|
|
1558
|
+
scope,
|
|
1559
|
+
bc,
|
|
1560
|
+
);
|
|
1561
|
+
const t1 = ctx.allocReg();
|
|
1562
|
+
this.emit(bc, [this.OP.ADD, t1, acc, exprReg], node);
|
|
1563
|
+
acc = t1;
|
|
1564
|
+
const quasiReg = ctx.allocReg();
|
|
1565
|
+
this.emit(
|
|
1566
|
+
bc,
|
|
1567
|
+
[
|
|
1568
|
+
this.OP.LOAD_CONST,
|
|
1569
|
+
quasiReg,
|
|
1570
|
+
b.constantOperand(n.quasis[i + 1].value.cooked ?? ""),
|
|
1571
|
+
],
|
|
1572
|
+
node,
|
|
1573
|
+
);
|
|
1574
|
+
const t2 = ctx.allocReg();
|
|
1575
|
+
this.emit(bc, [this.OP.ADD, t2, acc, quasiReg], node);
|
|
1576
|
+
acc = t2;
|
|
1577
|
+
}
|
|
1578
|
+
return acc;
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1541
1581
|
case "BinaryExpression": {
|
|
1542
1582
|
const n = node as t.BinaryExpression;
|
|
1543
1583
|
const lhsReg = this._compileExpr(n.left as t.Expression, scope, bc);
|
|
@@ -1579,15 +1619,68 @@ export class Compiler {
|
|
|
1579
1619
|
|
|
1580
1620
|
case "UpdateExpression": {
|
|
1581
1621
|
const n = node as t.UpdateExpression;
|
|
1622
|
+
const bumpOp = n.operator === "++" ? this.OP.ADD : this.OP.SUB;
|
|
1623
|
+
|
|
1624
|
+
// Shared: compute curReg +/- 1 into newReg, return [postfixResult, newReg]
|
|
1625
|
+
const applyBump = (curReg: number): [number, number] => {
|
|
1626
|
+
const postfixReg = n.prefix
|
|
1627
|
+
? -1
|
|
1628
|
+
: (() => {
|
|
1629
|
+
const r = ctx.allocReg();
|
|
1630
|
+
this.emit(bc, [this.OP.MOVE, r, curReg], node as t.Node);
|
|
1631
|
+
return r;
|
|
1632
|
+
})();
|
|
1633
|
+
const oneReg = ctx.allocReg();
|
|
1634
|
+
this.emit(
|
|
1635
|
+
bc,
|
|
1636
|
+
[this.OP.LOAD_CONST, oneReg, b.constantOperand(1)],
|
|
1637
|
+
node as t.Node,
|
|
1638
|
+
);
|
|
1639
|
+
const newReg = ctx.allocReg();
|
|
1640
|
+
this.emit(bc, [bumpOp, newReg, curReg, oneReg], node as t.Node);
|
|
1641
|
+
return [postfixReg, newReg];
|
|
1642
|
+
};
|
|
1643
|
+
|
|
1644
|
+
if (n.argument.type === "MemberExpression") {
|
|
1645
|
+
const mem = n.argument as t.MemberExpression;
|
|
1646
|
+
const objReg = this._compileExpr(mem.object, scope, bc);
|
|
1647
|
+
let keyReg: number;
|
|
1648
|
+
if (mem.computed) {
|
|
1649
|
+
keyReg = this._compileExpr(mem.property as t.Expression, scope, bc);
|
|
1650
|
+
} else {
|
|
1651
|
+
keyReg = ctx.allocReg();
|
|
1652
|
+
this.emit(
|
|
1653
|
+
bc,
|
|
1654
|
+
[
|
|
1655
|
+
this.OP.LOAD_CONST,
|
|
1656
|
+
keyReg,
|
|
1657
|
+
b.constantOperand((mem.property as t.Identifier).name),
|
|
1658
|
+
],
|
|
1659
|
+
node as t.Node,
|
|
1660
|
+
);
|
|
1661
|
+
}
|
|
1662
|
+
const curReg = ctx.allocReg();
|
|
1663
|
+
this.emit(
|
|
1664
|
+
bc,
|
|
1665
|
+
[this.OP.GET_PROP, curReg, objReg, keyReg],
|
|
1666
|
+
node as t.Node,
|
|
1667
|
+
);
|
|
1668
|
+
const [postfixReg, newReg] = applyBump(curReg);
|
|
1669
|
+
this.emit(
|
|
1670
|
+
bc,
|
|
1671
|
+
[this.OP.SET_PROP, objReg, keyReg, newReg],
|
|
1672
|
+
node as t.Node,
|
|
1673
|
+
);
|
|
1674
|
+
return n.prefix ? newReg : postfixReg;
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1582
1677
|
ok(
|
|
1583
1678
|
n.argument.type === "Identifier",
|
|
1584
|
-
"UpdateExpression requires identifier",
|
|
1679
|
+
"UpdateExpression requires identifier or member expression",
|
|
1585
1680
|
);
|
|
1586
1681
|
const name = (n.argument as t.Identifier).name;
|
|
1587
1682
|
const res = this._resolve(name, this._currentCtx);
|
|
1588
|
-
const bumpOp = n.operator === "++" ? this.OP.ADD : this.OP.SUB;
|
|
1589
1683
|
|
|
1590
|
-
// Load current value into a register (locals are already in place)
|
|
1591
1684
|
let curReg: number;
|
|
1592
1685
|
if (res.kind === "local") {
|
|
1593
1686
|
curReg = res.slot;
|
|
@@ -1607,27 +1700,8 @@ export class Compiler {
|
|
|
1607
1700
|
);
|
|
1608
1701
|
}
|
|
1609
1702
|
|
|
1610
|
-
|
|
1611
|
-
let resultReg: number;
|
|
1612
|
-
if (!n.prefix) {
|
|
1613
|
-
resultReg = ctx.allocReg();
|
|
1614
|
-
this.emit(bc, [this.OP.MOVE, resultReg, curReg], node as t.Node);
|
|
1615
|
-
} else {
|
|
1616
|
-
resultReg = -1; // placeholder – will become newReg below
|
|
1617
|
-
}
|
|
1618
|
-
|
|
1619
|
-
const oneReg = ctx.allocReg();
|
|
1620
|
-
this.emit(
|
|
1621
|
-
bc,
|
|
1622
|
-
[this.OP.LOAD_CONST, oneReg, b.constantOperand(1)],
|
|
1623
|
-
node as t.Node,
|
|
1624
|
-
);
|
|
1625
|
-
|
|
1626
|
-
// Compute new value (always into a fresh register)
|
|
1627
|
-
const newReg = ctx.allocReg();
|
|
1628
|
-
this.emit(bc, [bumpOp, newReg, curReg, oneReg], node as t.Node);
|
|
1703
|
+
const [postfixReg, newReg] = applyBump(curReg);
|
|
1629
1704
|
|
|
1630
|
-
// Write the new value back (local = MOVE, others = STORE_xxx)
|
|
1631
1705
|
if (res.kind === "local") {
|
|
1632
1706
|
this.emit(bc, [this.OP.MOVE, res.slot, newReg], node as t.Node);
|
|
1633
1707
|
} else if (res.kind === "upvalue") {
|
|
@@ -1644,12 +1718,7 @@ export class Compiler {
|
|
|
1644
1718
|
);
|
|
1645
1719
|
}
|
|
1646
1720
|
|
|
1647
|
-
|
|
1648
|
-
if (n.prefix) {
|
|
1649
|
-
resultReg = newReg;
|
|
1650
|
-
}
|
|
1651
|
-
|
|
1652
|
-
return resultReg;
|
|
1721
|
+
return n.prefix ? newReg : postfixReg;
|
|
1653
1722
|
}
|
|
1654
1723
|
|
|
1655
1724
|
case "AssignmentExpression": {
|
|
@@ -2138,13 +2207,12 @@ class Serializer {
|
|
|
2138
2207
|
return out;
|
|
2139
2208
|
}
|
|
2140
2209
|
|
|
2141
|
-
_serializeInstr(
|
|
2142
|
-
instr: b.Instruction,
|
|
2143
|
-
constants: any[],
|
|
2144
|
-
): { text: string; values: number[] } {
|
|
2210
|
+
_serializeInstr(instr: b.Instruction): { text: string; values: number[] } {
|
|
2145
2211
|
const op = instr[0] as number;
|
|
2146
2212
|
const operands = instr.slice(1) as number[];
|
|
2147
2213
|
|
|
2214
|
+
const constants = this.compiler.constants;
|
|
2215
|
+
|
|
2148
2216
|
const resolvedOperands = operands
|
|
2149
2217
|
.filter((operand) => (operand as any)?.placeholder !== true)
|
|
2150
2218
|
.map((o) => (o as any)?.resolvedValue ?? o);
|
|
@@ -2155,7 +2223,11 @@ class Serializer {
|
|
|
2155
2223
|
}
|
|
2156
2224
|
ok(op >= 0 && op <= 0xffff, `Opcode overflow (max 0xFFFF u16): ${op}`);
|
|
2157
2225
|
|
|
2158
|
-
|
|
2226
|
+
let name = this.OP_NAME[op];
|
|
2227
|
+
if (!name || name.includes("{")) {
|
|
2228
|
+
name = `OP_${op}`;
|
|
2229
|
+
}
|
|
2230
|
+
|
|
2159
2231
|
let comment = name;
|
|
2160
2232
|
|
|
2161
2233
|
function formatLoc(loc: t.Node["loc"]["start"]) {
|
|
@@ -2205,10 +2277,10 @@ class Serializer {
|
|
|
2205
2277
|
comment += ` reg[${dst}] PC=${resolvedOperands[1]} (params=${resolvedOperands[2]} regs=${resolvedOperands[3]} upvalues=${resolvedOperands[4]})`;
|
|
2206
2278
|
break;
|
|
2207
2279
|
case this.OP.CALL:
|
|
2208
|
-
comment += ` reg[${dst}] =
|
|
2280
|
+
comment += ` reg[${dst}] = reg[${resolvedOperands[1]}](${resolvedOperands[2]} args)`;
|
|
2209
2281
|
break;
|
|
2210
2282
|
case this.OP.CALL_METHOD:
|
|
2211
|
-
comment += ` reg[${dst}] =
|
|
2283
|
+
comment += ` reg[${dst}] = reg[${resolvedOperands[2]}](recv=reg[${resolvedOperands[1]}], ${resolvedOperands[3]} args)`;
|
|
2212
2284
|
break;
|
|
2213
2285
|
case this.OP.NEW:
|
|
2214
2286
|
comment += ` reg[${dst}] = new reg[${resolvedOperands[1]}](${resolvedOperands[2]} args)`;
|
|
@@ -2222,6 +2294,13 @@ class Serializer {
|
|
|
2222
2294
|
case this.OP.BUILD_OBJECT:
|
|
2223
2295
|
comment += ` reg[${dst}] = {${resolvedOperands[1]} pairs}`;
|
|
2224
2296
|
break;
|
|
2297
|
+
case this.OP.GET_PROP:
|
|
2298
|
+
comment += ` reg[${dst}] = reg[${resolvedOperands[1]}][reg[${resolvedOperands[2]}]]`;
|
|
2299
|
+
break;
|
|
2300
|
+
case this.OP.SET_PROP:
|
|
2301
|
+
comment += ` reg[${resolvedOperands[0]}][reg[${resolvedOperands[1]}]] = reg[${resolvedOperands[2]}]`;
|
|
2302
|
+
break;
|
|
2303
|
+
|
|
2225
2304
|
default:
|
|
2226
2305
|
comment +=
|
|
2227
2306
|
resolvedOperands.length === 1
|
|
@@ -2266,7 +2345,6 @@ class Serializer {
|
|
|
2266
2345
|
const originalName = compiler.OP_NAME[specializedOpInfo.originalOp];
|
|
2267
2346
|
compiler.OP_NAME[instr[0]] =
|
|
2268
2347
|
`${originalName}_${resolvedValues.join("_")}`;
|
|
2269
|
-
specializedOpInfo.resolvedOperands = operands;
|
|
2270
2348
|
}
|
|
2271
2349
|
|
|
2272
2350
|
serialized.push(instr);
|
|
@@ -2288,18 +2366,9 @@ class Serializer {
|
|
|
2288
2366
|
const mainRegCount = compiler.mainRegCount;
|
|
2289
2367
|
let sections = [];
|
|
2290
2368
|
|
|
2291
|
-
var textForm = [];
|
|
2292
2369
|
var initBody = [];
|
|
2293
|
-
|
|
2294
2370
|
var bytecodeResult = this._serializeBytecode(bytecode, compiler);
|
|
2295
2371
|
|
|
2296
|
-
for (const instr of bytecodeResult.bytecode) {
|
|
2297
|
-
const serialized = this._serializeInstr(instr, constants);
|
|
2298
|
-
textForm.push(serialized.text);
|
|
2299
|
-
}
|
|
2300
|
-
|
|
2301
|
-
initBody.push(textForm.map((line) => `// ${line}`).join("\n"));
|
|
2302
|
-
|
|
2303
2372
|
const flat = bytecodeResult.bytecode.flatMap((instr) => {
|
|
2304
2373
|
let filtered = instr.filter((x) => (x as any)?.placeholder !== true);
|
|
2305
2374
|
let resolved = filtered.map((x) => (x as any)?.resolvedValue ?? x);
|
|
@@ -2348,6 +2417,10 @@ export async function compileAndSerialize(
|
|
|
2348
2417
|
passes.push(specializedOpcodes);
|
|
2349
2418
|
}
|
|
2350
2419
|
|
|
2420
|
+
if (options.microOpcodes) {
|
|
2421
|
+
passes.push(microOpcodes);
|
|
2422
|
+
}
|
|
2423
|
+
|
|
2351
2424
|
if (options.macroOpcodes) {
|
|
2352
2425
|
passes.push(macroOpcodes);
|
|
2353
2426
|
}
|
|
@@ -2384,11 +2457,25 @@ export async function compileAndSerialize(
|
|
|
2384
2457
|
compiler,
|
|
2385
2458
|
);
|
|
2386
2459
|
|
|
2460
|
+
// This part was purposefully pulled out Serializer as OP_NAME's get resolved during obfuscateRuntime
|
|
2461
|
+
// So for the most useful comments, it's ran absolutely last
|
|
2462
|
+
// Tests also rely on correct comments so it's required
|
|
2463
|
+
const generateBytecodeComment = () => {
|
|
2464
|
+
var lines = [];
|
|
2465
|
+
for (const instr of bytecode) {
|
|
2466
|
+
const serialized = compiler.serializer._serializeInstr(instr);
|
|
2467
|
+
lines.push("// " + serialized.text);
|
|
2468
|
+
}
|
|
2469
|
+
|
|
2470
|
+
return lines.join("\n");
|
|
2471
|
+
};
|
|
2472
|
+
|
|
2387
2473
|
const code = await obfuscateRuntime(
|
|
2388
2474
|
runtimeSource,
|
|
2389
2475
|
bytecode,
|
|
2390
2476
|
options,
|
|
2391
2477
|
compiler,
|
|
2478
|
+
generateBytecodeComment,
|
|
2392
2479
|
);
|
|
2393
2480
|
|
|
2394
2481
|
return { code };
|
package/src/options.ts
CHANGED
|
@@ -6,6 +6,7 @@ export interface Options {
|
|
|
6
6
|
encodeBytecode?: boolean; // encode bytecode? when off, comments for instructions are added
|
|
7
7
|
selfModifying?: boolean; // do self-modifying bytecode for function bodies?
|
|
8
8
|
macroOpcodes?: boolean; // create combined opcodes for repeated instruction sequences?
|
|
9
|
+
microOpcodes?: boolean; // break opcodes into sub-opcodes?
|
|
9
10
|
specializedOpcodes?: boolean; // create specialized opcodes for commonly used opcode+operand pairs?
|
|
10
11
|
aliasedOpcodes?: boolean; // create duplicate opcodes for commonly used opcodes?
|
|
11
12
|
timingChecks?: boolean; // add timing checks to detect debuggers?
|