js-confuser-vm 0.1.1 → 0.1.2

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.
Files changed (58) hide show
  1. package/README.md +242 -89
  2. package/dist/compiler.js +583 -208
  3. package/dist/disassembler.js +58 -8
  4. package/dist/runtime.js +93 -74
  5. package/dist/template.js +81 -76
  6. package/dist/transforms/bytecode/concealConstants.js +2 -2
  7. package/dist/transforms/bytecode/controlFlowFlattening.js +143 -25
  8. package/dist/transforms/bytecode/dispatcher.js +3 -3
  9. package/dist/transforms/bytecode/resolveRegisters.js +19 -4
  10. package/dist/transforms/bytecode/selfModifying.js +88 -21
  11. package/dist/transforms/bytecode/specializedOpcodes.js +6 -3
  12. package/dist/transforms/bytecode/stringConcealing.js +253 -75
  13. package/dist/utils/ast-utils.js +61 -0
  14. package/dist/utils/op-utils.js +1 -0
  15. package/package.json +7 -1
  16. package/.gitmodules +0 -4
  17. package/.prettierignore +0 -1
  18. package/CHANGELOG.md +0 -358
  19. package/babel-plugin-inline-runtime.cjs +0 -34
  20. package/babel.config.json +0 -23
  21. package/bench.ts +0 -146
  22. package/disassemble.ts +0 -12
  23. package/index.ts +0 -43
  24. package/jest-strip-types.js +0 -10
  25. package/jest.config.js +0 -64
  26. package/output.disassembled.js +0 -41
  27. package/src/build-runtime.ts +0 -113
  28. package/src/compiler.ts +0 -2703
  29. package/src/disassembler.ts +0 -329
  30. package/src/index.ts +0 -24
  31. package/src/minify.ts +0 -21
  32. package/src/options.ts +0 -24
  33. package/src/runtime.ts +0 -956
  34. package/src/template.ts +0 -265
  35. package/src/transforms/bytecode/aliasedOpcodes.ts +0 -151
  36. package/src/transforms/bytecode/concealConstants.ts +0 -52
  37. package/src/transforms/bytecode/controlFlowFlattening.ts +0 -566
  38. package/src/transforms/bytecode/dispatcher.ts +0 -292
  39. package/src/transforms/bytecode/macroOpcodes.ts +0 -193
  40. package/src/transforms/bytecode/resolveConstants.ts +0 -126
  41. package/src/transforms/bytecode/resolveLabels.ts +0 -112
  42. package/src/transforms/bytecode/resolveRegisters.ts +0 -226
  43. package/src/transforms/bytecode/selfModifying.ts +0 -121
  44. package/src/transforms/bytecode/specializedOpcodes.ts +0 -164
  45. package/src/transforms/bytecode/stringConcealing.ts +0 -130
  46. package/src/transforms/runtime/aliasedOpcodes.ts +0 -191
  47. package/src/transforms/runtime/classObfuscation.ts +0 -59
  48. package/src/transforms/runtime/macroOpcodes.ts +0 -138
  49. package/src/transforms/runtime/minify.ts +0 -1
  50. package/src/transforms/runtime/shuffleOpcodes.ts +0 -24
  51. package/src/transforms/runtime/specializedOpcodes.ts +0 -161
  52. package/src/types.ts +0 -134
  53. package/src/utils/ast-utils.ts +0 -19
  54. package/src/utils/op-utils.ts +0 -46
  55. package/src/utils/pass-utils.ts +0 -126
  56. package/src/utils/profile-utils.ts +0 -3
  57. package/src/utils/random-utils.ts +0 -31
  58. package/tsconfig.json +0 -12
