zwesty
Write-ups / Day 7 / Binary
Day 7 · Wed, Oct 7 · Binary

Routine Check

Prey slaughtered

Reading a Kotlin JAR statically (javap, strings) · recognising a coroutine's continuation class · pulling a Base64 config out of the constant pool · decoding a CRC-checked config record · working out a checker's vocabulary · scripting a compounding, timed solve

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-07-kotlin-staging-logic (+ /time_trial, /mastery)
Solved
2026-10-07 to 2026-10-09, all three modes. Single Solve started 2026-10-07 16:22:00 → Mastery CORRECT 2026-10-09 17:28:46 UTC (2 days 1 h 7 min, nearly all of it waiting on one wording question)
Player
Zwesty (solo), with Claude Code (Opus 5.5: analysis, the solver, and the runs from our Kali VM) and the Homebase agent (GPT-6 Luna: the d02 config, the format probing, and the Time Trial)

The challenge

Kotlin coroutines don't run — they suspend, resume, and hop between states. Reconstruct the state machine from the metadata and figure out what this one was staging.

One evidence.zip (AES, password infected) holding Day 7's file and every earlier day's file. That makes 56 questions: Pool Party's 48 plus 8 new ones for d07. Every text answer is checked exactly, and the generic answer format adds "use the literal none only when the requested field is absent."

Day File in the zip answer_ids Format
d01 C ELF d01_artifact_sha256_1, d01_analysis_ elf_class, machine, elf_type, endianness, section_names, symbols, disassembly Exact text; JSON arrays for the three lists
d02 C++ ELF with a config record d02_artifact_sha256_1, d02_analysis_format, config_ campaign / domain / mutex / url / crc32_hex, xor_key_hex Exact text; 0x lowercase hex
d03 .NET DLL + runtimeconfig.json d03_artifact_sha256_1 and _2, format, machine, metadata_version, config_ campaign / domain / mutex Exact text; format is a lenient container_runtime pattern
d04 Go ELF SHA-256, architecture, format, five config_ fields Exact text; 0x hex
d05 Rust ELF SHA-256, architecture, format, five config_ fields Exact text; 0x hex
d06 Java JAR SHA-256, format, five config_ fields, hcfg_version Exact text; 0x hex; integer
d07 Kotlin JAR d07_artifact_sha256_1, d07_analysis_format, config_campaign / domain / mutex / url / crc32_hex, hcfg_version Exact text; 0x lowercase hex; base-10 integer

The new questions are worded generically ("What is the exact analysis format value for field d07_analysis_format…"). None of them asks about the coroutine itself.

Reading the prompt: what it said we'd need

Clue What it pointed to What I did about it
"Routine Check" (the title) A pun on coroutine, and a routine check of every earlier day Expected the same compounding layout as Days 3–6, with one Kotlin file added
"Kotlin coroutines … suspend, resume, and hop between states" The compiler turns a suspend function into a state machine: a continuation class with a label field and an invokeSuspend method Listed the classes with javap -p and found Day07Variant000$staged$1
"Reconstruct the state machine from the metadata" Kotlin's @Metadata annotations and the continuation's fields (label, L$0, result) Read them statically; the coroutine suspends once and resumes into evaluate(model)
"what this one was staging" The program's config: what it would hand to the next stage Looked for the HCFG record again, as in Days 3–6. It was a Base64 constant, as on Day 6
dNN_analysis_format, exact text, no hint A checker word with no stated vocabulary Left unguessed on Day 7 (see the Single Solve). It took the Homebase agent's long probing to find

Safety

Static analysis only. No binary or JAR was ever executed. The evidence stayed on our Kali VM and was read with javap -p, strings, and Python byte parsing (our archive reader loads zip and JAR members into memory). The coroutine logic was reconstructed from the class files, not by running the JVM.

Claude's safety filter stopped one step of the analysis, and we didn't work around it. The d02 config fields were answered by the Homebase agent instead.

What was in the files

