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

ELF Aware

Prey slaughtered

Reading ELF headers, section tables, and symbol tables · static disassembly of a tiny function · stripped vs unstripped binaries · reading questions literally · 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-01-c-elf-orientation (+ /time_trial, /mastery)
Points
Single Solve 2 · Time Trial 3 · Mastery 5
Solved
2026-10-02, all three modes. Single Solve 21:27:54 → Mastery CORRECT 22:43:48 UTC (1 h 16 min)
Player
Zwesty (solo), with Claude Code (Opus 5.5, Single Solve and analysis) and the Homebase agent (GPT-6 Luna, timed runs)

The challenge

Before you can reverse anything, you have to learn to read. These plain C binaries are your primer: headers, section tables, symbol tables. Start with readelf -h -S and work your way down.

Nine questions per binary. Every text answer is checked exactly (case and spelling matter).

answer_id Question Format
elf_class What ELF class is the executable? Exact text
endianness What byte order does the ELF header declare? Exact text
machine What machine architecture does the ELF header declare? Exact text
elf_type What ELF object type is this file? Exact text
marker Recover the exact string stored in the custom .huntress section Exact text
has_symtab Does the final file retain a .symtab section? true / false
exported_symbol Recover the designated analysis symbol Exact text
recover_code Recover the constant returned by recover_code Integer (exact text in some sets)
entry_point Give the ELF entry point as lowercase hex with a 0x prefix Lowercase hex

For exact text, the format adds: "use the literal none only when the requested field is absent." That sentence turned out to be the key to Mastery.

Reading the prompt: what it said we'd need

Clue What it pointed to What I did about it
"Start with readelf -h -S" The ELF header (class, data, machine, type, entry) and the section table readelf -h -S first, then -s for symbols, and -p / -x for section contents
"work your way down" Header → sections → symbols → code Go in that order, and stop at a tiny function's disassembly
"plain C binaries" Small gcc output: expect main, _start, the C runtime helpers, and one or two custom functions Separate the standard symbols from the designated ones
"the custom .huntress section" A non-standard section holding a string Dump it and take the NUL-terminated string
"Does the final file retain a .symtab?" Some files will be stripped Expect variants without symbols
"the constant returned by recover_code" A function by that name (I first read this as "the designated function's constant". That was wrong, as Mastery showed)
Exact-text answers, "exact" normalisation The checker wants specific spellings Get the certain answers accepted first, then test the wording on its own

Safety

Static analysis only. No binary was ever executed. Each ELF was extracted with its execute permission removed, then read with readelf and objdump. Constants were read from the disassembly, not by running the code.

What was in the files

Every binary was the same small C program: main, _start, the standard C runtime helpers, one custom function at 0x401120, and a custom .huntress section. The variants changed only the custom function's name, its constant, the marker, and whether the symbols were stripped.

The Single Solve's symbol-orientation.elf (15,560 B), from readelf -h:

  Class:                             ELF64
  Data:                              2's complement, little endian
  Type:                              EXEC (Executable file)
  Machine:                           Advanced Micro Devices X86-64
  Entry point address:               0x401030
  • 27 sections, including [14] .huntress (24 bytes at 0x402010) and [24] .symtab.
  • .huntress: BR01-SYMBOL-ORIENTATION, followed by a NUL byte.
  • Symbols: besides the standard ones, one GLOBAL FUNC recover_code at 0x401120 (6 bytes), and one LOCAL OBJECT huntress_marker (the .huntress string).
  • recover_code's whole body:

text 401120: mov $0x539,%eax 401125: ret

It returns 0x539 = 1337. The compiler also inlined the same constant into main (movl $0x539,…).

The four variants seen across the timed modes:

Variant (.huntress marker) .symtab Custom function at 0x401120 Its constant
BR01-SYMBOL-ORIENTATION yes recover_code 0x539 = 1337
BR01-IMMEDIATE-RECOVERY yes recover_code 0x1ca3 = 7331
BR01-SECTION-BEACON yes inspect_section (renamed) 0x801 = 2049
BR01-STRIPPED-PROFILE no (stripped) (no name), the same mov $imm,%eax; ret stub 0x1003 = 4099

Everything else was identical in every file: ELF64 / little endian / x86-64 / EXEC, and entry 0x401030.

How the stripped file's function was found without a name: two independent clues. 1. The same place: at 0x401120, exactly where recover_code sits in the other variants, is the same two-instruction stub. 2. main uses the same constant. _start hands 0x401020 to __libc_start_main, which makes 0x401020 main, and main starts by loading the same constant.

Single Solve: CORRECT

The certain answers went in together first (a partial submit), and came back accepted, remaining 5: - marker = BR01-SYMBOL-ORIENTATION - has_symtab = true - recover_code = 1337 - entry_point = 0x401030

exported_symbol = recover_code, tested alone → accepted. It's the only non-standard GLOBAL function. huntress_marker was the other candidate, but it's a LOCAL object.

Then the four header fields, one request each:

Field Rejected Accepted Style
elf_class — ELF64 readelf's word
endianness 2's complement, little endian, little endian little Short form
machine Advanced Micro Devices X86-64 x86-64 Short form
elf_type EXEC (Executable file), EXEC ET_EXEC C constant name

The checker's vocabulary is a mix of styles, so each field has to be worked out on its own. 11 requests in total. CORRECT, flag 4f330c02…4526.

