zwesty
Write-ups / Day 10 / Binary
Day 10 · Sat, Oct 10 · Binary

Zig Zag

Prey slaughtered

Reading ELF and PE section tables · matching a named section to the config record it holds · raw header numbers (ELF e_machine, COFF Machine) · reusing a compounding solver across ten languages · handling a Mastery that mixes ELF and PE builds

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-10-zig-cross-platform-services (+ /time_trial, /mastery)
Solved
2026-10-10, all three modes CORRECT. First start 13:00:05 → last CORRECT (Mastery) 14:19:15 UTC: 1 h 19 min, most of it the gap between the 9:00 automated run and the session where the last field was worked
Player
Zwesty (solo), with Claude Code (Opus 5.5): our automated morning solver (a scheduled script on our Kali VM) sent the first 99 answers, then Claude worked the new field, the solver rules, and the timed modes. The d01 ELF and d02 config add-ons the solver ran were built by the Homebase agent (GPT-6 Luna) on Day 9

The challenge

"Zig promises one binary for every platform. Compare the ELF and PE service descriptors side by side and prove they're the same program wearing different uniforms."

100 questions: 10 binaries × 10. One evidence.zip holds every binary from Days 1–10 (SHA-256 9c31fb3a…1fc4). The d01–d09 fields are the same ones as in Nimble (Day 9); d10 is new: a Zig build. Exact-text answers carry the usual rule: "use the literal none only when the requested field is absent."

The d10 questions:

answer_id Question (short) Format
d10_artifact_sha256_1 SHA-256 of the d10 file Exact text
d10_analysis_architecture Architecture Exact text
d10_analysis_format Container format Exact text
d10_analysis_machine Machine Base-10 integer
d10_analysis_config_campaign / _domain / _mutex / _url The four config values Exact text
d10_analysis_config_crc32_hex The config's CRC32 Lowercase hex with 0x
d10_analysis_has_hcfg_section New: does the file have an hcfg section? Enum true / false

The other 90, 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
d09 (Nim) SHA-256, architecture, format, machine (integer), config ×5, hcfg_offset

Reading the prompt: what it said we'd need

Clue What it pointed to What I did about it
"one binary for every platform" Zig cross-compiles, so the same program may arrive as an ELF or a PE Expected the timed sets to mix builds. Mastery set 2 was the PE
"Compare the ELF and PE service descriptors side by side" The config record (the "service descriptor") should look the same in both containers; only the wrapper differs Reused the existing HCFG finder unchanged; it read the config from both builds
"the same program wearing different uniforms" Header fields that do differ (container format, machine number) need a rule for each container Wrote the PE machine rule to mirror ELF's (raw header number) before sending it
The new field has_hcfg_section (enum) A question about where the record lives: in its own named section, or inside a general data section Read the section table and checked that a section named .hcfg held exactly the record before answering true
100 questions, 90 of them from earlier days A compounding challenge: the Days 1–9 solver should answer most of it The morning solver sent everything the solver and add-ons already covered; only the new field was worked by hand

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, so no binary member is ever written to disk. Every answer came from reading bytes: ELF and PE headers, the section tables, and the config record. The config values weren't recorded and aren't given here.

What was in the files

evidence.zip held one file per day, the same d01–d09 files as before, plus the new d10--variant-000.elf: a Zig x86-64 ELF (machine 62, architecture x86-64, format elf, all answered by the existing rules and accepted).

The config record is the same HCFG layout as the earlier days: a 12-byte header (HCFG magic, version, XOR key, length, CRC32) followed by the XOR'd JSON config. What was new is where Zig puts it: in its own section.

What Where Size
ELF section named .hcfg File offset 132160 380 bytes
The HCFG record Starts at the same offset 12-byte header + 368 bytes = 380 bytes, filling the section exactly

The timed variants: the Time Trial delivered one 100-question set; the solver answered all 100 from the new bytes. Mastery delivered three sets (300 answers). Set 2's d10 was d10--variant-001.exe: a native PE build, x86-64, with its own .hcfg section. That is the "different uniform" from the prompt. Each set's values weren't recorded, only that the solver re-derived them from each set's own download.

Single Solve: CORRECT (99 from the morning solver, the last one by Claude)

The morning solver (our Kali VM, 13:00:05–13:00:08 UTC): 99 of 100 accepted, no wrong submits. The scheduled solver found the challenge list at 9:00 ET, started Zig Zag, and sent three batches:

Pass Sent Result
Core Binary solver 84 accepted, 16 left
d01 ELF add-on 9 accepted, 7 left
d02 config add-on 6 accepted, 1 left

Time Trial and Mastery were not started: the solver only starts them after a Single Solve CORRECT.