Single Solve evidence.zip: 6,485,485 bytes, SHA-256 a9cd8efd…f450, matching the start reply.

  • d01 to d06 were byte-identical to Pool Party's. Its 38 accepted answers carried over unchanged.
  • d07 is d07--variant-000.jar (SHA-256 13238011…959b), 3,267 files. Almost all of it is the Kotlin standard library bundled in (a "fat JAR"). The program itself is a handful of classes:
  • Day07Variant000.class (Main-Class), with $main$1, $main$2, and Result.class.
  • Day07Variant000$staged$1.class: the coroutine's continuation. It holds the label state field, the spilled local L$0, result, and invokeSuspend. That is the state machine the prompt meant.
  • The state machine: staged(model) suspends through suspendCoroutine, then resumes into evaluate(model). It returns success for the default workstation model, as on Days 3 and 6.
  • The config: Day07Variant000.CONFIG is a Base64 string constant, the same trick as Day 6's JAR. Decoded, it's an HCFG record (magic, version, one-byte XOR key, length, CRC32, then XOR'd JSON): version 1, key 0x31, CRC32 0xcf3ea232. Fields, defanged:

text campaign BR07-000 domain node-000.day07.example[.]invalid url https://node-000.day07.example[.]invalid/api/BR07-000 mutex Global\HuntressBR07_000

The timed variants used the same template. The solver answered every field from the bytes, and its answers matched the Time Trial's accepted submit when re-checked offline. One difference mattered: Mastery set 2 swaps d02 (MinGW C++) and d04 (Go) for native Windows PE .exe files. The other days stay ELF / JAR / .NET. (The per-variant config values of the timed runs weren't recorded.)

Single Solve: CORRECT (1 submit from Claude, the rest from the Homebase agent)

Claude's one submit (2026-10-07, 16:23:18): 45 answers, accepted, 11 left. Pool Party's 38 carried over, plus the d07 SHA-256, the five config fields, and hcfg_version = 1.

What was left (11): the 7 d02 fields and the format answers for d04, d05, d06, and d07. No format was guessed for d07. By then ELF64, ELF (d02), ELF_GO, elf64-x86-64 (d04), ELF_Rust (d05), and JAR_JVM (d06) had all been rejected on earlier days. Day 3's accepted PE_CLI came from a lenient question that d04 onwards don't share. One more blind guess would not have taught anything.

Homebase agent, 2026-10-07 17:13:25: the d02 config. It answered the six d02 config fields in one batch: accepted, 51 of 56, 5 left (the five format answers).

Homebase agent, 2026-10-09: the format hunt. The agent probed the format fields one answer at a time. The Game Master log records 11 incorrect single-answer submits on this challenge between 13:27 and 15:13 UTC. The candidates it tried for Routine Check, in families:

Family Examples (all rejected) What it taught
Container + language ELF64_Go, ELF64_Rust, Rust_ELF64, JAR_Java, Java_JAR, JAR_Kotlin, Kotlin_JAR Not d03's container_runtime style
Case and separator variants JAR_KOTLIN, KOTLIN_JAR, JAR-Kotlin Not a spelling problem around the same idea
Packaging and metadata words FAT_JAR, MULTI_RELEASE_JAR, KOTLIN_RUNTIME, BUNDLED_JAR, JAR_KOTLIN_RUNTIME, kotlin_module Not a description of what's inside the JAR