Time Trial: CORRECT in 2.48 s (on the 4th attempt)

The timed runs were driven by the Homebase agent, using scripts it wrote on the Homebase. Claude analysed each failed delivery offline (downloads still work after expiry) and refined the rules between attempts.

Attempt What the delivery held What happened
1 One file: section-beacon (no function named recover_code) The agent stopped. Its rule assumed recover_code always exists
2 The start reply had 36 answer IDs (slot0_… to slot3_…) for the 9 questions The agent stopped on the ID/question count "mismatch" before downloading. Claude fetched the zip offline afterwards: 4 files, slot-0..3.elf, so it was really 4 slots × 9 questions
3 4 files; slot 3 stripped The analyser stopped 0.49 s after start, because there was no named function to find
4 Slots 0–2 symbol-orientation; slot 3 immediate-recovery CORRECT in 2.48 s. Every slot had a function named recover_code, so the uncertain rules never came up

Flag 32991031…c627.

Mastery: CORRECT in 3.42 s (on the 6th attempt)

Two sets, each its own evidence.zip with several slot files. The answer format for recover_code is fixed per set before any files exist: exact text in set 1, an integer in set 2.

Every uncertain field went in alone, so a wrong theory never cost the confirmed answers:

Attempt What was tested Result What it taught
1 Stripped slot: exported_symbol=none + recover_code=4099, together incorrect Sending two theories together hides which one is wrong
2 Section-beacon slots: exported_symbol=inspect_section / recover_code=2049 accepted / incorrect (in both sets) The renamed function is the designated symbol, but its constant isn't the "recover code"
3 Section-beacon, exact-text set: recover_code=none ✅ accepted recover_code means a function literally named recover_code; absent → none
3 Stripped, integer set: recover_code=4099 incorrect Stripping removes the name, so it's absent too
4 (the runner's guard fired: an absent-function slot in the integer set) nothing sent That set seemed to always include one, probably by design
5 Section-beacon, integer set: recover_code=0 incorrect Not "zero for absent" either
6 Section-beacon and stripped slots in the integer set: recover_code=none; stripped exported_symbol=none ✅ all accepted → CORRECT The "integer" label was a per-set template quirk; the absent-means-none rule holds everywhere

Flag a7984627…8033. Between attempts 5 and 6, both Claude and the Homebase agent re-examined the integer set's files independently. Neither found any other number in them. A friend's confirmation that Mastery was solvable is what prompted testing the rule (none) instead of another value.

The confirmed rules

Field Rule
Header ELF64 · little · x86-64 · ET_EXEC
marker The NUL-terminated string in .huntress
has_symtab Whether a .symtab section exists (stripped → false)
exported_symbol The single non-standard GLOBAL function's name; stripped → none
recover_code Only a function literally named recover_code counts → its constant. Renamed or stripped → none, whatever the format label says
entry_point The header's entry point, lowercase 0x hex

Time to solve

All three modes were solved on 2026-10-02 by 22:43:48 UTC, 1 h 16 min after the Single Solve started. The Single Solve took about 5 minutes; the Time Trial went through on its 4th attempt (2.48 s) and the Mastery on its 6th (3.42 s), with each failed attempt studied offline before the next.

Takeaways

  • Read the question literally. "The constant returned by recover_code" meant that name. When it's absent, the answer is none, not the constant of whatever replaced it.
  • A format label can be a template, not a fact. The "integer" label was set per set before the files existed. When a label conflicts with a confirmed rule, test the rule once.
  • Get the certain answers accepted first, then test each uncertain one on its own. Partial progress survives an incorrect, so a wrong theory costs nothing else.
  • A checker's vocabulary can mix styles (ELF64, little, x86-64, ET_EXEC). Confirm each field separately.
  • Expired attempts are still useful. Downloads keep working, so every failed delivery could be studied offline.
  • Stop safely, then widen the rule. Each of the agent's stops (renamed, slots, stripped) showed a variant the rules didn't cover yet.
  • When the evidence runs out, check which constraint you're treating as fixed before calling it unsolvable.

Appendix: the Homebase agent's run log and approach

From the Homebase agent's own records. It recorded only total times, not per-phase timings.**

Mode / attempt Started (UTC) Outcome Total
Time Trial 1 21:37:07 Stopped: section-beacon, no rule yet 47.3 s to report
Time Trial 2 21:40:07 Stopped: 36 IDs (didn't download) 17.4 s
Time Trial 3 21:45:28 Stopped: stripped slot (the analyser stopped at 0.49 s) 68.9 s to report
Time Trial 4 21:57:25 CORRECT, one 36-answer submit 2.476 s
Mastery 1–5 22:04 – 22:31 Each stopped or narrowed the rule (see the Mastery table) 1.4 – 4.4 s each
Mastery 6 22:43:45 CORRECT, a 67-answer step 1, then 5 single-answer submits 3.415 s

How its scripts work, in summary: an analyser reads each ELF statically: header, sections, symbol tables, the .huntress marker, and the custom function's return constant. It stops on anything ambiguous. A runner drives the Game Master: it takes the answer IDs and formats from the start reply, verifies each delivery's hash, maps each set's slots to its own files, and submits the confirmed answers in one batch and every uncertain one alone.

Its key lesson: "The answer IDs determine the file-to-slot mapping; the questions array can be a reusable template."