js-confuser-vm 0.0.3 → 0.0.5
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 +125 -28
- package/LICENSE +21 -21
- package/README.MD +370 -196
- package/babel-plugin-inline-runtime.cjs +34 -34
- package/babel.config.json +23 -23
- package/dist/build-runtime.js +53 -0
- package/dist/compiler.js +107 -117
- package/dist/runtime.js +78 -84
- package/dist/transforms/bytecode/macroOpcodes.js +152 -0
- package/dist/transforms/{resolveContants.js → bytecode/resolveContants.js} +16 -6
- package/dist/transforms/bytecode/resolveLabels.js +80 -0
- package/dist/transforms/{selfModifying.js → bytecode/selfModifying.js} +33 -33
- package/dist/transforms/bytecode/specializedOpcodes.js +103 -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 +102 -0
- package/dist/transforms/utils/op-utils.js +25 -0
- package/dist/{random.js → transforms/utils/random-utils.js} +3 -3
- package/dist/types.js +4 -2
- package/index.ts +34 -22
- package/jest-strip-types.js +10 -10
- package/jest.config.js +35 -28
- package/package.json +49 -48
- package/src/build-runtime.ts +57 -0
- package/src/compiler.ts +2069 -2066
- package/src/index.ts +14 -14
- package/src/minify.ts +21 -21
- package/src/options.ts +14 -12
- package/src/runtime.ts +771 -779
- package/src/transforms/bytecode/macroOpcodes.ts +177 -0
- package/src/transforms/bytecode/resolveContants.ts +62 -0
- package/src/transforms/bytecode/resolveLabels.ts +107 -0
- package/src/transforms/{selfModifying.ts → bytecode/selfModifying.ts} +37 -40
- package/src/transforms/bytecode/specializedOpcodes.ts +118 -0
- package/src/transforms/runtime/macroOpcodes.ts +111 -0
- package/src/transforms/runtime/minify.ts +1 -0
- package/src/transforms/runtime/shuffleOpcodes.ts +24 -0
- package/src/transforms/runtime/specializedOpcodes.ts +146 -0
- package/src/transforms/utils/op-utils.ts +26 -0
- package/src/{random.ts → transforms/utils/random-utils.ts} +31 -31
- package/src/types.ts +33 -24
- package/src/utilts.ts +3 -3
- package/tsconfig.json +12 -12
- package/dist/runtimeObf.js +0 -56
- package/dist/transforms/controlFlowFlattening.js +0 -22
- package/dist/transforms/resolveLabels.js +0 -59
- package/src/runtimeObf.ts +0 -62
- package/src/transforms/controlFlowFlattening.ts +0 -30
- package/src/transforms/resolveContants.ts +0 -42
- package/src/transforms/resolveLabels.ts +0 -83
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { minify as applyMinify } from "../../minify.ts";
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import * as t from "@babel/types";
|
|
2
|
+
import traverseImport from "@babel/traverse";
|
|
3
|
+
import { ok } from "assert";
|
|
4
|
+
import { shuffle } from "../utils/random-utils.ts";
|
|
5
|
+
const traverse = (traverseImport.default ||
|
|
6
|
+
traverseImport) as typeof traverseImport.default;
|
|
7
|
+
|
|
8
|
+
// Randomly reorder the switch cases inside the @SWITCH statement so the
|
|
9
|
+
// opcode handler order varies per build.
|
|
10
|
+
export function applyShuffleOpcodes(ast: t.File): void {
|
|
11
|
+
let switchStatement: t.SwitchStatement | null = null;
|
|
12
|
+
traverse(ast, {
|
|
13
|
+
SwitchStatement(path) {
|
|
14
|
+
if (path.node.leadingComments?.some((c) => c.value.includes("@SWITCH"))) {
|
|
15
|
+
switchStatement = path.node;
|
|
16
|
+
path.stop();
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
ok(switchStatement, "Could not find opcode handlers switch statement");
|
|
22
|
+
|
|
23
|
+
switchStatement.cases = shuffle(switchStatement.cases);
|
|
24
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
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
|
+
import type * as b from "../../types.ts";
|
|
6
|
+
|
|
7
|
+
const traverse = (traverseImport.default ||
|
|
8
|
+
traverseImport) as typeof traverseImport.default;
|
|
9
|
+
|
|
10
|
+
// Extract the real statement list from a SwitchCase consequent (identical to the
|
|
11
|
+
// helper used by applyMacroOpcodes so the two files stay in sync).
|
|
12
|
+
function extractCaseBody(switchCase: t.SwitchCase): t.Statement[] {
|
|
13
|
+
let stmts: t.Statement[];
|
|
14
|
+
if (
|
|
15
|
+
switchCase.consequent.length === 1 &&
|
|
16
|
+
t.isBlockStatement(switchCase.consequent[0])
|
|
17
|
+
) {
|
|
18
|
+
stmts = (switchCase.consequent[0] as t.BlockStatement).body;
|
|
19
|
+
} else {
|
|
20
|
+
stmts = switchCase.consequent as t.Statement[];
|
|
21
|
+
}
|
|
22
|
+
return stmts.filter((s) => !t.isBreakStatement(s) && !t.isEmptyStatement(s));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Inline a fixed numeric operand in place of every `this._operand()` call.
|
|
26
|
+
// Because specialized opcodes are only created for instructions that have
|
|
27
|
+
// *exactly one* numeric operand, every `_operand()` call inside the original
|
|
28
|
+
// handler is replaced by the constant value that was baked into the opcode.
|
|
29
|
+
function inlineFixedOperand(
|
|
30
|
+
bodyStmts: t.Statement[],
|
|
31
|
+
resolvedValue: number,
|
|
32
|
+
): void {
|
|
33
|
+
// Wrap the statements in a temporary BlockStatement so traverse has a root.
|
|
34
|
+
// The replacement mutates the original statement objects in place.
|
|
35
|
+
traverse(t.blockStatement(bodyStmts), {
|
|
36
|
+
noScope: true,
|
|
37
|
+
CallExpression(path) {
|
|
38
|
+
const callee = path.node.callee;
|
|
39
|
+
if (
|
|
40
|
+
t.isMemberExpression(callee) &&
|
|
41
|
+
t.isThisExpression(callee.object) &&
|
|
42
|
+
t.isIdentifier(callee.property, { name: "_operand" }) &&
|
|
43
|
+
path.node.arguments.length === 0
|
|
44
|
+
) {
|
|
45
|
+
path.replaceWith(t.numericLiteral(resolvedValue));
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Append a generated switch case for every entry in compiler.SPECIALIZED_OPS.
|
|
52
|
+
// Each case is a clone of the original opcode’s handler with `this._operand()`
|
|
53
|
+
// replaced by the constant integer that was captured at compile time.
|
|
54
|
+
// Must be called AFTER applyMacroOpcodes (so the original cases exist) but
|
|
55
|
+
// BEFORE applyShuffleOpcodes so the new specialized cases also get shuffled.
|
|
56
|
+
export function applySpecializedOpcodes(
|
|
57
|
+
ast: t.File,
|
|
58
|
+
bytecode: b.Bytecode,
|
|
59
|
+
compiler: Compiler,
|
|
60
|
+
): void {
|
|
61
|
+
let switchStatement: t.SwitchStatement | null = null;
|
|
62
|
+
traverse(ast, {
|
|
63
|
+
SwitchStatement(path) {
|
|
64
|
+
if (path.node.leadingComments?.some((c) => c.value.includes("@SWITCH"))) {
|
|
65
|
+
switchStatement = path.node;
|
|
66
|
+
path.stop();
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
ok(
|
|
72
|
+
switchStatement,
|
|
73
|
+
"Could not find @SWITCH statement for specialized opcodes",
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
// Build a map opName → SwitchCase from the existing OP.xxx case tests.
|
|
77
|
+
const nameToCaseMap = new Map<string, t.SwitchCase>();
|
|
78
|
+
for (const sc of (switchStatement as t.SwitchStatement).cases) {
|
|
79
|
+
const test = sc.test;
|
|
80
|
+
if (
|
|
81
|
+
test &&
|
|
82
|
+
t.isMemberExpression(test) &&
|
|
83
|
+
t.isIdentifier(test.object, { name: "OP" }) &&
|
|
84
|
+
t.isIdentifier(test.property)
|
|
85
|
+
) {
|
|
86
|
+
nameToCaseMap.set((test.property as t.Identifier).name, sc);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (!compiler.SPECIALIZED_OPS) return;
|
|
91
|
+
|
|
92
|
+
for (const [specialOpStr, info] of Object.entries(compiler.SPECIALIZED_OPS)) {
|
|
93
|
+
const specialOpCode = Number(specialOpStr);
|
|
94
|
+
const { originalOp, operand } = info as {
|
|
95
|
+
originalOp: number;
|
|
96
|
+
operand: number;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const newName = compiler.OP_NAME[specialOpCode];
|
|
100
|
+
const originalName = compiler.OP_NAME[originalOp];
|
|
101
|
+
if (!originalName) continue;
|
|
102
|
+
|
|
103
|
+
const originalCase = nameToCaseMap.get(originalName);
|
|
104
|
+
if (!originalCase) continue;
|
|
105
|
+
|
|
106
|
+
// Clone the original handler body
|
|
107
|
+
const bodyStmts: t.Statement[] = extractCaseBody(originalCase).map(
|
|
108
|
+
(s) => t.cloneNode(s, true) as t.Statement,
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
const placedOperand = info.resolvedOperand || { resolvedValue: 1337 };
|
|
112
|
+
ok(
|
|
113
|
+
placedOperand !== undefined,
|
|
114
|
+
`Could not find operand for original opcode ${newName}`,
|
|
115
|
+
);
|
|
116
|
+
|
|
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
|
+
// Replace this._operand() with the baked-in constant
|
|
125
|
+
inlineFixedOperand(bodyStmts, resolvedValue);
|
|
126
|
+
|
|
127
|
+
// Add a leading comment so the generated source stays readable
|
|
128
|
+
if (bodyStmts.length > 0) {
|
|
129
|
+
t.addComment(
|
|
130
|
+
bodyStmts[0],
|
|
131
|
+
"leading",
|
|
132
|
+
` ${compiler.OP_NAME[specialOpCode]} (specialized)`,
|
|
133
|
+
true,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
bodyStmts.push(t.breakStatement());
|
|
138
|
+
|
|
139
|
+
// Insert the new specialized case into the big switch
|
|
140
|
+
(switchStatement as t.SwitchStatement).cases.push(
|
|
141
|
+
t.switchCase(t.numericLiteral(specialOpCode), [
|
|
142
|
+
t.blockStatement(bodyStmts),
|
|
143
|
+
]),
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { getRandomInt } from "./random-utils.ts";
|
|
2
|
+
|
|
3
|
+
export const U16_MAX = 0xffff; // bytecode operands are u16
|
|
4
|
+
|
|
5
|
+
/** Returns the next free opcode slot, or -1 when the space is exhausted. */
|
|
6
|
+
export function nextFreeSlot(usedOpcodes: Set<number>): number {
|
|
7
|
+
if (usedOpcodes.size > U16_MAX) return -1;
|
|
8
|
+
let attempts = 0;
|
|
9
|
+
while (attempts++ < 512) {
|
|
10
|
+
const candidate = getRandomInt(0, U16_MAX);
|
|
11
|
+
if (!usedOpcodes.has(candidate)) {
|
|
12
|
+
usedOpcodes.add(candidate);
|
|
13
|
+
return candidate;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
// Fallback: linear scan from a random start
|
|
17
|
+
const start = getRandomInt(0, U16_MAX);
|
|
18
|
+
for (let i = 0; i <= U16_MAX; i++) {
|
|
19
|
+
const v = (start + i) & U16_MAX;
|
|
20
|
+
if (!usedOpcodes.has(v)) {
|
|
21
|
+
usedOpcodes.add(v);
|
|
22
|
+
return v;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return -1;
|
|
26
|
+
}
|
|
@@ -1,31 +1,31 @@
|
|
|
1
|
-
import { ok } from "assert";
|
|
2
|
-
|
|
3
|
-
export function getPlaceholder() {
|
|
4
|
-
return Math.random().toString(36).substring(2, 15);
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export function choice<T>(elements: T[]): T {
|
|
8
|
-
ok(elements.length > 0, "choice() called on empty sequence");
|
|
9
|
-
return elements[Math.floor(Math.random() * elements.length)];
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function getRandom(): number {
|
|
13
|
-
return Math.random();
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export function getRandomInt(min: number, max: number): number {
|
|
17
|
-
ok(min <= max, "min must be <= max");
|
|
18
|
-
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Shuffles an array in-place using the Fisher-Yates algorithm.
|
|
23
|
-
* @param array - The array to shuffle (mutated)
|
|
24
|
-
*/
|
|
25
|
-
export function shuffle<T>(array: T[]): T[] {
|
|
26
|
-
for (let i = array.length - 1; i > 0; i--) {
|
|
27
|
-
const j = Math.floor(Math.random() * (i + 1));
|
|
28
|
-
[array[i], array[j]] = [array[j], array[i]];
|
|
29
|
-
}
|
|
30
|
-
return array;
|
|
31
|
-
}
|
|
1
|
+
import { ok } from "assert";
|
|
2
|
+
|
|
3
|
+
export function getPlaceholder() {
|
|
4
|
+
return Math.random().toString(36).substring(2, 15);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function choice<T>(elements: T[]): T {
|
|
8
|
+
ok(elements.length > 0, "choice() called on empty sequence");
|
|
9
|
+
return elements[Math.floor(Math.random() * elements.length)];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function getRandom(): number {
|
|
13
|
+
return Math.random();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function getRandomInt(min: number, max: number): number {
|
|
17
|
+
ok(min <= max, "min must be <= max");
|
|
18
|
+
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Shuffles an array in-place using the Fisher-Yates algorithm.
|
|
23
|
+
* @param array - The array to shuffle (mutated)
|
|
24
|
+
*/
|
|
25
|
+
export function shuffle<T>(array: T[]): T[] {
|
|
26
|
+
for (let i = array.length - 1; i > 0; i--) {
|
|
27
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
28
|
+
[array[i], array[j]] = [array[j], array[i]];
|
|
29
|
+
}
|
|
30
|
+
return array;
|
|
31
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -1,24 +1,33 @@
|
|
|
1
|
-
// Bytecode supports both real instructions and IR pseudo-instructions
|
|
2
|
-
// Real instruction: [OP.ADD, 5]
|
|
3
|
-
// IR instruction: [null, { type: "defineLabel", label: "FN_ENTRY_1" }]
|
|
4
|
-
|
|
5
|
-
// IR instructions are used to hold symbolic information during compilation
|
|
6
|
-
// All "null" instructions are dropped before assembly time
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
+
export type InstrOperand =
|
|
10
|
+
| number
|
|
11
|
+
| Op<{ type: "number"; value: number }>
|
|
12
|
+
| Op<{ type: "label"; label: string; offset?: number }>
|
|
13
|
+
| Op<{ type: "defineLabel"; label: string }>
|
|
14
|
+
| Op<{ type: "constant"; value: any }>;
|
|
15
|
+
|
|
16
|
+
export interface Operand {
|
|
17
|
+
type: string;
|
|
18
|
+
placeholder?: boolean;
|
|
19
|
+
resolvedValue?: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type Op<T extends object> = Operand & T;
|
|
23
|
+
|
|
24
|
+
export type Instruction = [number | null, ...InstrOperand[]];
|
|
25
|
+
|
|
26
|
+
export type Bytecode = Instruction[];
|
|
27
|
+
|
|
28
|
+
export function constantOperand(value: any): Instruction[1] {
|
|
29
|
+
return {
|
|
30
|
+
type: "constant",
|
|
31
|
+
value: value,
|
|
32
|
+
};
|
|
33
|
+
}
|
package/src/utilts.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export function escapeRegex(s: string): string {
|
|
2
|
-
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3
|
-
}
|
|
1
|
+
export function escapeRegex(s: string): string {
|
|
2
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3
|
+
}
|
package/tsconfig.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "esnext",
|
|
4
|
-
"module": "nodenext",
|
|
5
|
-
"outDir": "./dist",
|
|
6
|
-
"rootDir": "./",
|
|
7
|
-
"strict": false,
|
|
8
|
-
"noEmit": true,
|
|
9
|
-
"types": ["node"],
|
|
10
|
-
"allowImportingTsExtensions": true
|
|
11
|
-
}
|
|
12
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "esnext",
|
|
4
|
+
"module": "nodenext",
|
|
5
|
+
"outDir": "./dist",
|
|
6
|
+
"rootDir": "./",
|
|
7
|
+
"strict": false,
|
|
8
|
+
"noEmit": true,
|
|
9
|
+
"types": ["node"],
|
|
10
|
+
"allowImportingTsExtensions": true
|
|
11
|
+
}
|
|
12
|
+
}
|
package/dist/runtimeObf.js
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { generate } from "@babel/generator";
|
|
2
|
-
import { parse } from "@babel/parser";
|
|
3
|
-
import traverseImport from "@babel/traverse";
|
|
4
|
-
import { ok } from "assert";
|
|
5
|
-
import { shuffle } from "./random.js";
|
|
6
|
-
import { minify } from "./minify.js";
|
|
7
|
-
const traverse = traverseImport.default || traverseImport;
|
|
8
|
-
export async function obfuscateRuntime(runtime, options) {
|
|
9
|
-
let ast;
|
|
10
|
-
try {
|
|
11
|
-
ast = parse(runtime, {
|
|
12
|
-
sourceType: "unambiguous"
|
|
13
|
-
});
|
|
14
|
-
} catch (error) {
|
|
15
|
-
throw new Error("VM-Runtime final parsing failed", {
|
|
16
|
-
cause: error
|
|
17
|
-
});
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// shuffle order of opcode handlers
|
|
21
|
-
|
|
22
|
-
if (options.shuffleOpcodes) {
|
|
23
|
-
let switchStatement = null;
|
|
24
|
-
traverse(ast, {
|
|
25
|
-
SwitchStatement(path) {
|
|
26
|
-
if (path.node.leadingComments?.some(comment => comment.value.includes("@SWITCH"))) {
|
|
27
|
-
switchStatement = path.node;
|
|
28
|
-
path.stop();
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
});
|
|
32
|
-
ok(switchStatement, "Could not find opcode handlers switch statement");
|
|
33
|
-
|
|
34
|
-
// simply shuffle the order of the cases
|
|
35
|
-
|
|
36
|
-
switchStatement.cases = shuffle(switchStatement.cases);
|
|
37
|
-
}
|
|
38
|
-
let generated;
|
|
39
|
-
try {
|
|
40
|
-
generated = generate(ast).code;
|
|
41
|
-
} catch (error) {
|
|
42
|
-
throw new Error("VM-Runtime final generation failed", {
|
|
43
|
-
cause: error
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
if (options.minify) {
|
|
47
|
-
try {
|
|
48
|
-
generated = await minify(generated);
|
|
49
|
-
} catch (error) {
|
|
50
|
-
throw new Error("VM-Runtime final minification failed", {
|
|
51
|
-
cause: error
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
return generated;
|
|
56
|
-
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Breaks functions into DAGs (Directed Acyclic Graphs)
|
|
3
|
-
*
|
|
4
|
-
* - 1. Break bytecode into chunks
|
|
5
|
-
* - 2. Shuffle chunks but remember their original position
|
|
6
|
-
* - 3. Create an effectively Switch statement inside a While loop, each case is a chunk, and the while loops exits on the last transition.
|
|
7
|
-
*
|
|
8
|
-
* The Switch statement:
|
|
9
|
-
*
|
|
10
|
-
* - 1. The state variable controls which case will run next
|
|
11
|
-
* - 2. At the end of each case, the state variable is updated to the next block of code.
|
|
12
|
-
* - 3. The while loop continues until the the state variable is the end state.
|
|
13
|
-
*/
|
|
14
|
-
export async function controlFlowFlattening(bytecode) {
|
|
15
|
-
// break bytecode into basic blocks
|
|
16
|
-
// 1. read bytecode and track the current label from the IR-instruction "defineLabel"
|
|
17
|
-
// 2. track any potential jumps inside this block using the IR-instruction operand "label"
|
|
18
|
-
// at this stage in the passing process, may still use these IR-instruction labels for jumps, meaning no effort is required to maintain absolute PCs
|
|
19
|
-
// create a bare CFF implementation of a simple switch dispatch loop effectively
|
|
20
|
-
// This CFF implementation should only apply to "easy jumps" such as conditional jump (if-statement)
|
|
21
|
-
// the complex and specific jumps for for..in shouldn't get flattened
|
|
22
|
-
}
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
// --- Label IR ---
|
|
2
|
-
// During compilation, jump targets are symbolic labels instead of hard-coded
|
|
3
|
-
// PC numbers. Two IR "pseudo operands" carry the label information:
|
|
4
|
-
//
|
|
5
|
-
// defineLabel operand : [null, {type:"defineLabel", label:"FN_ENTRY_1"}]
|
|
6
|
-
// Marks a position in the bytecode array.
|
|
7
|
-
// resolveLabels() strips these out entirely.
|
|
8
|
-
//
|
|
9
|
-
// label ref operand : [OP.JUMP, {type:"label", label:"FN_ENTRY_1"}]
|
|
10
|
-
// Used as the operand of any jump instruction. resolveLabels() replaces
|
|
11
|
-
// it with the integer PC that the corresponding defineLabel resolves to.
|
|
12
|
-
|
|
13
|
-
// Resolve symbolic labels to absolute PC indices within a bytecode array.
|
|
14
|
-
// defineLabel pseudo-instructions are stripped; label-ref operands become ints.
|
|
15
|
-
// Mutates `bc` in place so callers holding a reference see the resolved result.
|
|
16
|
-
export function resolveLabels(bc, compiler) {
|
|
17
|
-
// Pass 1 – walk the array and record each label's real PC, counting only
|
|
18
|
-
// real instructions (defineLabel pseudo-ops don't occupy a PC slot).
|
|
19
|
-
const labelToPc = new Map();
|
|
20
|
-
let realPc = 0;
|
|
21
|
-
for (const instr of bc) {
|
|
22
|
-
const op = instr[0];
|
|
23
|
-
const operand = instr[1];
|
|
24
|
-
if (op === null && operand !== null && typeof operand === "object" && operand.type === "defineLabel") {
|
|
25
|
-
labelToPc.set(operand.label, realPc);
|
|
26
|
-
} else {
|
|
27
|
-
realPc++;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// Pass 2 – build the resolved instruction list.
|
|
32
|
-
const resolved = [];
|
|
33
|
-
for (const instr of bc) {
|
|
34
|
-
const op = instr[0];
|
|
35
|
-
const operand = instr[1];
|
|
36
|
-
|
|
37
|
-
// Strip defineLabel pseudo-ops.
|
|
38
|
-
if (op === null && typeof operand === "object" && operand?.type === "defineLabel") {
|
|
39
|
-
continue;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// Replace label-ref operands with integer PCs.
|
|
43
|
-
if (operand !== undefined && operand !== null && typeof operand === "object" && operand.type === "label") {
|
|
44
|
-
const pc = labelToPc.get(operand.label);
|
|
45
|
-
if (pc === undefined) throw new Error(`Undefined label: ${operand.label}`);
|
|
46
|
-
resolved.push([op, pc + (operand.offset ?? 0)]);
|
|
47
|
-
} else {
|
|
48
|
-
resolved.push(instr);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Patch each function descriptor's startPc now that labels are resolved.
|
|
53
|
-
for (const desc of compiler.fnDescriptors) {
|
|
54
|
-
desc.startPc = labelToPc.get(desc.startLabel);
|
|
55
|
-
}
|
|
56
|
-
return {
|
|
57
|
-
bytecode: resolved
|
|
58
|
-
};
|
|
59
|
-
}
|
package/src/runtimeObf.ts
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import * as t from "@babel/types";
|
|
2
|
-
import { generate } from "@babel/generator";
|
|
3
|
-
import { parse } from "@babel/parser";
|
|
4
|
-
import traverseImport from "@babel/traverse";
|
|
5
|
-
import { ok } from "assert";
|
|
6
|
-
import { shuffle } from "./random.ts";
|
|
7
|
-
import type { Options } from "./options.ts";
|
|
8
|
-
import { minify } from "./minify.ts";
|
|
9
|
-
const traverse = (traverseImport.default ||
|
|
10
|
-
traverseImport) as typeof traverseImport.default;
|
|
11
|
-
|
|
12
|
-
export async function obfuscateRuntime(runtime: string, options: Options) {
|
|
13
|
-
let ast: t.File;
|
|
14
|
-
try {
|
|
15
|
-
ast = parse(runtime, {
|
|
16
|
-
sourceType: "unambiguous",
|
|
17
|
-
});
|
|
18
|
-
} catch (error) {
|
|
19
|
-
throw new Error("VM-Runtime final parsing failed", { cause: error });
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// shuffle order of opcode handlers
|
|
23
|
-
|
|
24
|
-
if (options.shuffleOpcodes) {
|
|
25
|
-
let switchStatement: t.SwitchStatement | null = null;
|
|
26
|
-
traverse(ast, {
|
|
27
|
-
SwitchStatement(path) {
|
|
28
|
-
if (
|
|
29
|
-
path.node.leadingComments?.some((comment) =>
|
|
30
|
-
comment.value.includes("@SWITCH"),
|
|
31
|
-
)
|
|
32
|
-
) {
|
|
33
|
-
switchStatement = path.node;
|
|
34
|
-
path.stop();
|
|
35
|
-
}
|
|
36
|
-
},
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
ok(switchStatement, "Could not find opcode handlers switch statement");
|
|
40
|
-
|
|
41
|
-
// simply shuffle the order of the cases
|
|
42
|
-
|
|
43
|
-
switchStatement.cases = shuffle(switchStatement.cases);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
let generated: string;
|
|
47
|
-
try {
|
|
48
|
-
generated = generate(ast).code;
|
|
49
|
-
} catch (error) {
|
|
50
|
-
throw new Error("VM-Runtime final generation failed", { cause: error });
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
if (options.minify) {
|
|
54
|
-
try {
|
|
55
|
-
generated = await minify(generated);
|
|
56
|
-
} catch (error) {
|
|
57
|
-
throw new Error("VM-Runtime final minification failed", { cause: error });
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return generated;
|
|
62
|
-
}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { Instruction } from "../types.ts";
|
|
2
|
-
|
|
3
|
-
interface BasicBlock {
|
|
4
|
-
label: string;
|
|
5
|
-
body: Instruction;
|
|
6
|
-
jumpLabels?: Set<string>;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Breaks functions into DAGs (Directed Acyclic Graphs)
|
|
11
|
-
*
|
|
12
|
-
* - 1. Break bytecode into chunks
|
|
13
|
-
* - 2. Shuffle chunks but remember their original position
|
|
14
|
-
* - 3. Create an effectively Switch statement inside a While loop, each case is a chunk, and the while loops exits on the last transition.
|
|
15
|
-
*
|
|
16
|
-
* The Switch statement:
|
|
17
|
-
*
|
|
18
|
-
* - 1. The state variable controls which case will run next
|
|
19
|
-
* - 2. At the end of each case, the state variable is updated to the next block of code.
|
|
20
|
-
* - 3. The while loop continues until the the state variable is the end state.
|
|
21
|
-
*/
|
|
22
|
-
export async function controlFlowFlattening(bytecode: Instruction) {
|
|
23
|
-
// break bytecode into basic blocks
|
|
24
|
-
// 1. read bytecode and track the current label from the IR-instruction "defineLabel"
|
|
25
|
-
// 2. track any potential jumps inside this block using the IR-instruction operand "label"
|
|
26
|
-
// at this stage in the passing process, may still use these IR-instruction labels for jumps, meaning no effort is required to maintain absolute PCs
|
|
27
|
-
// create a bare CFF implementation of a simple switch dispatch loop effectively
|
|
28
|
-
// This CFF implementation should only apply to "easy jumps" such as conditional jump (if-statement)
|
|
29
|
-
// the complex and specific jumps for for..in shouldn't get flattened
|
|
30
|
-
}
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import type { Bytecode, Instruction } from "../types.ts";
|
|
2
|
-
import { SOURCE_NODE_SYM } from "../compiler.ts";
|
|
3
|
-
|
|
4
|
-
// Resolve all {type:"constant", value} operands to integer indices into the
|
|
5
|
-
// constants pool. Returns both the resolved bytecode and the constants array
|
|
6
|
-
// so the Serializer can use it for comment generation and output.
|
|
7
|
-
export function resolveConstants(bc: Bytecode): {
|
|
8
|
-
bytecode: Bytecode;
|
|
9
|
-
constants: any[];
|
|
10
|
-
} {
|
|
11
|
-
const constants: any[] = [];
|
|
12
|
-
const constantsMap = new Map<any, number>();
|
|
13
|
-
|
|
14
|
-
function intern(value: any): number {
|
|
15
|
-
let idx = constantsMap.get(value);
|
|
16
|
-
if (typeof idx !== "number") {
|
|
17
|
-
idx = constants.length;
|
|
18
|
-
constantsMap.set(value, idx);
|
|
19
|
-
constants.push(value);
|
|
20
|
-
}
|
|
21
|
-
return idx;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const resolved: Bytecode = [];
|
|
25
|
-
for (const instr of bc) {
|
|
26
|
-
const [op, operand] = instr;
|
|
27
|
-
if (
|
|
28
|
-
operand !== undefined &&
|
|
29
|
-
operand !== null &&
|
|
30
|
-
typeof operand === "object" &&
|
|
31
|
-
(operand as any).type === "constant"
|
|
32
|
-
) {
|
|
33
|
-
const newInstr: Instruction = [op, intern((operand as any).value)];
|
|
34
|
-
(newInstr as any)[SOURCE_NODE_SYM] = (instr as any)[SOURCE_NODE_SYM];
|
|
35
|
-
resolved.push(newInstr);
|
|
36
|
-
} else {
|
|
37
|
-
resolved.push(instr);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
return { bytecode: resolved, constants };
|
|
42
|
-
}
|