(The agent's list of candidates is longer than the 11 incorrect submits in the log and has no per-submit times, so not every value can be matched to a logged submit.)

The break came on Mangled: cpp_format = lowercase elf, the family word objdump -f prints, was accepted after uppercase ELF had been rejected. The agent then carried the rule across every binary:

Time (UTC) Field Answer Result
15:32:35 d02_analysis_format elf accepted, 4 left
15:37:56 – 15:40:50 d04_analysis_format, d05_analysis_format, d07_analysis_format elf, elf, jar each accepted, 3 → 1 left
15:41:28 d06_analysis_format jar CORRECT (flag 07e71bf4…718f)

(The order of the three middle submits isn't recorded, but d07 was accepted before d06's CORRECT.) The format answer is just the lowercase container name: elf for every ELF, jar for every JAR.

Time Trial: CORRECT in 7.4 s (Homebase agent)

Run by the Homebase agent on 2026-10-09, as part of a batch of timed runs for Days 3–9. It used our shared Binary solver, which answers the SHA-256s, d03, and the d04+ architecture, config, CRC, hcfg_version, and format straight from the bytes, with add-ons for the d01 ELF questions and the d02 config, a reader that finds and CRC-checks the HCFG record (including inside Base64 runs), an archive reader for the AES zip and nested JARs, and a mapping from questions to files and sets.

started_at 2026-10-09T17:00:46.072Z   deadline 2026-10-09T17:00:57.072Z  (11 s)
answers 56/56
start 861 ms · artifact 971 ms · solve 4,565 ms · submit 979 ms · TOTAL 7,377 ms
result CORRECT  flag 79b1a357…6c2a

The 4.6 s solve was the slow part. Day 6's took 0.8 s with the same solver. The difference is d07's 3,267-file fat JAR: the archive reader then started one 7z process per JAR member. The same cost is what expired Swift Justice's Time Trial (12.8 s against a ~10 s window), whose zip also carries d07. Claude's fix, the same day: JARs are read in memory with Python's zipfile, and the config scan is cached. Re-checked offline, the solver gives the same 56 answers.

Mastery: CORRECT as untimed practice (Claude, from our Kali VM)

Attempt 1 (Homebase agent, 2026-10-09 17:01:04): stopped before submitting, nothing sent. The deadline was 17:01:16.435Z (12 s). Two problems:

  • Set 2 delivered d02 and d04 as native PE .exe files. set2.d02_analysis_format, set2.d04_analysis_format, and set2.d04_analysis_architecture had no confirmed value.
  • The d02 config add-on ignored setN. answer ids and .exe files, so set 1's and set 2's d02 config fields were missing too.

Fixes (Claude): the d02 config add-on now handles set ids and .exe; a new add-on answers the PE format and architecture, switched on once Nimble's Mastery confirmed pe and x86-64 in one submit (17:27).

Attempt 2 (our Kali VM, 17:28): the retry came back rotation_exhausted (no fresh variants left), so our solver continued with op=practice. Practice re-uses the last artifact, has no timer and no ranked time, but still completes the mode.

retry   409 rotation_exhausted  unique_attempts_remaining 0
practice started_at 2026-10-09T17:28:44.148Z   deadline None   practice True
answers 112 (2 sets × 56)
TOTAL 3,870 ms
result CORRECT  flag 2536fd3a…185b

(The per-phase start / artifact / solve / submit split for this run wasn't recorded.) Honest note: this Mastery counts as complete, but it was untimed practice, not a ranked run.

Time to solve

The Single Solve started on 2026-10-07 at 16:22 UTC and went CORRECT on 2026-10-09 at 15:41:28 UTC, 47 h 19 min later, once the Homebase agent had found the format vocabulary. The Time Trial (Homebase agent) went CORRECT at 17:00 in 7.4 s, and the Mastery went CORRECT at 17:28:46 as unranked, untimed practice, 2 days 1 h 7 min after the first start.

Takeaways

  • The prompt and the questions can point at different things. The prompt was about the coroutine state machine; the questions asked for hashes, the config, and a format word. Reading the continuation class was worth it for understanding, but the points were in the constant pool.
  • Compounding challenges reward carry-over. d01–d06 were byte-identical to Pool Party's, so 38 answers were free. Checking the hashes first is what made that safe.
  • Don't guess when the vocabulary is unknown. Holding back the d07 format cost nothing. The answer came from one confirmed spelling elsewhere (Mangled's elf) and was then applied everywhere.
  • The simplest word was the answer. Dozens of compound spellings failed; lowercase elf / jar, the container name, was it.
  • Big archives break timed runs in non-obvious ways. A 3,267-member fat JAR turned one process per member into seconds of overhead. Read archives in memory and scan once.
  • A new variant shape can appear only in Mastery. Set 2's PE files had no rule until Nimble confirmed one. Stopping before submitting kept the answers clean.