js-confuser-vm 0.1.0 → 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.
- package/README.md +281 -147
- package/dist/build-runtime.js +41 -15
- package/dist/compiler.js +714 -265
- package/dist/disassembler.js +367 -0
- package/dist/index.js +7 -2
- package/dist/runtime.js +160 -119
- package/dist/template.js +163 -42
- package/dist/transforms/bytecode/aliasedOpcodes.js +4 -1
- package/dist/transforms/bytecode/concealConstants.js +2 -2
- package/dist/transforms/bytecode/controlFlowFlattening.js +569 -0
- package/dist/transforms/bytecode/dispatcher.js +15 -111
- package/dist/transforms/bytecode/macroOpcodes.js +2 -2
- package/{src/transforms/bytecode/resolveContants.ts → dist/transforms/bytecode/resolveConstants.js} +30 -56
- package/dist/transforms/bytecode/resolveRegisters.js +23 -4
- package/dist/transforms/bytecode/selfModifying.js +88 -21
- package/dist/transforms/bytecode/semanticOpcodes.js +162 -0
- package/dist/transforms/bytecode/specializedOpcodes.js +23 -12
- package/dist/transforms/bytecode/stringConcealing.js +288 -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 +75 -0
- package/dist/utils/op-utils.js +1 -2
- package/dist/utils/pass-utils.js +100 -0
- package/dist/utils/profile-utils.js +3 -0
- package/package.json +8 -1
- package/.gitmodules +0 -4
- package/.prettierignore +0 -1
- package/CHANGELOG.md +0 -335
- package/babel-plugin-inline-runtime.cjs +0 -34
- package/babel.config.json +0 -23
- package/index.ts +0 -38
- package/jest-strip-types.js +0 -10
- package/jest.config.js +0 -52
- package/src/build-runtime.ts +0 -78
- package/src/compiler.ts +0 -2593
- package/src/index.ts +0 -14
- package/src/minify.ts +0 -21
- package/src/options.ts +0 -18
- package/src/runtime.ts +0 -923
- package/src/template.ts +0 -141
- package/src/transforms/bytecode/aliasedOpcodes.ts +0 -148
- package/src/transforms/bytecode/concealConstants.ts +0 -52
- package/src/transforms/bytecode/dispatcher.ts +0 -398
- package/src/transforms/bytecode/macroOpcodes.ts +0 -193
- package/src/transforms/bytecode/microOpcodes.ts +0 -291
- package/src/transforms/bytecode/resolveLabels.ts +0 -112
- package/src/transforms/bytecode/resolveRegisters.ts +0 -221
- package/src/transforms/bytecode/selfModifying.ts +0 -121
- package/src/transforms/bytecode/specializedOpcodes.ts +0 -153
- package/src/transforms/runtime/aliasedOpcodes.ts +0 -191
- package/src/transforms/runtime/internalVariables.ts +0 -270
- package/src/transforms/runtime/macroOpcodes.ts +0 -138
- package/src/transforms/runtime/microOpcodes.ts +0 -93
- package/src/transforms/runtime/minify.ts +0 -1
- package/src/transforms/runtime/shuffleOpcodes.ts +0 -24
- package/src/transforms/runtime/specializedOpcodes.ts +0 -156
- package/src/types.ts +0 -93
- package/src/utils/op-utils.ts +0 -48
- package/src/utils/random-utils.ts +0 -31
- package/tsconfig.json +0 -12
|
@@ -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,75 @@
|
|
|
1
|
+
import traverseImport from "@babel/traverse";
|
|
2
|
+
const traverse = traverseImport.default || traverseImport;
|
|
3
|
+
|
|
4
|
+
// Recursively visits every statement reachable from `stmts` within the current
|
|
5
|
+
// function scope — traversing into blocks, if branches, loop bodies, switch
|
|
6
|
+
// cases, try/catch/finally, and labeled statements — but never crossing into
|
|
7
|
+
// nested FunctionDeclaration/FunctionExpression bodies (those are separate scopes).
|
|
8
|
+
//
|
|
9
|
+
// `visit` is called for each statement before its children are traversed.
|
|
10
|
+
// ForStatement init and ForInStatement left VariableDeclarations are also
|
|
11
|
+
// passed to `visit` so callers don't need to special-case them.
|
|
12
|
+
export function walkHoistScope(stmts, visit) {
|
|
13
|
+
for (const stmt of stmts) {
|
|
14
|
+
visit(stmt);
|
|
15
|
+
switch (stmt.type) {
|
|
16
|
+
case "BlockStatement":
|
|
17
|
+
walkHoistScope(stmt.body, visit);
|
|
18
|
+
break;
|
|
19
|
+
case "IfStatement":
|
|
20
|
+
{
|
|
21
|
+
const cons = stmt.consequent.type === "BlockStatement" ? stmt.consequent.body : [stmt.consequent];
|
|
22
|
+
walkHoistScope(cons, visit);
|
|
23
|
+
if (stmt.alternate) {
|
|
24
|
+
const alt = stmt.alternate.type === "BlockStatement" ? stmt.alternate.body : [stmt.alternate];
|
|
25
|
+
walkHoistScope(alt, visit);
|
|
26
|
+
}
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
case "WhileStatement":
|
|
30
|
+
case "DoWhileStatement":
|
|
31
|
+
{
|
|
32
|
+
const body = stmt.body.type === "BlockStatement" ? stmt.body.body : [stmt.body];
|
|
33
|
+
walkHoistScope(body, visit);
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
case "ForStatement":
|
|
37
|
+
{
|
|
38
|
+
if (stmt.init?.type === "VariableDeclaration") visit(stmt.init);
|
|
39
|
+
const body = stmt.body.type === "BlockStatement" ? stmt.body.body : [stmt.body];
|
|
40
|
+
walkHoistScope(body, visit);
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
case "ForInStatement":
|
|
44
|
+
{
|
|
45
|
+
if (stmt.left.type === "VariableDeclaration") visit(stmt.left);
|
|
46
|
+
const body = stmt.body.type === "BlockStatement" ? stmt.body.body : [stmt.body];
|
|
47
|
+
walkHoistScope(body, visit);
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
case "SwitchStatement":
|
|
51
|
+
for (const c of stmt.cases) walkHoistScope(c.consequent, visit);
|
|
52
|
+
break;
|
|
53
|
+
case "TryStatement":
|
|
54
|
+
walkHoistScope(stmt.block.body, visit);
|
|
55
|
+
if (stmt.handler) walkHoistScope(stmt.handler.body.body, visit);
|
|
56
|
+
if (stmt.finalizer) walkHoistScope(stmt.finalizer.body, visit);
|
|
57
|
+
break;
|
|
58
|
+
case "LabeledStatement":
|
|
59
|
+
walkHoistScope([stmt.body], visit);
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
export function getSwitchStatement(ast) {
|
|
65
|
+
let switchStatement = null;
|
|
66
|
+
traverse(ast, {
|
|
67
|
+
SwitchStatement(path) {
|
|
68
|
+
if (path.node.leadingComments?.some(c => c.value.includes("@SWITCH"))) {
|
|
69
|
+
switchStatement = path.node;
|
|
70
|
+
path.stop();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
return switchStatement;
|
|
75
|
+
}
|
package/dist/utils/op-utils.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { getRandomInt } from "./random-utils.js";
|
|
2
2
|
export const U16_MAX = 0xffff; // bytecode operands are u16
|
|
3
|
+
export const U32_MAX = 0xffffffff; // max sentinel / operand value
|
|
3
4
|
|
|
4
5
|
/** Returns the next free opcode slot, or -1 when the space is exhausted. */
|
|
5
6
|
export function nextFreeSlot(compiler) {
|
|
@@ -13,7 +14,6 @@ export function nextFreeSlot(compiler) {
|
|
|
13
14
|
while (attempts++ < 512) {
|
|
14
15
|
const candidate = getRandomInt(0, U16_MAX);
|
|
15
16
|
if (!usedOpcodes.has(candidate)) {
|
|
16
|
-
usedOpcodes.add(candidate);
|
|
17
17
|
return candidate;
|
|
18
18
|
}
|
|
19
19
|
}
|
|
@@ -24,7 +24,6 @@ export function nextFreeSlot(compiler) {
|
|
|
24
24
|
for (let i = 0; i <= U16_MAX; i++) {
|
|
25
25
|
const v = start + i & U16_MAX;
|
|
26
26
|
if (!usedOpcodes.has(v)) {
|
|
27
|
-
usedOpcodes.add(v);
|
|
28
27
|
return v;
|
|
29
28
|
}
|
|
30
29
|
}
|
|
@@ -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/package.json
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "js-confuser-vm",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
|
+
"files": [
|
|
6
|
+
"dist",
|
|
7
|
+
"LICENSE",
|
|
8
|
+
"README.md",
|
|
9
|
+
"package.json"
|
|
10
|
+
],
|
|
5
11
|
"scripts": {
|
|
6
12
|
"build": "babel src --out-dir dist --extensions \".ts,.js\"",
|
|
7
13
|
"index": "cross-env NODE_OPTIONS=\"--disable-warning=ExperimentalWarning\" node index.ts",
|
|
@@ -36,6 +42,7 @@
|
|
|
36
42
|
"@babel/core": "^7.29.0",
|
|
37
43
|
"@babel/preset-env": "^7.29.0",
|
|
38
44
|
"@babel/preset-typescript": "^7.28.5",
|
|
45
|
+
"@types/jest": "^30.0.0",
|
|
39
46
|
"@types/node": "^25.3.0",
|
|
40
47
|
"babel-plugin-module-resolver": "^5.0.2",
|
|
41
48
|
"babel-plugin-replace-import-extension": "^1.1.5",
|
package/.gitmodules
DELETED
package/.prettierignore
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
*.md
|
package/CHANGELOG.md
DELETED
|
@@ -1,335 +0,0 @@
|
|
|
1
|
-
## `0.1.0` Dispatcher, Virtual Registers, and more
|
|
2
|
-
|
|
3
|
-
- Added new option `dispatcher` which creates a middleman block to process jumps.
|
|
4
|
-
|
|
5
|
-
```js
|
|
6
|
-
// Input Code
|
|
7
|
-
if (true) {
|
|
8
|
-
console.log("Hello world!");
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
// Before
|
|
12
|
-
// fn_0_0:
|
|
13
|
-
// [0, 0, 0, 0], LOAD_CONST reg[0] = true 1:4-1:8
|
|
14
|
-
// [40, 0, 29], JUMP_IF_FALSE [0, if_else_1] 1:0-3:1
|
|
15
|
-
// [2, 0, 1, 0], LOAD_GLOBAL reg[0] = console 2:2-2:9
|
|
16
|
-
// [0, 1, 2, 0], LOAD_CONST reg[1] = "log" 2:2-2:29
|
|
17
|
-
// [8, 2, 0, 1], GET_PROP reg[2] = reg[0][reg[1]] 2:2-2:29
|
|
18
|
-
// [0, 1, 3, 0], LOAD_CONST reg[1] = "Hello world!" 2:14-2:28
|
|
19
|
-
// [43, 3, 0, 2, 1, 1], CALL_METHOD reg[3] = reg[2](recv=reg[0], 1 args) 2:2-2:29
|
|
20
|
-
// if_else_1:
|
|
21
|
-
// [0, 0, 4, 0], LOAD_CONST reg[0] = undefined
|
|
22
|
-
// [45, 0], RETURN reg[0]
|
|
23
|
-
|
|
24
|
-
// What this looks like decompiled:
|
|
25
|
-
// fn_0_0:
|
|
26
|
-
r0 = true
|
|
27
|
-
if (!r0) goto if_else_1
|
|
28
|
-
r0 = console
|
|
29
|
-
r1 = "log"
|
|
30
|
-
r2 = r0[r1] // console.log
|
|
31
|
-
r1 = "Hello world!"
|
|
32
|
-
r3 = r2.call(r0, r1) // console.log("Hello world!")
|
|
33
|
-
// if_else_1:
|
|
34
|
-
r0 = undefined
|
|
35
|
-
return r0
|
|
36
|
-
|
|
37
|
-
// After
|
|
38
|
-
// fn_0_0:
|
|
39
|
-
// [47, 2, 57, 2, 5, 0], MAKE_CLOSURE reg[2] PC=fn_2_3 (params=2 regs=5 upvalues=0)
|
|
40
|
-
// [0, 3, 0, 0], LOAD_CONST reg[3] = true 1:4-1:8
|
|
41
|
-
// [41, 3, 21], JUMP_IF_TRUE [3, if_else_1_skip_5]
|
|
42
|
-
// [1, 0, 43020], LOAD_INT reg[0] = if_else_1
|
|
43
|
-
// [1, 1, 40151], LOAD_INT reg[1] = 40151
|
|
44
|
-
// [39, 49], JUMP dispatcher_4
|
|
45
|
-
// if_else_1_skip_5:
|
|
46
|
-
// [2, 3, 1, 0], LOAD_GLOBAL reg[3] = console 2:2-2:9
|
|
47
|
-
// [0, 4, 2, 0], LOAD_CONST reg[4] = "log" 2:2-2:29
|
|
48
|
-
// [8, 5, 3, 4], GET_PROP reg[5] = reg[3][reg[4]] 2:2-2:29
|
|
49
|
-
// [0, 4, 3, 0], LOAD_CONST reg[4] = "Hello world!" 2:14-2:28
|
|
50
|
-
// [43, 6, 3, 5, 1, 4], CALL_METHOD reg[6] = reg[5](recv=reg[3], 1 args) 2:2-2:29
|
|
51
|
-
// if_else_1:
|
|
52
|
-
// [0, 3, 4, 0], LOAD_CONST reg[3] = undefined
|
|
53
|
-
// [45, 3], RETURN reg[3]
|
|
54
|
-
// dispatcher_4:
|
|
55
|
-
// [42, 0, 2, 2, 0, 1], CALL reg[0] = reg[2](reg[0], reg[1])
|
|
56
|
-
// [58, 0], JUMP_REG PC = reg[0]
|
|
57
|
-
// fn_2_3:
|
|
58
|
-
// [18, 2, 0, 1], BXOR [2, 0, 1]
|
|
59
|
-
// [0, 3, 5, 0], LOAD_CONST reg[3] = 52048
|
|
60
|
-
// [11, 4, 2, 3], ADD [4, 2, 3]
|
|
61
|
-
// [0, 2, 6, 0], LOAD_CONST reg[2] = 65535
|
|
62
|
-
// [16, 3, 4, 2], BAND [3, 4, 2]
|
|
63
|
-
// [45, 3], RETURN reg[3]
|
|
64
|
-
|
|
65
|
-
// What this looks like decompiled:
|
|
66
|
-
// fn_0_0:
|
|
67
|
-
r2 = MakeClosure(fn_2_3, params=2)
|
|
68
|
-
r3 = true
|
|
69
|
-
if (r3) goto if_else_1_skip
|
|
70
|
-
r0 = 43020
|
|
71
|
-
r1 = 40151
|
|
72
|
-
goto dispatcher
|
|
73
|
-
// if_else_1_skip:
|
|
74
|
-
r3 = console
|
|
75
|
-
r4 = "log"
|
|
76
|
-
r5 = r3[r4] // console.log
|
|
77
|
-
r4 = "Hello world!"
|
|
78
|
-
r6 = r5.call(r3, r4) // console.log("Hello world!")
|
|
79
|
-
// if_else_1:
|
|
80
|
-
r3 = undefined
|
|
81
|
-
return r3
|
|
82
|
-
|
|
83
|
-
// dispatcher:
|
|
84
|
-
r0 = r2(r0, r1) // decode(encodedPC, siteKey)
|
|
85
|
-
goto *r0 // indirect jump
|
|
86
|
-
|
|
87
|
-
// fn_2_3:
|
|
88
|
-
function decode(x, k) {
|
|
89
|
-
return ((x ^ k) + 52048) & 65535;
|
|
90
|
-
}
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
- Improved registers:
|
|
94
|
-
- - Now flattened into a single array with base/stack pointer mechanics
|
|
95
|
-
- - Improved compiler to support virtual registers, allowing passes to introduce scope-specific registers, with a final pass responsible for allocation and freeing
|
|
96
|
-
|
|
97
|
-
- Added a template system to insert synthetic JavaScript as native bytecode, similar to [JS-Confuser's template system](https://github.com/MichaelXF/js-confuser/blob/b6ef304f77b7fc27cb9cc8d7d841e50a4200d7bc/docs/Template.md)
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
## `0.0.9` Micro Opcodes
|
|
102
|
-
|
|
103
|
-
- Added new option `microOpcodes` which breaks opcodes into multiple sub-opcodes.
|
|
104
|
-
|
|
105
|
-
```js
|
|
106
|
-
// Input Code
|
|
107
|
-
console.log("Hello world!");
|
|
108
|
-
|
|
109
|
-
// Before
|
|
110
|
-
// [2, 1, 0, 0], LOAD_GLOBAL reg[1] = console 1:0-1:7
|
|
111
|
-
// [0, 2, 1, 0], LOAD_CONST reg[2] = "log" 1:0-1:27
|
|
112
|
-
// [8, 3, 1, 2], GET_PROP reg[3] = reg[1][reg[2]] 1:0-1:27
|
|
113
|
-
// [0, 4, 2, 0], LOAD_CONST reg[4] = "Hello world!" 1:12-1:26
|
|
114
|
-
// [43, 5, 1, 3, 1, 4], CALL_METHOD reg[5] = reg[3](recv=reg[1], 1 args) 1:0-1:27
|
|
115
|
-
|
|
116
|
-
// What the opcode "LOAD_CONST" looks like:
|
|
117
|
-
case OP.LOAD_CONST:
|
|
118
|
-
var dst = this._operand();
|
|
119
|
-
frame.regs[dst] = this._constant();
|
|
120
|
-
break;
|
|
121
|
-
|
|
122
|
-
// After
|
|
123
|
-
// [60, 1], MICRO_LOAD_GLOBAL_0 1 1:0-1:7
|
|
124
|
-
// [61, 0, 0], MICRO_LOAD_GLOBAL_1 [0, 0]
|
|
125
|
-
// [62], MICRO_LOAD_GLOBAL_2
|
|
126
|
-
// [63], MICRO_LOAD_GLOBAL_3
|
|
127
|
-
// [58, 2], MICRO_LOAD_CONST_0 2 1:0-1:27
|
|
128
|
-
// [59, 1, 0], MICRO_LOAD_CONST_1 [1, 0]
|
|
129
|
-
// [64, 3], MICRO_GET_PROP_0 3 1:0-1:27
|
|
130
|
-
// [65, 1], MICRO_GET_PROP_1 1
|
|
131
|
-
// [66, 2], MICRO_GET_PROP_2 2
|
|
132
|
-
// [67], MICRO_GET_PROP_3
|
|
133
|
-
// [58, 4], MICRO_LOAD_CONST_0 4 1:12-1:26
|
|
134
|
-
// [59, 2, 0], MICRO_LOAD_CONST_1 [2, 0]
|
|
135
|
-
// [43, 5, 1, 3, 1, 4], CALL_METHOD reg[5] = reg[3](recv=reg[1], 1 args) 1:0-1:27
|
|
136
|
-
|
|
137
|
-
// What the opcodes "MICRO_LOAD_CONST_0" (58) and "MICRO_LOAD_CONST_1" (59) look like:
|
|
138
|
-
case 58:
|
|
139
|
-
// MICRO_LOAD_CONST_0
|
|
140
|
-
this._internals[0] = this._operand();
|
|
141
|
-
break;
|
|
142
|
-
case 59:
|
|
143
|
-
// MICRO_LOAD_CONST_1
|
|
144
|
-
frame.regs[this._internals[0]] = this._constant();
|
|
145
|
-
break;
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
- Fixed `Macro Opcodes` possibly clashing variables when merging opcode handlers.
|
|
149
|
-
|
|
150
|
-
- Added support for update expressions on member expressions (`object.prop++`, `object.prop--`)
|
|
151
|
-
|
|
152
|
-
- Added support for Template literals (ES6 feature added for convenience)
|
|
153
|
-
|
|
154
|
-
- Added programs [cash.min.js](https://github.com/fabiospampinato/cash/blob/master/dist/cash.min.js) and [sha256.js](https://gist.github.com/bryanchow/1649353) to the test suite
|
|
155
|
-
|
|
156
|
-
## `0.0.6` Register based
|
|
157
|
-
|
|
158
|
-
- Switched from stack-based to register-based VM.
|
|
159
|
-
|
|
160
|
-
- `Specialized Opcodes` now applies to any fixed-size instruction, instead of just singular operands.
|
|
161
|
-
- - Specialized Opcodes never applies to N-sized instructions, such as `MAKE_CLOSURE`, `BUILD_ARRAY`, `CALL`, etc.
|
|
162
|
-
|
|
163
|
-
- `Macro Opcodes` can now include jumping/terminating opcodes if it's the last instruction in the sequence.
|
|
164
|
-
|
|
165
|
-
- Added new option `aliasedOpcodes` which creates duplicate opcodes, including variants with shuffled operand order.
|
|
166
|
-
|
|
167
|
-
```js
|
|
168
|
-
// Input Code
|
|
169
|
-
console.log("Hello, world!");
|
|
170
|
-
|
|
171
|
-
// Before
|
|
172
|
-
// [2, 1, 0], LOAD_GLOBAL reg[1] = console 1:0-1:7
|
|
173
|
-
// [0, 2, 1], LOAD_CONST reg[2] = "log" 1:0-1:28
|
|
174
|
-
// [8, 3, 1, 2], GET_PROP [3, 1, 2] 1:0-1:28
|
|
175
|
-
// [0, 4, 2], LOAD_CONST reg[4] = "Hello, world!" 1:12-1:27
|
|
176
|
-
// [43, 5, 1, 3, 1, 4], CALL_METHOD reg[5] = method(recv=reg[1], fn=reg[3], 1 args)1:0-1:28
|
|
177
|
-
// [0, 1, 3], LOAD_CONST reg[1] = undefined
|
|
178
|
-
// [45, 1], RETURN reg[1]
|
|
179
|
-
|
|
180
|
-
// What the opcode "LOAD_GLOBAL" looks like:
|
|
181
|
-
case OP.LOAD_GLOBAL:
|
|
182
|
-
var dst = this._operand();
|
|
183
|
-
frame.regs[dst] = this.globals[this.constants[this._operand()]];
|
|
184
|
-
break;
|
|
185
|
-
|
|
186
|
-
// After
|
|
187
|
-
// [52040, 0, 1], ALIAS_LOAD_GLOBAL_1_0 [0, 1] 1:0-1:7
|
|
188
|
-
// [24862, 1, 2], ALIAS_LOAD_CONST_1_0 [1, 2] 1:0-1:28
|
|
189
|
-
// [25202, 1, 2, 3], ALIAS_GET_PROP_1_2_0 [1, 2, 3] 1:0-1:28
|
|
190
|
-
// [24862, 2, 4], ALIAS_LOAD_CONST_1_0 [2, 4] 1:12-1:27
|
|
191
|
-
// [43, 5, 1, 3, 1, 4], CALL_METHOD reg[5] = method(recv=reg[1], fn=reg[3], 1 args)1:0-1:28
|
|
192
|
-
// [24862, 3, 1], ALIAS_LOAD_CONST_1_0 [3, 1]
|
|
193
|
-
// [51807, 1], ALIAS_RETURN_0 1
|
|
194
|
-
|
|
195
|
-
// What the opcode "ALIAS_LOAD_GLOBAL_1_0" (52040) looks like:
|
|
196
|
-
case 52040:
|
|
197
|
-
// ALIAS_LOAD_GLOBAL_1_0 (order: [1,0])
|
|
198
|
-
let _unsortedOperands = [this._operand(), this._operand()];
|
|
199
|
-
let _operands = [_unsortedOperands[1], _unsortedOperands[0]];
|
|
200
|
-
var dst = _operands[0];
|
|
201
|
-
frame.regs[dst] = this.globals[this.constants[_operands[1]]];
|
|
202
|
-
break;
|
|
203
|
-
```
|
|
204
|
-
|
|
205
|
-
- Added new option `concealConstants` which XOR decrypts numbers and strings at runtime.
|
|
206
|
-
|
|
207
|
-
- Top level variables are now renamed and not exposed globally. To export a global function, you can use `window.MyGlobalFunction = function(){...}`
|
|
208
|
-
|
|
209
|
-
- Accessing an undeclared global variable will throw a ReferenceError
|
|
210
|
-
|
|
211
|
-
## `0.0.5` Generated Opcodes
|
|
212
|
-
|
|
213
|
-
- Added new option `specializedOpcodes` which creates specialized opcodes for commonly used opcode+operand pairs.
|
|
214
|
-
|
|
215
|
-
```js
|
|
216
|
-
// Input Code
|
|
217
|
-
console.log("Hello world!");
|
|
218
|
-
|
|
219
|
-
// Before
|
|
220
|
-
// [3, 0], LOAD_GLOBAL "console" 1:0-1:7
|
|
221
|
-
// [0, 1], LOAD_CONST "log" 1:0-1:27
|
|
222
|
-
// [5], GET_PROP 1:0-1:27
|
|
223
|
-
// [0, 2], LOAD_CONST "Hello world!" 1:12-1:26
|
|
224
|
-
// [12, 1], CALL_METHOD (1 args) 1:0-1:27
|
|
225
|
-
// [14], POP 1:0-1:28
|
|
226
|
-
|
|
227
|
-
// What the opcode "LOAD_GLOBAL" looks like:
|
|
228
|
-
case OP.LOAD_GLOBAL:
|
|
229
|
-
this._push(this.globals[this.constants[this._operand()]]);
|
|
230
|
-
break;
|
|
231
|
-
|
|
232
|
-
// After
|
|
233
|
-
// [64], LOAD_GLOBAL_0 1:0-1:7
|
|
234
|
-
// [65], LOAD_CONST_1 1:0-1:27
|
|
235
|
-
// [5], GET_PROP 1:0-1:27
|
|
236
|
-
// [66], LOAD_CONST_2 1:12-1:26
|
|
237
|
-
// [67], CALL_METHOD_1 1:0-1:27
|
|
238
|
-
// [14], POP 1:0-1:28
|
|
239
|
-
|
|
240
|
-
// What the opcode "LOAD_GLOBAL_0" (64) looks like:
|
|
241
|
-
case 64:
|
|
242
|
-
// LOAD_GLOBAL_0 (specialized)
|
|
243
|
-
this._push(this.globals[this.constants[0]]);
|
|
244
|
-
break;
|
|
245
|
-
```
|
|
246
|
-
|
|
247
|
-
- Added new option `macroOpcodes` which combines multiple opcodes commonly used from your bytecode
|
|
248
|
-
|
|
249
|
-
```js
|
|
250
|
-
// Input Code
|
|
251
|
-
console.log("Hello world!");
|
|
252
|
-
console.log("Hello world!");
|
|
253
|
-
|
|
254
|
-
// Before
|
|
255
|
-
// [3, 0], LOAD_GLOBAL "console" 1:0-1:7
|
|
256
|
-
// [0, 1], LOAD_CONST "log" 1:0-1:27
|
|
257
|
-
// [5], GET_PROP 1:0-1:27
|
|
258
|
-
// [0, 2], LOAD_CONST "Hello world!" 1:12-1:26
|
|
259
|
-
// [12, 1], CALL_METHOD (1 args) 1:0-1:27
|
|
260
|
-
// [14], POP 1:0-1:28
|
|
261
|
-
// [3, 0], LOAD_GLOBAL "console" 2:0-2:7
|
|
262
|
-
// [0, 1], LOAD_CONST "log" 2:0-2:27
|
|
263
|
-
// [5], GET_PROP 2:0-2:27
|
|
264
|
-
// [0, 2], LOAD_CONST "Hello world!" 2:12-2:26
|
|
265
|
-
// [12, 1], CALL_METHOD (1 args) 2:0-2:27
|
|
266
|
-
// [14], POP 2:0-2:28
|
|
267
|
-
|
|
268
|
-
// After
|
|
269
|
-
// [64, 0, 1, 2], LOAD_GLOBAL,LOAD_CONST,GET_PROP,LOAD_CONST [0, 1, 2]
|
|
270
|
-
// [12, 1], CALL_METHOD (1 args) 1:0-1:27
|
|
271
|
-
// [14], POP 1:0-1:28
|
|
272
|
-
// [64, 0, 1, 2], LOAD_GLOBAL,LOAD_CONST,GET_PROP,LOAD_CONST [0, 1, 2]
|
|
273
|
-
// [12, 1], CALL_METHOD (1 args) 2:0-2:27
|
|
274
|
-
// [14], POP 2:0-2:28
|
|
275
|
-
|
|
276
|
-
// What the opcode "LOAD_GLOBAL,LOAD_CONST,GET_PROP,LOAD_CONST" (64) looks like:
|
|
277
|
-
case 64:
|
|
278
|
-
{
|
|
279
|
-
// LOAD_GLOBAL
|
|
280
|
-
this._push(this.globals[this.constants[this._operand()]]);
|
|
281
|
-
// LOAD_CONST
|
|
282
|
-
this._push(this.constants[this._operand()]);
|
|
283
|
-
// GET_PROP
|
|
284
|
-
// Stack: [..., obj, key] -> [..., obj, obj[key]]
|
|
285
|
-
// obj is PEEKED (not popped) - CALL_METHOD needs it as receiver
|
|
286
|
-
var key = this._pop();
|
|
287
|
-
var obj = this.peek();
|
|
288
|
-
this._push(obj[key]);
|
|
289
|
-
// LOAD_CONST
|
|
290
|
-
this._push(this.constants[this._operand()]);
|
|
291
|
-
break;
|
|
292
|
-
}
|
|
293
|
-
```
|
|
294
|
-
|
|
295
|
-
- Flattened the bytecode. Now, instructions can read as many operands as needed, and it's unclear to distinguish between opcodes and operands:
|
|
296
|
-
|
|
297
|
-
```js
|
|
298
|
-
// Before (Operands clearly visible)
|
|
299
|
-
var BYTECODE = [[3, 0], [0, 1], [5, undefined], [0, 2], [12, 1], [14, undefined], [3, 0], [0, 1], [5, undefined], [0, 2], [12, 1], [14, undefined], [13, undefined]];
|
|
300
|
-
|
|
301
|
-
// After (Flattened with multi-operand instruction support)
|
|
302
|
-
var BYTECODE = [3, 0, 0, 1, 5, 0, 2, 12, 1, 14, 3, 0, 0, 1, 5, 0, 2, 12, 1, 14, 13];
|
|
303
|
-
```
|
|
304
|
-
|
|
305
|
-
- Changed the bytecode to use ushorts (16-bit ints) allowing a max value of 65,535 for opcodes and operands.
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
## `0.0.3` First update
|
|
309
|
-
|
|
310
|
-
- Created [Website Playground](https://development--confuser.netlify.app/vm)
|
|
311
|
-
|
|
312
|
-
- Added partial support for `try..catch` - The `finally` operator is not supported yet
|
|
313
|
-
- More ES5 coverage: getter/setters, debugger statement
|
|
314
|
-
- Improved compilation process:
|
|
315
|
-
- Parsing:
|
|
316
|
-
JS -> AST
|
|
317
|
-
Done by [@babel/parser](https://www.npmjs.com/package/@babel/parser)
|
|
318
|
-
- Compilation:
|
|
319
|
-
AST -> IR bytecode.
|
|
320
|
-
This bytecode contains pseudo instructions and symbolic values for things like jump labels and constants
|
|
321
|
-
- Transform passes (Assembler):
|
|
322
|
-
Transform passes obfuscate and finally prepare the pseudo bytecode to be runnable. Here, all jump labels get converted into absolute PCs
|
|
323
|
-
- Serializer:
|
|
324
|
-
The bytecode is printed into the array form or encoded string if you have `encodeBytecode` enabled
|
|
325
|
-
- Generating:
|
|
326
|
-
This includes two sub-stages:
|
|
327
|
-
- 1) Creating (another parsing->transforming->generating) the VM Runtime with the given options (randomized op codes, shuffled handlers)
|
|
328
|
-
- 2) Placing the final bytecode into this VM Runtime
|
|
329
|
-
|
|
330
|
-
- Typescript is now transpiled for NPM
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
## `0.0.2` First release
|
|
335
|
-
|