@@ -1,161 +0,0 @@
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 { getOpcodeToCaseMap } from "./macroOpcodes.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 inlineFixedOperands(
30
- newName: string, // for debugging
31
- info: any,
32
- bodyStmts: t.Statement[],
33
- resolvedValues: number[],
34
- ): void {
35
- // Wrap the statements in a temporary BlockStatement so traverse has a root.
36
- // The replacement mutates the original statement objects in place.
37
- var replaced = 0;
38
- function consumeOperand() {
39
- const resolvedValue = resolvedValues[replaced++];
40
- ok(
41
- typeof resolvedValue === "number",
42
- `Expected a numeric operand value, got ${resolvedValue}`,
43
- );
44
- return t.numericLiteral(resolvedValue);
45
- }
46
-
47
- traverse(t.blockStatement(bodyStmts), {
48
- noScope: true,
49
- CallExpression(path) {
50
- const callee = path.node.callee;
51
-
52
- const isMethodCall = (methodName) => {
53
- return (
54
- t.isMemberExpression(callee) &&
55
- t.isThisExpression(callee.object) &&
56
- t.isIdentifier(callee.property, { name: methodName }) &&
57
- path.node.arguments.length === 0
58
- );
59
- };
60
-
61
- if (isMethodCall("_operand")) {
62
- path.replaceWith(consumeOperand());
63
- }
64
-
65
- if (isMethodCall("_constant")) {
66
- path.node.arguments = [consumeOperand(), consumeOperand()];
67
- }
68
- },
69
- });
70
-
71
- if (replaced !== resolvedValues.length) {
72
- console.error(resolvedValues, info);
73
- throw new Error(
74
- `Specialized Opcode Inline Error: Given ${resolvedValues.length} operands to replace, but only found ${replaced} for ${newName}`,
75
- );
76
- }
77
- }
78
-
79
- // Append a generated switch case for every entry in compiler.SPECIALIZED_OPS.
80
- // Each case is a clone of the original opcode’s handler with `this._operand()`
81
- // replaced by the constant integer that was captured at compile time.
82
- // Must be called AFTER applyMacroOpcodes (so the original cases exist) but
83
- // BEFORE applyShuffleOpcodes so the new specialized cases also get shuffled.
84
- export function applySpecializedOpcodes(ast: t.File, compiler: Compiler): void {
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(
96
- switchStatement,
97
- "Could not find @SWITCH statement for specialized opcodes",
98
- );
99
-
100
- // Build a map opName → SwitchCase from the existing OP.xxx case tests.
101
- const opcodeToCaseMap = getOpcodeToCaseMap(switchStatement, compiler);
102
-
103
- if (!compiler.SPECIALIZED_OPS) return;
104
-
105
- for (const [specialOpStr, info] of Object.entries(compiler.SPECIALIZED_OPS)) {
106
- const specialOpCode = Number(specialOpStr);
107
- const { originalOp, operands } = info;
108
-
109
- let newName = compiler.OP_NAME[specialOpCode];
110
- const originalName = compiler.OP_NAME[originalOp];
111
- const originalCase = opcodeToCaseMap.get(originalOp);
112
-
113
- ok(
114
- originalCase,
115
- `Could not find original case for opcode ${originalName} (${originalOp})`,
116
- );
117
-
118
- // Clone the original handler body
119
- const bodyStmts: t.Statement[] = extractCaseBody(originalCase).map(
120
- (s) => t.cloneNode(s, true) as t.Statement,
121
- );
122
-
123
- const placedOperands = info.operands;
124
- ok(placedOperands, `Could not find operand for original opcode ${newName}`);
125
-
126
- const resolvedValues = placedOperands
127
- // .filter((x) => !(x as any)?.placeholder)
128
- .map((placedOperand) => {
129
- return (placedOperand as any)?.resolvedValue ?? placedOperand;
130
- });
131
-
132
- if (resolvedValues.find((v) => typeof v !== "number")) {
133
- throw new Error("Expected all resolved operand values to be numbers");
134
- }
135
-
136
- newName = `${originalName}_${resolvedValues.join("_")}`;
137
- compiler.OP_NAME[specialOpCode] = newName;
138
-
139
- // Replace this._operand() with the baked-in constant
140
- inlineFixedOperands(newName, info, bodyStmts, resolvedValues);
141
-
142
- // Add a leading comment so the generated source stays readable
143
- if (bodyStmts.length > 0) {
144
- t.addComment(
145
- bodyStmts[0],
146
- "leading",
147
- ` ${compiler.OP_NAME[specialOpCode]} (specialized)`,
148
- true,
149
- );
150
- }
151
-
152
- bodyStmts.push(t.breakStatement());
153
-
154
- // Insert the new specialized case into the big switch
155
- (switchStatement as t.SwitchStatement).cases.push(
156
- t.switchCase(t.numericLiteral(specialOpCode), [
157
- t.blockStatement(bodyStmts),
158
- ]),
159
- );
160
- }
161
- }
package/src/types.ts DELETED
@@ -1,134 +0,0 @@
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, 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
- // A virtual register reference emitted by the compiler.
10
- // fnId identifies which function's register file this belongs to.
11
- // resolveRegisters() replaces these with concrete slot indices (type:"number").
12
- export type RegisterOperand = Op<{
13
- type: "register";
14
- id: number;
15
- fnId: number;
16
- kind?: string;
17
- scopeId?: string | number;
18
- // If true, resolveRegisters always assigns this register to the "local::"
19
- // pool (no slot reuse). Set by passes that emit registers whose live ranges
20
- // span CFF dispatch-loop back-edges — regions the linear-scan liveness
21
- // analysis cannot reason about.
22
- pinned?: boolean;
23
- }>;
24
-
25
- // A placeholder for a function's concrete regCount, emitted in MAKE_CLOSURE.
26
- // resolveRegisters() fills resolvedValue once it knows the concrete slot count.
27
- export type FnRegCountOperand = Op<{ type: "fnRegCount"; fnId: number }>;
28
-
29
- // IR pseudo-instruction that marks the end of a register's live range.
30
- // Emitted as [null, FreeRegOperand] so it is dropped before final assembly.
31
- //
32
- // NOTE: resolveRegisters() already computes correct lastUse from the last real
33
- // operand appearance, so freeReg is EXTRANEOUS for any programmatically generated
34
- // IR — the scanner will find the tightest possible range without it.
35
- // It is only useful when a register has a late syntactic appearance that does
36
- // NOT reflect its true logical end-of-life (e.g. a read emitted purely for
37
- // side-effects long after the value is logically dead). No current pass in this
38
- // codebase emits freeReg; it is kept as an extension point only.
39
- export type FreeRegOperand = Op<{
40
- type: "freeReg";
41
- fnId: number;
42
- id: number;
43
- kind?: string;
44
- scopeId?: string | number;
45
- }>;
46
-
47
- export type InstrOperand =
48
- | number
49
- | Op<{ type: "number"; value?: number }>
50
- | Op<{
51
- type: "label";
52
- label: string;
53
- offset?: number;
54
- transform?: (resolvedPC: number) => number;
55
- }>
56
- | Op<{ type: "defineLabel"; label: string }>
57
- | Op<{ type: "constant"; value: any }>
58
- | RegisterOperand
59
- | FnRegCountOperand
60
- | FreeRegOperand;
61
-
62
- export interface Operand {
63
- type: string;
64
- placeholder?: boolean; // This operand will not be emitted in the final bytecode, but used as a reference
65
- resolvedValue?: number; // This operand knows its resolved value, but kept as a object to keep metadata info available
66
- }
67
-
68
- type Op<T extends object> = Operand & T;
69
-
70
- export type Instruction = [number | null, ...InstrOperand[]];
71
-
72
- export type Bytecode = Instruction[];
73
-
74
- export function constantOperand(value: any): Instruction[1] {
75
- return {
76
- type: "constant",
77
- value: value,
78
- };
79
- }
80
-
81
- export function registerOperand(
82
- id: number,
83
- fnId: number,
84
- metadata: Partial<Pick<RegisterOperand, "kind" | "scopeId">> = {},
85
- ): RegisterOperand {
86
- return { type: "register", id, fnId, ...metadata };
87
- }
88
-
89
- export function fnRegCountOperand(fnId: number): FnRegCountOperand {
90
- return { type: "fnRegCount", fnId };
91
- }
92
-
93
- export function freeRegOperand(reg: RegisterOperand): FreeRegOperand {
94
- const op: FreeRegOperand = { type: "freeReg", fnId: reg.fnId, id: reg.id };
95
- if (reg.kind !== undefined) op.kind = reg.kind;
96
- if (reg.scopeId !== undefined) op.scopeId = reg.scopeId;
97
- return op;
98
- }
99
-
100
- export interface ObfuscationResult {
101
- code: string;
102
- profileData?: {
103
- handlerCount: number;
104
-
105
- /**
106
- * How long the entire obfuscation process takes (ms)
107
- */
108
- obfuscationTime?: number;
109
-
110
- /**
111
- * How long @babel/parser takes (ms)
112
- */
113
- parseTime?: number;
114
-
115
- /**
116
- * How long @babel/generator takes (ms)
117
- */
118
- generateTime?: number;
119
-
120
- /**
121
- * How long the Compiler#compileAST takes (ms)
122
- */
123
- compileTime?: number;
124
-
125
- transforms: {
126
- [transformName: string]: {
127
- fileSize?: number;
128
- bytecodeSize?: number;
129
- transformTime?: number;
130
- handlerCount?: number;
131
- };
132
- };
133
- };
134
- }
@@ -1,19 +0,0 @@
1
- import * as t from "@babel/types";
2
- import traverseImport from "@babel/traverse";
3
-
4
- const traverse = (traverseImport.default ||
5
- traverseImport) as typeof traverseImport.default;
6
-
7
- export function getSwitchStatement(ast: t.File) {
8
- let switchStatement: t.SwitchStatement | null = null;
9
- traverse(ast, {
10
- SwitchStatement(path) {
11
- if (path.node.leadingComments?.some((c) => c.value.includes("@SWITCH"))) {
12
- switchStatement = path.node;
13
- path.stop();
14
- }
15
- },
16
- });
17
-
18
- return switchStatement;
19
- }
@@ -1,46 +0,0 @@
1
- import { getRandomInt } from "./random-utils.ts";
2
- import * as b from "../types.ts";
3
- import { Compiler } from "../compiler.ts";
4
-
5
- export const U16_MAX = 0xffff; // bytecode operands are u16
6
-
7
- /** Returns the next free opcode slot, or -1 when the space is exhausted. */
8
- export function nextFreeSlot(compiler: Compiler): number {
9
- // ── Collect used opcodes exactly as specified ─────────────────────────────
10
- const usedOpcodes = new Set<number>(
11
- Object.keys(compiler.OP_NAME)
12
- .map((k) => parseInt(k, 10))
13
- .filter((v) => !isNaN(v)) as number[],
14
- );
15
-
16
- if (usedOpcodes.size > U16_MAX) return -1;
17
-
18
- // Random opcode
19
- if (compiler.options.randomizeOpcodes) {
20
- let attempts = 0;
21
- while (attempts++ < 512) {
22
- const candidate = getRandomInt(0, U16_MAX);
23
- if (!usedOpcodes.has(candidate)) {
24
- return candidate;
25
- }
26
- }
27
- }
28
-
29
- // Fallback: linear scan from a random start
30
- const start = Object.keys(compiler.OP_NAME).length;
31
- for (let i = 0; i <= U16_MAX; i++) {
32
- const v = (start + i) & U16_MAX;
33
- if (!usedOpcodes.has(v)) {
34
- return v;
35
- }
36
- }
37
- return -1;
38
- }
39
-
40
- export function getInstructionSize(instr: b.Instruction): number {
41
- if (instr[0] === null) return 0;
42
-
43
- const size = instr.filter((op) => (op as any)?.placeholder !== true).length;
44
-
45
- return size;
46
- }
@@ -1,126 +0,0 @@
1
- // Shared utilities for bytecode transformation passes.
2
- //
3
- // All three patterns below are identical across dispatcher, controlFlowFlattening,
4
- // and stringConcealing. Centralising them here keeps each pass focused on its
5
- // own logic and makes the shared contract explicit.
6
-
7
- import type { Bytecode, RegisterOperand, InstrOperand } from "../types.ts";
8
- import * as b from "../types.ts";
9
- import { Compiler } from "../compiler.ts";
10
-
11
- // Return a fresh RegisterOperand object with the same (id, fnId).
12
- // IMPORTANT: operand objects must be unique throughout compilation —
13
- // other passes (e.g. specializedOpcodes) mutate operands in-place and a
14
- // shared reference would corrupt both sites.
15
- export function ref(r: RegisterOperand): RegisterOperand {
16
- return b.registerOperand(r.id, r.fnId);
17
- }
18
-
19
- // Scan bc and return the highest virtual register id seen for each fnId.
20
- // Used by passes that allocate new registers after the compiler has finished.
21
- export function buildMaxIdMap(bc: Bytecode): Map<number, number> {
22
- const maxId = new Map<number, number>();
23
- for (const instr of bc) {
24
- for (let j = 1; j < instr.length; j++) {
25
- const op = instr[j] as any;
26
- if (op && op.type === "register") {
27
- const cur = maxId.get(op.fnId) ?? -1;
28
- if (op.id > cur) maxId.set(op.fnId, op.id);
29
- }
30
- }
31
- }
32
- return maxId;
33
- }
34
-
35
- // Allocate the next virtual register id for fnId, updating maxId in-place.
36
- export function allocReg(
37
- fnId: number,
38
- maxId: Map<number, number>,
39
- ): RegisterOperand {
40
- const next = (maxId.get(fnId) ?? -1) + 1;
41
- maxId.set(fnId, next);
42
- return b.registerOperand(next, fnId);
43
- }
44
-
45
- // Return the label string if the operand is a { type:"label" } object,
46
- // otherwise return null. Used by passes that need to identify jump targets.
47
- export function extractLabel(op: InstrOperand | undefined): string | null {
48
- if (op && typeof op === "object" && (op as any).type === "label")
49
- return (op as any).label as string;
50
- return null;
51
- }
52
-
53
- // Walk bc, call transform() for every function body, and reassemble the output.
54
- //
55
- // For each function entry label the scanner collects all instructions up to the
56
- // next entry label (or end-of-bytecode) into fnInstrs and passes them to
57
- // transform() along with the function's fnId.
58
- //
59
- // The transform callback returns:
60
- // instrs — the (possibly rewritten) function body to emit in place of fnInstrs
61
- // tail — optional bytecode to append AFTER all function bodies
62
- // (e.g. template-compiled decode closures)
63
- //
64
- // Instructions that appear before any entry label (the top-level preamble) are
65
- // passed through unchanged.
66
- export function forEachFunction(
67
- bc: Bytecode,
68
- compiler: Compiler,
69
- transform: (
70
- fnInstrs: Bytecode,
71
- fnId: number,
72
- ) => { instrs: Bytecode; tail?: Bytecode },
73
- ): { bytecode: Bytecode } {
74
- const entryLabels = new Set(compiler.fnDescriptors.map((d) => d.entryLabel));
75
- const entryLabelToFnId = new Map(
76
- compiler.fnDescriptors.map((d) => [d.entryLabel!, d._fnIdx!]),
77
- );
78
-
79
- const result: Bytecode = [];
80
- const tails: Bytecode[] = [];
81
- let i = 0;
82
-
83
- while (i < bc.length) {
84
- const instr = bc[i];
85
- const [op, operand0] = instr;
86
- const isEntryLabel =
87
- op === null &&
88
- (operand0 as any)?.type === "defineLabel" &&
89
- entryLabels.has((operand0 as any).label);
90
-
91
- if (!isEntryLabel) {
92
- result.push(instr);
93
- i++;
94
- continue;
95
- }
96
-
97
- const entryLabel = (operand0 as any).label as string;
98
- const fnId = entryLabelToFnId.get(entryLabel)!;
99
- i++; // step past the defineLabel itself
100
-
101
- const fnInstrs: Bytecode = [];
102
- while (i < bc.length) {
103
- const next = bc[i];
104
- const [nextOp, nextOp0] = next;
105
- if (
106
- nextOp === null &&
107
- (nextOp0 as any)?.type === "defineLabel" &&
108
- entryLabels.has((nextOp0 as any).label)
109
- )
110
- break;
111
- fnInstrs.push(next);
112
- i++;
113
- }
114
-
115
- result.push(instr); // emit the entry defineLabel
116
- const { instrs, tail } = transform(fnInstrs, fnId);
117
- result.push(...instrs);
118
- if (tail && tail.length > 0) tails.push(tail);
119
- }
120
-
121
- for (const tail of tails) {
122
- result.push(...tail);
123
- }
124
-
125
- return { bytecode: result };
126
- }
@@ -1,3 +0,0 @@
1
- export function now() {
2
- return performance?.now() || Date.now();
3
- }
@@ -1,31 +0,0 @@
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/tsconfig.json DELETED
@@ -1,12 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "esnext",
4
- "module": "nodenext",
5
- "outDir": "./dist",
6
- "rootDir": "./",
7
- "strict": false,
8
- "noEmit": true,
9
- "types": ["node", "jest"],
10
- "allowImportingTsExtensions": true
11
- }
12
- }