zwesty
Write-ups / Day 5 / Binary
Day 5 · Mon, Oct 5 · Binary

Rust Belt

Prey slaughtered

Spotting a Rust binary from its toolchain metadata · one language-independent config extractor (HCFG, CRC-checked) · carrying confirmed answers across compounding challenges · working out an exact-text checker's vocabulary · finishing an exhausted Mastery in practice mode

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-05-rust-runtime-shapes (+ /time_trial, /mastery)
Solved
2026-10-07 to 2026-10-09, all three modes. Single Solve started 2026-10-07 16:13:05 → Mastery CORRECT 2026-10-09 17:28:39 UTC (2 days 1 h 16 min, almost all of it waiting on one field)
Player
Zwesty (solo), with Claude Code (Opus 5.5: Single Solve, our binary solver and its fixes, Mastery from our Kali VM) and the Homebase agent (GPT-6 "Luna": d02 config, the format breakthrough, Time Trial)

The challenge

Rust leaves its own fingerprints: mangled symbols with a distinct accent, panic strings that confess, trait objects hiding in plain sight. Learn the accent.

One evidence.zip (AES, password infected) holding every binary from Days 1–5, and 40 questions: Go Fish's 32 plus 8 for the new Rust file. Every question uses the same template: "What is the exact <field> value for field <answer_id> in the delivered evidence set?" All are checked with exact normalisation.

answer_id What it asks for Format
d01_artifact_sha256_1, d01_analysis_elf_class / machine / elf_type / endianness Day 1's C ELF: hash and header fields Exact text
d01_analysis_section_names, d01_analysis_symbols Day 1's section and symbol lists Compact JSON array, exact order
d01_analysis_disassembly recover_code's instructions JSON array; empty if recover_code is absent
d02_artifact_sha256_1, d02_analysis_format Day 2's C++ ELF Exact text
d02_analysis_config_campaign / domain / mutex / url Day 2's config Exact text
d02_analysis_config_crc32_hex, d02_analysis_xor_key_hex Day 2's config checksum and key Lowercase hex with 0x
d03_artifact_sha256_1 / _2, d03_analysis_machine / metadata_version, config campaign / domain / mutex Day 3's .NET DLL and its runtimeconfig Exact text
d03_analysis_format Day 3's container "one container_runtime token; CLI, CLR, or .NET may name the managed part"
d04_artifact_sha256_1, d04_analysis_architecture / format, config campaign / domain / mutex / url Day 4's Go ELF Exact text
d04_analysis_config_crc32_hex Day 4's config checksum Lowercase hex with 0x
d05_artifact_sha256_1 The Rust ELF's SHA-256 Exact text
d05_analysis_architecture, d05_analysis_format Its machine and its format Exact text
d05_analysis_config_campaign / domain / mutex / url Its config Exact text
d05_analysis_config_crc32_hex Its config checksum Lowercase hex with 0x

The exact-text format adds: "use the literal none only when the requested field is absent." d05_analysis_format is the field this write-up is mostly about. The question gives no hint about its vocabulary. Only Day 3's version of the field does.

Reading the prompt: what it said we'd need

Clue What it pointed to What I did about it
"Rust leaves its own fingerprints" The new file is a Rust binary, and it can be recognised as one Confirmed it from the toolchain string in .comment (rustc 1.90.0) and Rust standard-library source paths
"mangled symbols with a distinct accent" Rust's own symbol mangling, different from C++'s The file is not stripped, so the symbols are there. No question asked about them, so none are recorded here
"panic strings that confess" Rust's panic messages carry source paths The Homebase agent saw paths into Rust core and the standard library. Again, no question needed them
"trait objects hiding in plain sight" Rust's vtables for dyn Trait Not needed for any answer
The 8 d05 questions are the same fields as Day 4's Same config layout as the Go binary, so the same extractor should work Ran our HCFG config extractor on the Rust file: one CRC-checked hit
d05_analysis_format, "exact artifact-derived value" A label for the file's format, in a vocabulary nobody had confirmed yet Day 3's hint (a "container_runtime token") suggested ELF_Rust. That hint was for Day 3 only (see below)

In short, the prompt teaches how to recognise Rust, but the questions only asked what Day 4 asked. Recognising the language mattered for triage, and it was misleading for the format field.

Safety

Static analysis only. No binary was ever executed. The evidence was unpacked and read on our Kali VM (Claude, Single Solve) or in the Huntress AI Homebase (the agent, timed runs), and the artifacts stayed there. Our binary solver only reads bytes: it hashes files, parses headers, and scans for the config record. It never runs anything from the evidence.

