zwesty
Write-ups / Day 8 / Binary
Day 8 · Thu, Oct 8 · Binary

Swift Justice

Prey slaughtered

Reading ELF header fields as raw numbers · finding a config record in any language's binary · ZIP central-directory compression methods · reading a 3,000-member JAR in memory · making a solver fit a 10-second window · working out a checker's vocabulary

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-08-swift-network-configuration (+ /time_trial, /mastery)
Solved
2026-10-08 → 2026-10-09, all three modes. Single Solve started 2026-10-08 20:19:15 → Mastery CORRECT 2026-10-09 17:32:17 UTC (21 h 13 min, most of it waiting on one wording question)
Player
Zwesty (solo), with Claude Code (Opus 5.5: core solver, Single Solve, the speed fix, the final timed runs from our Kali VM) and the Homebase agent (GPT-6 Luna: the d01/d02 add-ons, the format answers, the first Time Trial)

The challenge

Swift binaries speak in protocols and hide their networking in plain sight. Follow the symbols to the URL configuration and see where this one phones home.

One evidence.zip (AES, password infected) with eight binaries, ten questions each: 80 questions. Day 8 is a compounding challenge: the zip carries every earlier day's file (d01–d07) plus the new one, d08, a Swift ELF.

File answer_ids (dNN_artifact_… / dNN_analysis_…) Formats
d01 C ELF sha256_1, elf_class, machine, section_names, symbols, disassembly, elf_type, endianness, entry_point, has_symtab Exact text, JSON arrays, 0x hex, true/false
d02 C++ ELF sha256_1, format, config_campaign / domain / mutex / url, config_crc32_hex, xor_key_hex, has_cpp_typeinfo, has_cpp_vtable Exact text, 0x hex, true/false
d03 .NET DLL + runtimeconfig.json sha256_1, sha256_2, format, machine, metadata_version, config ×4, config_crc32_hex Exact text, 0x hex
d04 Go ELF, d05 Rust ELF sha256_1, architecture, format, config ×4, config_crc32_hex, hcfg_version, xor_key_hex Exact text, 0x hex
d06 Java JAR, d07 Kotlin JAR sha256_1, format, config ×4, config_crc32_hex, hcfg_version, xor_key_hex, compression_methods Exact text, 0x hex, JSON array
d08 Swift ELF sha256_1, architecture, elf_class, format, machine, config ×4, config_crc32_hex Exact text, base-10 integer, 0x hex

Bold = new on Day 8. The formats that mattered:

  • d08_analysis_elf_class and d08_analysis_machine: "base-10 integer with no units, separators, or explanatory text". On Day 1 the same field names took words (ELF64, x86-64).
  • compression_methods: "compact valid JSON array with exact values and order".
  • xor_key_hex, config_crc32_hex: "lowercase hexadecimal with a 0x prefix".
  • Exact text: "use the literal none only when the requested field is absent".

Reading the prompt: what it said we'd need

Clue What it pointed to What I did about it
"Swift binaries speak in protocols" Swift's runtime metadata: protocol conformances, mangled $s… symbols, a large runtime Expected a big binary. It was 67 MB, so anything per-byte had to be fast
"hide their networking in plain sight" The C2 settings sit in the file as data, not built at run time Used the language-independent config scanner already proven on Days 3–7, instead of reading Swift code
"Follow the symbols to the URL configuration" The binary is not stripped; a symbol leads to the config Confirmed it was unstripped, but didn't need to follow the symbols: the config record is found by its own signature anywhere in the file
"see where this one phones home" config_url and config_domain Answered from the decoded record
elf_class / machine as base-10 integers Raw header numbers, not readelf's words Tested which number each one meant (see Single Solve)
"exact values and order" on compression_methods A list built from the ZIP's own entries, in file order Distinct methods in first-seen order
The zip carries d01–d07 again Compounding: earlier answers carry over byte-for-byte Re-used every accepted rule by SHA-256; only the new fields needed new work

Safety

Static analysis only. No binary was ever executed. The evidence stayed on our Kali VM; the AES zip was read with 7z straight into memory, and the JARs inside it were opened in memory too. The 67 MB Swift ELF was never run and never disassembled: its answers came from the ELF header, the file's hash, and a signature scan for the config record.

Claude's safety filter stopped one step of the analysis, and we didn't work around it. The Homebase agent answered the d02 config fields instead, on every binary day, including this one.

What was in the files

