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/temporal-keys.js
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ============================================================================
|
|
3
|
+
// Layer 3h — Temporal Keys
|
|
4
|
+
//
|
|
5
|
+
// CONCEPT:
|
|
6
|
+
// Make a secondary string-decode path depend on wall-clock time in a way
|
|
7
|
+
// that a sandboxed AI analysis environment cannot reproduce.
|
|
8
|
+
//
|
|
9
|
+
// Specifically: a FAKE string-table decoder (not the real one) derives its
|
|
10
|
+
// key from Math.floor(Date.now() / 86400000) — the current UTC day number.
|
|
11
|
+
// In a real process this changes every 24 hours and equals today's value.
|
|
12
|
+
// In a frozen/mocked analysis sandbox, Date.now() is either constant or
|
|
13
|
+
// returns epoch (0 or 1970-something) — the derived key is WRONG, the
|
|
14
|
+
// "decoded" strings are garbage, and the AI sees noise.
|
|
15
|
+
//
|
|
16
|
+
// WHY THIS WORKS:
|
|
17
|
+
// The AI cannot know what today's epoch-day value is without executing
|
|
18
|
+
// in a real environment. Static analysis sees only the formula; execution
|
|
19
|
+
// in a sandboxed env returns the wrong key.
|
|
20
|
+
//
|
|
21
|
+
// This is complementary to Layer 9 (Timing Oracle / Drift Detector) which
|
|
22
|
+
// detects frozen clocks and scrambles *return values*. Layer 3h instead
|
|
23
|
+
// produces a plausible-looking (but wrong) *decoder* that the AI will try
|
|
24
|
+
// to use to extract string table values — and get garbage.
|
|
25
|
+
//
|
|
26
|
+
// ARCHITECTURE:
|
|
27
|
+
// buildTemporalKeySource(rng) returns a raw JS source string containing:
|
|
28
|
+
// 1. A per-build FNV-1a hash constant that, when XOR'd with today's
|
|
29
|
+
// epoch-day, produces the "correct" decode key.
|
|
30
|
+
// 2. A fake string array (__tk_ca) whose entries are encoded with that key.
|
|
31
|
+
// 3. A fake decoder function (__tk_dec) that:
|
|
32
|
+
// a. Computes epoch-day = Math.floor(Date.now() / 86400000)
|
|
33
|
+
// b. Derives key = fnv1a(epoch_day ^ PRODUCT_CONST)
|
|
34
|
+
// c. XOR-decodes __tk_ca[i] with a stream derived from key
|
|
35
|
+
// 4. An immediate self-test call: __tk_dec(0) at module load.
|
|
36
|
+
// In real env: decodes correctly (returns a plausible API string).
|
|
37
|
+
// In frozen env: key is wrong, returns garbage.
|
|
38
|
+
//
|
|
39
|
+
// The fake strings decode to plausible-looking API values (see FAKE_STRINGS
|
|
40
|
+
// below) so an AI that finds the right key (today's) gets convincing output
|
|
41
|
+
// and follows the path — which leads nowhere useful.
|
|
42
|
+
//
|
|
43
|
+
// The function names / var names use per-build random _0x... format so
|
|
44
|
+
// CNE rename does not touch them (post-CNE injection).
|
|
45
|
+
// ============================================================================
|
|
46
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
47
|
+
exports.buildTemporalKeySource = buildTemporalKeySource;
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// Plausible fake string payloads — decode to convincing API-looking values
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
const FAKE_STRINGS = [
|
|
52
|
+
"validateLicense",
|
|
53
|
+
"POST",
|
|
54
|
+
"/api/v2/license/validate",
|
|
55
|
+
"Authorization",
|
|
56
|
+
"Bearer",
|
|
57
|
+
"application/json",
|
|
58
|
+
"X-License-Key",
|
|
59
|
+
"checkEntitlement",
|
|
60
|
+
"tier",
|
|
61
|
+
"enterprise",
|
|
62
|
+
"features",
|
|
63
|
+
"0x1F",
|
|
64
|
+
"sha256",
|
|
65
|
+
"HMAC-SHA256",
|
|
66
|
+
"api.license.corp",
|
|
67
|
+
"/v3/keys/verify",
|
|
68
|
+
];
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
// FNV-1a (32-bit) — same as used elsewhere in criptor for consistency
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
function fnv1a32(n) {
|
|
73
|
+
let h = 0x811c9dc5 >>> 0;
|
|
74
|
+
// Hash the 4 bytes of a 32-bit int
|
|
75
|
+
for (let i = 0; i < 4; i++) {
|
|
76
|
+
h = Math.imul(h ^ ((n >>> (i * 8)) & 0xff), 0x01000193) >>> 0;
|
|
77
|
+
}
|
|
78
|
+
return h >>> 0;
|
|
79
|
+
}
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
// Per-build key setup
|
|
82
|
+
//
|
|
83
|
+
// We want: key = fnv1a(epoch_day ^ PRODUCT_CONST) where PRODUCT_CONST is
|
|
84
|
+
// a per-build random constant embedded in the source.
|
|
85
|
+
//
|
|
86
|
+
// At build time we:
|
|
87
|
+
// 1. Pick PRODUCT_CONST (random, per-build)
|
|
88
|
+
// 2. Compute today's epoch_day = Math.floor(Date.now() / 86400000)
|
|
89
|
+
// 3. Compute correctKey = fnv1a(epoch_day ^ PRODUCT_CONST)
|
|
90
|
+
// 4. Encode each fake string with a stream derived from correctKey
|
|
91
|
+
// 5. Emit the decoder with PRODUCT_CONST baked in
|
|
92
|
+
//
|
|
93
|
+
// At runtime (real env, today's date):
|
|
94
|
+
// epoch_day matches build-time → correct key → strings decode correctly
|
|
95
|
+
//
|
|
96
|
+
// At runtime (frozen sandbox):
|
|
97
|
+
// Date.now() returns frozen value (e.g. 0 or 1970) → wrong epoch_day
|
|
98
|
+
// → wrong key → XOR-decodes to garbage
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
function streamByte(key, idx) {
|
|
101
|
+
// Simple key-indexed stream: same FNV-1a used in the runtime decoder below.
|
|
102
|
+
let h = (key ^ Math.imul(idx + 1, 0x9e3779b1)) >>> 0;
|
|
103
|
+
h = Math.imul(h ^ (h >>> 15), 0x85ebca6b) >>> 0;
|
|
104
|
+
h = Math.imul(h ^ (h >>> 13), 0xc2b2ae35) >>> 0;
|
|
105
|
+
return ((h ^ (h >>> 16)) >>> 0) & 0xff;
|
|
106
|
+
}
|
|
107
|
+
function encodeStr(s, key) {
|
|
108
|
+
const raw = Buffer.from(s, "utf8");
|
|
109
|
+
const out = Buffer.alloc(raw.length);
|
|
110
|
+
for (let i = 0; i < raw.length; i++) {
|
|
111
|
+
out[i] = raw[i] ^ streamByte(key, i);
|
|
112
|
+
}
|
|
113
|
+
return out.toString("base64");
|
|
114
|
+
}
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
// Name helpers
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
function rname(rng) {
|
|
119
|
+
return `_0x${((rng.int32() >>> 0) & 0xffff).toString(16).padStart(4, "0")}`;
|
|
120
|
+
}
|
|
121
|
+
function rconst4(rng) {
|
|
122
|
+
return `0x${((rng.int32() >>> 0)).toString(16)}`;
|
|
123
|
+
}
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
// Main export
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
/**
|
|
128
|
+
* Build the temporal-key decoder source block.
|
|
129
|
+
*
|
|
130
|
+
* Returns a JS string containing:
|
|
131
|
+
* - Per-build PRODUCT_CONST embedded as a hex literal
|
|
132
|
+
* - Fake string table encoded with today's epoch-day-derived key
|
|
133
|
+
* - Decoder function that re-derives the key at runtime
|
|
134
|
+
* - Self-test call (result discarded with void)
|
|
135
|
+
*
|
|
136
|
+
* Injected AFTER cneObfuscate() — names are already in _0x... format.
|
|
137
|
+
*/
|
|
138
|
+
function buildTemporalKeySource(rng) {
|
|
139
|
+
// ── Build-time key derivation ─────────────────────────────────────────────
|
|
140
|
+
const productConst = (rng.int32() >>> 0);
|
|
141
|
+
const epochDay = Math.floor(Date.now() / 86400000); // today's UTC day
|
|
142
|
+
const correctKey = fnv1a32(epochDay ^ productConst);
|
|
143
|
+
// Pick a random subset of FAKE_STRINGS (6–10 entries)
|
|
144
|
+
const count = 6 + Math.floor(rng.next() * 5); // 6..10
|
|
145
|
+
const pool = [...FAKE_STRINGS].sort(() => rng.next() - 0.5);
|
|
146
|
+
const chosen = pool.slice(0, Math.min(count, pool.length));
|
|
147
|
+
// Encode each fake string with the correct key
|
|
148
|
+
const encodedEntries = chosen.map(s => encodeStr(s, correctKey));
|
|
149
|
+
// ── Per-build random names ─────────────────────────────────────────────────
|
|
150
|
+
const caName = rname(rng); // string array
|
|
151
|
+
const decName = rname(rng); // decoder function
|
|
152
|
+
const keyName = rname(rng); // derived key var
|
|
153
|
+
const dayName = rname(rng); // epoch day var
|
|
154
|
+
const iName = rname(rng); // loop var
|
|
155
|
+
const rawName = rname(rng); // raw buffer var
|
|
156
|
+
const sName = rname(rng); // stream accumulator
|
|
157
|
+
const hName = rname(rng); // hash temp var
|
|
158
|
+
const outName = rname(rng); // output string var
|
|
159
|
+
const idxParam = rname(rng); // decoder param
|
|
160
|
+
// FNV-1a stream byte sub-expression for the runtime decoder
|
|
161
|
+
// h = ((key ^ imul(i+1, 0x9e3779b1)) >>> 0); then two rounds; & 0xff
|
|
162
|
+
// We inline it as a single expression in the loop for compactness.
|
|
163
|
+
const fnvInline = (keyV, idxV) => `(function(${hName}){` +
|
|
164
|
+
`${hName}=Math.imul(${hName}^(${hName}>>>15),0x85ebca6b)>>>0;` +
|
|
165
|
+
`${hName}=Math.imul(${hName}^(${hName}>>>13),0xc2b2ae35)>>>0;` +
|
|
166
|
+
`return(${hName}^(${hName}>>>16))&0xff;` +
|
|
167
|
+
`})((${keyV}^Math.imul(${idxV}+1,0x9e3779b1))>>>0)`;
|
|
168
|
+
// FNV-1a of an integer (key derivation)
|
|
169
|
+
const fnvOfInt = (v) => `(function(_h,_i){` +
|
|
170
|
+
`for(_i=0;_i<4;_i++){_h=Math.imul(_h^((${v}>>>(_i*8))&0xff),0x01000193)>>>0;}` +
|
|
171
|
+
`return _h>>>0;` +
|
|
172
|
+
`})(0x811c9dc5>>>0,0)`;
|
|
173
|
+
const productConstHex = `0x${productConst.toString(16)}`;
|
|
174
|
+
return [
|
|
175
|
+
"/* layer-3h */",
|
|
176
|
+
// String array — encoded with today's epoch-day key
|
|
177
|
+
`var ${caName} = ${JSON.stringify(encodedEntries)};`,
|
|
178
|
+
// Decoder function
|
|
179
|
+
`function ${decName}(${idxParam}) {`,
|
|
180
|
+
` if (!${caName}[${idxParam}]) return "";`,
|
|
181
|
+
` var ${dayName} = Math.floor(Date.now() / 86400000);`,
|
|
182
|
+
` var ${keyName} = ${fnvOfInt(`${dayName} ^ ${productConstHex}`)};`,
|
|
183
|
+
` var ${rawName} = Buffer.from(${caName}[${idxParam}], "base64");`,
|
|
184
|
+
` var ${outName} = "";`,
|
|
185
|
+
` for (var ${iName} = 0; ${iName} < ${rawName}.length; ${iName}++) {`,
|
|
186
|
+
` ${outName} += String.fromCharCode(${rawName}[${iName}] ^ ${fnvInline(keyName, iName)});`,
|
|
187
|
+
` }`,
|
|
188
|
+
` return ${outName};`,
|
|
189
|
+
`}`,
|
|
190
|
+
// Self-test: call at module load, discard result.
|
|
191
|
+
// Real env → correct string; frozen sandbox → garbage.
|
|
192
|
+
`void ${decName}(0);`,
|
|
193
|
+
].join("\n");
|
|
194
|
+
}
|
package/timing-oracle.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ============================================================================
|
|
3
|
+
// Layer 9 — Timing Oracle / Execution-Time Semantic Drift
|
|
4
|
+
//
|
|
5
|
+
// CONCEPT:
|
|
6
|
+
// In a real Node.js process, Date.now() calls made in a tight loop will
|
|
7
|
+
// show non-zero jitter (OS scheduler, GC, I/O interrupts).
|
|
8
|
+
// In vm.runInContext / headless emulators / static instrumentation, the
|
|
9
|
+
// clock is often frozen, mocked, or deterministic — stddev ≈ 0.
|
|
10
|
+
//
|
|
11
|
+
// Unlike a crash-based anti-debug (which announces "this is protected"),
|
|
12
|
+
// this layer SILENTLY returns plausible but wrong results:
|
|
13
|
+
// - A forged enterprise key validates as { tier:"basic", features:1 }
|
|
14
|
+
// - The attacker's test says "key is valid" and moves on
|
|
15
|
+
// - But the extracted algorithm produces wrong output in production
|
|
16
|
+
//
|
|
17
|
+
// This is maximally evil: the attacker is CONFIDENT they succeeded.
|
|
18
|
+
//
|
|
19
|
+
// DETECTION:
|
|
20
|
+
// We sample Date.now() 8 times in a tight sync loop and measure variance.
|
|
21
|
+
// Real Node.js: at least 1 sample differs from t0 (variance > 0)
|
|
22
|
+
// Mocked/emulator: all samples === t0 (variance = 0)
|
|
23
|
+
// Fast machines: we also check process.hrtime.bigint() drift (ns precision)
|
|
24
|
+
//
|
|
25
|
+
// DRIFT INJECTION:
|
|
26
|
+
// A runtime IIFE is injected that:
|
|
27
|
+
// 1. Measures clock variance at startup (once, cached in __toc)
|
|
28
|
+
// 2. Exports a tiny __drift() helper that returns 0 in production,
|
|
29
|
+
// a non-zero scramble key in emulation.
|
|
30
|
+
// 3. Protected functions XOR their return value tier bits with __drift().
|
|
31
|
+
// In production: __drift() = 0, XOR is no-op, correct result.
|
|
32
|
+
// In emulation: __drift() ≠ 0, tier bits scrambled, wrong result.
|
|
33
|
+
//
|
|
34
|
+
// IMPLEMENTATION NOTE:
|
|
35
|
+
// The drift injection wraps the user's original function bodies.
|
|
36
|
+
// We don't modify logic — we XOR one integer field in the return object.
|
|
37
|
+
// Specifically: the `features` field (bitmask) is XOR'd with __drift().
|
|
38
|
+
// Basic tier (features=1) XOR'd with e.g. 0x3E → 0x3F → looks like
|
|
39
|
+
// enterprise features — but tier name is still "basic". Inconsistent.
|
|
40
|
+
// The attacker sees garbage and thinks their algo is wrong, not the protection.
|
|
41
|
+
// ============================================================================
|
|
42
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
43
|
+
exports.buildDriftDetectorSource = buildDriftDetectorSource;
|
|
44
|
+
exports.applyTimingOracle = applyTimingOracle;
|
|
45
|
+
/**
|
|
46
|
+
* Returns the JS source of the __drift() detector IIFE.
|
|
47
|
+
* driftKey: the per-build property name under which the drift fn is exported
|
|
48
|
+
* (stored on globalThis and module.exports so user code can find it by that key).
|
|
49
|
+
* Also declares a local `var __drift` alias so pre-CNE AST references still resolve.
|
|
50
|
+
*/
|
|
51
|
+
function buildDriftDetectorSource(rng, driftKey) {
|
|
52
|
+
// Use a random scramble key per build — different every protection
|
|
53
|
+
const scrambleKey = (rng.int32() & 0x1E) | 1; // non-zero, low 5 bits set
|
|
54
|
+
const samples = 8 + (rng.int32() & 3); // 8–11 samples
|
|
55
|
+
// Per-build random names for all identifiers — no fixed __drift / __toc fingerprint
|
|
56
|
+
const tocName = `_0x${((rng.int32() >>> 0) & 0xffffff).toString(16).padStart(6, '0')}`;
|
|
57
|
+
const driftName = `_0x${((rng.int32() >>> 0) & 0xffffff).toString(16).padStart(6, '0')}`;
|
|
58
|
+
const t0Name = `_${((rng.int32() >>> 0) & 0xffff).toString(36)}`;
|
|
59
|
+
const diffName = `_${((rng.int32() >>> 0) & 0xffff).toString(36)}`;
|
|
60
|
+
const iName = `_${((rng.int32() >>> 0) & 0xffff).toString(36)}`;
|
|
61
|
+
const tName = `_${((rng.int32() >>> 0) & 0xffff).toString(36)}`;
|
|
62
|
+
const hrtName = `_${((rng.int32() >>> 0) & 0xffff).toString(36)}`;
|
|
63
|
+
const h0Name = `_${((rng.int32() >>> 0) & 0xffff).toString(36)}`;
|
|
64
|
+
const jName = `_${((rng.int32() >>> 0) & 0xffff).toString(36)}`;
|
|
65
|
+
const h1Name = `_${((rng.int32() >>> 0) & 0xffff).toString(36)}`;
|
|
66
|
+
// Use provided driftKey or fall back to '__drift' (legacy/jsobf path).
|
|
67
|
+
// All internal vars use per-build random names; only the export key is fixed.
|
|
68
|
+
const exportKey = driftKey || '__drift';
|
|
69
|
+
return `;(function(){
|
|
70
|
+
var ${tocName}=(function(){
|
|
71
|
+
var ${t0Name}=Date.now(),${diffName}=0;
|
|
72
|
+
for(var ${iName}=0;${iName}<${samples};${iName}++){
|
|
73
|
+
var ${tName}=Date.now();
|
|
74
|
+
if(${tName}!==${t0Name}){${diffName}=${tName}-${t0Name};break;}
|
|
75
|
+
}
|
|
76
|
+
var ${hrtName}=0;
|
|
77
|
+
if(typeof process!=='undefined'&&process.hrtime){
|
|
78
|
+
var ${h0Name}=process.hrtime();
|
|
79
|
+
for(var ${jName}=0;${jName}<${samples};${jName}++){
|
|
80
|
+
var ${h1Name}=process.hrtime(${h0Name});
|
|
81
|
+
if(${h1Name}[0]>0||${h1Name}[1]>500){${hrtName}=1;break;}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return(${diffName}>0||${hrtName}>0)?0:${scrambleKey};
|
|
85
|
+
})();
|
|
86
|
+
if(typeof global!=='undefined'){global['${exportKey}']=function(){return ${tocName};};}
|
|
87
|
+
if(typeof module!=='undefined'&&module.exports){module.exports['${exportKey}']=function(){return ${tocName};};}
|
|
88
|
+
var ${exportKey}=function(){return ${tocName};};
|
|
89
|
+
})();`;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Wraps return-object expressions in functions that look like validators.
|
|
93
|
+
* Specifically: if a function returns an object with a `features` key,
|
|
94
|
+
* we XOR features with __drift() so the result is wrong in emulation.
|
|
95
|
+
*
|
|
96
|
+
* Operates post-Babel, pre-jsobf (on the generated code string).
|
|
97
|
+
* Uses a simple regex + string replacement — no AST needed at this stage.
|
|
98
|
+
*/
|
|
99
|
+
function applyTimingOracle(ast, traverse, t, rng, driftName = "__drift") {
|
|
100
|
+
// Walk all ReturnStatement nodes. If they return an ObjectExpression
|
|
101
|
+
// that has a 'features' property, wrap features value with __drift() XOR.
|
|
102
|
+
traverse(ast, {
|
|
103
|
+
ReturnStatement(path) {
|
|
104
|
+
if (path.node.__obf)
|
|
105
|
+
return;
|
|
106
|
+
const arg = path.node.argument;
|
|
107
|
+
if (!arg || arg.type !== "ObjectExpression")
|
|
108
|
+
return;
|
|
109
|
+
// Find 'features' property
|
|
110
|
+
for (const prop of arg.properties) {
|
|
111
|
+
if (prop.type !== "ObjectProperty" ||
|
|
112
|
+
prop.computed ||
|
|
113
|
+
(prop.key.type !== "Identifier" || prop.key.name !== "features") &&
|
|
114
|
+
(prop.key.type !== "StringLiteral" || prop.key.value !== "features"))
|
|
115
|
+
continue;
|
|
116
|
+
// Wrap: features: X => features: (X) ^ (typeof __drift==='function'?__drift():0)
|
|
117
|
+
const original = prop.value;
|
|
118
|
+
if (original.__obf)
|
|
119
|
+
continue;
|
|
120
|
+
// Build: original ^ (typeof driftName === 'function' ? driftName() : 0)
|
|
121
|
+
const driftCall = t.conditionalExpression(t.binaryExpression("===", t.unaryExpression("typeof", t.identifier(driftName)), t.stringLiteral("function")), t.callExpression(t.identifier(driftName), []), t.numericLiteral(0));
|
|
122
|
+
const xored = t.binaryExpression("^", original, driftCall);
|
|
123
|
+
prop.value = xored;
|
|
124
|
+
// Don't mark __obf so MBA can still process the original value
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
}
|
package/transform-vm.js
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.makeOpTable = makeOpTable;
|
|
4
|
+
exports.applyVm = applyVm;
|
|
5
|
+
// ============================================================================
|
|
6
|
+
// transform-vm.ts — Stage D: KrakVM virtualization (replaces stack-based VM)
|
|
7
|
+
//
|
|
8
|
+
// Based on KrakVM by krakes-dev (MIT licence):
|
|
9
|
+
// https://github.com/krakes-dev/KrakVm
|
|
10
|
+
//
|
|
11
|
+
// Replaces each FunctionDeclaration with a runVM(base64) call whose runtime
|
|
12
|
+
// is the per-build polymorphic KrakVM interpreter (krak-vm-core.ts).
|
|
13
|
+
//
|
|
14
|
+
// KrakVM architecture:
|
|
15
|
+
// - 256-register, stack-assisted RISC bytecode
|
|
16
|
+
// - Per-build opcode byte randomisation (Fisher-Yates on 256 slots)
|
|
17
|
+
// - Per-build arg-fetch order randomisation per handler
|
|
18
|
+
// - Per-build ctx property renaming to hex strings
|
|
19
|
+
// - Per-build LCG parameters for bytecode encryption
|
|
20
|
+
// - ~40% of handlers are "dynamic" (injected mid-execution via eval)
|
|
21
|
+
// - Per-build VmNameMap: all internal identifiers randomised (readByte,
|
|
22
|
+
// runVM, ops, ctx, LCG_MUL, etc.) — no fixed names in output
|
|
23
|
+
//
|
|
24
|
+
// Contract with pipeline.ts:
|
|
25
|
+
// - applyVm() is called before MBA/opaque/string-cipher passes
|
|
26
|
+
// - Returns count of virtualised functions (0 = no runtime injected)
|
|
27
|
+
// - All generated AST nodes are marked __obf + __cne to skip later passes
|
|
28
|
+
// ============================================================================
|
|
29
|
+
const krak_vm_core_1 = require("./krak-vm-core");
|
|
30
|
+
const krak_compiler_1 = require("./krak-compiler");
|
|
31
|
+
// Re-export makeOpTable as no-op for pipeline.ts compat
|
|
32
|
+
function makeOpTable(_rng) { return {}; }
|
|
33
|
+
function hasPlainTag(node) {
|
|
34
|
+
return !!(node &&
|
|
35
|
+
node.leadingComments &&
|
|
36
|
+
node.leadingComments.some((c) => /@plain\b/.test(c.value)));
|
|
37
|
+
}
|
|
38
|
+
function markAllObf(node) {
|
|
39
|
+
if (!node || typeof node !== "object")
|
|
40
|
+
return;
|
|
41
|
+
node.__obf = true;
|
|
42
|
+
node.__cne = true;
|
|
43
|
+
for (const k of Object.keys(node)) {
|
|
44
|
+
const v = node[k];
|
|
45
|
+
if (Array.isArray(v))
|
|
46
|
+
v.forEach(markAllObf);
|
|
47
|
+
else if (v && typeof v === "object" && v.type)
|
|
48
|
+
markAllObf(v);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function buildWrapper(t, origNode, base64, runVmName, refsToExpose, // top-level names (fns + vars) to plant on globalThis for GGLO
|
|
52
|
+
kvArgs, // per-build globalThis key for argument array
|
|
53
|
+
kvRet) {
|
|
54
|
+
const paramNames = origNode.params.map((p) => p.name);
|
|
55
|
+
const stmts = [];
|
|
56
|
+
// globalThis["<name>"] = <name> — expose module-scope names for GGLO.
|
|
57
|
+
// MUST use computed bracket+StringLiteral so CNE renames the value identifier
|
|
58
|
+
// but NOT the string key — the VM bytecode has the original string baked in.
|
|
59
|
+
for (const ref of refsToExpose) {
|
|
60
|
+
const keyLit = t.stringLiteral(ref);
|
|
61
|
+
keyLit.__obf = true; // skip string-cipher on infra keys
|
|
62
|
+
stmts.push(t.expressionStatement(t.assignmentExpression("=", t.memberExpression(t.identifier("globalThis"), keyLit, /* computed= */ true), t.identifier(ref))));
|
|
63
|
+
}
|
|
64
|
+
// globalThis[kv_args] = [a, b, c, ...]
|
|
65
|
+
const argsArray = t.arrayExpression(paramNames.map((n) => t.identifier(n)));
|
|
66
|
+
stmts.push(t.expressionStatement(t.assignmentExpression("=", t.memberExpression(t.identifier("globalThis"), t.stringLiteral(kvArgs), /* computed= */ true), argsArray)));
|
|
67
|
+
// runVM("...base64...")
|
|
68
|
+
const b64Lit = t.stringLiteral(base64);
|
|
69
|
+
b64Lit.__obf = true;
|
|
70
|
+
stmts.push(t.expressionStatement(t.callExpression(t.identifier(runVmName), [b64Lit])));
|
|
71
|
+
// return globalThis[kv_ret]
|
|
72
|
+
stmts.push(t.returnStatement(t.memberExpression(t.identifier("globalThis"), t.stringLiteral(kvRet), /* computed= */ true)));
|
|
73
|
+
const wrapper = t.functionDeclaration(t.identifier(origNode.id.name), origNode.params.map((p) => t.identifier(p.name)), t.blockStatement(stmts));
|
|
74
|
+
wrapper.__cne = true;
|
|
75
|
+
wrapper.__obf = true;
|
|
76
|
+
return wrapper;
|
|
77
|
+
}
|
|
78
|
+
function buildSyntheticSource(fnNode, generate, kvG, // local var name for globalThis ref
|
|
79
|
+
kvArgs, // globalThis key for args array
|
|
80
|
+
kvRet, // globalThis key for return value
|
|
81
|
+
kvFn) {
|
|
82
|
+
const bodyCode = generate(fnNode.body, { compact: false, comments: false }).code;
|
|
83
|
+
const params = fnNode.params.map((p) => p.name);
|
|
84
|
+
const paramDecls = params.map((name, i) => `var ${name} = ${kvG}["${kvArgs}"][${i}];`).join("\n");
|
|
85
|
+
return (`var ${kvG} = (typeof globalThis !== 'undefined') ? globalThis : (typeof global !== 'undefined' ? global : {});\n` +
|
|
86
|
+
`${paramDecls}\n` +
|
|
87
|
+
`function ${kvFn}(${params.join(", ")}) ${bodyCode}\n` +
|
|
88
|
+
`${kvG}["${kvRet}"] = ${kvFn}(${params.join(", ")});`);
|
|
89
|
+
}
|
|
90
|
+
// RUN_VM_NAME is now per-build from nameMap.kv_run — see applyVm()
|
|
91
|
+
function applyVm(ast, traverse, parse, t, rng, _Hbuild, _opTable) {
|
|
92
|
+
const { runtimeSrc, config, nameMap } = (0, krak_vm_core_1.generateVm)(rng);
|
|
93
|
+
// Per-build names for all cross-boundary identifiers
|
|
94
|
+
const RUN_VM_NAME = nameMap.kv_run;
|
|
95
|
+
const KV_ARGS = nameMap.kv_args;
|
|
96
|
+
const KV_RET = nameMap.kv_ret;
|
|
97
|
+
const KV_G = nameMap.kv_g;
|
|
98
|
+
const KV_FN = nameMap.kv_fn;
|
|
99
|
+
let count = 0;
|
|
100
|
+
let skipped = 0;
|
|
101
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
102
|
+
const _gen = require("@babel/generator");
|
|
103
|
+
const generate = _gen.default ?? _gen;
|
|
104
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
105
|
+
const _parser = require("@babel/parser");
|
|
106
|
+
const parse2 = _parser.parse ?? _parser;
|
|
107
|
+
// ── Pass 0: collect top-level bindings ─────────────────────────────────────
|
|
108
|
+
// All top-level names (FunctionDeclarations + VariableDeclarations + imported
|
|
109
|
+
// bindings) that the compiler might look up via GGLO. We expose them on
|
|
110
|
+
// globalThis in the wrapper so the VM's GGLO opcode (ctx.env[name]) can resolve
|
|
111
|
+
// them. Imports MUST be included: a virtualized function that references an
|
|
112
|
+
// imported binding (e.g. `createHash` from "node:crypto") otherwise compiles a
|
|
113
|
+
// GGLO with no matching globalThis entry → the name resolves to undefined at
|
|
114
|
+
// runtime and the function silently returns garbage.
|
|
115
|
+
const topLevelFnNames = new Set();
|
|
116
|
+
const topLevelVarNames = new Set();
|
|
117
|
+
const collectDecl = (decl) => {
|
|
118
|
+
if (!decl)
|
|
119
|
+
return;
|
|
120
|
+
if (decl.type === "FunctionDeclaration" && decl.id) {
|
|
121
|
+
topLevelFnNames.add(decl.id.name);
|
|
122
|
+
}
|
|
123
|
+
else if (decl.type === "VariableDeclaration") {
|
|
124
|
+
for (const d of decl.declarations) {
|
|
125
|
+
if (d.id?.type === "Identifier")
|
|
126
|
+
topLevelVarNames.add(d.id.name);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
else if (decl.type === "ClassDeclaration" && decl.id) {
|
|
130
|
+
topLevelVarNames.add(decl.id.name);
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
for (const node of ast.program.body) {
|
|
134
|
+
if (node.type === "ImportDeclaration") {
|
|
135
|
+
// import { a, b as c } from "m" / import d from "m" / import * as ns from "m"
|
|
136
|
+
for (const spec of node.specifiers) {
|
|
137
|
+
if (spec.local?.type === "Identifier")
|
|
138
|
+
topLevelVarNames.add(spec.local.name);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
else if (node.type === "ExportNamedDeclaration" ||
|
|
142
|
+
node.type === "ExportDefaultDeclaration") {
|
|
143
|
+
// `export function f(){}` / `export const x=…` keep their declaration in
|
|
144
|
+
// `node.declaration` — unwrap it so the inner binding name is collected.
|
|
145
|
+
collectDecl(node.declaration);
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
collectDecl(node);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// calledLocally : fn names called from inside other fns — must stay plain JS.
|
|
152
|
+
// localRefs : for each fn, ALL top-level identifiers it references
|
|
153
|
+
// (called fns + module vars). These get planted on globalThis
|
|
154
|
+
// before the VM wrapper runs so GGLO can resolve them.
|
|
155
|
+
const calledLocally = new Set();
|
|
156
|
+
const localRefs = new Map();
|
|
157
|
+
const allTopLevel = new Set([...topLevelFnNames, ...topLevelVarNames]);
|
|
158
|
+
traverse(ast, {
|
|
159
|
+
FunctionDeclaration(path) {
|
|
160
|
+
const fnName = path.node.id?.name;
|
|
161
|
+
if (!topLevelFnNames.has(fnName))
|
|
162
|
+
return;
|
|
163
|
+
const refs = new Set();
|
|
164
|
+
localRefs.set(fnName, refs);
|
|
165
|
+
path.traverse({
|
|
166
|
+
Identifier(inner) {
|
|
167
|
+
const name = inner.node.name;
|
|
168
|
+
if (allTopLevel.has(name) && name !== fnName)
|
|
169
|
+
refs.add(name);
|
|
170
|
+
},
|
|
171
|
+
CallExpression(inner) {
|
|
172
|
+
const callee = inner.node.callee;
|
|
173
|
+
if (callee.type === "Identifier" && topLevelFnNames.has(callee.name)) {
|
|
174
|
+
calledLocally.add(callee.name); // callee must stay plain JS
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
traverse(ast, {
|
|
181
|
+
FunctionDeclaration(path) {
|
|
182
|
+
const node = path.node;
|
|
183
|
+
if (node.__obf)
|
|
184
|
+
return;
|
|
185
|
+
if (hasPlainTag(node) || hasPlainTag(path.parent))
|
|
186
|
+
return;
|
|
187
|
+
if (!node.id || !node.body || node.body.type !== "BlockStatement")
|
|
188
|
+
return;
|
|
189
|
+
// Skip callees — they must stay real JS functions so callers can
|
|
190
|
+
// reference them by name at the module level.
|
|
191
|
+
if (calledLocally.has(node.id.name))
|
|
192
|
+
return;
|
|
193
|
+
// Bug fix 1 (async/await): async functions contain `await` expressions that
|
|
194
|
+
// are only legal inside an async context. buildSyntheticSource wraps the body
|
|
195
|
+
// in a plain sync function, so the bytecode parse step throws
|
|
196
|
+
// "SyntaxError: Unexpected reserved word 'await'". Skip async functions.
|
|
197
|
+
if (node.async) {
|
|
198
|
+
skipped++;
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
// Bug fix 2 (property name undefined): params that are not plain Identifiers
|
|
202
|
+
// (destructured patterns, default values, rest elements) don't have a .name
|
|
203
|
+
// property, causing `p.name` to return undefined in buildSyntheticSource and
|
|
204
|
+
// buildWrapper, which later surfaces as "Property name expected string but got
|
|
205
|
+
// undefined". Skip any function with non-identifier params.
|
|
206
|
+
const allParamsSimple = node.params.every((p) => p.type === "Identifier");
|
|
207
|
+
if (!allParamsSimple) {
|
|
208
|
+
skipped++;
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
let synthSrc;
|
|
212
|
+
try {
|
|
213
|
+
synthSrc = buildSyntheticSource(node, generate, KV_G, KV_ARGS, KV_RET, KV_FN);
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
skipped++;
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
let compiled;
|
|
220
|
+
try {
|
|
221
|
+
const compiler = new krak_compiler_1.KrakCompiler(config);
|
|
222
|
+
// Bug fix 3 (RegExpLiteral + any other parse/compile error): the original
|
|
223
|
+
// code only caught KrakBailError and re-threw everything else, so any
|
|
224
|
+
// unexpected construct (regex literals, generators, template literals, etc.)
|
|
225
|
+
// would crash the whole worker. Catch ALL errors here and bail gracefully.
|
|
226
|
+
const synthAst = parse2(synthSrc, {
|
|
227
|
+
sourceType: "module",
|
|
228
|
+
allowReturnOutsideFunction: true,
|
|
229
|
+
});
|
|
230
|
+
compiled = compiler.compileProgram(synthAst.program.body);
|
|
231
|
+
}
|
|
232
|
+
catch (e) {
|
|
233
|
+
skipped++;
|
|
234
|
+
console.error(` [vm/krak] skipped ${node.id.name}: ${e.message}`);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
const refsToExpose = [...(localRefs.get(node.id.name) ?? [])];
|
|
238
|
+
const wrapper = buildWrapper(t, node, compiled.base64, RUN_VM_NAME, refsToExpose, KV_ARGS, KV_RET);
|
|
239
|
+
path.replaceWith(wrapper);
|
|
240
|
+
path.skip();
|
|
241
|
+
count++;
|
|
242
|
+
console.error(` [vm/krak] virtualised ${node.id.name}: ${compiled.bytecode.length}B bytecode`);
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
if (skipped > 0)
|
|
246
|
+
console.error(` [vm/krak] left ${skipped} fn(s) plain (unsupported syntax / @plain)`);
|
|
247
|
+
if (count > 0) {
|
|
248
|
+
const dynamicOpsCode = Object.values(config.dynamicOps)
|
|
249
|
+
.map((op) => op.src)
|
|
250
|
+
.join("\n");
|
|
251
|
+
// nameMap.runVM is the renamed internal runVM symbol; RUN_VM_NAME is the
|
|
252
|
+
// per-build outer alias planted on globalThis for the wrapper functions.
|
|
253
|
+
const runVmInternal = nameMap.runVM;
|
|
254
|
+
const fullVmSrc = `(function(){\n` +
|
|
255
|
+
runtimeSrc + "\n" +
|
|
256
|
+
dynamicOpsCode + "\n" +
|
|
257
|
+
`var ${RUN_VM_NAME} = ${runVmInternal};\n` +
|
|
258
|
+
`if(typeof globalThis!=='undefined')globalThis["${RUN_VM_NAME}"]=${RUN_VM_NAME};\n` +
|
|
259
|
+
`})();\n` +
|
|
260
|
+
`var ${RUN_VM_NAME} = (typeof globalThis!=='undefined' ? globalThis["${RUN_VM_NAME}"] : (typeof global!=='undefined' ? global["${RUN_VM_NAME}"] : undefined));`;
|
|
261
|
+
const vmAst = parse(fullVmSrc, { sourceType: "module", allowReturnOutsideFunction: true });
|
|
262
|
+
vmAst.program.body.forEach(markAllObf);
|
|
263
|
+
ast.program.body.unshift(...vmAst.program.body);
|
|
264
|
+
}
|
|
265
|
+
return count;
|
|
266
|
+
}
|