zwesty
Write-ups / Day 9 / Binary
Day 9 · Fri, Oct 9 · Binary

Nimble

Prey slaughtered

Spotting a Nim binary by its runtime strings · finding a config record in .rodata · validating a match instead of trusting a magic number · file offsets · reusing a compounding solver across nine languages · native PE vs ELF answers

STTMClaude Homebase Both

Flags are shortened to their first 8 and last 4 characters because they're unique per team.

Challenge ID
binary-reverse-engineering-day-09-nim-generated-runtime (+ /time_trial, /mastery)
Solved
2026-10-09, all three modes CORRECT. First start 14:00:45 → last CORRECT (Mastery) 17:27:05 UTC: 3 h 26 min, most of it waiting on the format rule and the d01/d02 add-ons
Player
Zwesty (solo), with Claude Code (Opus 5.5: analysis, solver fixes, and the final Mastery from our Kali VM) and the Homebase agent (GPT-6 Luna: the d01 ELF and d02 config add-ons, the format probing, and the Time Trial)

The challenge

"Nim compiles down to C, but the generated names give it away. Learn to spot Nim's runtime fingerprints hiding inside C-shaped clothing."

90 questions: 9 binaries × 10. One evidence.zip holds every binary from Days 1–9. d01–d08 are the same files as in Swift Justice (Day 8); d09 is new: a Nim ELF. Exact-text answers carry the usual rule: "use the literal none only when the requested field is absent."

The d09 questions:

answer_id Question (short) Format
d09_artifact_sha256_1 SHA-256 of the d09 file Exact text
d09_analysis_architecture Architecture Exact text
d09_analysis_format Container format Exact text
d09_analysis_machine Machine Base-10 integer
d09_analysis_config_campaign / _domain / _mutex / _url The four config values Exact text
d09_analysis_config_crc32_hex The config's CRC32 Lowercase hex with 0x
d09_analysis_hcfg_offset New: where the HCFG record sits Base-10 integer

The other 80, carried over from earlier days:

Binary Fields
d01 (C ELF) SHA-256, ELF class, machine, section names (JSON), symbols (JSON), disassembly (JSON), ELF type, endianness, entry point, has_symtab
d02 (C++ ELF) SHA-256, format, config campaign / domain / mutex / URL / CRC32, XOR key, has_cpp_typeinfo, has_cpp_vtable
d03 (.NET DLL) Two SHA-256s, format, machine, metadata version, config ×5
d04 (Go), d05 (Rust) SHA-256, architecture, format, config ×5, hcfg_version, XOR key
d06 (Java), d07 (Kotlin) SHA-256, format, config ×5, hcfg_version, XOR key, compression methods (JSON)
d08 (Swift) SHA-256, architecture, ELF class (integer), format, machine (integer), config ×5

Reading the prompt: what it said we'd need

Clue What it pointed to What I did about it
"Nim compiles down to C" A native ELF built through a C compiler, so the same ELF header rules as the other ELF days Reused the confirmed rules: architecture x86-64, machine = raw e_machine (62)
"the generated names give it away", "runtime fingerprints" Nim leaves readable runtime names and strings (the file is not stripped) Expected readable literals. That turned out to be the trap: the HCFG magic itself is one of them
"hiding inside C-shaped clothing" Treat it like a C binary and look in the usual places The config was a string literal in .rodata, not a custom section
The new field hcfg_offset (integer) A question about where the record is, not what it says. Three readings: the HCFG header, the stored (Base64) text, or the Nim string's length prefix Picked "where the stored record starts" and sent it alone
90 questions, 80 of them from earlier days A compounding challenge: the Days 3–8 solver should answer most of it Ran our shared Binary solver and only worked on what was new

Safety

Static analysis only. No binary was ever executed. The evidence stayed on our Kali VM. The solver reads the AES zip's members straight into memory (via 7z x -so), so no binary member is ever written to disk. Every answer came from reading bytes: ELF headers, .rodata, and the config record. The config domains are fake (*.example.invalid) and are written defanged here.

Claude's safety filter stopped one step of the analysis, and we didn't work around it. The Homebase agent covered that part (see the Single Solve).

What was in the files

