js-confuser-vm 0.1.0 → 0.1.1
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 +23 -0
- package/README.md +75 -94
- package/bench.ts +146 -0
- package/disassemble.ts +12 -0
- package/dist/build-runtime.js +41 -15
- package/dist/compiler.js +134 -60
- package/dist/disassembler.js +317 -0
- package/dist/index.js +7 -2
- package/dist/runtime.js +68 -46
- package/dist/template.js +116 -0
- package/dist/transforms/bytecode/aliasedOpcodes.js +4 -1
- package/dist/transforms/bytecode/controlFlowFlattening.js +451 -0
- package/dist/transforms/bytecode/dispatcher.js +13 -109
- package/dist/transforms/bytecode/macroOpcodes.js +2 -2
- package/dist/transforms/bytecode/resolveConstants.js +100 -0
- package/dist/transforms/bytecode/resolveRegisters.js +4 -0
- package/dist/transforms/bytecode/semanticOpcodes.js +162 -0
- package/dist/transforms/bytecode/specializedOpcodes.js +18 -10
- package/dist/transforms/bytecode/stringConcealing.js +110 -0
- package/dist/transforms/runtime/classObfuscation.js +43 -0
- package/dist/transforms/runtime/handlerTable.js +91 -0
- package/dist/transforms/runtime/semanticOpcodes.js +35 -0
- package/dist/transforms/runtime/specializedOpcodes.js +11 -5
- package/dist/types.js +1 -1
- package/dist/utils/ast-utils.js +14 -0
- package/dist/utils/op-utils.js +0 -2
- package/dist/utils/pass-utils.js +100 -0
- package/dist/utils/profile-utils.js +3 -0
- package/index.ts +22 -17
- package/jest.config.js +14 -2
- package/output.disassembled.js +41 -0
- package/package.json +2 -1
- package/src/build-runtime.ts +113 -78
- package/src/compiler.ts +2703 -2593
- package/src/disassembler.ts +329 -0
- package/src/index.ts +12 -2
- package/src/options.ts +7 -1
- package/src/runtime.ts +84 -51
- package/src/template.ts +125 -1
- package/src/transforms/bytecode/aliasedOpcodes.ts +4 -1
- package/src/transforms/bytecode/controlFlowFlattening.ts +566 -0
- package/src/transforms/bytecode/dispatcher.ts +19 -125
- package/src/transforms/bytecode/macroOpcodes.ts +2 -2
- package/src/transforms/bytecode/resolveRegisters.ts +5 -0
- package/src/transforms/bytecode/specializedOpcodes.ts +22 -11
- package/src/transforms/bytecode/stringConcealing.ts +130 -0
- package/src/transforms/runtime/classObfuscation.ts +59 -0
- package/src/transforms/runtime/specializedOpcodes.ts +14 -9
- package/src/types.ts +42 -1
- package/src/utils/ast-utils.ts +19 -0
- package/src/utils/op-utils.ts +0 -2
- package/src/utils/pass-utils.ts +126 -0
- package/src/utils/profile-utils.ts +3 -0
- package/tsconfig.json +1 -1
- package/src/transforms/bytecode/microOpcodes.ts +0 -291
- package/src/transforms/runtime/internalVariables.ts +0 -270
- package/src/transforms/runtime/microOpcodes.ts +0 -93
- /package/src/transforms/bytecode/{resolveContants.ts → resolveConstants.ts} +0 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import * as t from "@babel/types";
|
|
2
|
+
import { shuffle } from "../../utils/random-utils.js";
|
|
3
|
+
function hasComment(node, text) {
|
|
4
|
+
const all = [...(node.leadingComments ?? []), ...(node.innerComments ?? []), ...(node.trailingComments ?? [])];
|
|
5
|
+
return all.some(c => c.value.includes(text));
|
|
6
|
+
}
|
|
7
|
+
function isPrototypeAssignment(stmt) {
|
|
8
|
+
if (!t.isExpressionStatement(stmt)) return false;
|
|
9
|
+
const expr = stmt.expression;
|
|
10
|
+
if (!t.isAssignmentExpression(expr)) return false;
|
|
11
|
+
const left = expr.left;
|
|
12
|
+
return t.isMemberExpression(left) && t.isMemberExpression(left.object) && t.isIdentifier(left.object.property, {
|
|
13
|
+
name: "prototype"
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
export function applyClassObfuscation(ast, _compiler) {
|
|
17
|
+
const body = ast.program.body;
|
|
18
|
+
|
|
19
|
+
// Split at the first statement that carries the @BOOT comment.
|
|
20
|
+
// Everything from that statement onward is the boot section and must stay last.
|
|
21
|
+
let bootIdx = body.findIndex(stmt => hasComment(stmt, "@BOOT"));
|
|
22
|
+
if (bootIdx === -1) bootIdx = body.length;
|
|
23
|
+
const shufflable = body.slice(0, bootIdx);
|
|
24
|
+
const boot = body.slice(bootIdx);
|
|
25
|
+
|
|
26
|
+
// Partition the shufflable section into two independent groups.
|
|
27
|
+
// Group A: variable/function declarations (constructors, standalone vars).
|
|
28
|
+
// Group B: prototype method assignments (X.prototype.Y = ...).
|
|
29
|
+
// Both groups are shuffled independently; A always precedes B so that
|
|
30
|
+
// constructors are defined before methods reference them.
|
|
31
|
+
const varDecls = [];
|
|
32
|
+
const methodDefs = [];
|
|
33
|
+
for (const stmt of shufflable) {
|
|
34
|
+
if (isPrototypeAssignment(stmt)) {
|
|
35
|
+
methodDefs.push(stmt);
|
|
36
|
+
} else {
|
|
37
|
+
varDecls.push(stmt);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
shuffle(varDecls);
|
|
41
|
+
shuffle(methodDefs);
|
|
42
|
+
ast.program.body = [...varDecls, ...methodDefs, ...boot];
|
|
43
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import * as t from "@babel/types";
|
|
2
|
+
import traverseImport from "@babel/traverse";
|
|
3
|
+
import { ok } from "assert";
|
|
4
|
+
import { parse } from "@babel/parser";
|
|
5
|
+
const traverse = traverseImport.default || traverseImport;
|
|
6
|
+
|
|
7
|
+
// Converts the switch-case dispatch into a handler table:
|
|
8
|
+
//
|
|
9
|
+
// Before (in .run):
|
|
10
|
+
// switch(op) { case OP.ADD: { ... break; } default: { ... } }
|
|
11
|
+
//
|
|
12
|
+
// After (in .init):
|
|
13
|
+
// this[OP.ADD] = function(){ ... }
|
|
14
|
+
// this["default"] = function(){ ... }
|
|
15
|
+
//
|
|
16
|
+
// After (in .run, replacing the switch):
|
|
17
|
+
// if(!this[op]) this["default"]();
|
|
18
|
+
// else this[op]();
|
|
19
|
+
//
|
|
20
|
+
export function applyHandlerTable(ast) {
|
|
21
|
+
// 1. Find the @SWITCH statement
|
|
22
|
+
let switchPath = null;
|
|
23
|
+
traverse(ast, {
|
|
24
|
+
SwitchStatement(path) {
|
|
25
|
+
if (path.node.leadingComments?.some(c => c.value.includes("@SWITCH"))) {
|
|
26
|
+
switchPath = path;
|
|
27
|
+
path.stop();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
ok(switchPath, "Could not find opcode handlers switch statement");
|
|
32
|
+
const switchNode = switchPath.node;
|
|
33
|
+
const discriminant = switchNode.discriminant; // `op`
|
|
34
|
+
|
|
35
|
+
// 2. Find the @INIT method
|
|
36
|
+
let initPath = null;
|
|
37
|
+
traverse(ast, {
|
|
38
|
+
BlockStatement(path) {
|
|
39
|
+
if (path.node.innerComments?.some(c => c.value.includes("@INIT"))) {
|
|
40
|
+
initPath = path;
|
|
41
|
+
path.stop();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
ok(initPath, "Could not find @INIT method");
|
|
46
|
+
const initFn = initPath.parentPath;
|
|
47
|
+
|
|
48
|
+
// 3. Build handler assignments for each case
|
|
49
|
+
const handlerAssignments = [];
|
|
50
|
+
for (const switchCase of switchNode.cases) {
|
|
51
|
+
// Strip trailing `break` from body
|
|
52
|
+
let body = [...switchCase.consequent];
|
|
53
|
+
if (body.length === 1 && t.isBlockStatement(body[0])) {
|
|
54
|
+
body = body[0].body;
|
|
55
|
+
}
|
|
56
|
+
if (body.length > 0 && t.isBreakStatement(body[body.length - 1])) {
|
|
57
|
+
body.pop();
|
|
58
|
+
}
|
|
59
|
+
body.unshift(...parse("var frame = this._currentFrame; var base = frame._base; var pc = frame._pc - 1; var regs = this._regs; ").program.body);
|
|
60
|
+
const block = t.blockStatement(body);
|
|
61
|
+
traverse(block, {
|
|
62
|
+
noScope: true,
|
|
63
|
+
ThisExpression(path) {
|
|
64
|
+
path.replaceWith(t.identifier("_this"));
|
|
65
|
+
},
|
|
66
|
+
Function(path) {
|
|
67
|
+
path.skip();
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Key: the case test, or "default" for the default case
|
|
72
|
+
const key = switchCase.test ? switchCase.test : t.stringLiteral("default");
|
|
73
|
+
|
|
74
|
+
// this[key] = function(){ ...body }
|
|
75
|
+
const handlerFn = t.functionExpression(null, [], block);
|
|
76
|
+
const assignment = t.expressionStatement(t.assignmentExpression("=", t.memberExpression(t.thisExpression(), key, true), handlerFn));
|
|
77
|
+
handlerAssignments.push(assignment);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 4. Inject handler assignments into the @INIT body
|
|
81
|
+
initPath.node.body = handlerAssignments;
|
|
82
|
+
|
|
83
|
+
// 5. Replace the switch statement with handler dispatch:
|
|
84
|
+
// if(!this[op]) this["default"]();
|
|
85
|
+
// else this[op]();
|
|
86
|
+
const thisLookup = t.memberExpression(t.thisExpression(), discriminant, true);
|
|
87
|
+
const defaultCall = t.expressionStatement(t.callExpression(t.memberExpression(t.thisExpression(), t.stringLiteral("default"), true), []));
|
|
88
|
+
const handlerCall = t.expressionStatement(t.callExpression(thisLookup, []));
|
|
89
|
+
const dispatch = t.ifStatement(t.unaryExpression("!", thisLookup), t.blockStatement([defaultCall]), t.blockStatement([handlerCall]));
|
|
90
|
+
switchPath.replaceWith(dispatch);
|
|
91
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { parse } from "@babel/parser";
|
|
2
|
+
import * as t from "@babel/types";
|
|
3
|
+
import traverseImport from "@babel/traverse";
|
|
4
|
+
import { ok } from "assert";
|
|
5
|
+
const traverse = traverseImport.default || traverseImport;
|
|
6
|
+
function parseStatements(code) {
|
|
7
|
+
const parsed = parse(code, {
|
|
8
|
+
sourceType: "unambiguous"
|
|
9
|
+
});
|
|
10
|
+
const [statement] = parsed.program.body;
|
|
11
|
+
ok(statement && t.isBlockStatement(statement), "Expected semantic opcode block");
|
|
12
|
+
return statement.body;
|
|
13
|
+
}
|
|
14
|
+
export function applySemanticOpcodes(ast, compiler) {
|
|
15
|
+
let switchStatement = null;
|
|
16
|
+
traverse(ast, {
|
|
17
|
+
SwitchStatement(path) {
|
|
18
|
+
if (path.node.leadingComments?.some(c => c.value.includes("@SWITCH"))) {
|
|
19
|
+
switchStatement = path.node;
|
|
20
|
+
path.stop();
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
ok(switchStatement, "Could not find @SWITCH statement for semantic opcodes");
|
|
25
|
+
for (const [semanticOpStr, info] of Object.entries(compiler.SEMANTIC_OPS)) {
|
|
26
|
+
const semanticOpCode = Number(semanticOpStr);
|
|
27
|
+
const originalName = compiler.OP_NAME[info.originalOp] ?? `OP_${info.originalOp}`;
|
|
28
|
+
const bodyStmts = parseStatements(info.code).map(statement => t.cloneNode(statement, true));
|
|
29
|
+
if (bodyStmts.length > 0) {
|
|
30
|
+
t.addComment(bodyStmts[0], "leading", ` ${compiler.OP_NAME[semanticOpCode]} -> ${originalName}`, true);
|
|
31
|
+
}
|
|
32
|
+
bodyStmts.push(t.breakStatement());
|
|
33
|
+
switchStatement.cases.push(t.switchCase(t.numericLiteral(semanticOpCode), [t.blockStatement(bodyStmts)]));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -20,7 +20,9 @@ function extractCaseBody(switchCase) {
|
|
|
20
20
|
// Because specialized opcodes are only created for instructions that have
|
|
21
21
|
// *exactly one* numeric operand, every `_operand()` call inside the original
|
|
22
22
|
// handler is replaced by the constant value that was baked into the opcode.
|
|
23
|
-
function inlineFixedOperands(
|
|
23
|
+
function inlineFixedOperands(newName,
|
|
24
|
+
// for debugging
|
|
25
|
+
info, bodyStmts, resolvedValues) {
|
|
24
26
|
// Wrap the statements in a temporary BlockStatement so traverse has a root.
|
|
25
27
|
// The replacement mutates the original statement objects in place.
|
|
26
28
|
var replaced = 0;
|
|
@@ -46,7 +48,10 @@ function inlineFixedOperands(bodyStmts, resolvedValues) {
|
|
|
46
48
|
}
|
|
47
49
|
}
|
|
48
50
|
});
|
|
49
|
-
|
|
51
|
+
if (replaced !== resolvedValues.length) {
|
|
52
|
+
console.error(resolvedValues, info);
|
|
53
|
+
throw new Error(`Specialized Opcode Inline Error: Given ${resolvedValues.length} operands to replace, but only found ${replaced} for ${newName}`);
|
|
54
|
+
}
|
|
50
55
|
}
|
|
51
56
|
|
|
52
57
|
// Append a generated switch case for every entry in compiler.SPECIALIZED_OPS.
|
|
@@ -84,18 +89,19 @@ export function applySpecializedOpcodes(ast, compiler) {
|
|
|
84
89
|
const bodyStmts = extractCaseBody(originalCase).map(s => t.cloneNode(s, true));
|
|
85
90
|
const placedOperands = info.operands;
|
|
86
91
|
ok(placedOperands, `Could not find operand for original opcode ${newName}`);
|
|
87
|
-
const resolvedValues = placedOperands
|
|
92
|
+
const resolvedValues = placedOperands
|
|
93
|
+
// .filter((x) => !(x as any)?.placeholder)
|
|
94
|
+
.map(placedOperand => {
|
|
88
95
|
return placedOperand?.resolvedValue ?? placedOperand;
|
|
89
96
|
});
|
|
90
97
|
if (resolvedValues.find(v => typeof v !== "number")) {
|
|
91
|
-
console.error(info);
|
|
92
98
|
throw new Error("Expected all resolved operand values to be numbers");
|
|
93
99
|
}
|
|
94
100
|
newName = `${originalName}_${resolvedValues.join("_")}`;
|
|
95
101
|
compiler.OP_NAME[specialOpCode] = newName;
|
|
96
102
|
|
|
97
103
|
// Replace this._operand() with the baked-in constant
|
|
98
|
-
inlineFixedOperands(bodyStmts, resolvedValues);
|
|
104
|
+
inlineFixedOperands(newName, info, bodyStmts, resolvedValues);
|
|
99
105
|
|
|
100
106
|
// Add a leading comment so the generated source stays readable
|
|
101
107
|
if (bodyStmts.length > 0) {
|
package/dist/types.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
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]
|
|
2
|
+
// Real instruction: [OP.ADD, 5] or multi-operand: [OP.MAKE_CLOSURE, labelRef, 2, 3, 0, 0]
|
|
3
3
|
// IR instruction: [null, { type: "defineLabel", label: "FN_ENTRY_1" }]
|
|
4
4
|
|
|
5
5
|
// IR instructions are used to hold symbolic information during compilation
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import traverseImport from "@babel/traverse";
|
|
2
|
+
const traverse = traverseImport.default || traverseImport;
|
|
3
|
+
export function getSwitchStatement(ast) {
|
|
4
|
+
let switchStatement = null;
|
|
5
|
+
traverse(ast, {
|
|
6
|
+
SwitchStatement(path) {
|
|
7
|
+
if (path.node.leadingComments?.some(c => c.value.includes("@SWITCH"))) {
|
|
8
|
+
switchStatement = path.node;
|
|
9
|
+
path.stop();
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
return switchStatement;
|
|
14
|
+
}
|
package/dist/utils/op-utils.js
CHANGED
|
@@ -13,7 +13,6 @@ export function nextFreeSlot(compiler) {
|
|
|
13
13
|
while (attempts++ < 512) {
|
|
14
14
|
const candidate = getRandomInt(0, U16_MAX);
|
|
15
15
|
if (!usedOpcodes.has(candidate)) {
|
|
16
|
-
usedOpcodes.add(candidate);
|
|
17
16
|
return candidate;
|
|
18
17
|
}
|
|
19
18
|
}
|
|
@@ -24,7 +23,6 @@ export function nextFreeSlot(compiler) {
|
|
|
24
23
|
for (let i = 0; i <= U16_MAX; i++) {
|
|
25
24
|
const v = start + i & U16_MAX;
|
|
26
25
|
if (!usedOpcodes.has(v)) {
|
|
27
|
-
usedOpcodes.add(v);
|
|
28
26
|
return v;
|
|
29
27
|
}
|
|
30
28
|
}
|
|
@@ -0,0 +1,100 @@
|
|
|
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 * as b from "../types.js";
|
|
8
|
+
// Return a fresh RegisterOperand object with the same (id, fnId).
|
|
9
|
+
// IMPORTANT: operand objects must be unique throughout compilation —
|
|
10
|
+
// other passes (e.g. specializedOpcodes) mutate operands in-place and a
|
|
11
|
+
// shared reference would corrupt both sites.
|
|
12
|
+
export function ref(r) {
|
|
13
|
+
return b.registerOperand(r.id, r.fnId);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Scan bc and return the highest virtual register id seen for each fnId.
|
|
17
|
+
// Used by passes that allocate new registers after the compiler has finished.
|
|
18
|
+
export function buildMaxIdMap(bc) {
|
|
19
|
+
const maxId = new Map();
|
|
20
|
+
for (const instr of bc) {
|
|
21
|
+
for (let j = 1; j < instr.length; j++) {
|
|
22
|
+
const op = instr[j];
|
|
23
|
+
if (op && op.type === "register") {
|
|
24
|
+
const cur = maxId.get(op.fnId) ?? -1;
|
|
25
|
+
if (op.id > cur) maxId.set(op.fnId, op.id);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return maxId;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Allocate the next virtual register id for fnId, updating maxId in-place.
|
|
33
|
+
export function allocReg(fnId, maxId) {
|
|
34
|
+
const next = (maxId.get(fnId) ?? -1) + 1;
|
|
35
|
+
maxId.set(fnId, next);
|
|
36
|
+
return b.registerOperand(next, fnId);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Return the label string if the operand is a { type:"label" } object,
|
|
40
|
+
// otherwise return null. Used by passes that need to identify jump targets.
|
|
41
|
+
export function extractLabel(op) {
|
|
42
|
+
if (op && typeof op === "object" && op.type === "label") return op.label;
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Walk bc, call transform() for every function body, and reassemble the output.
|
|
47
|
+
//
|
|
48
|
+
// For each function entry label the scanner collects all instructions up to the
|
|
49
|
+
// next entry label (or end-of-bytecode) into fnInstrs and passes them to
|
|
50
|
+
// transform() along with the function's fnId.
|
|
51
|
+
//
|
|
52
|
+
// The transform callback returns:
|
|
53
|
+
// instrs — the (possibly rewritten) function body to emit in place of fnInstrs
|
|
54
|
+
// tail — optional bytecode to append AFTER all function bodies
|
|
55
|
+
// (e.g. template-compiled decode closures)
|
|
56
|
+
//
|
|
57
|
+
// Instructions that appear before any entry label (the top-level preamble) are
|
|
58
|
+
// passed through unchanged.
|
|
59
|
+
export function forEachFunction(bc, compiler, transform) {
|
|
60
|
+
const entryLabels = new Set(compiler.fnDescriptors.map(d => d.entryLabel));
|
|
61
|
+
const entryLabelToFnId = new Map(compiler.fnDescriptors.map(d => [d.entryLabel, d._fnIdx]));
|
|
62
|
+
const result = [];
|
|
63
|
+
const tails = [];
|
|
64
|
+
let i = 0;
|
|
65
|
+
while (i < bc.length) {
|
|
66
|
+
const instr = bc[i];
|
|
67
|
+
const [op, operand0] = instr;
|
|
68
|
+
const isEntryLabel = op === null && operand0?.type === "defineLabel" && entryLabels.has(operand0.label);
|
|
69
|
+
if (!isEntryLabel) {
|
|
70
|
+
result.push(instr);
|
|
71
|
+
i++;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const entryLabel = operand0.label;
|
|
75
|
+
const fnId = entryLabelToFnId.get(entryLabel);
|
|
76
|
+
i++; // step past the defineLabel itself
|
|
77
|
+
|
|
78
|
+
const fnInstrs = [];
|
|
79
|
+
while (i < bc.length) {
|
|
80
|
+
const next = bc[i];
|
|
81
|
+
const [nextOp, nextOp0] = next;
|
|
82
|
+
if (nextOp === null && nextOp0?.type === "defineLabel" && entryLabels.has(nextOp0.label)) break;
|
|
83
|
+
fnInstrs.push(next);
|
|
84
|
+
i++;
|
|
85
|
+
}
|
|
86
|
+
result.push(instr); // emit the entry defineLabel
|
|
87
|
+
const {
|
|
88
|
+
instrs,
|
|
89
|
+
tail
|
|
90
|
+
} = transform(fnInstrs, fnId);
|
|
91
|
+
result.push(...instrs);
|
|
92
|
+
if (tail && tail.length > 0) tails.push(tail);
|
|
93
|
+
}
|
|
94
|
+
for (const tail of tails) {
|
|
95
|
+
result.push(...tail);
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
bytecode: result
|
|
99
|
+
};
|
|
100
|
+
}
|
package/index.ts
CHANGED
|
@@ -7,32 +7,37 @@ async function main() {
|
|
|
7
7
|
|
|
8
8
|
const { code: orginalOutput } = await JsConfuserVM.obfuscate(sourceCode, {});
|
|
9
9
|
|
|
10
|
-
const
|
|
10
|
+
const result = await JsConfuserVM.obfuscate(sourceCode, {
|
|
11
11
|
target: "browser", // or "node"
|
|
12
|
-
randomizeOpcodes: true, // randomize the opcode numbers?
|
|
13
|
-
shuffleOpcodes: true, // shuffle order of opcode handlers in the runtime?
|
|
14
|
-
encodeBytecode: true, // encode the bytecode array?
|
|
15
|
-
concealConstants: true, // conceal strings and integers in the constant pool?
|
|
16
|
-
dispatcher: true, // create middleman blocks to process jumps?
|
|
17
|
-
selfModifying: true, // do self-modifying bytecode for function bodies?
|
|
18
|
-
macroOpcodes: true, // create combined opcodes for repeated instruction sequences?
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
12
|
+
// randomizeOpcodes: true, // randomize the opcode numbers?
|
|
13
|
+
// shuffleOpcodes: true, // shuffle order of opcode handlers in the runtime?
|
|
14
|
+
// encodeBytecode: true, // encode the bytecode array?
|
|
15
|
+
// concealConstants: true, // conceal strings and integers in the constant pool?
|
|
16
|
+
// dispatcher: true, // create middleman blocks to process jumps?
|
|
17
|
+
// selfModifying: true, // do self-modifying bytecode for function bodies?
|
|
18
|
+
// macroOpcodes: true, // create combined opcodes for repeated instruction sequences?
|
|
19
|
+
// specializedOpcodes: true, // create specialized opcodes for commonly used opcode+operand pairs?
|
|
20
|
+
// aliasedOpcodes: true, // create duplicate opcodes for commonly used opcodes?
|
|
21
|
+
// timingChecks: true, // add timing checks to detect debuggers?
|
|
22
|
+
// minify: true, // pass final output through Google Closure Compiler? (Renames VM class properties)
|
|
23
|
+
controlFlowFlattening: true,
|
|
24
|
+
// stringConcealing: true,
|
|
25
|
+
verbose: true,
|
|
26
|
+
// classObfuscation: true,
|
|
24
27
|
});
|
|
25
28
|
|
|
26
29
|
writeFileSync("output.original.js", orginalOutput, "utf-8");
|
|
27
|
-
writeFileSync("output.js",
|
|
30
|
+
writeFileSync("output.js", result.code, "utf-8");
|
|
31
|
+
|
|
32
|
+
console.log("Saved");
|
|
28
33
|
|
|
29
34
|
// Eval the code like our test suite does
|
|
30
35
|
var window = { TEST_OUTPUT: null };
|
|
31
|
-
eval(
|
|
36
|
+
eval(result.code);
|
|
32
37
|
console.log(window.TEST_OUTPUT);
|
|
33
38
|
|
|
34
|
-
|
|
35
|
-
|
|
39
|
+
delete result.code;
|
|
40
|
+
console.log(JSON.stringify(result));
|
|
36
41
|
}
|
|
37
42
|
|
|
38
43
|
main();
|
package/jest.config.js
CHANGED
|
@@ -6,7 +6,6 @@ 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 } },
|
|
10
9
|
{
|
|
11
10
|
displayName: "specializedOpcodes",
|
|
12
11
|
VM_OPTIONS: { specializedOpcodes: true },
|
|
@@ -23,6 +22,18 @@ const OPTIONS_MATRIX = [
|
|
|
23
22
|
displayName: "dispatcher",
|
|
24
23
|
VM_OPTIONS: { dispatcher: true },
|
|
25
24
|
},
|
|
25
|
+
{
|
|
26
|
+
displayName: "controlFlowFlattening",
|
|
27
|
+
VM_OPTIONS: { controlFlowFlattening: true },
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
displayName: "stringConcealing",
|
|
31
|
+
VM_OPTIONS: { stringConcealing: true },
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
displayName: "classObfuscation",
|
|
35
|
+
VM_OPTIONS: { classObfuscation: true },
|
|
36
|
+
},
|
|
26
37
|
{
|
|
27
38
|
displayName: "all",
|
|
28
39
|
VM_OPTIONS: {
|
|
@@ -32,11 +43,12 @@ const OPTIONS_MATRIX = [
|
|
|
32
43
|
selfModifying: true,
|
|
33
44
|
timingChecks: true,
|
|
34
45
|
macroOpcodes: true,
|
|
35
|
-
microOpcodes: true,
|
|
36
46
|
specializedOpcodes: true,
|
|
37
47
|
aliasedOpcodes: true,
|
|
38
48
|
concealConstants: true,
|
|
39
49
|
dispatcher: true,
|
|
50
|
+
stringConcealing: true,
|
|
51
|
+
classObfuscation: true,
|
|
40
52
|
},
|
|
41
53
|
},
|
|
42
54
|
];
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// fn_0_0:
|
|
2
|
+
r1 = 969
|
|
3
|
+
r2 = r1
|
|
4
|
+
// while_top_5:
|
|
5
|
+
r3 = 4439
|
|
6
|
+
r4 = r2 !== r3
|
|
7
|
+
if (!r4) goto: while_exit_6
|
|
8
|
+
r5 = 969
|
|
9
|
+
r6 = r2 === r5
|
|
10
|
+
if (!r6) goto: if_else_7
|
|
11
|
+
goto: cff_block_2
|
|
12
|
+
// if_else_7:
|
|
13
|
+
r7 = 1317
|
|
14
|
+
r8 = r2 === r7
|
|
15
|
+
if (!r8) goto: if_else_8
|
|
16
|
+
goto: cff_block_3
|
|
17
|
+
// if_else_8:
|
|
18
|
+
r9 = 58894
|
|
19
|
+
r10 = r2 === r9
|
|
20
|
+
if (!r10) goto: if_else_9
|
|
21
|
+
goto: if_else_1
|
|
22
|
+
// if_else_9:
|
|
23
|
+
goto: while_top_5
|
|
24
|
+
// while_exit_6:
|
|
25
|
+
// cff_block_3:
|
|
26
|
+
r11 = "Hello World"
|
|
27
|
+
r0 = r11
|
|
28
|
+
r2 = 58894
|
|
29
|
+
goto: while_top_5
|
|
30
|
+
// if_else_1:
|
|
31
|
+
r11 = undefined
|
|
32
|
+
return r11
|
|
33
|
+
// cff_block_2:
|
|
34
|
+
r0 = undefined
|
|
35
|
+
r11 = true
|
|
36
|
+
if (r11) goto: cff_skip_10
|
|
37
|
+
r2 = 58894
|
|
38
|
+
goto: while_top_5
|
|
39
|
+
// cff_skip_10:
|
|
40
|
+
r2 = 1317
|
|
41
|
+
goto: while_top_5
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "js-confuser-vm",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"build": "babel src --out-dir dist --extensions \".ts,.js\"",
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
"@babel/core": "^7.29.0",
|
|
37
37
|
"@babel/preset-env": "^7.29.0",
|
|
38
38
|
"@babel/preset-typescript": "^7.28.5",
|
|
39
|
+
"@types/jest": "^30.0.0",
|
|
39
40
|
"@types/node": "^25.3.0",
|
|
40
41
|
"babel-plugin-module-resolver": "^5.0.2",
|
|
41
42
|
"babel-plugin-replace-import-extension": "^1.1.5",
|