js-confuser-vm 0.0.1 → 0.0.3
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 +28 -0
- package/README.MD +197 -0
- package/babel-plugin-inline-runtime.cjs +34 -0
- package/babel.config.json +23 -0
- package/dist/compiler.js +1771 -0
- package/dist/index.js +10 -0
- package/dist/minify.js +18 -0
- package/dist/options.js +1 -0
- package/dist/random.js +27 -0
- package/dist/runtime.js +755 -0
- package/dist/runtimeObf.js +56 -0
- package/dist/transforms/controlFlowFlattening.js +22 -0
- package/dist/transforms/resolveContants.js +33 -0
- package/dist/transforms/resolveLabels.js +59 -0
- package/dist/transforms/selfModifying.js +107 -0
- package/dist/types.js +13 -0
- package/dist/utilts.js +3 -0
- package/index.ts +17 -12
- package/jest.config.js +26 -5
- package/package.json +13 -6
- package/src/compiler.ts +1122 -673
- package/src/index.ts +14 -0
- package/src/minify.ts +21 -0
- package/src/options.ts +12 -0
- package/src/random.ts +31 -0
- package/src/runtime.ts +609 -461
- package/src/runtimeObf.ts +62 -0
- package/src/transforms/controlFlowFlattening.ts +30 -0
- package/src/transforms/resolveContants.ts +42 -0
- package/src/transforms/resolveLabels.ts +83 -0
- package/src/transforms/selfModifying.ts +124 -0
- package/src/types.ts +24 -0
- package/src/utilts.ts +3 -0
- package/.claude/settings.local.json +0 -8
- package/ReadME.MD +0 -164
- package/input.js +0 -15
- package/minify.js +0 -17
- package/minify_empty_externs.js +0 -4
- package/obfuscate.js +0 -12
- package/src/index.js +0 -5
- package/src/random.js +0 -3
|
@@ -0,0 +1,62 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
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
|
+
import type { Instruction } from "../types.ts";
|
|
14
|
+
import { Compiler } from "../compiler.ts";
|
|
15
|
+
|
|
16
|
+
// Resolve symbolic labels to absolute PC indices within a bytecode array.
|
|
17
|
+
// defineLabel pseudo-instructions are stripped; label-ref operands become ints.
|
|
18
|
+
// Mutates `bc` in place so callers holding a reference see the resolved result.
|
|
19
|
+
export function resolveLabels(
|
|
20
|
+
bc: Instruction[],
|
|
21
|
+
compiler: Compiler,
|
|
22
|
+
): {
|
|
23
|
+
bytecode: Instruction[];
|
|
24
|
+
} {
|
|
25
|
+
// Pass 1 – walk the array and record each label's real PC, counting only
|
|
26
|
+
// real instructions (defineLabel pseudo-ops don't occupy a PC slot).
|
|
27
|
+
const labelToPc = new Map<string, number>();
|
|
28
|
+
let realPc = 0;
|
|
29
|
+
for (const instr of bc) {
|
|
30
|
+
const op = instr[0];
|
|
31
|
+
const operand = instr[1];
|
|
32
|
+
if (
|
|
33
|
+
op === null &&
|
|
34
|
+
operand !== null &&
|
|
35
|
+
typeof operand === "object" &&
|
|
36
|
+
operand.type === "defineLabel"
|
|
37
|
+
) {
|
|
38
|
+
labelToPc.set(operand.label, realPc);
|
|
39
|
+
} else {
|
|
40
|
+
realPc++;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Pass 2 – build the resolved instruction list.
|
|
45
|
+
const resolved: any[] = [];
|
|
46
|
+
for (const instr of bc) {
|
|
47
|
+
const op = instr[0];
|
|
48
|
+
const operand = instr[1];
|
|
49
|
+
|
|
50
|
+
// Strip defineLabel pseudo-ops.
|
|
51
|
+
if (
|
|
52
|
+
op === null &&
|
|
53
|
+
typeof operand === "object" &&
|
|
54
|
+
operand?.type === "defineLabel"
|
|
55
|
+
) {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Replace label-ref operands with integer PCs.
|
|
60
|
+
if (
|
|
61
|
+
operand !== undefined &&
|
|
62
|
+
operand !== null &&
|
|
63
|
+
typeof operand === "object" &&
|
|
64
|
+
operand.type === "label"
|
|
65
|
+
) {
|
|
66
|
+
const pc = labelToPc.get(operand.label);
|
|
67
|
+
if (pc === undefined)
|
|
68
|
+
throw new Error(`Undefined label: ${operand.label}`);
|
|
69
|
+
resolved.push([op, pc + (operand.offset ?? 0)]);
|
|
70
|
+
} else {
|
|
71
|
+
resolved.push(instr);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Patch each function descriptor's startPc now that labels are resolved.
|
|
76
|
+
for (const desc of compiler.fnDescriptors) {
|
|
77
|
+
desc.startPc = labelToPc.get(desc.startLabel);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
bytecode: resolved,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { Bytecode, Instruction } from "../types.ts";
|
|
2
|
+
import { Compiler } from "../compiler.ts";
|
|
3
|
+
|
|
4
|
+
export function selfModifying(
|
|
5
|
+
bc: Bytecode,
|
|
6
|
+
compiler: Compiler,
|
|
7
|
+
): { bytecode: Bytecode } {
|
|
8
|
+
// Walk the bytecode looking for "defineLabel" pseudo-ops, which start basic
|
|
9
|
+
// blocks. For each block we collect the body (instructions between the label
|
|
10
|
+
// and the next label/jump terminator), move it to the end of the bytecode
|
|
11
|
+
// under a fresh "patch_LXX" label, and replace it in-place with:
|
|
12
|
+
//
|
|
13
|
+
// defineLabel ("originalLabel") ← kept as-is (pseudo-op)
|
|
14
|
+
// LOAD_INT { label: patch_LXX, offset: N } ← push slice-end PC
|
|
15
|
+
// LOAD_INT { label: patch_LXX } ← push slice-start PC
|
|
16
|
+
// PATCH { label: originalLabel, offset: 3 } ← destPc = L+3
|
|
17
|
+
// LOAD_INT 0 × N ← N placeholder instructions
|
|
18
|
+
//
|
|
19
|
+
// PATCH pops (start, end) from the stack and copies bytecode[start..end) to
|
|
20
|
+
// bytecode[destPc..]. Since destPc = L+3 = first placeholder, the body is
|
|
21
|
+
// written exactly over the placeholder region on the first call. Subsequent
|
|
22
|
+
// calls are idempotent (same bytes written again). Execution falls through
|
|
23
|
+
// from PATCH into the freshly-patched body at L+3, then continues naturally
|
|
24
|
+
// to whatever terminator (JUMP/RETURN) follows at L+3+N.
|
|
25
|
+
|
|
26
|
+
const { OP, JUMP_OPS } = compiler;
|
|
27
|
+
|
|
28
|
+
const result: Bytecode = [];
|
|
29
|
+
const appended: Bytecode = [];
|
|
30
|
+
let patchCount = 0;
|
|
31
|
+
|
|
32
|
+
let i = 0;
|
|
33
|
+
while (i < bc.length) {
|
|
34
|
+
const instr = bc[i];
|
|
35
|
+
const [op, operand] = instr;
|
|
36
|
+
|
|
37
|
+
// Detect a defineLabel pseudo-op — start of a new basic block.
|
|
38
|
+
if (
|
|
39
|
+
op === null &&
|
|
40
|
+
operand !== null &&
|
|
41
|
+
typeof operand === "object" &&
|
|
42
|
+
(operand as any).type === "defineLabel"
|
|
43
|
+
) {
|
|
44
|
+
const originalLabel = (operand as any).label as string;
|
|
45
|
+
result.push(instr); // keep the defineLabel marker
|
|
46
|
+
i++;
|
|
47
|
+
|
|
48
|
+
// Collect body: everything after the label until the next terminator.
|
|
49
|
+
let j = i;
|
|
50
|
+
while (j < bc.length) {
|
|
51
|
+
const [nextOp, nextOperand] = bc[j];
|
|
52
|
+
|
|
53
|
+
// Another defineLabel = boundary of the next block.
|
|
54
|
+
if (
|
|
55
|
+
nextOp === null &&
|
|
56
|
+
typeof nextOperand === "object" &&
|
|
57
|
+
(nextOperand as any)?.type === "defineLabel"
|
|
58
|
+
) {
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Jump instructions, RETURN, and DATA (function header words) all
|
|
63
|
+
// terminate the body without being included in it.
|
|
64
|
+
if (
|
|
65
|
+
nextOp !== null &&
|
|
66
|
+
(JUMP_OPS.has(nextOp) || nextOp === OP.RETURN || nextOp === OP.DATA)
|
|
67
|
+
) {
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
j++;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const body = bc.slice(i, j);
|
|
75
|
+
const N = body.length;
|
|
76
|
+
|
|
77
|
+
if (N === 0) {
|
|
78
|
+
// Nothing to transform — label is immediately followed by a terminator.
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const patchLabel = `patch_${originalLabel}_${patchCount++}`;
|
|
83
|
+
|
|
84
|
+
// ── Stub (3 real instructions) ──────────────────────────────────────
|
|
85
|
+
// LOAD_INT pushes the end-index of the body slice (patchLabel_pc + N).
|
|
86
|
+
// LOAD_INT pushes the start-index (patchLabel_pc).
|
|
87
|
+
// Stack before PATCH: [end (bottom), start (top)].
|
|
88
|
+
// PATCH: slice(pop()=start, pop()=end) copies the body to destPc = L+3.
|
|
89
|
+
result.push([
|
|
90
|
+
OP.LOAD_INT as number,
|
|
91
|
+
{ type: "label", label: patchLabel, offset: N },
|
|
92
|
+
]);
|
|
93
|
+
result.push([
|
|
94
|
+
OP.LOAD_INT as number,
|
|
95
|
+
{ type: "label", label: patchLabel },
|
|
96
|
+
]);
|
|
97
|
+
result.push([
|
|
98
|
+
OP.PATCH as number,
|
|
99
|
+
{ type: "label", label: originalLabel, offset: 3 },
|
|
100
|
+
]);
|
|
101
|
+
|
|
102
|
+
// ── Placeholders (N instructions) ───────────────────────────────────
|
|
103
|
+
// These are overwritten by PATCH on the first execution. They never
|
|
104
|
+
// execute as LOAD_INT 0 in a correct run.
|
|
105
|
+
for (let p = 0; p < N; p++) {
|
|
106
|
+
result.push([OP.LOAD_INT as number, 0]);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ── Append real body at end ─────────────────────────────────────────
|
|
110
|
+
appended.push([null, { type: "defineLabel", label: patchLabel }]);
|
|
111
|
+
for (const bodyInstr of body) {
|
|
112
|
+
appended.push(bodyInstr);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
i = j; // skip over the original body in the input array
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
result.push(instr);
|
|
120
|
+
i++;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return { bytecode: [...result, ...appended] };
|
|
124
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
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
|
+
export type Instruction = [
|
|
8
|
+
number | null,
|
|
9
|
+
(
|
|
10
|
+
| number
|
|
11
|
+
| { type: "label"; label: string; offset?: number }
|
|
12
|
+
| { type: "defineLabel"; label: string }
|
|
13
|
+
| { type: "constant"; value: any }
|
|
14
|
+
)?,
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
export type Bytecode = Instruction[];
|
|
18
|
+
|
|
19
|
+
export function constantOperand(value: any): Instruction[1] {
|
|
20
|
+
return {
|
|
21
|
+
type: "constant",
|
|
22
|
+
value: value,
|
|
23
|
+
};
|
|
24
|
+
}
|
package/src/utilts.ts
ADDED
package/ReadME.MD
DELETED
|
@@ -1,164 +0,0 @@
|
|
|
1
|
-
# JS Confuser VM
|
|
2
|
-
|
|
3
|
-
**Requires Node v24.13.1 or higher**
|
|
4
|
-
|
|
5
|
-
- ES5 support only. No complex features: async, generator, and even try..catch are beyond scope.
|
|
6
|
-
- Experimental. Expect issues.
|
|
7
|
-
|
|
8
|
-
### Usage
|
|
9
|
-
|
|
10
|
-
```shell
|
|
11
|
-
$ git clone https://github.com/MichaelXF/js-confuser-vm
|
|
12
|
-
```
|
|
13
|
-
|
|
14
|
-
- Example Script:
|
|
15
|
-
|
|
16
|
-
```js
|
|
17
|
-
import { virtualize } from "./src/index.js";
|
|
18
|
-
import { readFileSync, writeFileSync } from "fs";
|
|
19
|
-
|
|
20
|
-
const sourceCode = readFileSync("input.js", "utf-8");
|
|
21
|
-
const { code: output } = virtualize(sourceCode);
|
|
22
|
-
|
|
23
|
-
writeFileSync("output.js", output, "utf-8");
|
|
24
|
-
console.log(output);
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
- Input/Output:
|
|
28
|
-
|
|
29
|
-
```js
|
|
30
|
-
function fibonacci(num) {
|
|
31
|
-
var a = 0,
|
|
32
|
-
b = 1,
|
|
33
|
-
c = num;
|
|
34
|
-
while (num-- > 1) {
|
|
35
|
-
c = a + b;
|
|
36
|
-
a = b;
|
|
37
|
-
b = c;
|
|
38
|
-
}
|
|
39
|
-
return c;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
for (var i = 1; i <= 25; i++) {
|
|
43
|
-
console.log(i, fibonacci(i));
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/*
|
|
47
|
-
function d(a){a=typeof Buffer!=="undefined"?Buffer.from(a,"base64"):Uint8Array.from(atob(a),function(b){return b.charCodeAt(0)});for(var h=new Int32Array(a.length/4),c=0;c<h.length;c++)h[c]=a[c*4]|a[c*4+1]<<8|a[c*4+2]<<16|a[c*4+3]<<24;return h}var f=Symbol();function l(a,h){this.g=a;this.m=h;this.n=!1;this.p=void 0}function m(a){return a.n?a.p:a.g.b[a.m]}function n(a,h){a.n?a.p=h:a.g.b[a.m]=h}function p(a){this.f=a;this.l=[];this.prototype={}}
|
|
48
|
-
function w(a,h){this.r=a;this.b=Array(a.f.t).fill(void 0);this.d=a.f.u;this.x=h!==void 0?h:void 0;this.o=null}function x(a,h,c){this.q=a;this.e=h;this.i=c;this.a=[];this.h=[];this.j=[];this.c=new w(new p({k:0,t:0,u:0}))}function y(a,h){a.a.push(h)}function z(a){return a.a.pop()}function A(a){return a.a[a.a.length-1]}function E(a,h,c){for(var b=0;b<a.j.length;b++){var e=a.j[b];if(e.g===h&&e.m===c)return e}e=new l(h,c);a.j.push(e);return e}
|
|
49
|
-
function F(a,h){a.j=a.j.filter(function(c){return c.g===h?(c.p=c.g.b[c.m],c.n=!0,!1):!0})}
|
|
50
|
-
function G(a){for(var h=performance.now();;){var c=a.c,b=a.q;if(c.d>=b.length)break;b=b[c.d++];var e=b&255;b>>>=8;var g=performance.now(),k=g-h>1E3;h=g;k&&(e=13);switch(e){case 0:y(a,a.e[b]);break;case 1:y(a,c.b[b]);break;case 2:c.b[b]=z(a);break;case 3:y(a,a.i[a.e[b]]);break;case 4:a.i[a.e[b]]=z(a);break;case 5:c=z(a);b=A(a);y(a,b[c]);break;case 6:b=z(a);y(a,z(a)+b);break;case 7:b=z(a);y(a,z(a)-b);break;case 8:b=z(a);y(a,z(a)*b);break;case 9:b=z(a);y(a,z(a)/b);break;case 36:b=z(a);y(a,z(a)%b);break;
|
|
51
|
-
case 37:b=z(a);y(a,z(a)&b);break;case 38:b=z(a);y(a,z(a)|b);break;case 39:b=z(a);y(a,z(a)^b);break;case 40:b=z(a);y(a,z(a)<<b);break;case 41:b=z(a);y(a,z(a)>>b);break;case 42:b=z(a);y(a,z(a)>>>b);break;case 15:b=z(a);y(a,z(a)<b);break;case 16:b=z(a);y(a,z(a)>b);break;case 17:b=z(a);y(a,z(a)===b);break;case 20:b=z(a);y(a,z(a)<=b);break;case 21:b=z(a);y(a,z(a)>=b);break;case 22:b=z(a);y(a,z(a)!==b);break;case 52:b=z(a);y(a,z(a)==b);break;case 53:b=z(a);y(a,z(a)!=b);break;case 46:b=z(a);y(a,z(a)in b);
|
|
52
|
-
break;case 47:c=z(a);b=z(a);if(typeof c==="function")y(a,b instanceof c);else{c=c.prototype;b=Object.getPrototypeOf(b);for(e=!1;b!==null;){if(b===c){e=!0;break}b=Object.getPrototypeOf(b)}y(a,e)}break;case 25:y(a,-z(a));break;case 26:y(a,z(a));break;case 27:y(a,!z(a));break;case 28:y(a,~z(a));break;case 29:y(a,typeof z(a));break;case 30:z(a);y(a);break;case 31:b=z(a);e=Object.prototype.hasOwnProperty.call(a.i,b)?a.i[b]:void 0;y(a,typeof e);break;case 18:c.d=b;break;case 19:z(a)||(c.d=b);break;case 44:A(a)?
|
|
53
|
-
c.d=b:z(a);break;case 43:A(a)?z(a):c.d=b;break;case 10:g=a.e[b];e=new p(g);for(b=0;b<g.v.length;b++)k=g.v[b],k.y?e.l.push(E(a,c,k.w)):e.l.push(c.r.l[k.w]);var t=a;b=function(B){return function(){for(var u=Array.prototype.slice.call(arguments),C=new x(t.q,t.e,t.i),v=new w(B,this),q=0;q<u.length;q++)v.b[q]=u[q];v.b[B.f.k]=u;C.c=v;return G(C)}}(e);b[f]=e;b.prototype=e.prototype;y(a,b);break;case 23:y(a,m(c.r.l[b]));break;case 24:n(c.r.l[b],z(a));break;case 32:b=a.a.splice(a.a.length-b);y(a,b);break;
|
|
54
|
-
case 33:c=a.a.splice(a.a.length-b*2);e={};for(b=0;b<c.length;b+=2)e[c[b]]=c[b+1];y(a,e);break;case 34:e=z(a);c=z(a);b=z(a);b[c]=e;y(a,e);break;case 35:c=z(a);b=z(a);y(a,b[c]);break;case 45:c=z(a);b=z(a);y(a,delete b[c]);break;case 11:c=a.a.splice(a.a.length-b);if((e=z(a))&&e[f]){e=e[f];g=new w(e);for(b=0;b<c.length;b++)g.b[b]=c[b];g.b[e.f.k]=c;a.h.push(a.c);a.c=g}else y(a,e.apply(null,c));break;case 12:c=a.a.splice(a.a.length-b);e=z(a);b=z(a);if(e&&e[f]){e=e[f];g=new w(e,b);for(b=0;b<c.length;b++)g.b[b]=
|
|
55
|
-
c[b];g.b[e.f.k]=c;a.h.push(a.c);a.c=g}else y(a,e.apply(b,c));break;case 48:y(a,c.x);break;case 49:c=a.a.splice(a.a.length-b);if((e=z(a))&&e[f]){e=e[f];b=Object.create(e.prototype||null);g=new w(e,b);g.o=b;for(b=0;b<c.length;b++)g.b[b]=c[b];g.b[e.f.k]=c;a.h.push(a.c);a.c=g}else y(a,Reflect.construct(e,c));break;case 13:b=z(a);F(a,c);if(a.h.length===0)return b;c.o===null||typeof b==="object"&&b!==null||(b=c.o);a.c=a.h.pop();y(a,b);break;case 14:z(a);break;case 50:y(a,A(a));break;case 51:throw z(a);
|
|
56
|
-
case 54:b=z(a);c=[];if(b!==null&&b!==void 0)for(e=Object.create(null),g=Object(b);g!==null;){k=Object.getOwnPropertyNames(g);for(b=0;b<k.length;b++){var r=k[b];if(!(r in e)){e[r]=!0;var D=Object.getOwnPropertyDescriptor(g,r);D&&D.enumerable&&c.push(r)}}g=Object.getPrototypeOf(g)}y(a,{keys:c,s:0});break;case 55:e=z(a);e.s>=e.keys.length?c.d=b:y(a,e.keys[e.s++]);break;case 56:c=z(a);e=a.e[b];e=d(e);for(b=0;b<e.length;b++)a.q[c+b]=e[b];break;default:throw Error("Unknown opcode: "+e+" at pc "+(c.d-1));
|
|
57
|
-
}}}var H={},I;for(I of Object.getOwnPropertyNames(globalThis))H[I]=globalThis[I];typeof window!=="undefined"&&(H.window=window);H.undefined=void 0;H.Infinity=Infinity;H.NaN=NaN;
|
|
58
|
-
G(new x(d("CgMAAAQEAAAAAQAABAUAAAMFAAAABgAAFAAAABMYAAADBwAAAAgAAAUAAAADBQAAAwQAAAMFAAALAQAADAIAAA4AAAADBQAAMgAAAAABAAAGAAAABAUAAA4AAAASBAAADQAAAAAKAAA4CQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="),[0,1,void 0,{k:1,t:5,v:[],u:25},"fibonacci","i",25,"console","log","AAAAAAICAAAAAQAAAgMAAAEAAAACBAAAAQAAADIAAAAAAQAABwAAAAIAAAAAAQAAEAAAABM4AAABAgAAAQMAAAYAAAACBAAAAQQAAA4AAAABAwAAAgIAAAECAAAOAAAAAQQAAAIDAAABAwAADgAAABIhAAABBAAADQAAAAACAAANAAAADQAAAA==",
|
|
59
|
-
27],H));
|
|
60
|
-
*/
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
### Features
|
|
64
|
-
|
|
65
|
-
- [x] functions: call, arguments, return
|
|
66
|
-
- [x] closures and nested functions
|
|
67
|
-
- [x] literals
|
|
68
|
-
- [x] binary expressions
|
|
69
|
-
- [x] unary expressions
|
|
70
|
-
- [x] update expressions
|
|
71
|
-
- [x] if statements
|
|
72
|
-
- [x] while, do-while, for loops
|
|
73
|
-
- [x] get property
|
|
74
|
-
- [x] logical expressions
|
|
75
|
-
- [x] array, object expression
|
|
76
|
-
- [x] function expression
|
|
77
|
-
- [x] default arguments in functions
|
|
78
|
-
- [x] sequence expression
|
|
79
|
-
- [x] conditional expression (ternary operator)
|
|
80
|
-
- [x] delete operator
|
|
81
|
-
- [x] in / instanceof
|
|
82
|
-
- [x] this, new expression
|
|
83
|
-
- [x] arguments
|
|
84
|
-
- [x] Infinity, NaN
|
|
85
|
-
- [x] break/continue
|
|
86
|
-
- [x] switch statement
|
|
87
|
-
- [x] throw statement
|
|
88
|
-
- [x] labeled statements
|
|
89
|
-
- [x] for..in loop
|
|
90
|
-
|
|
91
|
-
### Missing
|
|
92
|
-
|
|
93
|
-
- [ ] try..catch
|
|
94
|
-
- [ ] RegExp literals
|
|
95
|
-
- [ ] with statement
|
|
96
|
-
- [ ] arguments.callee, argument parameter syncing
|
|
97
|
-
- [ ] getter/setters
|
|
98
|
-
|
|
99
|
-
### Hardening
|
|
100
|
-
|
|
101
|
-
- [x] opcode randomization per build
|
|
102
|
-
- [x] property name concealment of vm internals
|
|
103
|
-
- - Google Closure Compiler aggressively renames our class props
|
|
104
|
-
- [ ] shuffled handler order
|
|
105
|
-
- [ ] dead handlers
|
|
106
|
-
- [ ] dead bytecode insertion
|
|
107
|
-
- [ ] macro opcodes
|
|
108
|
-
- [x] encoded bytecode array and words
|
|
109
|
-
- [x] self-modifying bytecode
|
|
110
|
-
- [x] timing checks
|
|
111
|
-
- [ ] low-level bytecode obfuscations
|
|
112
|
-
- [ ] stack protection
|
|
113
|
-
- [ ] control flow integrity
|
|
114
|
-
|
|
115
|
-
### Minification
|
|
116
|
-
|
|
117
|
-
`minify.js` uses Google Closure Compiler to minify the JS VM and renames (most) VM property names. This approach helps keep the VM Compiler simple and lets Google do the heavy lifting.
|
|
118
|
-
|
|
119
|
-
### No Try Catch
|
|
120
|
-
|
|
121
|
-
Try..Catch is complex operator that may not get added. You can use Try..Catch by defining an outside helper function:
|
|
122
|
-
|
|
123
|
-
```js
|
|
124
|
-
function TryCatch(cb) {
|
|
125
|
-
try {
|
|
126
|
-
return { value: cb() };
|
|
127
|
-
} catch (error) {
|
|
128
|
-
return { error };
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
## ES5 Only
|
|
134
|
-
|
|
135
|
-
This VM Compiler only supports ES5 JavaScript. Most ES6+ features are syntax sugar that can be transpiled down relatively easily. This is a design decision to keep the VM wrapper simple and the bytecode more uniform. Having opcodes dedicated for classes and methods makes them standout more for attackers to debug easier. Keeping things simple enables easier hardening improvements.
|
|
136
|
-
|
|
137
|
-
Please transpile your code down first using [Babel](https://github.com/babel/babel).
|
|
138
|
-
|
|
139
|
-
### Project:
|
|
140
|
-
|
|
141
|
-
- Stack based VM
|
|
142
|
-
- Lua-style Closure and Upvalue model
|
|
143
|
-
- CPython-style opcodes and codegen
|
|
144
|
-
- Compiler is in src/compiler.ts
|
|
145
|
-
- Runtime is in src/runtime.ts
|
|
146
|
-
- "Typescript"
|
|
147
|
-
- - This "Typescript" projects uses Node's new flag `--experimental-strip-types`. This is means we can run `node index.ts` directly!
|
|
148
|
-
|
|
149
|
-
### Use with JS-Confuser
|
|
150
|
-
|
|
151
|
-
JS-Confuser is recommended to be applied *after* virtualizing your source code. JS-Confuser's CFF can safeguard and obfuscate your VM internals - adding a layer of obscurity and preventing analysis of the opcodes.
|
|
152
|
-
|
|
153
|
-
### WIP
|
|
154
|
-
|
|
155
|
-
- 120 tests, 90.16% coverage
|
|
156
|
-
- [Test262 (es5-tests)](https://github.com/tc39/test262/tree/es5-tests) percentage: 43.26%
|
|
157
|
-
|
|
158
|
-
### Made with AI
|
|
159
|
-
|
|
160
|
-
This project has been created with the help of AI. Expect issues.
|
|
161
|
-
|
|
162
|
-
### License
|
|
163
|
-
|
|
164
|
-
MIT License
|
package/input.js
DELETED
package/minify.js
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import ClosureCompiler from "google-closure-compiler";
|
|
2
|
-
|
|
3
|
-
const compiler = new ClosureCompiler({
|
|
4
|
-
js: "output.js",
|
|
5
|
-
js_output_file: "output.min.js",
|
|
6
|
-
compilation_level: "ADVANCED",
|
|
7
|
-
|
|
8
|
-
warning_level: "QUIET",
|
|
9
|
-
env: "CUSTOM", // removes all default externs
|
|
10
|
-
externs: "minify_empty_externs.js", // pass a blank file to satisfy the flag
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
compiler.run((exitCode, stdOut, stdErr) => {
|
|
14
|
-
if (stdErr) console.error(stdErr);
|
|
15
|
-
if (exitCode !== 0) process.exit(exitCode);
|
|
16
|
-
console.log("Done -> output.min.js");
|
|
17
|
-
});
|
package/minify_empty_externs.js
DELETED
package/obfuscate.js
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import JsConfuser from "../js-confuser/dist/index.js";
|
|
2
|
-
import { readFileSync, writeFileSync } from "fs";
|
|
3
|
-
|
|
4
|
-
const minified = readFileSync("output.min.js", "utf-8");
|
|
5
|
-
|
|
6
|
-
JsConfuser.obfuscate(minified, {
|
|
7
|
-
target: "browser",
|
|
8
|
-
renameVariables: true,
|
|
9
|
-
controlFlowFlattening: true,
|
|
10
|
-
}).then((result) => {
|
|
11
|
-
writeFileSync("output.obf.js", result.code, "utf-8");
|
|
12
|
-
});
|
package/src/index.js
DELETED
package/src/random.js
DELETED