The one left: d10_analysis_has_hcfg_section. No rule existed; the field was new.

  1. Claude read the d10 ELF's section table statically. It has a section named .hcfg at file offset 132160, size 380.
  2. The section is the config record and nothing else. The HCFG record starts at the section's offset, and its 12-byte header plus 368-byte body add up to exactly 380. So the name isn't a coincidence or a decoy.
  3. It was sent alone: true → CORRECT (14:14:58). Single Solve flag: e28a4e79…b2e1.
single_solve  started_at 2026-10-10T13:00:05.667Z  deadline None
13:00:08  core 84 → accepted (16 left) · d01 ELF add-on 9 → accepted (7 left) · d02 config add-on 6 → accepted (1 left)
14:14:58  d10_analysis_has_hcfg_section = true (sent alone) → CORRECT
result CORRECT  flag e28a4e79…b2e1

The solver: our shared Binary solver answers every confirmed field from the bytes. Three rules were added today:

  • Section names: returns the section names of an ELF or a PE (by walking the COFF section table after the optional header), and nothing for any other file.
  • has_hcfg_section: true if a section is named .hcfg, false if the file has a section table without one, and no answer for files that are neither ELF nor PE.
  • PE machine (added after the Mastery stop below): the raw COFF Machine number, mirroring ELF's raw e_machine.

The changes were tested before going live: the updated solver was re-run against all 44 saved earlier sets and had to give byte-identical answers on 44/44. There's still no dedicated unit test file for the binary solver; that snapshot run is the regression check.

Time Trial: CORRECT in 3.6 s (first attempt)

Run from our Kali VM right after the Single Solve. The new has_hcfg_section rule answered the d10 field on the fresh set.

time_trial  started_at 2026-10-10T14:16:35.089Z  deadline not recorded
100 answer_ids, coverage 100/100
start 579 · artifact 1474 · solve 637 · submit 906 · TOTAL 3,600 ms
result CORRECT  flag 91f7df60…60cd

Mastery: CORRECT on the retry

1. Attempt 1, 14:16:42: stopped before submitting. The run downloaded three sets. Set 2's d10 was a native PE (d10--variant-001.exe, x86-64, also with an .hcfg section), and machine had no PE rule: the Day 3 rule only covers the .NET DLL, and the Day 8 rule only covers ELF. Every other PE field already had a confirmed rule from Day 9. The solver left the field unanswered, and the run stopped safely. Nothing was sent.

2. The PE rule: ELF's accepted machine is the raw e_machine number (62), not a name. The PE mirror is the raw COFF Machine field: 0x8664 = 34404. Because a Mastery is all or nothing, a wrong value would fail the whole attempt; everything else in it was already confirmed.

3. Retry, 14:19:09: one 300-answer submit → CORRECT.

mastery attempt 1  started_at 2026-10-10T14:16:42.482Z  3 sets · set 2's d10 is a PE · stopped before submit, nothing sent
mastery retry      started_at 2026-10-10T14:19:09.775Z
300 answer_ids · set2.d10_analysis_machine = 34404
retry 1615 · artifact 2141 · solve 1945 · submit 1027 · TOTAL 6,730 ms
result CORRECT  flag 54190b17…7ef6

This one submit confirmed the PE machine rule. It is now part of the solver.

The HTTP 429 that morning didn't touch this challenge. The same morning run also started the Script challenge Bind Over Matter, whose add-ons sent about 22 single-answer submits in 11 s; the Game Master answered 429 rate_limited and the solver stopped as designed. Zig Zag's three batches had already been accepted. Our Game Master client now spaces consecutive submits 1 s apart.

Time to solve

The morning solver started the Single Solve at 13:00:05 UTC and had 99 of 100 accepted three seconds later; the last field went in by hand at 14:14:58 for the CORRECT, 1 h 14 min 53 s after the start. The Time Trial went CORRECT on its first attempt in 3.6 s, and the Mastery went CORRECT on the retry at 14:19:15 after the first attempt stopped without sending, 1 h 19 min 10 s after the first start.

Takeaways

  • A compounding challenge runs itself now. The scheduled solver sent 99 of 100 before anyone sat down; the real work was one new field.
  • A section name is evidence, but check what's in it. .hcfg was trusted only after the record was found filling the section exactly.
  • Zig keeps the config in its own section, so the section table is the first place to look. Earlier days hid it in .rodata or a string literal.
  • "One binary for every platform" means the timed sets can switch containers. Every header-level field needs a rule per container, ELF and PE.
  • Mirror a confirmed rule instead of guessing a new style. ELF machine was the raw number, so PE machine was too (34404), and it went in first time.
  • Leaving a field blank is a safe stop. Attempt 1 sent nothing; the retry came with the rule worked out.
  • Test solver changes against every earlier set. 44/44 identical before going live meant the new rules broke nothing.
  • Fast single-answer loops trip rate limits. The 429 hit another challenge's add-ons the same morning; submits are now spaced 1 s apart.