js-confuser-vm 0.0.5 → 0.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +112 -2
- package/README.MD +249 -106
- package/dist/build-runtime.js +22 -3
- package/dist/compiler.js +864 -801
- package/dist/runtime.js +414 -333
- package/dist/transforms/bytecode/aliasedOpcodes.js +134 -0
- package/dist/transforms/bytecode/concealConstants.js +31 -0
- package/dist/transforms/bytecode/macroOpcodes.js +37 -23
- package/dist/transforms/bytecode/microOpcodes.js +236 -0
- package/dist/transforms/bytecode/resolveContants.js +69 -12
- package/dist/transforms/bytecode/resolveLabels.js +5 -3
- package/dist/transforms/bytecode/selfModifying.js +3 -2
- package/dist/transforms/bytecode/specializedOpcodes.js +54 -39
- package/dist/transforms/runtime/aliasedOpcodes.js +134 -0
- package/dist/transforms/runtime/internalVariables.js +202 -0
- package/dist/transforms/runtime/macroOpcodes.js +30 -18
- package/dist/transforms/runtime/microOpcodes.js +76 -0
- package/dist/transforms/runtime/shuffleOpcodes.js +1 -1
- package/dist/transforms/runtime/specializedOpcodes.js +36 -29
- package/dist/utils/op-utils.js +36 -0
- package/dist/utils/random-utils.js +27 -0
- package/index.ts +11 -8
- package/jest.config.js +12 -0
- package/package.json +1 -1
- package/src/build-runtime.ts +25 -4
- package/src/compiler.ts +2482 -2069
- package/src/options.ts +3 -0
- package/src/runtime.ts +842 -771
- package/src/transforms/bytecode/aliasedOpcodes.ts +148 -0
- package/src/transforms/bytecode/concealConstants.ts +52 -0
- package/src/transforms/bytecode/macroOpcodes.ts +49 -33
- package/src/transforms/bytecode/microOpcodes.ts +291 -0
- package/src/transforms/bytecode/resolveContants.ts +82 -18
- package/src/transforms/bytecode/resolveLabels.ts +5 -4
- package/src/transforms/bytecode/selfModifying.ts +3 -3
- package/src/transforms/bytecode/specializedOpcodes.ts +85 -46
- package/src/transforms/runtime/aliasedOpcodes.ts +191 -0
- package/src/transforms/runtime/internalVariables.ts +270 -0
- package/src/transforms/runtime/macroOpcodes.ts +47 -20
- package/src/transforms/runtime/microOpcodes.ts +93 -0
- package/src/transforms/runtime/shuffleOpcodes.ts +1 -1
- package/src/transforms/runtime/specializedOpcodes.ts +56 -46
- package/src/types.ts +1 -1
- package/src/utils/op-utils.ts +46 -0
- package/src/transforms/utils/op-utils.ts +0 -26
- package/src/utilts.ts +0 -3
- /package/src/{transforms/utils → utils}/random-utils.ts +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as t from "@babel/types";
|
|
2
2
|
import traverseImport from "@babel/traverse";
|
|
3
3
|
import { ok } from "assert";
|
|
4
|
+
import { getOpcodeToCaseMap } from "./macroOpcodes.js";
|
|
4
5
|
const traverse = traverseImport.default || traverseImport;
|
|
5
6
|
|
|
6
7
|
// Extract the real statement list from a SwitchCase consequent (identical to the
|
|
@@ -19,20 +20,33 @@ function extractCaseBody(switchCase) {
|
|
|
19
20
|
// Because specialized opcodes are only created for instructions that have
|
|
20
21
|
// *exactly one* numeric operand, every `_operand()` call inside the original
|
|
21
22
|
// handler is replaced by the constant value that was baked into the opcode.
|
|
22
|
-
function
|
|
23
|
+
function inlineFixedOperands(bodyStmts, resolvedValues) {
|
|
23
24
|
// Wrap the statements in a temporary BlockStatement so traverse has a root.
|
|
24
25
|
// The replacement mutates the original statement objects in place.
|
|
26
|
+
var replaced = 0;
|
|
27
|
+
function consumeOperand() {
|
|
28
|
+
const resolvedValue = resolvedValues[replaced++];
|
|
29
|
+
ok(typeof resolvedValue === "number", `Expected a numeric operand value, got ${resolvedValue}`);
|
|
30
|
+
return t.numericLiteral(resolvedValue);
|
|
31
|
+
}
|
|
25
32
|
traverse(t.blockStatement(bodyStmts), {
|
|
26
33
|
noScope: true,
|
|
27
34
|
CallExpression(path) {
|
|
28
35
|
const callee = path.node.callee;
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
path.
|
|
36
|
+
const isMethodCall = methodName => {
|
|
37
|
+
return t.isMemberExpression(callee) && t.isThisExpression(callee.object) && t.isIdentifier(callee.property, {
|
|
38
|
+
name: methodName
|
|
39
|
+
}) && path.node.arguments.length === 0;
|
|
40
|
+
};
|
|
41
|
+
if (isMethodCall("_operand")) {
|
|
42
|
+
path.replaceWith(consumeOperand());
|
|
43
|
+
}
|
|
44
|
+
if (isMethodCall("_constant")) {
|
|
45
|
+
path.node.arguments = [consumeOperand(), consumeOperand()];
|
|
33
46
|
}
|
|
34
47
|
}
|
|
35
48
|
});
|
|
49
|
+
ok(replaced === resolvedValues.length, `Expected to replace ${resolvedValues.length} operands, but replaced ${replaced}`);
|
|
36
50
|
}
|
|
37
51
|
|
|
38
52
|
// Append a generated switch case for every entry in compiler.SPECIALIZED_OPS.
|
|
@@ -40,7 +54,7 @@ function inlineFixedOperand(bodyStmts, resolvedValue) {
|
|
|
40
54
|
// replaced by the constant integer that was captured at compile time.
|
|
41
55
|
// Must be called AFTER applyMacroOpcodes (so the original cases exist) but
|
|
42
56
|
// BEFORE applyShuffleOpcodes so the new specialized cases also get shuffled.
|
|
43
|
-
export function applySpecializedOpcodes(ast,
|
|
57
|
+
export function applySpecializedOpcodes(ast, compiler) {
|
|
44
58
|
let switchStatement = null;
|
|
45
59
|
traverse(ast, {
|
|
46
60
|
SwitchStatement(path) {
|
|
@@ -53,42 +67,35 @@ export function applySpecializedOpcodes(ast, bytecode, compiler) {
|
|
|
53
67
|
ok(switchStatement, "Could not find @SWITCH statement for specialized opcodes");
|
|
54
68
|
|
|
55
69
|
// Build a map opName → SwitchCase from the existing OP.xxx case tests.
|
|
56
|
-
const
|
|
57
|
-
for (const sc of switchStatement.cases) {
|
|
58
|
-
const test = sc.test;
|
|
59
|
-
if (test && t.isMemberExpression(test) && t.isIdentifier(test.object, {
|
|
60
|
-
name: "OP"
|
|
61
|
-
}) && t.isIdentifier(test.property)) {
|
|
62
|
-
nameToCaseMap.set(test.property.name, sc);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
70
|
+
const opcodeToCaseMap = getOpcodeToCaseMap(switchStatement, compiler);
|
|
65
71
|
if (!compiler.SPECIALIZED_OPS) return;
|
|
66
72
|
for (const [specialOpStr, info] of Object.entries(compiler.SPECIALIZED_OPS)) {
|
|
67
73
|
const specialOpCode = Number(specialOpStr);
|
|
68
74
|
const {
|
|
69
75
|
originalOp,
|
|
70
|
-
|
|
76
|
+
operands
|
|
71
77
|
} = info;
|
|
72
|
-
|
|
78
|
+
let newName = compiler.OP_NAME[specialOpCode];
|
|
73
79
|
const originalName = compiler.OP_NAME[originalOp];
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
if (!originalCase) continue;
|
|
80
|
+
const originalCase = opcodeToCaseMap.get(originalOp);
|
|
81
|
+
ok(originalCase, `Could not find original case for opcode ${originalName} (${originalOp})`);
|
|
77
82
|
|
|
78
83
|
// Clone the original handler body
|
|
79
84
|
const bodyStmts = extractCaseBody(originalCase).map(s => t.cloneNode(s, true));
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if (typeof
|
|
86
|
-
console.error(
|
|
85
|
+
const placedOperands = info.operands;
|
|
86
|
+
ok(placedOperands, `Could not find operand for original opcode ${newName}`);
|
|
87
|
+
const resolvedValues = placedOperands.map(placedOperand => {
|
|
88
|
+
return placedOperand?.resolvedValue ?? placedOperand;
|
|
89
|
+
});
|
|
90
|
+
if (resolvedValues.find(v => typeof v !== "number")) {
|
|
91
|
+
console.error(info);
|
|
92
|
+
throw new Error("Expected all resolved operand values to be numbers");
|
|
87
93
|
}
|
|
88
|
-
|
|
94
|
+
newName = `${originalName}_${resolvedValues.join("_")}`;
|
|
95
|
+
compiler.OP_NAME[specialOpCode] = newName;
|
|
89
96
|
|
|
90
97
|
// Replace this._operand() with the baked-in constant
|
|
91
|
-
|
|
98
|
+
inlineFixedOperands(bodyStmts, resolvedValues);
|
|
92
99
|
|
|
93
100
|
// Add a leading comment so the generated source stays readable
|
|
94
101
|
if (bodyStmts.length > 0) {
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { getRandomInt } from "./random-utils.js";
|
|
2
|
+
export const U16_MAX = 0xffff; // bytecode operands are u16
|
|
3
|
+
|
|
4
|
+
/** Returns the next free opcode slot, or -1 when the space is exhausted. */
|
|
5
|
+
export function nextFreeSlot(compiler) {
|
|
6
|
+
// ── Collect used opcodes exactly as specified ─────────────────────────────
|
|
7
|
+
const usedOpcodes = new Set(Object.keys(compiler.OP_NAME).map(k => parseInt(k, 10)).filter(v => !isNaN(v)));
|
|
8
|
+
if (usedOpcodes.size > U16_MAX) return -1;
|
|
9
|
+
|
|
10
|
+
// Random opcode
|
|
11
|
+
if (compiler.options.randomizeOpcodes) {
|
|
12
|
+
let attempts = 0;
|
|
13
|
+
while (attempts++ < 512) {
|
|
14
|
+
const candidate = getRandomInt(0, U16_MAX);
|
|
15
|
+
if (!usedOpcodes.has(candidate)) {
|
|
16
|
+
usedOpcodes.add(candidate);
|
|
17
|
+
return candidate;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Fallback: linear scan from a random start
|
|
23
|
+
const start = Object.keys(compiler.OP_NAME).length;
|
|
24
|
+
for (let i = 0; i <= U16_MAX; i++) {
|
|
25
|
+
const v = start + i & U16_MAX;
|
|
26
|
+
if (!usedOpcodes.has(v)) {
|
|
27
|
+
usedOpcodes.add(v);
|
|
28
|
+
return v;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return -1;
|
|
32
|
+
}
|
|
33
|
+
export function getInstructionSize(instr) {
|
|
34
|
+
const size = instr.filter(op => op?.placeholder !== true).length;
|
|
35
|
+
return size;
|
|
36
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { ok } from "assert";
|
|
2
|
+
export function getPlaceholder() {
|
|
3
|
+
return Math.random().toString(36).substring(2, 15);
|
|
4
|
+
}
|
|
5
|
+
export function choice(elements) {
|
|
6
|
+
ok(elements.length > 0, "choice() called on empty sequence");
|
|
7
|
+
return elements[Math.floor(Math.random() * elements.length)];
|
|
8
|
+
}
|
|
9
|
+
export function getRandom() {
|
|
10
|
+
return Math.random();
|
|
11
|
+
}
|
|
12
|
+
export function getRandomInt(min, max) {
|
|
13
|
+
ok(min <= max, "min must be <= max");
|
|
14
|
+
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Shuffles an array in-place using the Fisher-Yates algorithm.
|
|
19
|
+
* @param array - The array to shuffle (mutated)
|
|
20
|
+
*/
|
|
21
|
+
export function shuffle(array) {
|
|
22
|
+
for (let i = array.length - 1; i > 0; i--) {
|
|
23
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
24
|
+
[array[i], array[j]] = [array[j], array[i]];
|
|
25
|
+
}
|
|
26
|
+
return array;
|
|
27
|
+
}
|
package/index.ts
CHANGED
|
@@ -9,14 +9,17 @@ async function main() {
|
|
|
9
9
|
|
|
10
10
|
const { code: output } = await JsConfuserVM.obfuscate(sourceCode, {
|
|
11
11
|
target: "browser", // or "node"
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
+
selfModifying: true, // do self-modifying bytecode for function bodies?
|
|
17
|
+
macroOpcodes: true, // create combined opcodes for repeated instruction sequences?
|
|
18
|
+
microOpcodes: true, // break opcodes into sub-opcodes?
|
|
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)
|
|
20
23
|
});
|
|
21
24
|
|
|
22
25
|
writeFileSync("output.original.js", orginalOutput, "utf-8");
|
package/jest.config.js
CHANGED
|
@@ -6,10 +6,19 @@ const OPTIONS_MATRIX = [
|
|
|
6
6
|
{ displayName: "selfModifying", VM_OPTIONS: { selfModifying: true } },
|
|
7
7
|
{ displayName: "timingChecks", VM_OPTIONS: { timingChecks: true } },
|
|
8
8
|
{ displayName: "macroOpcodes", VM_OPTIONS: { macroOpcodes: true } },
|
|
9
|
+
{ displayName: "microOpcodes", VM_OPTIONS: { microOpcodes: true } },
|
|
9
10
|
{
|
|
10
11
|
displayName: "specializedOpcodes",
|
|
11
12
|
VM_OPTIONS: { specializedOpcodes: true },
|
|
12
13
|
},
|
|
14
|
+
{
|
|
15
|
+
displayName: "aliasedOpcodes",
|
|
16
|
+
VM_OPTIONS: { aliasedOpcodes: true },
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
displayName: "concealConstants",
|
|
20
|
+
VM_OPTIONS: { concealConstants: true },
|
|
21
|
+
},
|
|
13
22
|
{
|
|
14
23
|
displayName: "all",
|
|
15
24
|
VM_OPTIONS: {
|
|
@@ -19,7 +28,10 @@ const OPTIONS_MATRIX = [
|
|
|
19
28
|
selfModifying: true,
|
|
20
29
|
timingChecks: true,
|
|
21
30
|
macroOpcodes: true,
|
|
31
|
+
microOpcodes: true,
|
|
22
32
|
specializedOpcodes: true,
|
|
33
|
+
aliasedOpcodes: true,
|
|
34
|
+
concealConstants: true,
|
|
23
35
|
},
|
|
24
36
|
},
|
|
25
37
|
];
|
package/package.json
CHANGED
package/src/build-runtime.ts
CHANGED
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
import { generate } from "@babel/generator";
|
|
2
|
-
import { parse
|
|
2
|
+
import { parse } from "@babel/parser";
|
|
3
3
|
import type * as t from "@babel/types";
|
|
4
4
|
import type { Options } from "./options.ts";
|
|
5
5
|
import { applyMacroOpcodes } from "./transforms/runtime/macroOpcodes.ts";
|
|
6
|
+
import { applyMicroOpcodes } from "./transforms/runtime/microOpcodes.ts";
|
|
7
|
+
import { applyInteralVariablesToRuntime } from "./transforms/runtime/internalVariables.ts";
|
|
6
8
|
import { applyShuffleOpcodes } from "./transforms/runtime/shuffleOpcodes.ts";
|
|
7
9
|
import { applyMinify } from "./transforms/runtime/minify.ts";
|
|
8
10
|
import { Compiler } from "./compiler.ts";
|
|
9
11
|
import { applySpecializedOpcodes } from "./transforms/runtime/specializedOpcodes.ts";
|
|
12
|
+
import { applyAliasedOpcodes } from "./transforms/runtime/aliasedOpcodes.ts";
|
|
10
13
|
import type * as b from "./types.ts";
|
|
11
14
|
|
|
12
15
|
export async function obfuscateRuntime(
|
|
13
16
|
runtime: string,
|
|
14
17
|
bytecode: b.Bytecode,
|
|
15
18
|
options: Options,
|
|
16
|
-
compiler
|
|
19
|
+
compiler: Compiler,
|
|
20
|
+
generateBytecodeComment,
|
|
17
21
|
) {
|
|
18
22
|
let ast: t.File;
|
|
19
23
|
try {
|
|
@@ -22,9 +26,18 @@ export async function obfuscateRuntime(
|
|
|
22
26
|
throw new Error("VM-Runtime final parsing failed", { cause: error });
|
|
23
27
|
}
|
|
24
28
|
|
|
25
|
-
//
|
|
29
|
+
// Specialized opcode cases must be applied BEFORE shuffleOpcodes
|
|
26
30
|
if (options.specializedOpcodes) {
|
|
27
|
-
applySpecializedOpcodes(ast,
|
|
31
|
+
applySpecializedOpcodes(ast, compiler);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (options.microOpcodes) {
|
|
35
|
+
applyInteralVariablesToRuntime(ast, compiler);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Micro opcode cases must be applied BEFORE shuffleOpcodes
|
|
39
|
+
if (options.microOpcodes && Object.keys(compiler.MICRO_OPS).length > 0) {
|
|
40
|
+
applyMicroOpcodes(ast, compiler);
|
|
28
41
|
}
|
|
29
42
|
|
|
30
43
|
// Macro opcode cases must be applied BEFORE shuffleOpcodes
|
|
@@ -32,6 +45,11 @@ export async function obfuscateRuntime(
|
|
|
32
45
|
applyMacroOpcodes(ast, compiler);
|
|
33
46
|
}
|
|
34
47
|
|
|
48
|
+
// Aliased opcode cases must be applied BEFORE shuffleOpcodes
|
|
49
|
+
if (options.aliasedOpcodes) {
|
|
50
|
+
applyAliasedOpcodes(ast, compiler);
|
|
51
|
+
}
|
|
52
|
+
|
|
35
53
|
// Shuffle opcode handle order
|
|
36
54
|
if (options.shuffleOpcodes) {
|
|
37
55
|
applyShuffleOpcodes(ast);
|
|
@@ -44,6 +62,9 @@ export async function obfuscateRuntime(
|
|
|
44
62
|
throw new Error("VM-Runtime final generation failed", { cause: error });
|
|
45
63
|
}
|
|
46
64
|
|
|
65
|
+
// Add comment here for more accurate opcode names
|
|
66
|
+
generated = generateBytecodeComment() + "\n" + generated;
|
|
67
|
+
|
|
47
68
|
// Minify code?
|
|
48
69
|
if (options.minify) {
|
|
49
70
|
try {
|