Eight binaries, one shared trick. Every file except d01 carries the same kind of config record: an HCFG record (a 4-byte magic, a version byte, a one-byte XOR key, a length, a CRC32, then the JSON config XOR'd with the key). It's the same in C++, .NET, Go, Rust, Java, Kotlin, and Swift, so one scanner reads them all. A hit only counts if its CRC32 matches, so a stray HCFG can't produce an answer.

File What it is Notes
d01 C ELF (Day 1's primer) Header, sections, symbols, recover_code
d02 C++ ELF Itanium _ZTI… (typeinfo) and _ZTV… (vtable) symbols present → both true
d03 .NET DLL + runtimeconfig.json Config record inside the managed DLL
d04 / d05 Go / Rust ELF XOR keys 0x51 / 0x61
d06 Java JAR, 4 entries XOR key 0x71; methods 0 (stored, the directory) and 8 (deflate, the files)
d07 Kotlin JAR, 3,267 entries (the Kotlin runtime is bundled) XOR key 0x31; the same two methods
d08 Swift ELF, 67 MB, x86-64, dynamically linked, not stripped SHA-256 a700832c…b529

d08's decoded config (defanged by design: the challenge uses *.example.invalid):

campaign  BR08-000
domain    node-000.day08.example.invalid
mutex     Global\HuntressBR08_000
url       https://node-000.day08.example.invalid/api/BR08-000
crc32     0x6a033bfd

The timed variants used the same template with different values. The Time Trial's evidence.zip was 31,587,536 bytes (the Single Solve's was 31,454,414), and Mastery sent five sets, each its own ~31.5 MB download. The per-variant values weren't recorded; the solver read them from the bytes each time.

Single Solve: CORRECT (59 by Claude, 21 by the Homebase agent's add-ons and probes)

Claude's part, 2026-10-08: 59 of 80 in under 3 minutes

The certain answers went first, then each new field type in its own batch:

Batch Answers Result
Known field types: every SHA-256, d03's header and config, d04–d08 config, architecture, hcfg_version 47 accepted
xor_key_hex for d04–d07 (from the CRC-checked record, 0x%02x) 4 accepted
d02 has_cpp_typeinfo / has_cpp_vtable (_ZTI / _ZTV symbols present) 2 accepted (true, true)
d08 elf_class = 2 and machine = 62, together 2 incorrect
d03 config_url, config_crc32_hex 2 accepted
d08 machine = 62 alone 1 accepted: the raw e_machine
d08 elf_class = 64 alone 1 accepted: the class as a bit width
d06 compression_methods = [0,8] 1 accepted
d07 compression_methods = [0,8] 1 accepted

The one wrong submit, and why. Two new field types went in one batch. The Game Master fails the whole batch on any wrong answer and doesn't say which one, so a right answer (machine = 62) was hidden behind a wrong one. Re-sent alone:

  • machine = 62: accepted. The raw e_machine value (62 = EM_X86_64).
  • elf_class = 2: wrong. 2 is the raw EI_CLASS byte (ELFCLASS64). The checker wanted 64, the bit width.

So the two "integer" fields don't follow the same rule: one is the raw header number, the other is a decoded meaning. It cost one incorrect batch; nothing already accepted was lost.

compression_methods: the distinct method numbers from the JAR's central directory, in first-seen order, as integers. d06 has 4 entries (method 0 for the directory entry, 8 for the files), so [0,8]. d07's 3,267 entries use the same two.

The solver: our shared Binary solver. It opens the evidence through our archive reader, matches each dNN--variant-N member to its questions, and answers only fields whose method the Game Master has confirmed: hashes, d03's PE/.NET header fields, every config field via the config scanner, architecture, hcfg_version, xor_key_hex, the C++ typeinfo/vtable booleans, and Day 8's raw header numbers and compression methods. It still gave no wrong answer on Days 3–7.

Left open (21): the 9 d01 analysis fields, 6 d02 config fields, and 6 format answers (d02, d04–d08).

The Homebase agent's part

  • 2026-10-08 20:36 UTC: the agent's first run submitted nothing new; coverage was 68/80.
  • 20:38: its d01 ELF add-on (Day 1's analyser, extended for the new entry_point and has_symtab) answered all 9 d01 fields: accepted.
  • 20:43: the d02 config add-on hadn't run, because the range of days it applied to stopped before Day 8. The agent widened it to Day 3 onwards, and its 6 d02 answers were accepted. 74 of 80; only the six format answers left.

The format answers, 2026-10-09

The long-open field. Every ELF and JAR day had a format question with no hint beyond "exact artifact-derived value", and many wordings had been rejected on other challenges (ELF64, ELF, ELF_GO, elf64-x86-64, ELF64_Rust, JAR_Java, and more). The Homebase agent's persistent probing found it on Mangled: lowercase elf, the family word objdump -f prints. The JARs took lowercase jar.

On Swift Justice the agent sent the six answers one at a time, 15:33–15:42 UTC: elf for d02, d04, d05, and d08, jar for d06 and d07. All accepted, the last (d07 jar) → CORRECT. No wrong answer on Swift itself.

Single Solve flag: e8894961…6273. (Read back with a read-only op=status on 2026-10-09.)

Time Trial: CORRECT in 4.5 s (on the 2nd attempt)

Attempt 1 (Homebase agent): expired

Homebase agent, timed run
started_at 2026-10-09T17:02:40.138Z   deadline 2026-10-09T17:02:50.138Z   (10 s window)
answers   80/80 covered
start 2,263 ms · artifact 3,555 ms · solve 12,779 ms · submit 660 ms · TOTAL 19,257 ms
result    expired

Every answer was ready; the solve was just too slow. The archive reader started one 7z process per member of each nested zip, and d07's Kotlin JAR alone has 3,267 members. On top of that, the core solver re-scanned each file for the config record once per question.

The fix (Claude, same afternoon): - The archive reader now reads unencrypted inner zips (the JARs) in memory with Python's zipfile, and only falls back to 7z for encrypted ones. It also caches each evidence file's members, so the core solver and every add-on share one read. - The core solver caches the config scan per file. - The whole Swift solve went from 28 s to 2.4 s on our Kali VM. Re-run offline against every saved run, it gave the same answers as all seven Time Trial submits that day.

Attempt 2 (Claude, from our Kali VM): CORRECT

retry from our Kali VM; unique_attempts_remaining 111; ranked
started_at 2026-10-09T17:27:50.738Z   deadline not recorded
artifact  evidence.zip 31,587,536 bytes (sha256 checked)
answers   80
retry 1,108 ms · artifact 983 ms · solve 1,698 ms · submit 700 ms · TOTAL 4,488 ms
result    CORRECT  flag 25ce6358…faf4

The retry reply is the new attempt, so our solver used it directly instead of calling start again.

Mastery: CORRECT in 14.3 s (first attempt)

Mastery waits for a CORRECT Time Trial, and on 2026-10-09 the other binary Masteries had shown that set 2 swaps d02 and d04 for native Windows PE .exe files. By this run the solver had the PE rules (format pe, architecture x86-64, the C++ booleans from COFF symbols), confirmed on Nimble's Mastery five minutes earlier, and the d02 config add-on read setN. ids and .exe files.

fresh start from our Kali VM; unique_attempts_remaining 31; ranked
started_at 2026-10-09T17:32:03.550Z   deadline not recorded
artifacts 01- to 05-evidence.zip, ~31.5 MB each (5 sets)
answers   400 (5 × 80)
start 731 ms · artifact 2,940 ms · solve 8,016 ms · submit 2,658 ms · TOTAL 14,344 ms
result    CORRECT  flag c77f55cc…ac9c

A fresh, ranked run, not a practice attempt (the Day 3–7 Masteries had to fall back to practice after rotation_exhausted). Which of the five sets were PE sets wasn't recorded.

Time to solve

The Single Solve started on 2026-10-08 at 20:19:15 UTC, had 59 of 80 answers within 2 min 41 s and 74 by 20:43, then went CORRECT on 2026-10-09 at 15:42:35 UTC once the format vocabulary was known, 19 h 23 min after the start. The first Time Trial expired (a 12.8 s solve against a 10 s window); the retry went CORRECT in 4.5 s at 17:27, and the ranked Mastery went CORRECT on its first attempt at 17:32:17, 21 h 13 min after the first start.

Takeaways

  • One new field type per batch. Two new fields sent together cost an incorrect batch and hid a right answer.
  • The same field name can change meaning. Day 1's elf_class was ELF64; Day 8's integer elf_class was 64, not the raw byte 2, while the integer machine was the raw 62. Read each field's format, then test it alone.
  • Don't reverse what you can scan for. A CRC-checked signature scan found the config in a 67 MB Swift binary in seconds, with no Swift-specific work.
  • Speed is a design issue, not a tuning one. One process per archive member was fine on small files and fatal with a 3,267-member JAR. Read nested zips in memory and scan each file once.
  • An expired attempt still teaches. Every answer was right; only the timing failed, so the fix was purely about speed.
  • Check each add-on's day range after each new day. Six d02 answers sat unused for one run because a day range stopped short.
  • Persistent, one-at-a-time probing solved format: lowercase elf / jar.