ToxicMiner
- Category: rev
“We’ve chained your terminal to a rogue CPU miner that has zero interest in the blockchain and every interest in turning your processor into weapons-grade slag. Dissect the binary before you bake alive.”
ToxicMiner.exe is a static build of cpuminer-multi 1.3.7 (MSVC, with libcurl/OpenSSL/jansson/pthreads-win32) carrying a few author patches. It is not packed and has no TLS-callback trickery — the whole thing is a real miner with one hijacked function, and the flag is gated behind a fake “proof-of-work” whose only real input is your disk’s volume serial number.
Solution
1. Find the patch under the miner
The patched main forces algorithm #39 (sha256d), sets the default pool to 127.0.0.1, and ships a Base64 decoy pool password m1n3_y0ur_0wn_bus1n3ss@flare.com — note the wrong domain (.com, not flare-on.com), so it is a trap, not the flag. The real logic is the hijacked scan-hash routine at sub_140134f70:
- It reads the current drive’s volume serial number via
GetVolumeInformationA. - If the pool username is 64 hex characters, it is parsed as a 32-byte target.
- For each nonce it computes
SHA256d(serial ‖ nonce); when that equals the target it callsderive_key(serial, 0x0b501e7e)(sub_140134ca0), uses the result to RC4-decrypt a 51-byte blob at0x1401bd3b0(sub_140134d70), printsAccepted: <flag>, and exits.
The username/nonce mining is just there to reach the code path. derive_key depends only on the volume serial and the constant 0x0b501e7e — the sole unknown is a 32-bit serial, so this is a 2³² search with the @flare-on.com suffix as the oracle.
2. The trap: “SHA-256” is not SHA-256
derive_key computes SHA256d(serial ‖ 0x0b501e7e) and takes the first 16 bytes of the digest as the RC4 key. The compression function sub_140135240 looks exactly like textbook SHA-256 — correct IV, correct round constants, correct message schedule — so it is tempting to reimplement it and brute-force offline. That is the trap, and it cost two full 40-minute searches that could never have hit.
Reading the function all the way to its ret reveals one extra step after the standard Davies-Meyer feed-forward (traced from 0x140136230–0x140136264):
state[0] += a; ... state[7] += h; // normal SHA-256 feed-forward
uint32_t w0 = state[0], w1 = state[1];
uint8_t tval = MIXTAB[w0 & 0xff]; // 256-byte table @ 0x1401bd270
uint32_t off = w1 % 28; // computed via a magic-multiply divide
uint32_t mixval; memcpy(&mixval, (uint8_t*)state + off, 4); // UNALIGNED read into state
state[0] = (uint32_t)tval ^ mixval ^ w0; // word 0 only; words 1..7 stay standard
Only word 0 is corrupted, unconditionally, on every call — so it applies to both compressions of the double hash. Getting this bit-exact (the % 28 is a 0x24924925 magic-multiply, the read into state is unaligned) is fiddly and error-prone.
3. Skip the reimplementation — call the binary’s own code
Rather than perfectly clone a deliberately-mutated hash, the robust move is to let the binary compute it. ToxicMiner.exe maps cleanly with LoadLibraryA, so a tiny Windows harness resolves derive_key and rc4 by RVA and brute-forces the serial by calling the real functions — no crypto modelling at all. A sanity check against a known-good decrypt (the analysis VM’s own serial 0xB2C74207) confirms the harness before the sweep:
HMODULE h = LoadLibraryA("ToxicMiner.exe");
uintptr_t base = (uintptr_t)h;
derive_key_fn derive_key = (derive_key_fn)(base + 0x134ca0);
rc4_fn rc4 = (rc4_fn)(base + 0x134d70);
uint8_t *ct = (uint8_t*)(base + 0x1bd3b0); // 51-byte ciphertext
for (uint64_t s = 0; s <= 0xffffffff; s++) {
uint8_t key[32], out[52] = {0};
derive_key((uint32_t)s, 0x0b501e7e, key);
rc4(key, 16, ct, 51, out);
if (looks_like_flag(out, 51)) // printable & ends "@flare-on.com"
printf("FOUND serial=0x%08llx flag=%s\n", s, out);
}
Full harness: harness_brute.c. The 2³² sweep finds a single match at serial 0x01CEC01D.
Flag: cpu_m3lt3d_4nd_all_i_g0t_w4s_th1s_fl4g@flare-on.com
Takeaways
- A matching IV, K-table, and message schedule are not proof a function is stock SHA-256. Read it to the final
ret— the mutation here was a single extra XOR into word 0, invisible unless you trace past the feed-forward. - When a primitive is only slightly non-standard and it is embedded in a loadable binary, calling the binary’s own function via
LoadLibrary+ RVA is faster and more reliable than reimplementing it. You brute-force against ground truth instead of against your own possibly-wrong model.