The question part one left open
Part one covered two techniques. One was a four‑line stager pointed at a single Vercel URL — a genuine single point of failure, and one we confirmed dead: HTTP 451, DEPLOYMENT_DISABLED, Vercel's own platform blocking it before the attacker's code ever runs.
The other technique was built specifically not to have that weakness. It resolves its command server by reading a transaction off the public Ethereum blockchain — five RPC providers raced in parallel, a wider historical scan as fallback, a block‑explorer API as a last resort. We called this out at the time as the more resilient of the two, and deliberately didn't check whether it was actually still live, because doing that meant connecting to real attacker infrastructure, and we weren't going to do that from a machine or network connected to anything real.
That's still true. What changed is we set up infrastructure where it isn't — and decided the open question was worth actually answering, especially once it became clear our collaborator was still being actively hit.
Deobfuscating the address, by hand
The obfuscator used on this payload includes a "self‑defending" trick: a table of roughly 280 short string fragments, deliberately shuffled, with a matching unshuffle routine that has to run once before any fragment resolves correctly. In part one, we flagged this as the reason we couldn't recover the exact watched wallet address through static reading alone — the shuffle is specifically designed to defeat that.
The unshuffle routine itself, though, is completely inert. It doesn't touch the network, doesn't read a file, doesn't run a command — it just reorders items in an in‑memory list until a checksum‑style check confirms it's back in order. We extracted only that routine, the string table, and the tiny lookup function that reads from it — verified, by grepping the extracted file, that it contained zero references to require, child_process, spawn, eval, or any networking API — and ran nothing else.
// four table lookups + a literal suffix, lowercased const WATCHED_FROM_ADDRESS = (frag1+frag2+frag3+frag4+'1a').toLowerCase(); // resolves to: "0xa322e5f3d311d3080e6f0121063e9adc2490ef1a"
From there, finding out whether that address had done anything recently didn't require touching the attacker at all — it's public blockchain data, queryable from any RPC provider, the same way anyone can look up any Ethereum address. We asked for the latest block, rounded down to the nearest thousand the same way the payload's own logic does, and checked the small set of "anchor" blocks its resolver would check.
Two of them had a transaction from that exact address.
Then we decoded the recipient address
The malware doesn't send funds to that address for any normal reason — the recipient field is data, not a wallet. Two 4‑byte windows of it decode into IPv4 addresses; that's the C2 lookup's entire purpose. We decoded ours the same way the malware would.
Both IP slots were set to the identical address. The twelve bytes left over — unused by the malware's own parsing logic — spell out, in plain ASCII, a message addressed to exactly the kind of automated resolver we'd just built. Whoever holds the private key for this address knew precisely how the lookup logic works, used both available slots on purpose, and had room left over to say hello.
Where our first read was wrong
Our initial conclusion was that this looked like a sinkhole or a troll — someone else had taken control of the beacon and was redirecting infected hosts to a dead end, or the operator was mocking researchers who'd get this far. Either way, our instinct was: probably nothing live behind it, not worth the risk of connecting.
That reasoning had a gap, and it was pointed out to us directly: a taunt embedded in unused address bytes proves nothing about whether the IP those bytes also encode is still serving something real. The two things aren't in tension. An operator confident enough to leave a message for researchers is also an operator who might not care that researchers are looking — which is a very different, more concerning read than "already cleaned up."
Given a colleague was reporting live, ongoing compromise that very morning, treating "looks like a joke" as equivalent to "confirmed dead" wasn't a conclusion we should have reached without checking. So we checked.
Checking it properly
Same discipline as the stager check in part one: network‑isolated, disposable infrastructure with no path back to us, fetch‑only, no redirects followed, nothing executed or evaluated regardless of what came back. We hit both URL paths the payload's own logic uses, with the same spoofed User‑Agent and the same custom header the real malware sends, so there was no "wrong fingerprint" excuse for it to serve something different than what a real infection would get.
Both delivery channels we predicted from static analysis in part one — an XOR‑obscured response body, and a second copy hidden in a custom response header — showed up exactly as described. This is not a parked domain, not a dead deployment, not a static error page. It's a running application, actively responding to requests shaped like the ones real infected hosts send.
Where this stands right now
Update: we cracked it. The key material was exactly where we expected — sitting in the same inert string table as everything else — and the cipher itself turned out to be plain repeating‑key XOR, nothing exotic. What actually cost us the time wasn't the cryptography. It was three mistakes stacked on top of each other, all ours, none the malware's.
First, our own capture tooling corrupted the evidence before we ever got to the math: the script we used to safely "defang" captured content prepends a text warning banner directly onto the raw response bytes, and our later attempt to strip that banner back off used line‑counting on a stream that mixes text with arbitrary binary — binary content can contain the newline byte anywhere, so counting lines to find the boundary was never reliable. Second, we assumed the malware always decrypts from the response header; reading its actual request logic (not just the decrypt function in isolation) showed it branches on HTTP method, and the code path our fetch actually exercised uses the raw response body instead, with no base64 layer at all. Third, and this is the one that actually mattered: we had two keys and two endpoints, and we had them backwards. Reading the real call site — not just the function that takes a key as a parameter, but the specific place in the code that decides which key goes with which endpoint — showed the mapping was the reverse of the naming‑order assumption we'd made. Swap the two keys to match what the code actually does, and both responses decode to clean, 100%‑printable JavaScript on the first try.
What was actually inside
Neither decrypted response is the payload. Both are loaders — and each one points at a further endpoint we hadn't seen before, on the same server, that we then also fetched (fetch‑only, same discipline, still nothing executed).
One decrypts to a short, barely‑obfuscated bootstrap script whose entire job is to request a second path over HTTPS, decode what comes back with the same style of key, and hand the result to eval(). The other decrypts to a second, independently obfuscated blob — a different obfuscator, different variable‑naming convention, its own self‑contained string table — whose job is to do the same thing against a third path, this time over plain HTTP on a different port.
We followed both, read‑only. One of the two new endpoints returned real content: a large, structured JSON response carrying two further encoded blobs. Decoding the smaller of the two (still without executing anything) produced a hundred‑kilobyte, fully readable stage that itself constructs and wraps another function using a custom, non‑standard base64‑style alphabet — a fourth layer of indirection, and still not obviously the end of the chain.
We're deliberately not publishing the literal key strings or the exact byte offsets here. The methodology is fully described — a competent reader can reproduce every step — but handing out working decryption material for a currently‑live C2 isn't something we're going to do in a public post. We'd rather this be reproducible than turnkey.
This isn't a smash‑and‑grab credential stealer bolted onto a compromised repo. It's a maintained delivery pipeline: blockchain‑based address resolution feeding into at least four sequential, independently‑obfuscated loader stages, spread across two ports and at least five distinct URL paths on the same host. Whoever built this expected exactly the kind of analysis we just did, and layered accordingly.
One more thing worth flagging before we move to the review. Even without finishing the deepest decompression pass, the shape of the fourth stage is hard to misread: a callback signature matching (error, stdout, stderr), a branch that prefixes commands with py before running them, code that builds a REPL‑style prompt string, and a closing environment‑detection wrapper that checks for define, module, and angular so the same payload runs whether it lands in a browser tab, a Node process, or an Angular app. That's not a wallet‑file grep. That's the shape of arbitrary remote command execution — shell and Python both — built to run anywhere. We're stating this as what the code's structure strongly indicates, not as a fully decompiled, line‑by‑line confirmed capability list; the deepest layer uses a real, legitimate compression library (LZ‑String) wrapping its own further obfuscated strings, and we stopped short of fully unwinding every one of them for this post.
Code review: the loader chain
Part one reviewed the two techniques that got the backdoor onto disk. This is a review of what we found once we actually got inside the C2's own delivery pipeline — four stages deep, three different obfuscators, one shared design philosophy.
The loader chain — EtherHiding response through to the fourth stage
Every stage is disposable on its own. Each loader does almost nothing except fetch the next one and hand it to eval. If any single stage gets sinkholed, the operator can replace just that stage without touching the others or re‑compromising anything upstream. That's real operational engineering, not accidental complexity.
Three unrelated obfuscation tools, not one reused everywhere. The original payload, the two stage‑2 loaders, and the stage‑4 blob each use a visibly different obfuscator — different string‑table conventions, different control‑flow flattening styles. A single static signature won't catch this whole chain; a scanner tuned to one stage's fingerprint will walk straight past the others.
A real, maintained open‑source library (LZ‑String) doing legitimate work. Compressing the payload before embedding it isn't a novelty — it's a sensible, boring engineering choice that happens to also shrink the amount of suspicious‑looking text a scanner has to work with.
The key—to—endpoint mapping is arbitrary and undocumented, even to us as reverse engineers. There's no naming convention tying a key to the path it decrypts — we know because we got it backwards ourselves on the first pass. That's a small, almost accidental win for the attacker: it costs a defender real time even after the algorithm itself is fully broken.
Everything downstream of the first hop is still plain repeating‑key XOR. No key exchange, no per‑session rotation that we observed, no authentication of the response beyond a static header value. Once an analyst has the keys — which live in the same inert string tables as everything else — every stage after the first falls the same way. Depth of staging is not the same thing as depth of cryptography.
What this actually changes
"Looks like it's dead" needs a verb, not a vibe
We almost stopped at a plausible‑sounding interpretation of ambiguous evidence. The fix wasn't better intuition — it was actually sending the request, safely, instead of reasoning our way to a conclusion that was convenient to believe.
Obfuscation only defeats reading, not arithmetic
The self‑defending string table stopped us from reading the address directly. It didn't stop us from extracting the (inert) unshuffle logic and computing the real value ourselves — the obfuscation protects against casual analysis, not against someone willing to do the arithmetic.
Network isolation is what makes any of this responsible
None of this — the address lookup, the block queries, the live fetch — touched anything connected to our real infrastructure. That's not incidental; it's the only reason any of it was safe to do at all.
Confirming "still live" doesn't substitute for cleanup
Knowing the C2 is active is useful intelligence. It's not remediation. Whoever's actually infected still needs their own machine checked and cleaned, on its own timeline, regardless of what we find out here.
Check your own tooling before you trust the crypto
Every wrong turn in the decryption was ours, not the malware's: a defanging script that mixed a text banner into binary data, an assumption about which code path our request actually triggered, and a mapping we got backwards by trusting naming order instead of reading the real call site. The algorithm was simple the whole time. We just weren't reading our own evidence carefully enough to see it.