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

Stripped

Prey slaughtered

Comparing a stripped ELF with its symbolized twin, artifact numbering when one day has two files, HCFG config extraction, extending a compounding solver without changing old 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-15-assembly-and-symbol-loss (+ /time_trial, /mastery)
Solved
2026-10-15, all three modes; 14 min 42 s from first start to last CORRECT (09:00:46–09:15:28 ET)
Player
Zwesty (solo), with Claude Code (Opus 5.5): our automated morning solver (a scheduled script on our Kali VM) sent the known fields; a Claude helper agent solved the new ones, extended our shared binary solver, and ran both timed modes

The challenge

No symbols. No names. Just raw assembly and the calling conventions it can't hide. Reconstruct the stripped functions from the instructions alone — the stack never lies.

150 questions in the Single Solve: Days 1 to 14 as yesterday (140), plus 10 new d15_* fields. For the first time a day ships two files, a symbolized build and a stripped build of the same program, and most new fields name which one they mean:

answer_id Asks for Format
d15_artifact_sha256_1 SHA-256 of the first Day 15 file exact text
d15_artifact_sha256_2 SHA-256 of the second Day 15 file exact text
d15_analysis_stripped_architecture processor architecture of the stripped build exact text
d15_analysis_stripped_format container format of the stripped build exact text
d15_analysis_stripped_machine the stripped build's header machine field integer
d15_analysis_symbolized_architecture the same three, for the symbolized build exact text
d15_analysis_symbolized_format exact text
d15_analysis_symbolized_machine integer
d15_analysis_stripped_config_campaign config campaign from the stripped build exact text
d15_analysis_stripped_config_domain config domain from the stripped build exact text

Answer kinds across all 150: 80 exact text, 49 pattern, 11 integer, 5 JSON, 5 enum.

Reading the prompt: what it said we'd need

Clue What it pointed to What I did about it
"No symbols. No names." a binary with .symtab / .strtab removed (strip) Read both section tables. The stripped build is the symbolized one minus exactly those two sections
"Just raw assembly" function boundaries and names would have to come from the code itself Checked: the stripped main is the same 167 bytes at the same address as in the symbolized build. No question asked about functions, so nothing was reconstructed
"the calling conventions it can't hide" System V x86-64 calls into libc through the PLT (getenv, strcmp, printf) The dynamic symbols survive stripping, so imports stay readable. Not asked about
"the stack never lies" stack-frame reasoning in a disassembler Not needed: every question was a header field, a hash, or the embedded config
two files for one day which file is "artifact 1"? The one judgement call of the day: zip order (symbolized first) vs name order (stripped first). Zip order was right

Safety

Static only. The evidence zips (≈32 MB each, AES-encrypted with the usual lab password) were read on our Kali VM. The two Day 15 files were extracted into a scratch folder on the VM with the execute bit removed, and read with readelf, objdump, and the solver's own parsers. No binary was executed, and nothing from the evidence left the VM. This write-up was built from our own records and the saved replies, with no Game Master calls.

What was in the files

One evidence.zip with 17 members. The Day 1 to 14 files were unchanged: all 140 earlier answers today equal yesterday's. Zip comment: huntress:day15-single.

The two new files, listed in this order in the zip:

Zip order Member Size Notes
1 d15--variant-000-symbolized.elf 16,160 bytes has .symtab and .strtab (30 sections)
2 d15--variant-000-stripped.elf 14,360 bytes 28 sections, file says "stripped"
  • Both are 64-bit little-endian x86-64 ELF executables (EXEC, e_machine 62), entry point 0x401060, dynamically linked against glibc. Imports: getenv, strcmp, printf, __libc_start_main.
  • They are the same build. Bytes 64 to 0x3028 (every loaded section) are identical. The only differences are three ELF header fields that describe the section table (its offset, count, and string-table index) and the two symbol sections the stripped file lacks.
  • The symbol table holds only the usual C runtime pieces plus main (167 bytes at 0x401146). Stripping removed names, not code.
  • .rodata holds an environment-variable name (HUNTRESS_HOST_MODEL), a few branch words (analysis, sandbox, decoy, …), and a trace template that describes itself as harmless. Defanged excerpt: "family":"symbol_loss_equivalent_trace","effects":"synthetic_decode_only","network":"none".
  • The HCFG record sits in its own .hcfg section at file offset 0x210d in both files: raw (not Base64), version 1, XOR key 0xb1, 367 bytes, CRC-32 0xc348914e. Keys: campaign BR15-000, domain node-000.day15.example.invalid, plus ipv4 (a documentation-range address), url, mutex, windows_path, linux_path, service, host_model. All hosts are under .invalid.

The timed variants used the same template with different values. Each zip again listed the symbolized build first:

Run Day 15 files Other files
Single Solve variant-000 (BR15-000) all variant-000
Time Trial variant-005 (BR15-005) mixed variants
Mastery sets 1–5 variant-004, 003, 002, 001, 000 sets 1, 3, and 5 carry PE .exe builds of d02, d04, and d10

Single Solve (morning solver first, then the helper agent)

Our automated morning solver started the attempt and sent what it already knew. It couldn't see the Day 15 files at all: they're named d15--variant-000-<tag>.elf, and its file-name pattern only knew dNN--variant-N.<ext>. So it answered none of the 10 new ids and stopped, as designed.

Batch Sent Result Open after
core (solver) 125 accepted 25
Day 1 ELF basics plug-in (solver) 9 accepted 16
Day 2 config plug-in (solver) 6 accepted 10

