jsguardian 1.2.0
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/adversarial-tokens.js +176 -0
- package/ai-antipattern.js +235 -0
- package/ai-callgraph-poison.js +331 -0
- package/ai-confusion.js +644 -0
- package/ai-semantic-poison.js +276 -0
- package/canary.js +158 -0
- package/cne.js +686 -0
- package/index.js +248 -0
- package/integrity.js +47 -0
- package/jsobf-config.js +38 -0
- package/krak-compiler.js +1480 -0
- package/krak-vm-core.js +892 -0
- package/layers.js +136 -0
- package/opaque-pred.js +32 -0
- package/package.json +32 -0
- package/pipeline.js +327 -0
- package/prng.js +28 -0
- package/signature-break.js +101 -0
- package/temporal-keys.js +194 -0
- package/timing-oracle.js +129 -0
- package/transform-vm.js +266 -0
- package/transforms.js +371 -0
- package/vm-poison.js +247 -0
package/cne.js
ADDED
|
@@ -0,0 +1,686 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ============================================================================
|
|
3
|
+
// CNE — Criptor Native Engine (replaces javascript-obfuscator)
|
|
4
|
+
//
|
|
5
|
+
// Design goals:
|
|
6
|
+
// - ZERO public deobfuscation tooling written against this output
|
|
7
|
+
// - No recognisable string-array/shuffler/RC4/wrapper-function pattern
|
|
8
|
+
// - Full scope-aware identifier rename (every name, including injected
|
|
9
|
+
// __hsh / __anc / __vmq / __ksq) → _0x<hex><salt> names
|
|
10
|
+
// - Inline XOR string encoding with per-string random key (no central table)
|
|
11
|
+
// - Hex-literal conversion for all numeric literals
|
|
12
|
+
// - Variable-length string splitting (2–12 chars, random, seed-driven)
|
|
13
|
+
// - Control-flow flattening on function bodies (state-machine dispatcher)
|
|
14
|
+
// - Opaque true/false constants guarding dead branches
|
|
15
|
+
// - Self-contained: one Babel AST pass, no external tool dependency
|
|
16
|
+
//
|
|
17
|
+
// What we intentionally do NOT do here (already handled by earlier pipeline stages):
|
|
18
|
+
// - MBA constant encoding (transforms.ts applyConstMba)
|
|
19
|
+
// - Opaque predicates (transforms.ts applyOpaque)
|
|
20
|
+
// - VM bytecode virtualization (transform-vm.ts)
|
|
21
|
+
// - String cipher (custom RC4) (transforms.ts applyStringCipher)
|
|
22
|
+
// - Integrity anchors __anc/__hsh (integrity.ts + pipeline.ts)
|
|
23
|
+
// ============================================================================
|
|
24
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
25
|
+
if (k2 === undefined) k2 = k;
|
|
26
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
27
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
28
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
29
|
+
}
|
|
30
|
+
Object.defineProperty(o, k2, desc);
|
|
31
|
+
}) : (function(o, m, k, k2) {
|
|
32
|
+
if (k2 === undefined) k2 = k;
|
|
33
|
+
o[k2] = m[k];
|
|
34
|
+
}));
|
|
35
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
36
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
37
|
+
}) : function(o, v) {
|
|
38
|
+
o["default"] = v;
|
|
39
|
+
});
|
|
40
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
41
|
+
var ownKeys = function(o) {
|
|
42
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
43
|
+
var ar = [];
|
|
44
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
45
|
+
return ar;
|
|
46
|
+
};
|
|
47
|
+
return ownKeys(o);
|
|
48
|
+
};
|
|
49
|
+
return function (mod) {
|
|
50
|
+
if (mod && mod.__esModule) return mod;
|
|
51
|
+
var result = {};
|
|
52
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
53
|
+
__setModuleDefault(result, mod);
|
|
54
|
+
return result;
|
|
55
|
+
};
|
|
56
|
+
})();
|
|
57
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
58
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
59
|
+
};
|
|
60
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
61
|
+
exports.makeNameGen = makeNameGen;
|
|
62
|
+
exports.normalizeEsmExports = normalizeEsmExports;
|
|
63
|
+
exports.applyFullRename = applyFullRename;
|
|
64
|
+
exports.applyHexLiterals = applyHexLiterals;
|
|
65
|
+
exports.applyStringSplit = applyStringSplit;
|
|
66
|
+
exports.applyCneFlattening = applyCneFlattening;
|
|
67
|
+
exports.applyOpaqueConstants = applyOpaqueConstants;
|
|
68
|
+
exports.applyDeadCodeInjection = applyDeadCodeInjection;
|
|
69
|
+
exports.buildEncodedAnchorSource = buildEncodedAnchorSource;
|
|
70
|
+
exports.obfuscateExportNames = obfuscateExportNames;
|
|
71
|
+
exports.cneObfuscate = cneObfuscate;
|
|
72
|
+
const parser_1 = require("@babel/parser");
|
|
73
|
+
const traverse_1 = __importDefault(require("@babel/traverse"));
|
|
74
|
+
const generator_1 = __importDefault(require("@babel/generator"));
|
|
75
|
+
const t = __importStar(require("@babel/types"));
|
|
76
|
+
const traverse = traverse_1.default.default || traverse_1.default;
|
|
77
|
+
const generate = generator_1.default.default || generator_1.default;
|
|
78
|
+
// ----------------------------------------------------------------------------
|
|
79
|
+
// Helpers
|
|
80
|
+
// ----------------------------------------------------------------------------
|
|
81
|
+
function mark(node) { if (node)
|
|
82
|
+
node.__cne = true; }
|
|
83
|
+
function markDeep(node) {
|
|
84
|
+
if (!node || typeof node !== "object")
|
|
85
|
+
return;
|
|
86
|
+
node.__cne = true;
|
|
87
|
+
for (const k of Object.keys(node)) {
|
|
88
|
+
const v = node[k];
|
|
89
|
+
if (Array.isArray(v))
|
|
90
|
+
v.forEach(markDeep);
|
|
91
|
+
else if (v && typeof v === "object" && v.type)
|
|
92
|
+
markDeep(v);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// ----------------------------------------------------------------------------
|
|
96
|
+
// A — Identifier Name Generator
|
|
97
|
+
//
|
|
98
|
+
// All output names look like _0x<4 hex><salt suffix>
|
|
99
|
+
// Salt is per-build (derived from seed), suffix is per-identifier index.
|
|
100
|
+
// Format: _0x + 4 hex chars from (seed XOR idx rotated) + 1-2 salt chars
|
|
101
|
+
//
|
|
102
|
+
// Crucially: we rename ALL binding names in scope — including our own injected
|
|
103
|
+
// names like __hsh, __anc, __vmq, __ksq, __dc, __bk, __ca, __drift, __toc.
|
|
104
|
+
// ----------------------------------------------------------------------------
|
|
105
|
+
function makeNameGen(seed) {
|
|
106
|
+
// Two independent hash rounds to spread entropy
|
|
107
|
+
const a = (seed ^ 0xdeadbeef) >>> 0;
|
|
108
|
+
const b = (seed ^ 0xcafebabe) >>> 0;
|
|
109
|
+
return (idx) => {
|
|
110
|
+
let h = (a ^ Math.imul(idx + 1, 0x9e3779b9)) >>> 0;
|
|
111
|
+
h = Math.imul(h ^ (h >>> 15), 0x85ebca6b);
|
|
112
|
+
h = Math.imul(h ^ (h >>> 13), 0xc2b2ae35);
|
|
113
|
+
h = (h ^ (h >>> 16)) >>> 0;
|
|
114
|
+
const part1 = (h & 0xffff).toString(16).padStart(4, "0");
|
|
115
|
+
const part2 = ((b ^ (idx * 0x6b43)) & 0xff).toString(16).padStart(2, "0");
|
|
116
|
+
return `_0x${part1}${part2}`;
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
// ----------------------------------------------------------------------------
|
|
120
|
+
// B — Full Scope Rename
|
|
121
|
+
//
|
|
122
|
+
// Walks every scope in the AST. For each scope, collects all local bindings
|
|
123
|
+
// (var, let, const, function, param) and renames them with the name generator.
|
|
124
|
+
// Uses Babel's scope.rename() so all references update atomically.
|
|
125
|
+
//
|
|
126
|
+
// Special handling:
|
|
127
|
+
// - Names already looking like _0x... are left alone (already renamed or
|
|
128
|
+
// externally injected from an earlier pass we don't want to double-rename).
|
|
129
|
+
// - Global references (no binding found) are left alone.
|
|
130
|
+
// - module.exports / exports.X property names are NOT renamed (they're
|
|
131
|
+
// external API surface).
|
|
132
|
+
// ----------------------------------------------------------------------------
|
|
133
|
+
// ----------------------------------------------------------------------------
|
|
134
|
+
// ESM export normalisation — MUST run before applyFullRename.
|
|
135
|
+
//
|
|
136
|
+
// Babel's scope.rename() is buggy for *declaration exports* (`export class Foo`,
|
|
137
|
+
// `export function Foo`, `export default …`): the binding path points at the
|
|
138
|
+
// inner declaration while the export lives on the wrapping node, so renaming
|
|
139
|
+
// desyncs the auto-generated specifier from the declaration and emits
|
|
140
|
+
// "Export 'X' is not defined" (rejected by both Babel re-parse and V8).
|
|
141
|
+
// Known upstream bugs: babel/babel#11591, #9266, PR #3629.
|
|
142
|
+
//
|
|
143
|
+
// Workaround (community-standard): rewrite every declaration-export into a plain
|
|
144
|
+
// declaration followed by a separate `export { local as public }` specifier, so
|
|
145
|
+
// rename only ever touches ordinary declarations + ordinary reference
|
|
146
|
+
// identifiers — never the broken declaration-export path.
|
|
147
|
+
function normalizeEsmExports(ast) {
|
|
148
|
+
const body = ast.program.body;
|
|
149
|
+
const out = [];
|
|
150
|
+
let defIdx = 0;
|
|
151
|
+
for (const node of body) {
|
|
152
|
+
// `export <decl>` with no `from` source
|
|
153
|
+
if (t.isExportNamedDeclaration(node) && node.declaration && !node.source) {
|
|
154
|
+
const decl = node.declaration;
|
|
155
|
+
const names = [];
|
|
156
|
+
if (t.isFunctionDeclaration(decl) || t.isClassDeclaration(decl)) {
|
|
157
|
+
if (decl.id)
|
|
158
|
+
names.push(decl.id.name);
|
|
159
|
+
}
|
|
160
|
+
else if (t.isVariableDeclaration(decl)) {
|
|
161
|
+
for (const d of decl.declarations) {
|
|
162
|
+
if (t.isIdentifier(d.id))
|
|
163
|
+
names.push(d.id.name);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
out.push(decl);
|
|
167
|
+
if (names.length) {
|
|
168
|
+
out.push(t.exportNamedDeclaration(null, names.map((n) => t.exportSpecifier(t.identifier(n), t.identifier(n)))));
|
|
169
|
+
}
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
// `export default <decl|expr>`
|
|
173
|
+
if (t.isExportDefaultDeclaration(node)) {
|
|
174
|
+
const d = node.declaration;
|
|
175
|
+
if ((t.isFunctionDeclaration(d) || t.isClassDeclaration(d)) && d.id) {
|
|
176
|
+
out.push(d);
|
|
177
|
+
out.push(t.exportNamedDeclaration(null, [
|
|
178
|
+
t.exportSpecifier(t.identifier(d.id.name), t.identifier("default")),
|
|
179
|
+
]));
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
const local = `__crxDef${defIdx++}`;
|
|
183
|
+
const expr = t.isFunctionDeclaration(d) || t.isClassDeclaration(d)
|
|
184
|
+
? t.toExpression(d)
|
|
185
|
+
: d;
|
|
186
|
+
out.push(t.variableDeclaration("const", [
|
|
187
|
+
t.variableDeclarator(t.identifier(local), expr),
|
|
188
|
+
]));
|
|
189
|
+
out.push(t.exportNamedDeclaration(null, [
|
|
190
|
+
t.exportSpecifier(t.identifier(local), t.identifier("default")),
|
|
191
|
+
]));
|
|
192
|
+
}
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
out.push(node);
|
|
196
|
+
}
|
|
197
|
+
ast.program.body = out;
|
|
198
|
+
}
|
|
199
|
+
function applyFullRename(ast, _rng, nameGen) {
|
|
200
|
+
// Post-order (exit) traversal: rename bindings in children first, then parents.
|
|
201
|
+
// This avoids Babel scope-cache stale-reference bugs that occur when you
|
|
202
|
+
// rename a parent binding before its shadowing children have been processed.
|
|
203
|
+
//
|
|
204
|
+
// scope.rename(old, new) is Babel's own API — it finds every reference to
|
|
205
|
+
// `old` reachable from this scope (respecting shadows in child scopes) and
|
|
206
|
+
// rewrites them atomically including the declaration node.
|
|
207
|
+
let idx = 0;
|
|
208
|
+
traverse(ast, {
|
|
209
|
+
Scope: {
|
|
210
|
+
exit(path) {
|
|
211
|
+
const scope = path.scope;
|
|
212
|
+
// snapshot the binding names before we start renaming (rename mutates bindings)
|
|
213
|
+
const names = Object.keys(scope.bindings);
|
|
214
|
+
for (const name of names) {
|
|
215
|
+
const binding = scope.bindings[name];
|
|
216
|
+
if (!binding)
|
|
217
|
+
continue;
|
|
218
|
+
// Already mangled — skip
|
|
219
|
+
if (/^_0x[0-9a-f]{4,}/.test(name))
|
|
220
|
+
continue;
|
|
221
|
+
// Skip VM runtime internals: names inside __vmq / __ksq / __gfdXXXX functions
|
|
222
|
+
// These must stay stable because the VM runtime references them by name.
|
|
223
|
+
if (/^(__vmq|__ksq|__gfd)/.test(name))
|
|
224
|
+
continue;
|
|
225
|
+
// Walk up the scope chain to see if we're inside a VM runtime function
|
|
226
|
+
let _sc = path.scope;
|
|
227
|
+
let _inVmFn = false;
|
|
228
|
+
while (_sc) {
|
|
229
|
+
const _fn = _sc.block;
|
|
230
|
+
if (_fn && _fn.type === 'FunctionDeclaration' && _fn.id &&
|
|
231
|
+
/^(__vmq|__ksq|__gfd)/.test(_fn.id.name)) {
|
|
232
|
+
_inVmFn = true;
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
_sc = _sc.parent;
|
|
236
|
+
}
|
|
237
|
+
if (_inVmFn)
|
|
238
|
+
continue;
|
|
239
|
+
// Skip if the declaration node is marked __cne (our own injected infra)
|
|
240
|
+
if (binding.path?.node?.__cne)
|
|
241
|
+
continue;
|
|
242
|
+
if (binding.path?.parent?.__cne)
|
|
243
|
+
continue;
|
|
244
|
+
const newName = nameGen(idx++);
|
|
245
|
+
try {
|
|
246
|
+
scope.rename(name, newName);
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
// scope.rename can throw on parse-error-recovery phantom nodes — ignore
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
// ----------------------------------------------------------------------------
|
|
257
|
+
// C — Hex Numeric Literals
|
|
258
|
+
//
|
|
259
|
+
// Converts integer literals to hex representation so the output has no decimal
|
|
260
|
+
// numbers. Floating-point and special values (NaN, Infinity) are left as-is.
|
|
261
|
+
// Excludes literals already marked __cne (our own injected constants).
|
|
262
|
+
// ----------------------------------------------------------------------------
|
|
263
|
+
function applyHexLiterals(ast) {
|
|
264
|
+
traverse(ast, {
|
|
265
|
+
NumericLiteral(path) {
|
|
266
|
+
if (path.node.__cne)
|
|
267
|
+
return;
|
|
268
|
+
const v = path.node.value;
|
|
269
|
+
if (!Number.isInteger(v) || v < 0 || v > 0xffffffff)
|
|
270
|
+
return;
|
|
271
|
+
// Already hex — check if source is hex (not reliable from AST alone, so skip
|
|
272
|
+
// if value is small enough to not matter, or just always convert)
|
|
273
|
+
const hex = t.numericLiteral(v);
|
|
274
|
+
hex.extra = { raw: `0x${v.toString(16)}` };
|
|
275
|
+
mark(hex);
|
|
276
|
+
path.replaceWith(hex);
|
|
277
|
+
path.skip();
|
|
278
|
+
},
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
// ----------------------------------------------------------------------------
|
|
282
|
+
// D — Inline String Splitting
|
|
283
|
+
//
|
|
284
|
+
// Splits string literals into variable-size chunks (2–12 chars, seed-driven)
|
|
285
|
+
// concatenated with + operators. No central string table — each string is
|
|
286
|
+
// independently fragmented. No RC4, no base64 ciphertext, no shuffler.
|
|
287
|
+
//
|
|
288
|
+
// Also inserts synthetic "fake" string concatenations (~15% chance) that
|
|
289
|
+
// resolve to unused strings — makes manual reassembly ambiguous.
|
|
290
|
+
//
|
|
291
|
+
// Strings shorter than 4 chars are left as-is (splitting adds no noise).
|
|
292
|
+
// Strings already marked __cne (our runtime infra) are left as-is.
|
|
293
|
+
// ----------------------------------------------------------------------------
|
|
294
|
+
function applyStringSplit(ast, rng) {
|
|
295
|
+
traverse(ast, {
|
|
296
|
+
StringLiteral(path) {
|
|
297
|
+
if (path.node.__cne)
|
|
298
|
+
return;
|
|
299
|
+
const s = path.node.value;
|
|
300
|
+
if (s.length < 4)
|
|
301
|
+
return;
|
|
302
|
+
// Don't touch import/require paths
|
|
303
|
+
const p = path.parent;
|
|
304
|
+
if (t.isImportDeclaration(p) ||
|
|
305
|
+
t.isExportNamedDeclaration(p) ||
|
|
306
|
+
t.isExportAllDeclaration(p) ||
|
|
307
|
+
(t.isCallExpression(p) && p.callee && p.callee.name === "require"))
|
|
308
|
+
return;
|
|
309
|
+
// Don't touch property keys
|
|
310
|
+
if ((t.isObjectProperty(p) || t.isObjectMethod(p)) && p.key === path.node && !p.computed)
|
|
311
|
+
return;
|
|
312
|
+
if ((t.isClassMethod(p) || t.isClassProperty(p)) && p.key === path.node && !p.computed)
|
|
313
|
+
return;
|
|
314
|
+
// Split into variable-size chunks
|
|
315
|
+
const chunks = [];
|
|
316
|
+
let i = 0;
|
|
317
|
+
while (i < s.length) {
|
|
318
|
+
const size = 2 + Math.floor(rng.next() * 11); // 2..12
|
|
319
|
+
chunks.push(s.slice(i, i + size));
|
|
320
|
+
i += size;
|
|
321
|
+
}
|
|
322
|
+
if (chunks.length <= 1)
|
|
323
|
+
return; // too short to split
|
|
324
|
+
// Build left-associative + chain
|
|
325
|
+
let expr = makeStr(chunks[0]);
|
|
326
|
+
for (let j = 1; j < chunks.length; j++) {
|
|
327
|
+
expr = t.binaryExpression("+", expr, makeStr(chunks[j]));
|
|
328
|
+
mark(expr);
|
|
329
|
+
}
|
|
330
|
+
mark(expr);
|
|
331
|
+
path.replaceWith(expr);
|
|
332
|
+
path.skip();
|
|
333
|
+
},
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
function makeStr(s) {
|
|
337
|
+
const node = t.stringLiteral(s);
|
|
338
|
+
mark(node);
|
|
339
|
+
return node;
|
|
340
|
+
}
|
|
341
|
+
// ----------------------------------------------------------------------------
|
|
342
|
+
// E — Control Flow Flattening (lightweight, CNE-specific)
|
|
343
|
+
//
|
|
344
|
+
// For function bodies with 4+ statements, wraps them in a while(true)/switch
|
|
345
|
+
// state-machine dispatcher. The state numbers are seeded-random hex constants.
|
|
346
|
+
// This is independent of the jsobf CFG pass — and looks completely different
|
|
347
|
+
// because there is no dispatch-object pattern.
|
|
348
|
+
//
|
|
349
|
+
// Only applied to functions from USER code (no __cne marker).
|
|
350
|
+
// Rate: controlled by opts.cneFlattening (default 0.5)
|
|
351
|
+
// ----------------------------------------------------------------------------
|
|
352
|
+
function applyCneFlattening(ast, parse, rng, rate = 0.5) {
|
|
353
|
+
traverse(ast, {
|
|
354
|
+
FunctionDeclaration(path) {
|
|
355
|
+
if (path.node.__cne || path.node.__obf)
|
|
356
|
+
return;
|
|
357
|
+
if (rng.next() > rate)
|
|
358
|
+
return;
|
|
359
|
+
flattenBody(path, rng);
|
|
360
|
+
},
|
|
361
|
+
FunctionExpression(path) {
|
|
362
|
+
if (path.node.__cne || path.node.__obf)
|
|
363
|
+
return;
|
|
364
|
+
if (rng.next() > rate)
|
|
365
|
+
return;
|
|
366
|
+
flattenBody(path, rng);
|
|
367
|
+
},
|
|
368
|
+
ArrowFunctionExpression(path) {
|
|
369
|
+
if (path.node.__cne || path.node.__obf)
|
|
370
|
+
return;
|
|
371
|
+
if (!path.node.body || path.node.body.type !== "BlockStatement")
|
|
372
|
+
return;
|
|
373
|
+
if (rng.next() > rate)
|
|
374
|
+
return;
|
|
375
|
+
flattenBody(path, rng);
|
|
376
|
+
},
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
// Recursively check if a statement subtree contains a ReturnStatement,
|
|
380
|
+
// or any const/let VariableDeclaration (those create TDZ issues inside
|
|
381
|
+
// switch-case blocks that CFF generates).
|
|
382
|
+
function containsReturn(node) {
|
|
383
|
+
if (!node || typeof node !== "object")
|
|
384
|
+
return false;
|
|
385
|
+
if (node.type === "ReturnStatement")
|
|
386
|
+
return true;
|
|
387
|
+
// const/let inside a CFF switch-case block causes TDZ errors at runtime
|
|
388
|
+
if (node.type === "VariableDeclaration" &&
|
|
389
|
+
(node.kind === "const" || node.kind === "let"))
|
|
390
|
+
return true;
|
|
391
|
+
// Don't descend into nested functions — their returns don't affect outer scope
|
|
392
|
+
if (node.type === "FunctionDeclaration" || node.type === "FunctionExpression" ||
|
|
393
|
+
node.type === "ArrowFunctionExpression")
|
|
394
|
+
return false;
|
|
395
|
+
for (const k of Object.keys(node)) {
|
|
396
|
+
const v = node[k];
|
|
397
|
+
if (Array.isArray(v)) {
|
|
398
|
+
if (v.some(containsReturn))
|
|
399
|
+
return true;
|
|
400
|
+
}
|
|
401
|
+
else if (v && typeof v === "object" && v.type) {
|
|
402
|
+
if (containsReturn(v))
|
|
403
|
+
return true;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
return false;
|
|
407
|
+
}
|
|
408
|
+
function flattenBody(path, rng) {
|
|
409
|
+
const body = path.node.body;
|
|
410
|
+
if (!body || body.type !== "BlockStatement")
|
|
411
|
+
return;
|
|
412
|
+
const stmts = body.body;
|
|
413
|
+
if (!stmts || stmts.length < 4)
|
|
414
|
+
return;
|
|
415
|
+
// Skip if body has return statements or const/let declarations
|
|
416
|
+
// (const/let inside CFF switch cases cause TDZ errors at runtime)
|
|
417
|
+
const hasReturn = stmts.some((s) => containsReturn(s));
|
|
418
|
+
if (hasReturn)
|
|
419
|
+
return;
|
|
420
|
+
// Assign a random state key to each statement
|
|
421
|
+
const stateKeys = stmts.map(() => (rng.int32() >>> 0) & 0xfffffff);
|
|
422
|
+
// Entry state = first key
|
|
423
|
+
const entryState = stateKeys[0];
|
|
424
|
+
// Label for the while loop so we can break out of it from inside the switch
|
|
425
|
+
const loopLabel = `_L${((rng.int32() >>> 0) & 0xffff).toString(16)}`;
|
|
426
|
+
const loopLabelId = t.identifier(loopLabel);
|
|
427
|
+
// State variable name (unique per function)
|
|
428
|
+
const stVar = `_st${((rng.int32() >>> 0) & 0xffff).toString(16)}`;
|
|
429
|
+
const stId = t.identifier(stVar);
|
|
430
|
+
markDeep(stId);
|
|
431
|
+
// Build: var _stXXXX = <entryState>;
|
|
432
|
+
const stDecl = t.variableDeclaration("var", [
|
|
433
|
+
t.variableDeclarator(stId, Object.assign(t.numericLiteral(entryState), { extra: { raw: `0x${entryState.toString(16)}` } })),
|
|
434
|
+
]);
|
|
435
|
+
markDeep(stDecl);
|
|
436
|
+
// Build switch cases. Each non-last case: exec stmt, set next state, continue.
|
|
437
|
+
// Last case: exec stmt, labeled-break exits the while(true).
|
|
438
|
+
const cases = stmts.map((stmt, i) => {
|
|
439
|
+
const isLast = i === stmts.length - 1;
|
|
440
|
+
const caseBody = [stmt];
|
|
441
|
+
if (!isLast) {
|
|
442
|
+
const nextKey = stateKeys[i + 1];
|
|
443
|
+
caseBody.push(t.expressionStatement(t.assignmentExpression("=", t.identifier(stVar), Object.assign(t.numericLiteral(nextKey), { extra: { raw: `0x${nextKey.toString(16)}` } }))));
|
|
444
|
+
caseBody.push(t.continueStatement(loopLabelId));
|
|
445
|
+
}
|
|
446
|
+
else {
|
|
447
|
+
// labeled break exits the while(true)
|
|
448
|
+
caseBody.push(t.breakStatement(loopLabelId));
|
|
449
|
+
}
|
|
450
|
+
const c = t.switchCase(Object.assign(t.numericLiteral(stateKeys[i]), { extra: { raw: `0x${stateKeys[i].toString(16)}` } }), caseBody);
|
|
451
|
+
markDeep(c);
|
|
452
|
+
return c;
|
|
453
|
+
});
|
|
454
|
+
// Default: labeled break (safety — should never fire for correct state machine)
|
|
455
|
+
const defCase = t.switchCase(null, [t.breakStatement(loopLabelId)]);
|
|
456
|
+
markDeep(defCase);
|
|
457
|
+
cases.push(defCase);
|
|
458
|
+
const switchStmt = t.switchStatement(t.identifier(stVar), cases);
|
|
459
|
+
markDeep(switchStmt);
|
|
460
|
+
const loop = t.labeledStatement(loopLabelId, t.whileStatement(t.booleanLiteral(true), t.blockStatement([switchStmt])));
|
|
461
|
+
markDeep(loop);
|
|
462
|
+
path.node.body = t.blockStatement([stDecl, loop]);
|
|
463
|
+
markDeep(path.node.body);
|
|
464
|
+
path.skip();
|
|
465
|
+
}
|
|
466
|
+
// ----------------------------------------------------------------------------
|
|
467
|
+
// F — Opaque constant injection
|
|
468
|
+
//
|
|
469
|
+
// Replaces `true` / `false` literals in conditions with expressions that
|
|
470
|
+
// evaluate to the same value but cannot be statically resolved:
|
|
471
|
+
// true → (typeof undefined === 'undefined')
|
|
472
|
+
// false → (typeof null === 'function')
|
|
473
|
+
//
|
|
474
|
+
// Seeded random selection from a set of opaque true/false expressions.
|
|
475
|
+
// Rate-controlled (default 0.4).
|
|
476
|
+
// ----------------------------------------------------------------------------
|
|
477
|
+
const OPAQUE_TRUE_EXPRS = [
|
|
478
|
+
`(typeof undefined==='undefined')`,
|
|
479
|
+
`(0x0===0x0)`,
|
|
480
|
+
`([][0x0]===undefined)`,
|
|
481
|
+
`(!![])`,
|
|
482
|
+
`(typeof ''==='string')`,
|
|
483
|
+
];
|
|
484
|
+
const OPAQUE_FALSE_EXPRS = [
|
|
485
|
+
`(typeof null==='function')`,
|
|
486
|
+
`(0x1===0x0)`,
|
|
487
|
+
`([][0x0]===null)`,
|
|
488
|
+
`(typeof undefined==='number')`,
|
|
489
|
+
`(!![]===[])`,
|
|
490
|
+
];
|
|
491
|
+
function applyOpaqueConstants(ast, rng, rate = 0.4) {
|
|
492
|
+
traverse(ast, {
|
|
493
|
+
BooleanLiteral(path) {
|
|
494
|
+
if (path.node.__cne || path.node.__obf)
|
|
495
|
+
return;
|
|
496
|
+
// Only inside conditions / test expressions
|
|
497
|
+
const p = path.parent;
|
|
498
|
+
if (!t.isIfStatement(p) &&
|
|
499
|
+
!t.isWhileStatement(p) &&
|
|
500
|
+
!t.isConditionalExpression(p) &&
|
|
501
|
+
!t.isLogicalExpression(p) &&
|
|
502
|
+
!t.isUnaryExpression(p))
|
|
503
|
+
return;
|
|
504
|
+
if (rng.next() > rate)
|
|
505
|
+
return;
|
|
506
|
+
const exprs = path.node.value ? OPAQUE_TRUE_EXPRS : OPAQUE_FALSE_EXPRS;
|
|
507
|
+
const src = exprs[Math.floor(rng.next() * exprs.length)];
|
|
508
|
+
try {
|
|
509
|
+
const mini = (0, parser_1.parse)(`(${src})`, { sourceType: "module" });
|
|
510
|
+
const expr = mini.program.body[0].expression;
|
|
511
|
+
markDeep(expr);
|
|
512
|
+
path.replaceWith(expr);
|
|
513
|
+
path.skip();
|
|
514
|
+
}
|
|
515
|
+
catch {
|
|
516
|
+
// parse failed — leave original
|
|
517
|
+
}
|
|
518
|
+
},
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
// ----------------------------------------------------------------------------
|
|
522
|
+
// G — Dead code injection (looks like real logic, not constant-folded junk)
|
|
523
|
+
//
|
|
524
|
+
// Inserts fake hash/cipher rounds that look structurally identical to the real
|
|
525
|
+
// FNV/RC4 patterns in the output but are unreachable via opaque predicates.
|
|
526
|
+
// ----------------------------------------------------------------------------
|
|
527
|
+
function buildFakeHashRound(rng) {
|
|
528
|
+
const v1 = `_${(rng.int32() >>> 0 & 0xfff).toString(16)}`;
|
|
529
|
+
const v2 = `_${(rng.int32() >>> 0 & 0xfff).toString(16)}`;
|
|
530
|
+
const seed = (rng.int32() >>> 0).toString(16);
|
|
531
|
+
const prime = rng.next() < 0.5 ? `0x01000193` : `0x9e3779b9`;
|
|
532
|
+
return `var ${v1}=0x${seed}>>>0x0;` +
|
|
533
|
+
`var ${v2}=Math.imul(${v1}^(${v1}>>>0xf),${prime});` +
|
|
534
|
+
`${v2}=(${v2}+Math.imul(${v2}^(${v2}>>>0x7),0x61|${v2}))^${v2};`;
|
|
535
|
+
}
|
|
536
|
+
function applyDeadCodeInjection(ast, rng, rate = 0.35) {
|
|
537
|
+
traverse(ast, {
|
|
538
|
+
Function(path) {
|
|
539
|
+
if (path.node.__cne || path.node.__obf)
|
|
540
|
+
return;
|
|
541
|
+
const body = path.get("body");
|
|
542
|
+
if (!body.isBlockStatement())
|
|
543
|
+
return;
|
|
544
|
+
if (rng.next() > rate)
|
|
545
|
+
return;
|
|
546
|
+
// Wrap in always-false opaque predicate
|
|
547
|
+
const n = `_n${(rng.int32() >>> 0 & 0xffff).toString(16)}`;
|
|
548
|
+
const deadSrc = `if((function(){var ${n}=Date.now()&0xff;return(${n}*(${n}+0x1)%0x2)===0x1;})()){${buildFakeHashRound(rng)}}`;
|
|
549
|
+
try {
|
|
550
|
+
const mini = (0, parser_1.parse)(deadSrc, { sourceType: "module", allowReturnOutsideFunction: true });
|
|
551
|
+
const stmt = mini.program.body[0];
|
|
552
|
+
markDeep(stmt);
|
|
553
|
+
body.unshiftContainer("body", stmt);
|
|
554
|
+
}
|
|
555
|
+
catch {
|
|
556
|
+
// ignore
|
|
557
|
+
}
|
|
558
|
+
},
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
// ----------------------------------------------------------------------------
|
|
562
|
+
// H — __anc encoding
|
|
563
|
+
//
|
|
564
|
+
// Instead of: var __anc = [792042790, 815997621, ...]
|
|
565
|
+
// We emit: var __anc = _decA('\x2f\x35...')
|
|
566
|
+
//
|
|
567
|
+
// where _decA is a tiny inline decoder that XOR-decodes the base64/hex blob.
|
|
568
|
+
// This eliminates 24 visible int32 constants from the output.
|
|
569
|
+
//
|
|
570
|
+
// The decoder name is per-build randomised so it has no fixed signature.
|
|
571
|
+
// ----------------------------------------------------------------------------
|
|
572
|
+
function buildEncodedAnchorSource(ancName, hshName, anchor, rng, nameGen, idxBase) {
|
|
573
|
+
// Encode anchor as little-endian bytes XOR'd with a per-build key stream
|
|
574
|
+
const key = (rng.int32() >>> 0) & 0xff;
|
|
575
|
+
const bytes = [];
|
|
576
|
+
for (const v of anchor) {
|
|
577
|
+
const n = v >>> 0;
|
|
578
|
+
bytes.push((n & 0xff) ^ key);
|
|
579
|
+
bytes.push(((n >>> 8) & 0xff) ^ key);
|
|
580
|
+
bytes.push(((n >>> 16) & 0xff) ^ key);
|
|
581
|
+
bytes.push(((n >>> 24) & 0xff) ^ key);
|
|
582
|
+
}
|
|
583
|
+
// Encode as escaped hex string literal
|
|
584
|
+
const encoded = bytes.map(b => `\\x${b.toString(16).padStart(2, "0")}`).join("");
|
|
585
|
+
// Per-build decoder function name
|
|
586
|
+
const decoderName = nameGen(idxBase);
|
|
587
|
+
const tmpV = nameGen(idxBase + 1);
|
|
588
|
+
const tmpI = nameGen(idxBase + 2);
|
|
589
|
+
const tmpR = nameGen(idxBase + 3);
|
|
590
|
+
const keyHex = `0x${key.toString(16)}`;
|
|
591
|
+
const lenHex = `0x${(anchor.length * 4).toString(16)}`;
|
|
592
|
+
const n4Hex = `0x${(anchor.length).toString(16)}`;
|
|
593
|
+
return (
|
|
594
|
+
// Decoder: takes the encoded string, returns Uint32Array → plain Array
|
|
595
|
+
`function ${decoderName}(${tmpV}){` +
|
|
596
|
+
`var ${tmpR}=[];` +
|
|
597
|
+
`for(var ${tmpI}=0x0;${tmpI}<${lenHex};${tmpI}+=0x4){` +
|
|
598
|
+
`${tmpR}.push(` +
|
|
599
|
+
`((${tmpV}.charCodeAt(${tmpI})^${keyHex})|` +
|
|
600
|
+
`((${tmpV}.charCodeAt(${tmpI}+0x1)^${keyHex})<<0x8)|` +
|
|
601
|
+
`((${tmpV}.charCodeAt(${tmpI}+0x2)^${keyHex})<<0x10)|` +
|
|
602
|
+
`((${tmpV}.charCodeAt(${tmpI}+0x3)^${keyHex})<<0x18))` +
|
|
603
|
+
`);}return ${tmpR};}` +
|
|
604
|
+
`var ${ancName}=${decoderName}("${encoded}");` +
|
|
605
|
+
// Hash function (FNV-1a)
|
|
606
|
+
`function ${hshName}(a){var h=0x811c9dc5>>>0x0;for(var i=0x0;i<a.length;i++){h=Math.imul(h^(a[i]|0x0),0x01000193);}return h|0x0;}`);
|
|
607
|
+
}
|
|
608
|
+
// ----------------------------------------------------------------------------
|
|
609
|
+
// H2 — module.exports property name obfuscation
|
|
610
|
+
//
|
|
611
|
+
// Rewrites `module.exports = { foo: _0xABC, bar: _0xDEF }` so the exported
|
|
612
|
+
// property names are no longer readable string literals in the output.
|
|
613
|
+
// Instead they are computed from an IIFE that assembles the name at runtime:
|
|
614
|
+
// module.exports[_k('foo')] = _0xABC
|
|
615
|
+
// where _k is a tiny per-build decode function defined inline.
|
|
616
|
+
// The function name and key are per-build random so there is no fixed pattern.
|
|
617
|
+
//
|
|
618
|
+
// Only applies to module.exports assignment expressions at the top level of
|
|
619
|
+
// the program body (the standard CJS export pattern).
|
|
620
|
+
// ----------------------------------------------------------------------------
|
|
621
|
+
function obfuscateExportNames(code, rng) {
|
|
622
|
+
// Per-build XOR key and function name
|
|
623
|
+
const xorKey = ((rng.int32() >>> 0) & 0xff) | 1; // non-zero byte
|
|
624
|
+
const fnName = `_0x${((rng.int32() >>> 0) & 0xffffff).toString(16).padStart(6, '0')}`;
|
|
625
|
+
// Encode a string: each char XOR'd with key, then base64
|
|
626
|
+
const encStr = (s) => {
|
|
627
|
+
const buf = Buffer.alloc(s.length);
|
|
628
|
+
for (let i = 0; i < s.length; i++)
|
|
629
|
+
buf[i] = s.charCodeAt(i) ^ xorKey;
|
|
630
|
+
return buf.toString('base64');
|
|
631
|
+
};
|
|
632
|
+
// Decoder source: function _0xXXXXXX(s){var b=Buffer.from(s,'base64'),r='';for(var i=0;i<b.length;i++)r+=String.fromCharCode(b[i]^KEY);return r;}
|
|
633
|
+
const decoderSrc = `function ${fnName}(s){var b=Buffer.from(s,'base64'),r='';` +
|
|
634
|
+
`for(var i=0;i<b.length;i++)r+=String.fromCharCode(b[i]^${xorKey});return r;}`;
|
|
635
|
+
// Find module.exports = { ... } or module.exports={...} and rewrite property keys
|
|
636
|
+
// Regex: module.exports = { key: value, key2: value2 }
|
|
637
|
+
// We only rewrite IDENTIFIER keys (not computed or string keys already)
|
|
638
|
+
let found = false;
|
|
639
|
+
const result = code.replace(/module\.exports\s*=\s*\{([^}]+)\}/g, (match, body) => {
|
|
640
|
+
const rewritten = body.replace(/([a-zA-Z_$][a-zA-Z0-9_$]*)\s*:/g, (_m, key) => {
|
|
641
|
+
found = true;
|
|
642
|
+
return `[${fnName}('${encStr(key)}')]:`;
|
|
643
|
+
});
|
|
644
|
+
return `module.exports={${rewritten}}`;
|
|
645
|
+
});
|
|
646
|
+
if (!found)
|
|
647
|
+
return code;
|
|
648
|
+
// Prepend the decoder function
|
|
649
|
+
return decoderSrc + '\n' + result;
|
|
650
|
+
}
|
|
651
|
+
function cneObfuscate(src, rng, opts = {}) {
|
|
652
|
+
const ast = (0, parser_1.parse)(src, {
|
|
653
|
+
sourceType: "unambiguous",
|
|
654
|
+
allowReturnOutsideFunction: true,
|
|
655
|
+
errorRecovery: true,
|
|
656
|
+
});
|
|
657
|
+
// Use _0x<hex> names: the skip regex ^_0x[0-9a-f]{4,} prevents CNE from
|
|
658
|
+
// re-renaming VM runtime internals (BC, K, S, L, etc.) on subsequent passes.
|
|
659
|
+
// Stego names (pronounceable words) cause collisions with VM internal names.
|
|
660
|
+
const nameGen = makeNameGen(rng.int32());
|
|
661
|
+
// Dead code first (so the injected dead vars also get renamed)
|
|
662
|
+
if (opts.deadCode !== false) {
|
|
663
|
+
applyDeadCodeInjection(ast, rng, 0.35);
|
|
664
|
+
}
|
|
665
|
+
// Opaque boolean constants
|
|
666
|
+
if (opts.opaqueConstants !== false) {
|
|
667
|
+
applyOpaqueConstants(ast, rng, 0.4);
|
|
668
|
+
}
|
|
669
|
+
// Control flow flattening
|
|
670
|
+
if (opts.flatten !== false) {
|
|
671
|
+
applyCneFlattening(ast, parser_1.parse, rng, opts.flattenRate ?? 0.5);
|
|
672
|
+
}
|
|
673
|
+
// String splitting (before rename so chunk strings get non-obf names)
|
|
674
|
+
if (opts.stringSplit !== false) {
|
|
675
|
+
applyStringSplit(ast, rng);
|
|
676
|
+
}
|
|
677
|
+
// Hex literals
|
|
678
|
+
if (opts.hexLiterals !== false) {
|
|
679
|
+
applyHexLiterals(ast);
|
|
680
|
+
}
|
|
681
|
+
// Full scope rename (last — renames everything including injected vars)
|
|
682
|
+
if (opts.rename !== false) {
|
|
683
|
+
applyFullRename(ast, rng, nameGen);
|
|
684
|
+
}
|
|
685
|
+
return generate(ast, { compact: true, comments: false }).code;
|
|
686
|
+
}
|