simple-javascript-obf 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/.github/workflows/node.js.yml +31 -0
- package/LICENSE +21 -0
- package/README.md +92 -0
- package/THIRD_PARTY_NOTICES.md +82 -0
- package/bin/js-obf +228 -0
- package/bin/obf.sh +257 -0
- package/package.json +26 -0
- package/src/index.js +106 -0
- package/src/options.js +128 -0
- package/src/pipeline.js +56 -0
- package/src/plugins/antiHook.js +123 -0
- package/src/plugins/controlFlowFlatten.js +203 -0
- package/src/plugins/deadCode.js +82 -0
- package/src/plugins/encodeMembers.js +44 -0
- package/src/plugins/entry.js +31 -0
- package/src/plugins/rename.js +100 -0
- package/src/plugins/stringEncode.js +494 -0
- package/src/plugins/vm/ast-utils.js +58 -0
- package/src/plugins/vm/compiler.js +113 -0
- package/src/plugins/vm/constants.js +72 -0
- package/src/plugins/vm/emit.js +916 -0
- package/src/plugins/vm/encoding.js +252 -0
- package/src/plugins/vm/index.js +366 -0
- package/src/plugins/vm/mapping.js +24 -0
- package/src/plugins/vm/normalize.js +692 -0
- package/src/plugins/vm/runtime.js +1145 -0
- package/src/plugins/vm.js +1 -0
- package/src/utils/names.js +55 -0
- package/src/utils/reserved.js +57 -0
- package/src/utils/rng.js +55 -0
- package/src/utils/stream.js +97 -0
- package/src/utils/string.js +13 -0
- package/test/bench-runner.js +78 -0
- package/test/benchmark-source.js +35 -0
- package/test/benchmark-vm.js +160 -0
- package/test/dist/bench.obf.js +1 -0
- package/test/dist/bench.original.js +35 -0
- package/test/dist/bench.vm.js +1 -0
- package/test/dist/sample-input.obf.js +1 -0
- package/test/dist/sample-input.vm.js +1 -0
- package/test/generate-obf.js +38 -0
- package/test/obf-smoke.js +129 -0
- package/test/sample-input.js +23 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
function hasUnsupported(node) {
|
|
2
|
+
let unsupported = false;
|
|
3
|
+
const stack = [node];
|
|
4
|
+
while (stack.length) {
|
|
5
|
+
const current = stack.pop();
|
|
6
|
+
if (!current || unsupported) {
|
|
7
|
+
continue;
|
|
8
|
+
}
|
|
9
|
+
switch (current.type) {
|
|
10
|
+
case "TryStatement":
|
|
11
|
+
case "WithStatement":
|
|
12
|
+
case "LabeledStatement":
|
|
13
|
+
case "YieldExpression":
|
|
14
|
+
case "AwaitExpression":
|
|
15
|
+
unsupported = true;
|
|
16
|
+
break;
|
|
17
|
+
default:
|
|
18
|
+
break;
|
|
19
|
+
}
|
|
20
|
+
for (const key of Object.keys(current)) {
|
|
21
|
+
const value = current[key];
|
|
22
|
+
if (Array.isArray(value)) {
|
|
23
|
+
for (const child of value) {
|
|
24
|
+
if (child && typeof child.type === "string") {
|
|
25
|
+
stack.push(child);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
} else if (value && typeof value.type === "string") {
|
|
29
|
+
stack.push(value);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return unsupported;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function buildFlattenedBody(t, ctx, statements) {
|
|
37
|
+
const stateId = t.identifier(ctx.nameGen.next());
|
|
38
|
+
const lookupId = t.identifier(ctx.nameGen.next());
|
|
39
|
+
const seedId = t.identifier(ctx.nameGen.next());
|
|
40
|
+
const count = statements.length;
|
|
41
|
+
const order = Array.from({ length: count }, (_, i) => i);
|
|
42
|
+
ctx.rng.shuffle(order);
|
|
43
|
+
const seed = ctx.rng.int(0, count - 1);
|
|
44
|
+
|
|
45
|
+
const gcd = (a, b) => {
|
|
46
|
+
let x = a;
|
|
47
|
+
let y = b;
|
|
48
|
+
while (y !== 0) {
|
|
49
|
+
const temp = x % y;
|
|
50
|
+
x = y;
|
|
51
|
+
y = temp;
|
|
52
|
+
}
|
|
53
|
+
return x;
|
|
54
|
+
};
|
|
55
|
+
const multiplierCandidates = [7, 5, 11, 13, 17, 19];
|
|
56
|
+
let multiplier = 7;
|
|
57
|
+
for (const candidate of multiplierCandidates) {
|
|
58
|
+
if (gcd(count, candidate) === 1) {
|
|
59
|
+
multiplier = candidate;
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (gcd(count, multiplier) !== 1) {
|
|
64
|
+
multiplier = 1;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const encodeState = (index) => (index * multiplier + seed) % count;
|
|
68
|
+
const lookupValues = new Array(count);
|
|
69
|
+
for (let i = 0; i < count; i += 1) {
|
|
70
|
+
lookupValues[encodeState(i)] = i;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const buildStateExpr = (indexLiteral) =>
|
|
74
|
+
t.binaryExpression(
|
|
75
|
+
"%",
|
|
76
|
+
t.binaryExpression(
|
|
77
|
+
"+",
|
|
78
|
+
t.binaryExpression("*", indexLiteral, t.numericLiteral(multiplier)),
|
|
79
|
+
seedId
|
|
80
|
+
),
|
|
81
|
+
t.numericLiteral(count)
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const cases = order.map((index) => {
|
|
85
|
+
const stmt = statements[index];
|
|
86
|
+
const caseBody = [];
|
|
87
|
+
caseBody.push(stmt);
|
|
88
|
+
if (stmt.type !== "ReturnStatement" && stmt.type !== "ThrowStatement") {
|
|
89
|
+
const nextValue = index + 1 < count ? index + 1 : -1;
|
|
90
|
+
caseBody.push(
|
|
91
|
+
t.expressionStatement(
|
|
92
|
+
t.assignmentExpression(
|
|
93
|
+
"=",
|
|
94
|
+
stateId,
|
|
95
|
+
nextValue === -1
|
|
96
|
+
? t.numericLiteral(-1)
|
|
97
|
+
: buildStateExpr(t.numericLiteral(nextValue))
|
|
98
|
+
)
|
|
99
|
+
)
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
caseBody.push(t.breakStatement());
|
|
103
|
+
return t.switchCase(
|
|
104
|
+
t.numericLiteral(index),
|
|
105
|
+
[t.blockStatement(caseBody)]
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
cases.push(
|
|
110
|
+
t.switchCase(null, [
|
|
111
|
+
t.blockStatement([
|
|
112
|
+
t.expressionStatement(
|
|
113
|
+
t.assignmentExpression("=", stateId, t.numericLiteral(-1))
|
|
114
|
+
),
|
|
115
|
+
t.breakStatement(),
|
|
116
|
+
]),
|
|
117
|
+
])
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
const whileStmt = t.whileStatement(
|
|
121
|
+
t.binaryExpression("!==", stateId, t.numericLiteral(-1)),
|
|
122
|
+
t.blockStatement([
|
|
123
|
+
t.switchStatement(
|
|
124
|
+
t.memberExpression(lookupId, stateId, true),
|
|
125
|
+
cases
|
|
126
|
+
),
|
|
127
|
+
])
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
return [
|
|
131
|
+
t.variableDeclaration("const", [
|
|
132
|
+
t.variableDeclarator(seedId, t.numericLiteral(seed)),
|
|
133
|
+
]),
|
|
134
|
+
t.variableDeclaration("const", [
|
|
135
|
+
t.variableDeclarator(
|
|
136
|
+
lookupId,
|
|
137
|
+
t.arrayExpression(
|
|
138
|
+
lookupValues.map((value) => t.numericLiteral(value))
|
|
139
|
+
)
|
|
140
|
+
),
|
|
141
|
+
]),
|
|
142
|
+
t.variableDeclaration("let", [
|
|
143
|
+
t.variableDeclarator(stateId, buildStateExpr(t.numericLiteral(0))),
|
|
144
|
+
]),
|
|
145
|
+
whileStmt,
|
|
146
|
+
];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function flattenFunctionBody(path, ctx) {
|
|
150
|
+
const bodyPath = path.get("body");
|
|
151
|
+
if (!bodyPath.isBlockStatement()) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
const statements = bodyPath.node.body;
|
|
155
|
+
if (!statements || statements.length < ctx.options.cffOptions.minStatements) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
for (const stmt of statements) {
|
|
159
|
+
if (stmt.type === "VariableDeclaration" && stmt.kind !== "var") {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (stmt.type === "ClassDeclaration") {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (stmt.type === "FunctionDeclaration") {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (hasUnsupported(bodyPath.node)) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const directives = [];
|
|
174
|
+
const rest = [];
|
|
175
|
+
for (const stmt of statements) {
|
|
176
|
+
if (stmt.type === "ExpressionStatement" && stmt.directive) {
|
|
177
|
+
directives.push(stmt);
|
|
178
|
+
} else {
|
|
179
|
+
rest.push(stmt);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (rest.length < ctx.options.cffOptions.minStatements) {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const flattened = buildFlattenedBody(ctx.t, ctx, rest);
|
|
188
|
+
bodyPath.node.body = [...directives, ...flattened];
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function controlFlowFlatten(ast, ctx) {
|
|
192
|
+
const { traverse } = ctx;
|
|
193
|
+
traverse(ast, {
|
|
194
|
+
Function(path) {
|
|
195
|
+
if (path.node.generator) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
flattenFunctionBody(path, ctx);
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
module.exports = controlFlowFlatten;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
function makeOpaquePredicate(t) {
|
|
2
|
+
const nowCall = t.callExpression(
|
|
3
|
+
t.memberExpression(t.identifier("Date"), t.identifier("now")),
|
|
4
|
+
[]
|
|
5
|
+
);
|
|
6
|
+
return t.binaryExpression(
|
|
7
|
+
"!==",
|
|
8
|
+
t.binaryExpression("&", nowCall, t.numericLiteral(1)),
|
|
9
|
+
t.numericLiteral(2)
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function makeNoiseStatement(t) {
|
|
14
|
+
return t.expressionStatement(
|
|
15
|
+
t.callExpression(
|
|
16
|
+
t.memberExpression(t.identifier("Date"), t.identifier("now")),
|
|
17
|
+
[]
|
|
18
|
+
)
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function makeJunkStatement(t, name, rng) {
|
|
23
|
+
const id = t.identifier(name);
|
|
24
|
+
const seed = rng.int(1, 255);
|
|
25
|
+
return t.blockStatement([
|
|
26
|
+
t.variableDeclaration("let", [
|
|
27
|
+
t.variableDeclarator(id, t.numericLiteral(seed)),
|
|
28
|
+
]),
|
|
29
|
+
makeNoiseStatement(t),
|
|
30
|
+
t.expressionStatement(
|
|
31
|
+
t.assignmentExpression(
|
|
32
|
+
"^=",
|
|
33
|
+
id,
|
|
34
|
+
t.numericLiteral(rng.int(1, 255))
|
|
35
|
+
)
|
|
36
|
+
),
|
|
37
|
+
]);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function insertIntoBody(body, node, index) {
|
|
41
|
+
body.splice(index, 0, node);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function deadCode(ast, ctx) {
|
|
45
|
+
const { traverse, t, rng, options } = ctx;
|
|
46
|
+
const probability = options.deadCodeOptions.probability;
|
|
47
|
+
|
|
48
|
+
traverse(ast, {
|
|
49
|
+
BlockStatement(path) {
|
|
50
|
+
const body = path.node.body;
|
|
51
|
+
if (!body || body.length === 0) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (!rng.bool(probability)) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (path.parentPath && path.parentPath.isSwitchCase()) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
let directiveCount = 0;
|
|
61
|
+
while (
|
|
62
|
+
directiveCount < body.length &&
|
|
63
|
+
body[directiveCount].type === "ExpressionStatement" &&
|
|
64
|
+
body[directiveCount].directive
|
|
65
|
+
) {
|
|
66
|
+
directiveCount += 1;
|
|
67
|
+
}
|
|
68
|
+
const idx = rng.int(directiveCount, body.length);
|
|
69
|
+
const junkName = ctx.nameGen.next();
|
|
70
|
+
const junk = t.ifStatement(
|
|
71
|
+
makeOpaquePredicate(t),
|
|
72
|
+
makeJunkStatement(t, junkName, rng)
|
|
73
|
+
);
|
|
74
|
+
insertIntoBody(body, junk, idx);
|
|
75
|
+
if (rng.bool(probability * 0.5)) {
|
|
76
|
+
insertIntoBody(body, makeNoiseStatement(t), idx);
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
module.exports = deadCode;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
function makeGlobalAccess(t, name) {
|
|
2
|
+
return t.memberExpression(t.identifier("globalThis"), t.stringLiteral(name), true);
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function isGlobalIdentifier(path, name) {
|
|
6
|
+
return path.isIdentifier({ name }) && !path.scope.getBinding(name);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function encodeMembers(ast, ctx) {
|
|
10
|
+
const { traverse, t, options } = ctx;
|
|
11
|
+
const enabled = options.stringsOptions.encodeConsole !== false;
|
|
12
|
+
if (!enabled) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
traverse(ast, {
|
|
17
|
+
MemberExpression(path) {
|
|
18
|
+
const { node } = path;
|
|
19
|
+
const objectPath = path.get("object");
|
|
20
|
+
if (!isGlobalIdentifier(objectPath, "console")) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
node.object = makeGlobalAccess(t, "console");
|
|
24
|
+
if (!node.computed && t.isIdentifier(node.property)) {
|
|
25
|
+
node.property = t.stringLiteral(node.property.name);
|
|
26
|
+
node.computed = true;
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
OptionalMemberExpression(path) {
|
|
30
|
+
const { node } = path;
|
|
31
|
+
const objectPath = path.get("object");
|
|
32
|
+
if (!isGlobalIdentifier(objectPath, "console")) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
node.object = makeGlobalAccess(t, "console");
|
|
36
|
+
if (!node.computed && t.isIdentifier(node.property)) {
|
|
37
|
+
node.property = t.stringLiteral(node.property.name);
|
|
38
|
+
node.computed = true;
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = encodeMembers;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
function rewriteGlobalEntry(ast, ctx) {
|
|
2
|
+
const { traverse, t, nameGen } = ctx;
|
|
3
|
+
let replacement = null;
|
|
4
|
+
|
|
5
|
+
traverse(ast, {
|
|
6
|
+
Program(path) {
|
|
7
|
+
path.traverse({
|
|
8
|
+
MemberExpression(memberPath) {
|
|
9
|
+
const { node } = memberPath;
|
|
10
|
+
if (!t.isIdentifier(node.object, { name: "globalThis" })) {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const isDirect =
|
|
14
|
+
!node.computed && t.isIdentifier(node.property, { name: "__r" });
|
|
15
|
+
const isComputed =
|
|
16
|
+
node.computed && t.isStringLiteral(node.property, { value: "__r" });
|
|
17
|
+
if (!isDirect && !isComputed) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
if (!replacement) {
|
|
21
|
+
replacement = nameGen.next();
|
|
22
|
+
}
|
|
23
|
+
node.property = t.stringLiteral(replacement);
|
|
24
|
+
node.computed = true;
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
module.exports = rewriteGlobalEntry;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
function collectExportedNames(programPath) {
|
|
2
|
+
const exported = new Set();
|
|
3
|
+
for (const node of programPath.node.body) {
|
|
4
|
+
if (node.type === "ExportNamedDeclaration") {
|
|
5
|
+
if (node.declaration) {
|
|
6
|
+
if (node.declaration.id) {
|
|
7
|
+
exported.add(node.declaration.id.name);
|
|
8
|
+
}
|
|
9
|
+
if (node.declaration.declarations) {
|
|
10
|
+
for (const decl of node.declaration.declarations) {
|
|
11
|
+
if (decl.id && decl.id.name) {
|
|
12
|
+
exported.add(decl.id.name);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
if (node.specifiers) {
|
|
18
|
+
for (const spec of node.specifiers) {
|
|
19
|
+
if (spec.local && spec.local.name) {
|
|
20
|
+
exported.add(spec.local.name);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
if (node.type === "ExportDefaultDeclaration") {
|
|
26
|
+
if (node.declaration && node.declaration.id) {
|
|
27
|
+
exported.add(node.declaration.id.name);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return exported;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function renameScope(scope, ctx, reserved, usedNames) {
|
|
35
|
+
const names = Object.keys(scope.bindings);
|
|
36
|
+
for (const name of names) {
|
|
37
|
+
if (reserved.has(name)) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const binding = scope.bindings[name];
|
|
41
|
+
if (!binding || !binding.identifier) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (binding.path.isImportSpecifier() || binding.path.isImportDefaultSpecifier()) {
|
|
45
|
+
if (reserved.has(binding.identifier.name)) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
let newName = ctx.nameGen.next();
|
|
50
|
+
while (
|
|
51
|
+
scope.hasBinding(newName) ||
|
|
52
|
+
scope.hasGlobal(newName) ||
|
|
53
|
+
reserved.has(newName) ||
|
|
54
|
+
usedNames.has(newName)
|
|
55
|
+
) {
|
|
56
|
+
newName = ctx.nameGen.next();
|
|
57
|
+
}
|
|
58
|
+
scope.rename(name, newName);
|
|
59
|
+
usedNames.add(newName);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function renameIdentifiers(ast, ctx) {
|
|
64
|
+
const { traverse, options } = ctx;
|
|
65
|
+
|
|
66
|
+
traverse(ast, {
|
|
67
|
+
Program(path) {
|
|
68
|
+
const exported = collectExportedNames(path);
|
|
69
|
+
const reserved = new Set(options.renameOptions.reserved);
|
|
70
|
+
for (const name of exported) {
|
|
71
|
+
reserved.add(name);
|
|
72
|
+
}
|
|
73
|
+
const usedNames = new Set(reserved);
|
|
74
|
+
path.traverse({
|
|
75
|
+
Scopable(scopePath) {
|
|
76
|
+
if (!scopePath.scope) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
for (const name of Object.keys(scopePath.scope.bindings)) {
|
|
80
|
+
usedNames.add(name);
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
path.traverse({
|
|
86
|
+
Scopable(scopePath) {
|
|
87
|
+
if (scopePath.isProgram() && !options.renameOptions.renameGlobals) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (!scopePath.scope) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
renameScope(scopePath.scope, ctx, reserved, usedNames);
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
module.exports = renameIdentifiers;
|