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_ |
SHA-256 of the first Day 15 file | exact text |
d15_ |
SHA-256 of the second Day 15 file | exact text |
d15_ |
processor architecture of the stripped build | exact text |
d15_ |
container format of the stripped build | exact text |
d15_ |
the stripped build's header machine field | integer |
d15_ |
the same three, for the symbolized build | exact text |
d15_ |
exact text | |
d15_ |
integer | |
d15_ |
config campaign from the stripped build |
exact text |
d15_ |
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 . / . 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- |
16,160 bytes | has . and . (30 sections) |
| 2 | d15- |
14,360 bytes | 28 sections, file says "stripped" |
- Both are 64-bit little-endian x86-64 ELF executables (
EXEC,e_machine62), entry point0x401060, 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 at0x401146). Stripping removed names, not code. .rodataholds 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
.hcfgsection at file offset 0x210d in both files: raw (not Base64), version 1, XOR key0xb1, 367 bytes, CRC-320xc348914e. Keys:campaignBR15-000,domainnode-000.day15.example.invalid, plusipv4(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- (BR15-) |
all variant- |
| Time Trial | variant- (BR15-) |
mixed variants |
| Mastery sets 1–5 | variant-, 003, 002, 001, 000 |
sets 1, 3, and 5 carry PE . 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-, node- |
the solver's HCFG reader: one CRC-checked record, the same in both files | accepted | 8 |
| B2 architecture | x86- ×2 |
e_ 62, the confirmed word |
accepted | 6 |
| B3 format | elf ×2 |
ELF magic, the confirmed lowercase form | accepted | 4 |
| B4 machine | 62 ×2 |
raw e_ (the Day 8 rule) |
accepted | 2 |
sha256_ |
83a58b2e…4686 (symbolized) |
judgement: zip member order, candidate 1 of 2 | accepted | 1 |
sha256_ |
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, sosha256_1andsha256_2follow 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.hcfgsection). 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.