What was in the files

The Single Solve's evidence.zip: 1,916,070 bytes, SHA-256 6049e2c8…e117 (matched the start reply), AES-encrypted with infected.

  • d01–d04 were byte-identical to Go Fish's (SHA-256 checked): the C ELF, the C++ ELF with its config record, the .NET DLL with its runtimeconfig.json, and the Go ELF. See the Day 1–4 write-ups for those.
  • d05, the new file: d05--variant-000.elf, SHA-256 4ac8f95b…c247.

file on d05 (the Homebase agent's saved output):

ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, with debug_info, not stripped

What identifies it as Rust: - .comment names the compiler: rustc 1.90.0 (1159e78c4 2025-09-14). - It's not stripped and keeps its DWARF debug info, so Rust symbol names and source paths stay in the file. - Embedded source paths point into Rust's core and standard library (the Homebase agent's reading). - Like the Go binary, it also carries the lab's huntress.binary.dynamic_trace.v1 string next to HUNTRESS_HOST_MODE / HUNTRESS_HOST_MODEL (found by the agent's byte-string scan).

Where its config sits: in the same HCFG record the C++, .NET, and Go binaries carry: a magic, a version, a one-byte XOR key, a length, a CRC32 of the plaintext, then the XOR'd JSON. The Rust compiler didn't change the record at all. Our extractor scans the whole file for the magic and only accepts a hit whose CRC32 matches, so it didn't need to know which language built the file. (Which section or offset it sat at wasn't recorded.)

Field d05 value
XOR key / length / CRC32 0x61 / 368 bytes / 0x8f37c323 (CRC verified)
campaign BR05-000
domain node-000.day05.example[.]invalid
url https://node-000.day05.example[.]invalid/api/BR05-000
mutex Global\HuntressBR05_000
ipv4 (not asked) 198.51.100.100

The timed variants used the same template. The Time Trial's 40 answer IDs matched the Single Solve's. Mastery had two sets of 40 (80 answers), and set 2 swapped two of the files' type: d02 (MinGW C++) and d04 (Go) came as native Windows PE .exe files instead of ELFs. The d05 Rust file stayed an ELF in both sets. (The variants' own values aren't recorded here; the solver reads them fresh each run.)

Single Solve: CORRECT (on 2026-10-09, after a long wait on format)

The certain answers went in first, as one batch. Go Fish's 24 accepted answers were rebuilt from its request log, and the 7 new d05 answers added: the SHA-256, architecture x86-64 (Go Fish had already shown that the ELF machine spelling wins over amd64), and the 5 config fields. 31 sent → accepted, 9 left. The whole new day took one submit, because the config extractor was language-independent.

What was left (9): the 7 d02 fields and the format answers for d04 and d05.

The d02 config fields came from the Homebase agent. Claude's safety filter had stopped one step of the analysis during the d02 config work on Mangled, and we didn't work around it, so Claude didn't attempt them here. The agent's Day 2 config analyser answered all six; sent as their own batch on 2026-10-07 at 17:13: accepted, 3 left (d02_analysis_format, d04_analysis_format, d05_analysis_format).

The long-open format field

Every ELF and JAR day had the same open field, and every guess followed Day 3's hint. Day 3's .NET answer was PE_CLI (container + runtime), so a Rust ELF "should" be something like ELF_Rust. It wasn't.

When (UTC) Who Where Value sent for the Rust ELF's format Result
2026-10-07 16:13:23 Claude (our Kali VM) Rust Belt ELF_Rust incorrect. Stopped: the d02 and d04 formats were open too, so one pattern was likely
2026-10-08 Homebase agent Routine Check (same d05 file) ELF64_Rust, then Rust_ELF64 both incorrect
2026-10-09 13:27:16 – 15:25:23 Homebase agent Rust Belt ELF/Rust, ELF64_RUST, ELF_RUST, ELF64/Rust, Rust, RUSTC, ELF64_DEBUG_INFO, RUST_ELF (8 single-value submits) all incorrect

On 2026-10-09 the agent kept probing across all the open format fields. It worked through container + language spellings, file and MIME output, ABI names and header combinations on the other challenges. The answer was the simplest one: lowercase elf, the family name objdump -f prints (elf64-x86-64). Uppercase ELF had been rejected on Day 2, and the lowercase form hadn't been tried until then.

Once Mangled accepted elf, it unlocked every ELF format field at once, and jar did the same for the JARs. On Rust Belt:

Time (UTC) Field Value Result
15:31:29 d02_analysis_format elf accepted, 2 left
15:34:33 d05_analysis_format elf accepted, 1 left
15:35:23 d04_analysis_format elf CORRECT

Single Solve: 40 of 40. Flag c0446667…fa85.

The confirmed d05 answers: SHA-256 4ac8f95b…c247 · architecture x86-64 · format elf · the five config values above.

The solver: our core binary solver answers every SHA-256, architecture, format, and config field from the evidence bytes, using the HCFG extractor for the config and an archive reader to open the AES zip. format returns elf for anything with the ELF magic, jar for a JAR, and PE_CLI for Day 3's managed DLL. In the timed runs the d01 ELF fields came from the Homebase agent's Day 1 ELF reader and the d02 config from its config analyser (the Single Solve carried d01 over from Go Fish). A small helper on our Kali VM sent the Single Solve's explicit answers.

Time Trial: CORRECT in 2.5 s (first attempt)

Run by the Homebase agent, as part of its timed batch for Days 3–9. Our solvers covered all 40 answers, so it was one submit.

started_at 2026-10-09T16:58:58.970Z  deadline 2026-10-09T16:59:05.970Z  (40 answer_ids)
  (Homebase agent, coverage 40/40)
  start 449 ms · artifact 502 ms · solve 868 ms · submit 716 ms · TOTAL 2534 ms
result CORRECT  flag c1923710…4ca9

A 7-second window, finished in 2.5 s. Claude later re-ran the fixed solvers offline against the saved delivery: same 40 answers as the agent submitted.

Mastery: CORRECT in practice mode (from our Kali VM)

Attempt 1 (Homebase agent, 16:59:06): stopped before submitting. Nothing was sent.

started_at 2026-10-09T16:59:06.275Z  deadline 2026-10-09T16:59:13.275Z  (2 sets x 40 answer_ids)
  (Homebase agent)
  missing: set1.d02_config x6 · set2.d02_analysis_format · set2.d02_config x6
           set2.d04_analysis_architecture · set2.d04_analysis_format
result stopped before submit (coverage check)

Two things were missing: - The Day 2 config analyser ignored setN.-prefixed answer IDs and .exe files, so set 1's and set 2's d02 config went unanswered. - Set 2's d02 and d04 were native Windows PE files. There was no confirmed format or architecture value for a native PE.

The fixes (Claude, 2026-10-09): the config analyser handles set IDs and .exe; a new native-PE add-on holds the PE answers, off until confirmed. Both PE values were confirmed in one submit on Nimble's Mastery at 17:27 UTC: format pe and architecture x86-64 (machine 0x8664).

Attempt 2 (Claude, from our Kali VM). The retry was refused: Rust Belt had no fresh variants left. Practice mode continues on the last delivery with no timer and no Hall of Fame time, and still completes the mode.

17:28:36  retry     -> HTTP 409 rotation_exhausted (unique_attempts_remaining 0)
17:28:37  practice  -> started_at 2026-10-09T17:28:37.327Z, deadline None (practice)
  80 answers (2 sets x 40), set 2 with the PE answers pe / x86-64
  TOTAL 2742 ms
result CORRECT  flag 6ebbef89…1f55

(Only the total was recorded for this run, not the per-phase timings.) Correct on the first submit, because every field had a confirmed rule by then.

Time to solve

The Single Solve was completed on 2026-10-09 at 15:35:23 UTC: 31 of 40 answers were accepted 10 s after the start on 2026-10-07, and the three format fields stayed open for two days. The Time Trial went through first time in 2.53 s; the first Mastery attempt stopped before submitting, and the Mastery was finished at 17:28:39 UTC as untimed practice, so it is unranked, 2 days 1 h 16 min after the first start.

Takeaways

  • One language-independent extractor turned a new language into one submit. Our HCFG extractor finds the config by its magic and checks it by its CRC, so Rust, Go, C++, and .NET all looked the same to it.
  • A hint for one field isn't the rule for its neighbours. Day 3's "container_runtime token" hint shaped every guess for the ELF and JAR formats (ELF_Rust, ELF64_Rust, …). The real answer was just the container: elf.
  • Under exact normalisation, case is a separate guess. ELF was rejected on Day 2, and that hid elf for days.
  • One confirmed answer can unlock a whole lane. elf on Mangled closed the ELF format fields on seven challenges.
  • Recognising the language is triage, not the answer. The prompt taught Rust's fingerprints. The questions only wanted the hash, machine, container, and config.
  • Mastery can change the file type under you. Set 2 swapped two ELFs for PE files, so a coverage check before submitting is what kept the first attempt from sending guesses.
  • An exhausted Mastery still finishes in practice mode: no timer, no Hall of Fame, but the mode completes.