Skip to content

Instantly share code, notes, and snippets.

@struppigel
Last active September 16, 2026 18:12
Show Gist options
  • Select an option

  • Save struppigel/d24cdd42c39bde7190b7b0bfd69a514d to your computer and use it in GitHub Desktop.

Select an option

Save struppigel/d24cdd42c39bde7190b7b0bfd69a514d to your computer and use it in GitHub Desktop.
JS hook script with reflection (LLM generated)
// minimal_generic_hook.js — generic-hooking analysis harness (v3, pluggable loader)
// Run: set ELECTRON_RUN_AS_NODE=1 && launcher.exe minimal_generic_hook.js
//
// Spies every module the sample itself requires (auto-wrapping all functions/constructors)
// and PASSES THROUGH the real modules. Only child_process is neutralized (so it can't spawn an
// uninstrumented next stage). Real modules execute — run on an isolated VM with no Internet.
// ===== per-sample CONFIG (the only part you edit between samples) =====
const CONFIG = {
payload: "-put path to .jsc here-", // .jsc bytecode OR a .js entry
modulePaths: [
"-put modulepathes here-"
],
logFile: "log_min.txt",
watchdogMs: 30000, // raise if the interesting behavior fires late
loader: "auto", // "auto" (detect) | "bytenode" | "js"
// load: () => { ... }, // optional full manual override; if set, it wins over `loader`
};
// ===== 1) capture real tools BEFORE the sample can clobber the globals =====
const { setInterval, setTimeout } = require("node:timers");
const realExit = process.exit.bind(process);
const realLog = console.log.bind(console);
const fs = require("fs"); // our own (unspied) fs handle for logging
const EE = require("events").EventEmitter;
// ===== 2) buffered logging with timestamp + sequence (file + live console) =====
const L = CONFIG.logFile;
const W = [];
const t0 = Date.now();
let seq = 0;
const log = (m) => { const line = "+" + (Date.now() - t0) + "ms #" + (++seq) + " " + m; W.push(line); realLog(line); };
const flush = () => { if (W.length) { fs.appendFileSync(L, W.join("\n") + "\n"); W.length = 0; } };
fs.writeFileSync(L, "");
setInterval(flush, 500);
process.on("exit", flush);
// ===== 3) survive async failures so the run isn't cut short (network-off rejects, etc.) =====
process.on("unhandledRejection", (r) => log("[unhandledRejection] " + (r && r.message ? r.message : String(r))));
process.on("uncaughtException", (e) => log("[uncaughtException] " + (e && e.stack ? e.stack.split("\n")[0] : String(e))));
// ===== 4) keep the run alive; block the sample's exit; stop ourselves via the watchdog =====
process.exit = (code) => { log("[EXIT blocked] " + code); };
process.env.NODE_OPTIONS = ""; process.env.DEBUG = ""; // defuse the anti-debug env checks
setTimeout(() => { flush(); realExit(0); }, CONFIG.watchdogMs);
// ===== 5) make the app's bundled modules resolvable (front of the search path) =====
module.paths.unshift(...CONFIG.modulePaths);
// ===== 6) rich argument preview (Buffers, typed arrays, streams, errors, truncation, circular-safe) =====
function fmt(v, depth) {
if (v === null) return "null";
const t = typeof v;
if (t === "string") return JSON.stringify(v.length > 200 ? v.slice(0, 200) + "\u2026" : v);
if (t === "number" || t === "boolean" || t === "bigint") return String(v);
if (t === "undefined") return "undefined";
if (t === "symbol") return v.toString();
if (t === "function") return "[Function " + (v.name || "anonymous") + "]";
if (Buffer.isBuffer(v)) return "<Buffer " + v.length + " bytes>";
if (ArrayBuffer.isView(v)) return "<" + v.constructor.name + " " + v.byteLength + " bytes>";
if (v instanceof Error) return "Error(" + JSON.stringify(v.message) + ")";
if (Array.isArray(v)) {
if (depth <= 0) return "[Array(" + v.length + ")]";
return "[" + v.slice(0, 8).map((x) => fmt(x, depth - 1)).join(", ") + (v.length > 8 ? ", \u2026" : "") + "]";
}
if (t === "object") {
const ctor = v.constructor && v.constructor.name;
if (depth <= 0) return ctor && ctor !== "Object" ? "[" + ctor + "]" : "[Object]";
try {
const keys = Object.keys(v);
const body = keys.slice(0, 8).map((k) => k + ":" + fmt(v[k], depth - 1)).join(", ");
const tag = ctor && ctor !== "Object" ? ctor + " " : "";
return tag + "{" + body + (keys.length > 8 ? ", \u2026" : "") + "}";
} catch (e) { return ctor ? "[" + ctor + "]" : "[Object]"; }
}
return String(v);
}
const preview = (args) => {
try { return Array.from(args).map((a) => fmt(a, 2)).join(", ").slice(0, 400); }
catch (e) { return "<args>"; }
};
// ===== 7) generic spy: memoized, receiver-correct, wraps calls/constructions/members =====
const cache = new WeakMap(); // original -> its proxy (stable identity, no dup work, breaks cycles)
function spy(name, val, receiver) {
const t = val === null ? "null" : typeof val;
if (t !== "function" && t !== "object") return val;
if (cache.has(val)) return cache.get(val);
const handler = {
get(target, prop, recv) {
const p = Reflect.get(target, prop, recv);
if (typeof prop === "symbol") return p; // skip Symbol keys -> cuts engine-probe noise
if (typeof p === "function") return spy(name + "." + String(prop), p, target); // method: receiver = its object
if (p && typeof p === "object") return spy(name + "." + String(prop), p); // nested namespace (fs.promises)
return p;
},
};
if (t === "function") {
handler.apply = (target, thisArg, args) => {
log("[" + name + "()] " + preview(args));
// receiver fix: call with the REAL owning object, not our proxy (native methods reject a proxy receiver)
return Reflect.apply(target, receiver !== undefined ? receiver : thisArg, args);
};
handler.construct = (target, args, nt) => {
log("[new " + name + "] " + preview(args));
return Reflect.construct(target, args, nt);
};
}
const proxy = new Proxy(val, handler);
cache.set(val, proxy);
return proxy;
}
// ===== the one kept neutralization: never let the sample spawn an uninstrumented next stage =====
function fakeChildProcess() {
const proc = () => { const e = new EE(); e.stdout = new EE(); e.stderr = new EE();
e.stdin = { write() {}, end() {} }; e.pid = 1; e.kill = () => {};
setTimeout(() => e.emit("close", 0), 20); return e; };
const cb = (a) => a.find((x) => typeof x === "function");
return {
exec: (c, ...a) => { log("[EXEC] " + c); const f = cb(a); if (f) f(null, "", ""); return proc(); },
execSync: (c) => { log("[EXECSYNC] " + c); return Buffer.from(""); },
execFile: (file, ar, ...a) => { log("[EXECFILE] " + file + " " + preview(ar)); const f = cb(a); if (f) f(null, "", ""); return proc(); },
spawn: (c, ar) => { log("[SPAWN] " + c + " " + preview(ar)); return proc(); },
fork: (m, ar) => { log("[FORK] " + m + " " + preview(ar)); return proc(); },
};
}
// ===== generic require hook: wrap ONLY what the malware itself requires =====
const oR = module.constructor.prototype.require;
module.constructor.prototype.require = function (id) {
const real = oR.apply(this, arguments);
const caller = (this && this.filename) || "";
if (/[\\/]node_modules[\\/]/.test(caller)) return real; // library-internal require -> leave untouched
log("[REQ] " + id);
if (id === "child_process" || id === "node:child_process") return fakeChildProcess();
return spy(id, real); // everything else: generic spy + pass-through
};
// ===== payload loader: setting-driven with auto-detection, plus the manual override =====
function detectLoader(p) { // -> "bytenode" (V8 bytecode) or "js" (source)
try {
const head = fs.readFileSync(p).subarray(0, 64);
if (/\.jsc$/i.test(p) || head.includes(0)) return "bytenode"; // .jsc ext, or a null byte in the header
} catch (e) {}
return "js"; // UTF-8 source essentially never has a null byte up front
}
function loadPayload() {
if (typeof CONFIG.load === "function") { log("[loader] manual override"); return CONFIG.load(); }
const auto = CONFIG.loader === "auto";
const mode = auto ? detectLoader(CONFIG.payload) : CONFIG.loader;
if (mode === "bytenode") {
log("[loader] " + (auto ? "auto-detected " : "") + "bytenode (.jsc)");
try { require("bytenode"); }
catch (e) { log("[loader] bytenode not resolvable — run `npm i bytenode` or add its node_modules to CONFIG.modulePaths"); throw e; }
return require(CONFIG.payload);
}
log("[loader] " + (auto ? "auto-detected " : "") + "plain JS source");
return require(CONFIG.payload); // plain / obfuscated JS entry
}
// ===== go =====
log("[START] minimal generic hook v3");
try {
loadPayload();
} catch (e) {
log("[ERR] " + e.message);
log("[STACK] " + e.stack);
}
log("[END] synchronous load done");
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment