zwesty
Write-ups / Day 6 / Binary
Day 6 · Tue, Oct 6 · Binary

Pool Party

Prey slaughtered

Reading a JAR and its Java class files statically · the class-file constant pool · finding a Base64-encoded config blob · validating a config record by its CRC32 · carrying answers across compounding challenges · 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-06-java-bytecode-loader (+ /time_trial, /mastery)
Solved
2026-10-07 → 2026-10-09, all three modes. Single Solve started 2026-10-07 16:17:35 → Mastery CORRECT 2026-10-09 17:28:42 UTC (49 h 11 min, almost all of it waiting on one wording question)
Player
Zwesty (solo), with Claude Code (Opus 5.5: d06 analysis, the solver library, the Mastery finish) and the Homebase agent (GPT-6 "Luna": d02 config, the format answers, the Time Trial)

The challenge

Every Java class file carries its own phone book: the constant pool. Names, methods, invocations — it's all in there. Dive in and take attendance.

48 questions over one evidence.zip (AES, password infected). This is a compounding challenge: the zip carries every earlier Binary day's file as well (d01 C ELF, d02 C++ ELF, d03 .NET DLL, d04 Go ELF, d05 Rust ELF), plus the new d06 Java JAR. 40 of the 48 questions are Rust Belt's; 8 are new:

answer_id Question (paraphrased) Format
d06_artifact_sha256_1 SHA-256 of the d06 file Exact text
d06_analysis_format "exact analysis format value" Exact text (no hint at all)
d06_analysis_config_campaign Config campaign Exact text
d06_analysis_config_domain Config domain Exact text
d06_analysis_config_mutex Config mutex Exact text
d06_analysis_config_url Config URL Exact text
d06_analysis_config_crc32_hex Config CRC32 Lowercase hex with 0x
d06_analysis_hcfg_version The HCFG record's version Base-10 integer

Exact text adds: "use the literal none only when the requested field is absent." There is no architecture question for d06 (a JAR has none).

Reading the prompt: what it said we'd need

Clue What it pointed to What I did about it
"Every Java class file", ID java-bytecode-loader A JAR (a ZIP) of compiled .class files, and a "loader" that decodes something at run time Listed the JAR's members and read the class files with javap -v -p and strings. Never ran them
"its own phone book: the constant pool" Every string literal, class name, and method reference in a class lives in the constant pool. The title's "pool" Read the constant pool first, looking for a long literal that isn't a name
"Names, methods, invocations" The Methodref / NameAndType entries show which library calls the code makes decodeConfig() invokes java.util.Base64, which said how the config was stored
"take attendance" List what's there and answer from the list Read each config field from the decoded record
d06_analysis_hcfg_version The question names the HCFG record, the same config record as Days 2–5 Reused our HCFG extractor and taught it to look inside Base64
d01–d05 questions Compounding: the earlier files might be identical Checked the SHA-256s: byte-identical to Rust Belt's, so its accepted answers carried over
d06_analysis_format, "exact artifact-derived value" No container_runtime hint (Day 3 had one; PE_CLI was accepted) One guess, then left it open. It became the lane-wide format problem

Safety

Static analysis only. No challenge file was ever executed, and no JVM was started on any of them. The evidence stayed on our Kali VM. The JAR was opened as a ZIP and its classes read with javap (a disassembler; it doesn't run the code) and strings. The config was decoded from the bytes with our HCFG extractor. The timed-mode tooling reads archive members in memory and never writes a script or binary member to disk.

What was in the files

The Single Solve's evidence.zip: 1,919,090 bytes, SHA-256 f3f8f329…f8b6 (matched the start reply). d01–d05 were byte-identical to Rust Belt's, so this section covers only d06.

d06--variant-000.jar (SHA-256 6571ad6e…ae6c), four ZIP entries:

META-INF/                         (directory, stored)
META-INF/MANIFEST.MF              Main-Class: Day06Variant000
                                  Created-By: Huntress CTF deterministic factory
Day06Variant000.class             class file major version 65 (Java 21)
Day06Variant000$Result.class

The config: the class holds a static final String CONFIG, a long Base64 literal in the constant pool starting SENGRwF…. That decodes to HCFG\x01…: the same HCFG record as Days 2–5, just Base64'd. decodeConfig() decodes it with java.util.Base64. The record (described, not decoded here) is: the magic HCFG, a version byte, a one-byte XOR key, a length, a CRC32 of the plaintext, and then the XOR'd JSON. For d06:

version 1 · key 0x71 · length 368 · CRC32 0x121bc931 (matches)
campaign  BR06-000
domain    node-000.day06.example[.]invalid
url       https://node-000.day06.example[.]invalid/api/BR06-000
mutex     Global\HuntressBR06_000

(Brackets added; the domains are non-routable .invalid names.)

The rest of the class is a harmless stand-in for a loader. evaluate(model) reads HUNTRESS_HOST_MODEL and returns success, decoy, or failure for host models like workstation, analysis, or sandbox. It prints one huntress.binary.dynamic_trace.v1 JSON line that describes itself as modeled_command_dispatch / synthetic_dispatch_only, with network none.

The ZIP entries' compression methods (directory stored = 0, files deflated = 8) came back later: Days 8 and 9 ask d06_analysis_compression_methods, and [0,8] (distinct methods, first-seen order) was accepted there.

The timed deliveries came from the same factory, and the solver answers from the bytes either way, not from the Single Solve's values. Mastery set 2 was different: it delivered d02 (MinGW C++) and d04 (Go) as native Windows PE .exe files instead of ELFs. (The timed variants' own d06 values aren't recorded here.)

Single Solve: CORRECT (two days, mostly waiting)

Claude, 2026-10-07: 38 of 48 in one submit

One batch of 38 answers went in 29 seconds after the start: accepted, 10 left. It held Rust Belt's 31 carried-over answers plus the seven certain d06 ones:

Field Answer How
d06_artifact_sha256_1 6571ad6e…ae6c SHA-256 of the JAR as extracted
config_campaign / domain / url / mutex as above The decoded HCFG JSON
config_crc32_hex 0x121bc931 The record's stored CRC32, checked against the plaintext
hcfg_version 1 The record's version byte

How the config was found: our extractor already found the raw HCFG record in the C++, .NET, Go, and Rust binaries. It got a new mode: search the raw bytes, then every Base64 run that contains the Base64 form of the magic (SENG), decode it and search again. A hit only counts if its CRC32 matches, so a stray HCFG can't produce an answer. On the JAR it found exactly one record, inside the class file's constant pool, with a valid CRC.

Then one format guess, alone: JAR_JVM (modelled on Day 3's PE_CLI). incorrect. Claude stopped there.

What was left open (10): the seven d02 fields and the format answers for d04, d05, and d06. The d02 config fields were the blocked part: Claude's safety filter had stopped one step of the analysis on the d02 config work, and we didn't work around it. The d02 format and the other three were plain wording questions, not blocked.

The Homebase agent, 2026-10-07: d02 config

The Homebase agent's Day 2 config analyser answered the six d02 config fields in one batch at 17:13: accepted, 4 left (44 of 48). What remained was four format answers: d02, d04, d05, and d06.

The Homebase agent, 2026-10-09: the format answers

The format field was open on every ELF and JAR day. By 2026-10-09 Claude and the agent had already had ELF64, ELF, ELF_GO, elf64-x86-64, ELF_Rust, JAR_JVM, and (on Routine Check's copy of d06) JAR_Java and Java_JAR rejected. The agent kept probing, one answer per submit, each with a stated reason.

On Pool Party's attempt the Game Master rejected 26 single-answer probes between 12:51 and 15:26 UTC. The agent's log names 21 of them as d06 spellings, in roughly these families:

Idea Spellings rejected
Container alone JAR, ZIP, .jar, JAR_ARCHIVE, JAVA_ARCHIVE
Container + language JAR_JAVA, JAR/Java, JAR-Java, JAR_CLASSFILE
Language / runtime JAVA, Java, JVM, JAVA_21, JAVA_CLASS, JAVA_CLASSFILE, JAVA_BYTECODE
Tool output application/java-archive, the same with ; charset=binary, Java archive data (JAR), compiled Java class data, version 65.0
Magic bytes CAFEBABE

(The other five rejected probes on this attempt aren't itemised in the agent's log.)

The break came on Mangled: plain lowercase elf was CORRECT for its cpp_format. The agent then sent elf to Pool Party's three ELF format fields one at a time (15:32, 15:36, 15:37: accepted, 3 → 2 → 1 left), and jar for d06_analysis_format at 15:40:13: CORRECT.

The answer was the simplest thing on the list: the lowercase file-type word, for every ELF and every JAR. Flag 4174ff27…23a3.

Time Trial: CORRECT in 2.65 s (the Homebase agent)

Run by the Homebase agent with our core binary solver plus its Day 1 ELF and Day 2 config add-ons, 48/48 covered. A 7-second window.

started_at 2026-10-09T16:59:44.402Z  deadline 2026-10-09T16:59:51.402Z  (48 answer_ids)
  d06 format jar · config from the Base64 HCFG record · hcfg_version from its version byte
  start 553 ms · artifact 599 ms · solve 774 ms · submit 724 ms · TOTAL 2651 ms
result CORRECT  flag da3816f3…f583   (the agent's own log wrote …0583; the saved reply says …f583)

(From the agent's own log.) Claude later re-ran the fixed solvers offline against the saved run: the same answers as the agent's submit.

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

Attempt 1 (Homebase, 16:59:51): stopped before submitting, nothing sent

started_at 2026-10-09T16:59:51.881Z  deadline 2026-10-09T16:59:58.881Z  (2 sets × 48)
  stopped before submit: set 1's six d02_config fields, set 2's d02_analysis_format,
  six d02_config fields, d04_analysis_architecture and d04_analysis_format had no answer
result none (nothing sent; the attempt expired)

Two causes, both fixed by Claude afterwards: - Mastery set 2 swaps d02 and d04 for native PE .exe files. PE format and architecture had no confirmed value yet. - The Day 2 config analyser ignored setN. answer ids and .exe files, so it answered nothing in a Mastery. It now handles both; a shared helper maps each question to its own set's download and never answers from another set.

The PE values were confirmed on Nimble's Mastery at 17:27 (pe for format, x86-64 for architecture; both accepted in one submit), and were used in the next run (a native-PE add-on covers those fields).

Attempt 2 (our Kali VM, 17:28:40): rotation_exhausted, so practice

A fresh retry came back HTTP 409 rotation_exhausted (unique_attempts_remaining 0). The guidance says practice continues on the last artifact without a timer or Hall of Fame time and still completes the mode, so our runner used op=practice (a new practice option).

started_at 2026-10-09T17:28:40.371Z  deadline None  practice True
  retry → 409 rotation_exhausted (unique_attempts_remaining 0) → practice
  96 answers (2 sets × 48), set 2 with pe / x86-64 for the PE d02 and d04
  TOTAL 3030 ms
result CORRECT  flag 1c45d083…e43b

(Only the total was recorded for this run, not the per-phase timings.) This counts as a Mastery solve but not a ranked time.

Time to solve

The Single Solve was completed on 2026-10-09 at 15:40:13 UTC, 47 h 23 min after it started: 38 of 48 answers were accepted 29 s in, and the four format answers took two days. The Time Trial went through first time in 2.65 s; the first Mastery attempt stopped before submitting, and the Mastery was finished at 17:28:42 UTC as untimed practice, so it is unranked.

Takeaways

  • The title told us where to look. "Pool" was the constant pool, and the config was a static final String in it. In Java, every literal ends up there, so javap -v shows it without running anything.
  • Search for the encoded magic, not just the raw one. HCFG in Base64 always starts SENG, so one extra pass over Base64 runs found the same record a raw scan would have missed.
  • Validate every hit. The CRC32 check is what makes a blind scan safe to submit from.
  • Compounding challenges pay off on day one. d01–d05 were byte-identical to Rust Belt's, so 31 of the first 38 answers were already known; only d06 was new work.
  • A field with no format hint may want the plainest word. Twenty-one elaborate d06 spellings were rejected; lowercase jar was right. Try the bare, lowercase type early.
  • Mastery can change the file types under you. Set 2's PE swap stopped a solver that was 48/48 on the Time Trial. Add-ons must handle set ids and every file type a set can carry.
  • rotation_exhausted isn't the end. Practice finishes the mode, just without a ranked time.