Files
jodosjodos a1af57ff62 Detect EtherHiding PolinRider variant and generalize unknown-variant heuristic
Adds a third JS_VARIANTS entry for the dot-notation, decoder-array-free
payload that slipped past both known-signature matching and the old
generic heuristic (which required bracket notation, a decoder array,
and eval - all three, none of which this variant used). Also broadens
the generic heuristic to OR-based shape categories and adds a separate
technique-fingerprint detector for the EtherHiding blockchain C2
pattern, matched against unicode-decoded text so escaping individual
strings can't evade it.
2026-08-11 19:27:45 +02:00

55 lines
2.5 KiB
JavaScript

import { test, after } from "node:test";
import assert from "node:assert/strict";
import {
decodeUnicodeEscapes,
hasHeavyUnicodeEscapeDensity,
isDetachedSpawn,
hasEthRpcMethodLiteral,
} from "../src/signatures.js";
import { cleanupAll, LEGIT_I18N_MODULE, FUTURE_VARIANT_PAYLOAD, GENERIC_PAYLOAD } from "./helpers.js";
after(cleanupAll);
test("decodeUnicodeEscapes: plain text passes through; \\uXXXX sequences decode to characters", () => {
assert.equal(decodeUnicodeEscapes("plain text"), "plain text");
assert.equal(decodeUnicodeEscapes("\\u0068\\u0069"), "hi");
});
test("hasHeavyUnicodeEscapeDensity: false on empty/non-string/sparse legit i18n text, true on inlined-escape payload", () => {
assert.equal(hasHeavyUnicodeEscapeDensity(""), false);
assert.equal(hasHeavyUnicodeEscapeDensity(undefined), false);
assert.equal(hasHeavyUnicodeEscapeDensity(LEGIT_I18N_MODULE), false);
assert.equal(hasHeavyUnicodeEscapeDensity(FUTURE_VARIANT_PAYLOAD), true);
});
test("isDetachedSpawn: requires spawn + detached(true|!0) + .unref() together; matches minified !0 boolean", () => {
assert.equal(isDetachedSpawn(`spawn("node",[])`), false);
assert.equal(isDetachedSpawn(`spawn("node",[],{detached:true})`), false); // no .unref()
assert.equal(isDetachedSpawn(`const c=spawn("node",["-e","1"],{detached:!0});c.unref();`), true);
assert.equal(isDetachedSpawn(`const c=spawn("node",["-e","1"],{detached:true});c.unref();`), true);
});
test("hasEthRpcMethodLiteral: matches known EtherHiding RPC method names only", () => {
assert.equal(hasEthRpcMethodLiteral(`fetch("/api/eth_call")`), false);
assert.equal(hasEthRpcMethodLiteral(`method:"eth_getBlockByNumber"`), true);
assert.equal(hasEthRpcMethodLiteral(`method:"eth_getTransactionCount"`), true);
assert.equal(hasEthRpcMethodLiteral(`method:"eth_blockNumber"`), true);
});
test("decode-before-match matters: escaped RPC method name only matches after decoding", () => {
const escaped =
'"' +
"eth_getBlockByNumber"
.split("")
.map((c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, "0")}`)
.join("") +
'"';
assert.equal(hasEthRpcMethodLiteral(escaped), false, "raw escaped text should not match directly");
assert.equal(hasEthRpcMethodLiteral(decodeUnicodeEscapes(escaped)), true, "decoded text should match");
});
test("GENERIC_PAYLOAD (bracket+decoder-array variant) does not trip the new EtherHiding-specific signals", () => {
assert.equal(hasEthRpcMethodLiteral(GENERIC_PAYLOAD), false);
assert.equal(isDetachedSpawn(GENERIC_PAYLOAD), false);
});