← all writeups
MalwareHuntress CTF 2026

Bridge Troll

The same HCFG config format from Nimble, but cranked up to 22 language runtimes / 264 answer fields. With a generic parser (find HCFG, read key/length/CRC, XOR, verify the CRC) most of it falls out automatically — the fun is in the samples that don't store the blob plainly.

d11 (a D binary, built with LDC) has no literal HCFG bytes on disk at all. A byte search comes up empty, so the config must be built at runtime. Scanning for base64 runs instead finds 54 of them — 40 are D runtime string blobs, 13 are mangled symbol names that happen to be valid base64, and exactly one (at offset 0x59e90 in .rodata) decodes to a real HCFG record. The CRC check is what separates it from the noise; a plain "does it decode?" test gets fooled by D's long mangled names. You can confirm the binary actually uses that string in _Dmain:

4041b7: lea  rcx,[rip+0x55cd2]   ; -> 0x459e90, the base64 carrier in .rodata
4041be: mov  edx,0x1fc           ; length 508, exactly the run length
4041c6: call std.base64...decode
4041ce: mov  bpl,[rdx+0x5]       ; reads the XOR key from the decoded header

d15 ships the same program twice — symbolized and stripped — to test whether your extraction survives losing the symbols. It does, because a raw byte search for HCFG finds the record at the same offset (0x210d) in both files:

$ od -A x -t x1z -j 0x210d -N 12 d15-stripped.elf
00210d  48 43 46 47 01 b1 6f 01 4e 91 48 c3   HCFG........
        magic        ^v ^key  len=0x16f  crc=0xc348914e

The campaign string even sits in two places — plaintext in .rodata (what the program prints) and XOR-encoded inside the blob — so you can watch the key work byte by byte:

on disk   002126  f3 e3 80 84 9c 81 81 81
^ 0xb1            42 52 31 35 2d 30 30 30    B  R  1  5  -  0  0  0

0xf3 ^ 0xb1 = 0x42 = 'B', and so on → BR15-000. Other gotchas across the set: the sha256_N fields follow ZIP central-directory order (not alphabetical on d15/d16/d22), decoy HCFG headers that fail the CRC are scattered around, and d12 stores its record as a raw constant array loaded by movabs rax, 0x4153f0. All 264 accepted — build the parser once, then let the CRC separate signal from decoys.