evidence.zip (AES, password infected, SHA-256 a2b66b96…cc22) held one file per day: d01--variant-N.elf (C), d02 (C++ ELF), d03 (.NET DLL + runtimeconfig.json), d04 (Go), d05 (Rust), d06 (Java JAR), d07 (Kotlin JAR), d08 (Swift), and the new d09: a Nim x86-64 ELF, dynamically linked and not stripped.

The config record is the same HCFG layout as the earlier days: a 4-byte HCFG magic, a version byte, a one-byte XOR key, a length, a CRC32 of the plaintext, then the XOR'd JSON config. What was new is how Nim stores it.

Nim keeps string literals readable, length-prefixed, in .rodata. So the d09 file contained three things that looked like an HCFG record:

What Where What it really was
The literal HCFG .rodata, as a Nim string of length 4 A string constant in the program's own code. Not a record
A "record" right after it Next to that literal Zero length. Its CRC32 field read 0, and the CRC32 of nothing is also 0, so it passed the old check
A Base64 string literal .rodata, file offset 30696 (a Nim string: length 0x1fc, then SENGRwF…) The real record, Base64-encoded. SENG… is what HCFG looks like in Base64

The real record decoded to version 1, XOR key 0x51, CRC32 0x9614e1a8, campaign BR09-000, domain node-000.day09.example[.]invalid, plus a mutex and a URL (their values weren't recorded).

The timed variants: the Time Trial delivered one 90-question set; the solver answered all 90 from the new bytes. Mastery delivered five sets (01–05-evidence.zip, 450 answers). Sets 2 and 4 are PE sets: d02 is a MinGW C++ Windows .exe and d04 a Go Windows .exe, both x86-64, while every other file stays ELF / JAR / .NET. Each set's values weren't recorded, only that the solver re-derived them from each set's own download.

Single Solve: CORRECT (68 by Claude, the last 22 via the Homebase rules and add-ons)

Claude's part (our Kali VM, 14:00–14:02 UTC): 68 of 90 accepted, no wrong submits.

  1. The first run crashed. Our config finder scanned for HCFG and accepted any match whose CRC32 checked out. The bare Nim literal and its zero-length "record" both checked out, and the extractor fell over on them.
  2. The fix: a match only counts if its body is non-empty and decodes to JSON. A bare magic constant can't pass that. The finder already searched Base64 runs for records too (Day 6's Java class stores its record that way), so with the false hits gone it found the real one at 30696.
  3. 67 answers in one batch (every field type already confirmed on earlier days, plus d09's SHA-256, architecture x86-64, machine 62, and the config values): accepted, 23 left.
  4. hcfg_offset = 30696 alone: accepted (22 left). So "offset" means where the stored record starts in the file: the Base64 text, when it's encoded. That rule is now part of the solver.

The 22 that were left were not Nim questions:

Open Why Who closed it
7 × format (d02, d04–d09) The checker's wording had been unknown since Day 2 The Homebase agent, by persistent probing: lowercase elf for every ELF, jar for every JAR. The seven elf answers went in from our Kali VM at 16:23 (accepted, 15 left)
6 × d02 config (campaign, domain, mutex, URL, CRC32, XOR key) Claude's safety filter stopped one step of the analysis, and we didn't work around it The Homebase agent's d02 config add-on: 6 accepted at 16:50 (9 left)
9 × d01 analysis fields Left to the Homebase agent's d01 ELF add-on by design The Homebase agent's d01 ELF add-on, after widening the range of days it covers to Day 3 onward. 9 sent at 16:54 → CORRECT

The Homebase agent reported its coverage as "core 75 + d01 ELF add-on 9 + d02 config add-on 6 = 90/90".

Single Solve flag: 3643f0f7…bf86 (from the Homebase's saved reply).

The solver: our shared Binary solver answers every confirmed field from the bytes. It reads the zip in memory, maps each question to its file and set, finds and checks the config record, and reads ELF and COFF symbols. The d01 and d02 analysis fields come from the Homebase agent's add-ons. There's no dedicated test file for the binary solver; after each fix it was re-run offline against every saved delivery and had to give the same answers as every accepted submit. A small test suite covers the set mapping.

Time Trial: CORRECT in 22.5 s (Homebase, first attempt)

Run by the Homebase agent as part of a batch of timed runs for Days 3–9. Nim's Time Trial reported no deadline, so the slow solve didn't matter. The same solver code made Swift Justice's Time Trial expire about a minute earlier (one 7z process per JAR member; fixed afterwards: JARs read in memory, 28 s → 2.4 s on our Kali VM).

started_at 2026-10-09T17:03:59.054Z  deadline None (server reported none)
90 answer_ids, coverage 90/90
start 1,995 · artifact 5,350 · solve 14,239 · submit 890 · TOTAL 22,474 ms
result CORRECT  flag 7b9e996a…ba52

(From the agent's own log.) Claude re-ran the fixed solver against this delivery offline: same answers as the ones submitted.

Mastery: CORRECT from our Kali VM (the Homebase's attempt, finished by Claude)

1. Homebase, 17:04:42: stopped before submitting. The agent started the attempt (no deadline, 5 sets) and got HTTP 502 from the proxy while downloading an artifact. It stopped safely; nothing was sent.

2. Our Kali VM, 17:22:04: re-fetch, nothing sent. In timed modes start reconnects to the same attempt. Claude reconnected from our Kali VM and downloaded all five sets, checking each size and SHA-256. Coverage 444/450. The six gaps were all in the PE sets: setN.d02_analysis_format, setN.d04_analysis_format, and setN.d04_analysis_architecture for sets 2 and 4. Every binary Mastery had stopped on the same PE fields that afternoon, and no PE value had been confirmed yet.

3. The PE test: the full 444 plus pe for the four PE format fields and x86-64 for the two PE architecture fields. Fallbacks if rejected: PE, then amd64. The reasoning: every ELF is elf and every JAR is jar (lowercase container names); the Day 3 question calls "PE" the generic container label; and the ELF Go binaries' x86-64 had been accepted.

4. Our Kali VM, 17:26:47: one 450-answer submit → CORRECT.

started_at 2026-10-09T17:04:41.967Z (started by the Homebase agent; reconnected from our Kali VM)
deadline None · 5 sets (01–05-evidence.zip) · 450 answer_ids · sets 2 and 4 are PE sets
444 solver answers + setN.d02/d04_analysis_format = pe (sets 2, 4) + setN.d04_analysis_architecture = x86-64 (sets 2, 4)
artifact 2135 · solve 12872 · submit 2619 · TOTAL 18451 ms
result CORRECT  flag 877faf96…b24a

Because a Mastery only says CORRECT when every answer is right, this one submit confirmed both PE values at once: format pe and architecture x86-64. Claude switched them on in a PE add-on that had been written to stay off until confirmed, and the Days 3–7 and Swift Masteries were finished with the same rule.

Time to solve

The Single Solve started at 14:00:45 UTC, had 68 of 90 answers within two minutes, and went CORRECT at 16:54 UTC, 2 h 53 min after the start, once the format rule and the Homebase add-ons were in. The Time Trial (Homebase agent) went CORRECT on its first attempt in 22.5 s at 17:04, and the Mastery, interrupted by an HTTP 502 and finished from our Kali VM, went CORRECT at 17:27:05, 3 h 26 min after the first start.

Takeaways

  • A magic number is not a record. Any program that looks for its own config holds the magic as a constant. An extractor has to validate what follows (non-empty, CRC, parses as JSON), or it "finds" the config inside the code that searches for it.
  • Nim keeps literals readable and length-prefixed. That helps an analyst, but it also puts the search key in plain sight.
  • Pin down what "offset" means before sending. Header, encoded text, or length prefix are all defensible. Pick one and send it alone.
  • A compounding challenge is mostly last week's work. 67 of 90 went in from the existing solver in one batch; the real work was the one new field and the one new false positive.
  • No deadline doesn't mean the solver is fast enough. 14.2 s was fine here, and the same code expired Swift's Time Trial.
  • An HTTP 502 doesn't lose the attempt. start reconnects, so the run could be re-fetched and finished from another machine.
  • One all-or-nothing submit can confirm several guesses at once, but only if everything else in it is already certain.
  • Add-ons hard-code their day range. The d01 ELF add-on had to be widened before it would answer Day 9.