muaddib-scanner 2.11.143 → 2.11.145
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/package.json
CHANGED
package/src/rules/index.js
CHANGED
|
@@ -1148,7 +1148,7 @@ const RULES = {
|
|
|
1148
1148
|
severity: 'CRITICAL',
|
|
1149
1149
|
confidence: 'high',
|
|
1150
1150
|
domain: 'malware',
|
|
1151
|
-
description: 'Reference
|
|
1151
|
+
description: 'Reference dissimulee (charcode/base64/hex) OU enumeration de process.env cherchant un prefix marqueur MUADDIB (marker-agnostique), vers un marqueur d\'environnement d\'analyse — marqueur de sandbox MUAD\'DIB (MUADDIB_GVISOR), marqueur d\'un analyseur pair, ou nom de poison-token. Seules les formes obfusquees/enumerees sont matchees : le code de sandbox de MUAD\'DIB lui-meme et l\'outillage securite legitime referencent ces marqueurs en clair sans risque. Aucun package legitime n\'encode ni n\'enumere un check pour ces tripwires. Le marqueur est un honeytoken plante : garde public et stable pour que tout verificateur s\'auto-incrimine.',
|
|
1152
1152
|
references: [
|
|
1153
1153
|
'https://attack.mitre.org/techniques/T1497/',
|
|
1154
1154
|
'https://attack.mitre.org/techniques/T1480/'
|
|
@@ -47,34 +47,95 @@ const ANALYZER_MARKERS = [
|
|
|
47
47
|
'CHAINRADAR_'
|
|
48
48
|
];
|
|
49
49
|
|
|
50
|
-
// Bound the
|
|
51
|
-
const
|
|
50
|
+
// Bound the number of decode attempts so a pathological file can't blow the CPU budget.
|
|
51
|
+
const MAX_DECODE = 300;
|
|
52
52
|
// A char-code array literal: >=6 small integers, e.g. [77,85,65,68,68,73,66,...].
|
|
53
53
|
const CHARCODE_ARRAY_RE = /\[\s*(\d{1,3}(?:\s*,\s*\d{1,3}){5,})\s*\]/g;
|
|
54
|
+
// A base64 blob big enough to hide a marker (>=12 chars ~ a 9+ byte string).
|
|
55
|
+
const BASE64_RE = /['"`]([A-Za-z0-9+/]{12,}={0,2})['"`]/g;
|
|
56
|
+
// Hex-encoded string: a run of \xHH escapes, or a contiguous even-length hex literal.
|
|
57
|
+
const HEX_ESCAPE_RE = /((?:\\x[0-9a-fA-F]{2}){8,})/g;
|
|
58
|
+
const HEX_LITERAL_RE = /['"`]([0-9a-fA-F]{16,})['"`]/g;
|
|
59
|
+
|
|
60
|
+
// Return the first planted analyzer marker contained in `decoded`, or null.
|
|
61
|
+
function markerIn(decoded) {
|
|
62
|
+
if (!decoded) return null;
|
|
63
|
+
for (const m of ANALYZER_MARKERS) {
|
|
64
|
+
if (decoded.indexOf(m) !== -1) return m;
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
54
68
|
|
|
55
69
|
/**
|
|
56
|
-
* Return the first analyzer marker that appears
|
|
57
|
-
*
|
|
58
|
-
* Plaintext references are intentionally ignored (see module doc)
|
|
70
|
+
* Return the first analyzer marker that appears OBFUSCATED in `content` — hidden inside a
|
|
71
|
+
* charcode array, a base64 blob, or a hex string that decodes to text containing the marker —
|
|
72
|
+
* or null. Plaintext references are intentionally ignored (see module doc): MUAD'DIB's own
|
|
73
|
+
* sandbox code and legit security tooling reference these markers in the clear, but no benign
|
|
74
|
+
* package *encodes* a check for one. Bounded (MAX_DECODE total decodes), side-effect-free.
|
|
59
75
|
*/
|
|
60
76
|
function detectAnalyzerHoneytoken(content) {
|
|
61
|
-
if (!content
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
77
|
+
if (!content) return null;
|
|
78
|
+
let seen = 0;
|
|
79
|
+
|
|
80
|
+
// (1) charcode array: e.g. [77,85,65,68,...] whose codes decode to a marker string
|
|
81
|
+
if (content.indexOf('[') !== -1) {
|
|
82
|
+
CHARCODE_ARRAY_RE.lastIndex = 0;
|
|
83
|
+
let match;
|
|
84
|
+
while ((match = CHARCODE_ARRAY_RE.exec(content)) !== null) {
|
|
85
|
+
if (++seen > MAX_DECODE) return null;
|
|
86
|
+
const nums = match[1].split(',').map(n => parseInt(n, 10));
|
|
87
|
+
if (nums.some(n => !(n >= 9 && n <= 126))) continue; // printable ASCII only
|
|
88
|
+
let decoded; try { decoded = String.fromCharCode.apply(null, nums); } catch { continue; }
|
|
89
|
+
const m = markerIn(decoded); if (m) return m;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// (2) base64 blob: e.g. Buffer.from(b64, 'base64') where b64 decodes to a marker
|
|
94
|
+
BASE64_RE.lastIndex = 0;
|
|
95
|
+
let b;
|
|
96
|
+
while ((b = BASE64_RE.exec(content)) !== null) {
|
|
97
|
+
if (++seen > MAX_DECODE) return null;
|
|
98
|
+
let decoded; try { decoded = Buffer.from(b[1], 'base64').toString('latin1'); } catch { continue; }
|
|
99
|
+
const m = markerIn(decoded); if (m) return m;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// (3) hex string: '\x4d\x55...' escapes or a contiguous '4d5541...' literal
|
|
103
|
+
for (const re of [HEX_ESCAPE_RE, HEX_LITERAL_RE]) {
|
|
104
|
+
re.lastIndex = 0;
|
|
105
|
+
let h;
|
|
106
|
+
while ((h = re.exec(content)) !== null) {
|
|
107
|
+
if (++seen > MAX_DECODE) return null;
|
|
108
|
+
const pairs = h[1].match(/[0-9a-fA-F]{2}/g);
|
|
109
|
+
if (!pairs) continue;
|
|
110
|
+
let decoded; try { decoded = pairs.map(x => String.fromCharCode(parseInt(x, 16))).join(''); } catch { continue; }
|
|
111
|
+
const m = markerIn(decoded); if (m) return m;
|
|
73
112
|
}
|
|
74
113
|
}
|
|
114
|
+
|
|
75
115
|
return null;
|
|
76
116
|
}
|
|
77
117
|
|
|
118
|
+
// process.env KEY-ENUMERATION coupled with a test against an analysis-marker prefix
|
|
119
|
+
// (enumerate the env keys, then test each against the distinctive MUADDIB prefix — our own
|
|
120
|
+
// tool name, FP-safe; bare 'GVISOR' is a real runtime and is deliberately NOT matched).
|
|
121
|
+
// Marker-AGNOSTIC re: the exact var name (matches any MUADDIB_* var), unlike the
|
|
122
|
+
// exact-name form `process.env.MUADDIB_GVISOR`. FP-safe by coupling: a scanned package has no
|
|
123
|
+
// legitimate reason to enumerate the environment looking for OUR sandbox-marker prefix
|
|
124
|
+
// (verified: MUAD'DIB reads its own MUADDIB_* vars directly, never via enumerate+startsWith).
|
|
125
|
+
const ENV_ENUM_RE = /Object\s*\.\s*(?:keys|entries|getOwnPropertyNames)\s*\(\s*process\s*\.\s*env|for\b[^;{}]*\bin\b\s*process\s*\.\s*env/;
|
|
126
|
+
const ENV_KEY_MARKER_TEST_RE = /\.\s*(?:startsWith|includes|indexOf|search|match)\s*\(\s*['"`][^'"`]*MUADDIB|['"`][^'"`]*MUADDIB[^'"`]*['"`]\s*(?:===?|!==?)|(?:===?|!==?)\s*['"`][^'"`]*MUADDIB/;
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Detect generic `process.env` enumeration whose keys are tested for the distinctive MUADDIB
|
|
130
|
+
* prefix (case-sensitive; bare 'GVISOR' is a real runtime, deliberately not matched). Returns the matched marker family (string) or null. Marker-agnostic, FP-safe
|
|
131
|
+
* by coupling (env enumeration + our own tripwire prefix).
|
|
132
|
+
*/
|
|
133
|
+
function detectEnvMarkerEnumeration(content) {
|
|
134
|
+
if (!content || content.indexOf('process') === -1) return null;
|
|
135
|
+
if (!ENV_ENUM_RE.test(content)) return null;
|
|
136
|
+
return ENV_KEY_MARKER_TEST_RE.test(content) ? 'MUADDIB' : null;
|
|
137
|
+
}
|
|
138
|
+
|
|
78
139
|
/** Count silent `process.exit(` guards — the magnitude of the "detonation-gate wall". */
|
|
79
140
|
function countExitGuards(content) {
|
|
80
141
|
if (!content) return 0;
|
|
@@ -87,4 +148,4 @@ function hasHostRecon(content) {
|
|
|
87
148
|
return !!content && /\.\s*(?:hostname|userInfo|networkInterfaces)\s*\(/.test(content);
|
|
88
149
|
}
|
|
89
150
|
|
|
90
|
-
module.exports = { ANALYZER_MARKERS, detectAnalyzerHoneytoken, countExitGuards, hasHostRecon };
|
|
151
|
+
module.exports = { ANALYZER_MARKERS, detectAnalyzerHoneytoken, detectEnvMarkerEnumeration, countExitGuards, hasHostRecon };
|
|
@@ -5,8 +5,8 @@ const { networkDestinationsAllBenign } = require('../../sdk-destination.js');
|
|
|
5
5
|
function handlePostWalk(ctx) {
|
|
6
6
|
// ── Anti-analysis / sandbox evasion (2026, @longzy DPRK "Contagious Interview") ──
|
|
7
7
|
// We detect the EVASION rather than trying to hide the sandbox (an unwinnable arms race).
|
|
8
|
-
// (1) analyzer_honeytoken_reference (CRITICAL, FP~0): the file hides — behind charcode
|
|
9
|
-
// encoding — a check for a known
|
|
8
|
+
// (1) analyzer_honeytoken_reference (CRITICAL, FP~0): the file hides — behind charcode/base64/hex
|
|
9
|
+
// encoding, OR via process.env key-enumeration — a check for a known tripwire (our own MUADDIB_GVISOR,
|
|
10
10
|
// a peer analyzer marker, a poison-token name). Only the OBFUSCATED form is matched, so
|
|
11
11
|
// MUAD'DIB's own sandbox code (plaintext) and legit security tooling never trip it. The
|
|
12
12
|
// marker is a planted honeytoken: kept public+stable so any checker self-incriminates.
|
|
@@ -19,7 +19,14 @@ function handlePostWalk(ctx) {
|
|
|
19
19
|
ctx.threats.push({
|
|
20
20
|
type: 'analyzer_honeytoken_reference',
|
|
21
21
|
severity: 'CRITICAL',
|
|
22
|
-
message: `
|
|
22
|
+
message: `Obfuscated (charcode/base64/hex-encoded) reference to an analysis-environment marker ("${ctx.analyzerHoneytokenHit}") — sandbox/analyzer evasion. No legitimate package encodes a check for this tripwire.`,
|
|
23
|
+
file: ctx.relFile
|
|
24
|
+
});
|
|
25
|
+
} else if (ctx.envMarkerEnumHit) {
|
|
26
|
+
ctx.threats.push({
|
|
27
|
+
type: 'analyzer_honeytoken_reference',
|
|
28
|
+
severity: 'CRITICAL',
|
|
29
|
+
message: `Enumerates process.env keys and tests them against an analyzer-marker prefix (${ctx.envMarkerEnumHit}) — marker-agnostic sandbox/analyzer evasion (matches any MUADDIB_* var, not a fixed name). No legitimate package scans the environment for this tripwire.`,
|
|
23
30
|
file: ctx.relFile
|
|
24
31
|
});
|
|
25
32
|
} else if (
|
package/src/scanner/ast.js
CHANGED
|
@@ -13,7 +13,7 @@ const {
|
|
|
13
13
|
handleWithStatement,
|
|
14
14
|
handlePostWalk
|
|
15
15
|
} = require('./ast-detectors');
|
|
16
|
-
const { detectAnalyzerHoneytoken, countExitGuards, hasHostRecon } = require('./ast-detectors/anti-evasion.js');
|
|
16
|
+
const { detectAnalyzerHoneytoken, detectEnvMarkerEnumeration, countExitGuards, hasHostRecon } = require('./ast-detectors/anti-evasion.js');
|
|
17
17
|
|
|
18
18
|
// Check if credential keywords appear INSIDE regex literals or new RegExp() patterns.
|
|
19
19
|
// Only true when the keyword is part of the regex pattern itself, not just a string elsewhere in the file.
|
|
@@ -166,6 +166,7 @@ function analyzeFile(content, filePath, basePath) {
|
|
|
166
166
|
hasHostRecon: hasHostRecon(content),
|
|
167
167
|
hasProcessEnvRead: /process\s*\.\s*env\b/.test(content),
|
|
168
168
|
analyzerHoneytokenHit: detectAnalyzerHoneytoken(content),
|
|
169
|
+
envMarkerEnumHit: detectEnvMarkerEnumeration(content),
|
|
169
170
|
hasJsReverseShell: /\bnet\.Socket\b/.test(content) &&
|
|
170
171
|
/\.connect\s*\(/.test(content) &&
|
|
171
172
|
/\.pipe\b/.test(content) &&
|