The Claude helper agent read the two files statically, then sent the certain answers in one batch per field type and the judgement call alone:

Batch Answers How it was found Result Open after
B1 config BR15-000, node-000.day15.example.invalid the solver's HCFG reader: one CRC-checked record, the same in both files accepted 8
B2 architecture x86-64 ×2 e_machine 62, the confirmed word accepted 6
B3 format elf ×2 ELF magic, the confirmed lowercase form accepted 4
B4 machine 62 ×2 raw e_machine (the Day 8 rule) accepted 2
sha256_1 83a58b2e…4686 (symbolized) judgement: zip member order, candidate 1 of 2 accepted 1
sha256_2 b97e0d28…9c7e (stripped) follows from the above CORRECT

Why zip order first: on Day 3, the only other day with two files, zip order and name order agree, so that rule had never really been tested. Here they disagree. The zip's order is the generator's own, and "build, then strip" puts the symbolized file first. Name order (stripped first) was candidate 2 and was never needed.

CORRECT at 13:10:21 UTC, flag 0158a2a8…f74e.

The solver change

Before any timed run, our shared binary solver learned the new shape:

  • It recognises tagged builds (dNN--variant-N-<tag>.<ext>) and keeps them in zip order, so sha256_1 and sha256_2 follow the rule just confirmed. Days with untagged names still sort by name, as before.
  • dNN_analysis_<tag>_<field> picks the build named by <tag> and applies the field rules every earlier day already uses (format, architecture, machine, ELF class, config fields, CRC-32, XOR key, HCFG offset/version, the .hcfg section). If a Mastery set ever brings a native PE build, it answers the confirmed PE words (pe, x86-64).
  • Six new tests: zip order, Day 3 name order unchanged, routing by tag, a PE build, Mastery questions named delivery N: evidence.zip, and the saved Day 15 Single Solve equal to the accepted answers.
  • Checked before it went live, in a separate copy of the solver: a snapshot of all 94 saved Script and Binary sets was byte-identical on 93; the only change was the Day 15 Single gaining exactly its 10 accepted answers. Coverage 150/150. A regression rehearsal on the Binary lane (solve time against each attempt's clock, streamed vs whole answers on all 18 saved Binary Masteries) passed; the slowest solve used 16% of its clock.

Time Trial

started_at   2026-10-15T13:15:11.991Z   deadline 13:16:31.991Z (80 s)
start reply  ranked, hall_of_fame_eligible: true, unique_attempts_remaining: 63
artifact     evidence.zip, Day 15 = variant-005 (symbolized + stripped)
answers      150, one submit
             d15_artifact_sha256_1: aa74a880…02ec   d15_artifact_sha256_2: 0eda4bf4…9008
             d15_analysis_{stripped,symbolized}_architecture: x86-64, _format: elf, _machine: 62
             d15_analysis_stripped_config_campaign: BR15-005
             d15_analysis_stripped_config_domain: node-005.day15.example.invalid
laps         start 1059, artifact 1999, solve 681, ready 0, submit 1448, TOTAL 5187 ms
vs started_at: start +122, artifact +2121, solve +2802, ready +2802, submit +4250 ms
result       CORRECT 13:15:15.310Z   flag 4f4b2685…2cac

Mastery

started_at   2026-10-15T13:15:23.269Z   deadline 13:20:43.269Z (320 s)
start reply  ranked, hall_of_fame_eligible: true, unique_attempts_remaining: 15
artifacts    5 streamed downloads, 01..05-evidence.zip, 32.25-32.36 MB each
             Day 15 = variants 004/003/002/001/000; sets 1, 3, 5 carry PE builds of d02, d04, d10
answers      750 (150 per set), one submit
laps         start 1255, dl2 2214, dl1 75, dl5 68, dl3 33, dl4 145, artifact 0, solve 1630, ready 1,
             submit 2855, TOTAL 8277 ms
vs started_at: start +216, dl2 +2431, dl1 +2506, dl5 +2574, dl3 +2607, dl4 +2752,
             artifact +2752, solve +4382, ready +4383, submit +7239 ms
result       CORRECT 13:15:28.323Z   flag c86a390a…b2d9

Unlike Day 14's, both timed starts were ranked (hall_of_fame_eligible: true, attempts left). The challenge list marked both routes as supporting ranked starts, and the helper agent started them that way.

Time to solve

All three modes were solved by 09:15:28 ET, 14 min 42 s from the first start to the last CORRECT. The Single Solve went through at 09:10:21 after six accepted sends with no rejections; the solver change and its checks took the next five minutes, and the ranked Time Trial (+3.3 s) and Mastery (+5.1 s) each went through on the first submit.

Takeaways

  • The prompt promised disassembly; the questions asked for headers. "Reconstruct the stripped functions" never turned into a question. Reading the actual answer ids first saved the deep dive.
  • Two files for one day is a new shape. The file-name pattern was the only thing that broke; every field rule already existed. Check new member names against the solver's patterns first.
  • Zip order is the generator's order. When zip order and name order disagree, numbering follows the zip. Day 3 had never told the two apart.
  • Certain answers in batches by field type, the judgement call alone kept every send accepted: six sends, no rejections.
  • Check old answers before running the clock. The solver change went live only after 93 of 94 saved sets were byte-identical and the one change was exactly the new answers. Both timed modes were then CORRECT on the first try.