zwesty
Write-ups / Day 4 / Binary
Day 4 · Sun, Oct 4 · Binary

Go Fish

Prey slaughtered

Triage of a stripped Go binary (.gopclntab function names, .go.buildinfo) · finding and checking an embedded XOR'd config record · carrying answers across compounding days · working out a checker's vocabulary · ELF vs native PE in Mastery

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-04-go-runtime-fingerprints (+ /time_trial, /mastery)
Solved
2026-10-07 to 2026-10-09, all three modes. Single Solve started 2026-10-07 16:06:04 → Mastery CORRECT 2026-10-09 17:28:59 UTC (49 h 23 min, almost all of it waiting on one wording question)
Player
Zwesty (solo), with Claude Code (Opus 5.5, analysis, Single Solve, and the solver) and the Homebase agent (GPT-6 Luna: the d02 config fields, the format search, and the Time Trial)

The challenge

Go binaries are chatty: they ship with build info, function names, and file paths baked right in. Fish them out and map what this one was built to do.

A compounding challenge. One AES-encrypted evidence.zip (password infected) holds Days 1–4: the Day 1 C ELF, the Day 2 C++ ELF, the Day 3 .NET DLL with its runtimeconfig.json, and the new Day 4 Go ELF. 32 questions, 8 per day. Every question uses exact normalisation, and exact-text answers carry the usual line: "use the literal none only when the requested field is absent."

The 24 questions for Days 1–3 are the same IDs as Table Manners (Day 3). The 8 new ones:

answer_id Question Format
d04_artifact_sha256_1 The Go binary's SHA-256 Exact text
d04_analysis_architecture "exact analysis architecture value" Exact text
d04_analysis_format "exact analysis format value" Exact text
d04_analysis_config_campaign Config campaign Exact text
d04_analysis_config_domain Config domain Exact text
d04_analysis_config_mutex Config mutex Exact text
d04_analysis_config_url Config URL Exact text
d04_analysis_config_crc32_hex Config CRC32 Lowercase hex with 0x

The questions are generated from the field names ("What is the exact analysis format value for field d04_analysis_format…?"), so they give no hint about spelling. That became the whole story of this challenge.

Reading the prompt: what it said we'd need

Clue What it pointed to What I did about it
"Go binaries are chatty" Go keeps a lot of metadata even when stripped Expect .symtab to be gone and look in the Go-specific sections instead
"build info" The .go.buildinfo section: Go version, module path, build settings Read it as plain strings: go1.25.1, GOOS=linux, GOARCH=amd64, and the rest
"function names" The Go runtime's .gopclntab table, which survives stripping Wrote a small pclntab reader to list every function and its address (2,200 of them)
"file paths baked right in" Source paths in the pclntab file table Found _/work/variant-000/main.go: the program is one main package
"map what this one was built to do" The main.* functions are the program; the rest is runtime main.campaign and main.main; disassembled main.campaign to see what it reads
Field names config_campaign / domain / mutex / url / crc32_hex An embedded config, like Table Manners' .NET one main.campaign decodes a record; matched its layout to Day 3's and built a checked parser
architecture and format, no vocabulary given The checker's own spellings Test each one alone, after the certain answers are in

Safety

Static analysis only. No binary was ever executed. The evidence was downloaded and unpacked on our Kali VM (and, for the Homebase agent's runs, on the Homebase), never on the Windows host. The Go binary was read as bytes: section tables, strings, the pclntab, and a disassembly of one function. The config was decoded from the file's own bytes, not by running the code.

What was in the files

Single Solve delivery: evidence.zip, 852,723 bytes, SHA-256 7c99f149…9fdf (it matched the start reply), AES-encrypted, zip comment huntress:day04-single.

  • Days 1–3: byte-identical to Table Manners' files (SHA-256 checked: d01 a0d3aedd…7b9b, d02 e9ca3054…d0d7, d03 a98b993f…073d and d6109426…6cf3). So Table Manners' accepted answers carried straight over.
  • Day 4: d04--variant-000.elf, 1,867,924 bytes, SHA-256 d5aa218b…182f.

The Go binary, from its headers and sections: - ELF64, x86-64, ET_EXEC, statically linked, stripped of .symtab, but with .gopclntab and .go.buildinfo intact. - Build info (plain strings): go1.25.1, path _/work/variant-000, -buildmode=exe, -trimpath=true, CGO_ENABLED=0, GOARCH=amd64, GOOS=linux, GOAMD64=v1. - Functions from .gopclntab: main.campaign at 0x4c7380 and main.main at 0x4c74c0. main.evaluate was inlined, so its name appears only in the inline tree. - Other telling strings: HUNTRESS_HOST_MODEL, huntress.binary.dynamic_trace.v1, file_and_directory_discovery.

What main.campaign does (from its disassembly): it reads a 379-byte global byte slice, takes a one-byte XOR key and a length from the slice's header, un-XORs the rest, and JSON-decodes it. The same layout as Table Manners' .NET config.

The config record, at a high level. It's an HCFG record: the magic HCFG, a version byte (1), a one-byte XOR key (0x51 here), a 2-byte length (367), the CRC32 of the plaintext JSON (0x2b747600), then the XOR'd JSON. The CRC is what the config_crc32_hex question asks for, and it doubles as a check that the decode is right. The decoded config (defanged):

campaign   BR04-000
domain     node-000.day04.example[.]invalid
ipv4       198.51.100.80
url        https://node-000.day04.example[.]invalid/api/BR04-000
mutex      Global\HuntressBR04_000
service    HuntressLab-Day04-000
host_model workstation
(plus a windows_path and a linux_path)

The timed variants used the same template. The Time Trial delivered one evidence set of the same shape (32 answers), and the core solver answered it with the same rules. Mastery had two sets: set 1 was the same shape, but set 2 swapped two files for native Windows PE .exe files: d02 became a MinGW C++ PE, and d04 became a Go PE (still GOARCH=amd64, machine 0x8664). (The per-variant config values from the timed runs aren't quoted here.)

Single Solve: CORRECT (on 2026-10-09, after a two-day wording hunt)

Started 2026-10-07 16:06:04 UTC, no deadline. The certain answers went in first, then each uncertain one alone:

# What was sent Result
1 Table Manners' 17 accepted answers + the d04 SHA-256 (18) accepted, 14 left
2 campaign, domain, mutex, url, CRC32 0x2b747600 accepted, 9 left
3 architecture = amd64 (the build info's GOARCH) incorrect
4 format = ELF_GO (following d03's PE_CLI pattern) incorrect
5 architecture = x86-64 (Day 1's accepted machine spelling) accepted, 8 left
6 format = elf64-x86-64 (the BFD name) incorrect

Lesson from #3 and #5: "architecture" wants the ELF machine spelling (x86-64), not Go's own GOARCH (amd64), even in a challenge about Go build info.

That left 24 of 32. The other 8: d04_analysis_format, and the seven Day 2 fields that were still open in Table Manners (six config fields plus d02_analysis_format).

The Day 2 config fields came from the Homebase agent. Claude's safety filter had stopped one step of the analysis on the Day 2 config work, and we didn't work around it, so Claude didn't attempt them here. On 2026-10-07 at 17:13 UTC the Homebase agent's config analyser sent all six as one batch: accepted, 30 of 32.

The format saga

Two fields left, both format: d02_analysis_format and d04_analysis_format. The same open question sat in every ELF and JAR challenge from Day 2 to Day 8. The only accepted precedent was Day 3's PE_CLI, whose question spelled out "a container_runtime token". That precedent pointed every early guess in the wrong direction.

When (UTC) Who d04_analysis_format sent Result
10-07 16:07 Claude (our Kali VM) ELF_GO, elf64-x86-64 incorrect ×2
10-08 20:49 Our Kali VM none incorrect
10-08 Homebase agent (in Routine Check's copy of the same d04 field) ELF64_Go incorrect
10-09 13:25–15:26 Homebase agent ELF_Go, ELF/Go, go, ELF64_GO, ELF64-Go, Go, ELF64_GOLANG, GO_BUILDINFO, ELF64_STATIC, dynamic_trace_v1, dynamic_trace, GO_ELF incorrect ×12 (plus one HTTP 503 temporarily_unavailable at 13:32, retried)

The break came from Mangled. In the same persistent run, the Homebase agent tried lowercase elf for Mangled's cpp_format and it completed that challenge. In its own words, elf is "the lowercase ELF family token used in objdump -f output"; the uppercase ELF had been rejected there earlier. It then applied the same token everywhere:

When (UTC) Field Sent Result
2026-10-09 15:30:59 d02_analysis_format elf accepted, 1 left
2026-10-09 15:34:02 d04_analysis_format elf ✅ CORRECT

Single Solve CORRECT (Homebase agent). Flag 44df698c…1e01. 15 spellings were rejected for the Go format before the plainest one won. The confirmed rule: lowercase elf for every ELF, jar for every JAR, and Day 3's .NET stays PE_CLI.

The solver

The timed modes were run by our core binary solver (by Claude) plus the Homebase agent's add-ons. In plain terms: - An archive reader opens the AES evidence zip (and nested archives) in memory. - The core solver maps each dNN--variant-N.* member to its day, hashes every file for the sha256_K fields, reads headers for architecture (x86-64), and returns elf / jar for format. - An HCFG reader finds an HCFG record anywhere in any file and accepts it only when its CRC32 matches, so a stray "HCFG" can't produce an answer. It answers every day's config fields (it also reproduces Day 3's). - A pclntab reader lists a Go ELF's functions from .gopclntab (analysis aid; the answers don't need it). - A set matcher pairs Mastery's sets with their own downloads; the Homebase agent's Day 2 config analyser answers the Day 2 config fields; a native-PE add-on (Claude, 2026-10-09) answers the native-PE format and architecture once confirmed.

Time Trial: CORRECT in 2.1 s

Run by the Homebase agent, with our solvers as they stood after the format fix. One submit, 32 answers, first try. Claude later re-ran the fixed solvers offline against the saved delivery: same answers.

mode time_trial  (32 answer_ids)
started_at 2026-10-09T16:58:19.592Z  deadline 2026-10-09T16:58:26.592Z  (7 s window)
  start 508 ms · artifact 574 ms · solve 260 ms · submit 770 ms · TOTAL 2112 ms
result CORRECT  flag a49531d0…a690

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

Attempt 1 (Homebase agent, 16:58:28 UTC): stopped before submitting, nothing sent. Two sets of the 32 questions; the solver stopped on these gaps: - Set 1's six Day 2 config fields: the Day 2 config analyser ignored ids with a setN. prefix. - Set 2's d02 and d04 were native PE files (.exe). set2.d02_analysis_format, set2.d04_analysis_format, and set2.d04_analysis_architecture had no confirmed PE value, and the analyser skipped .exe for the six d02 config fields.

Fixes (Claude, same afternoon): the config analyser handles set ids and .exe; the new native-PE add-on holds the PE answers but stays off until confirmed. The candidates were pe (Day 3's question calls PE "the generic container label") and x86-64 (the accepted ELF spelling, ahead of amd64).

The PE values were confirmed on another challenge. At 17:27 UTC Nimble's Mastery (Day 9, whose sets 2 and 4 are PE sets) came back CORRECT with format = pe and architecture = x86-64. So the Go PE's architecture is x86-64 too, not its GOARCH=amd64, the same rule as the Go ELF.

Attempt 2 (our Kali VM): the retry was refused because the variants had run out, so the run fell back to practice mode: no clock and no Hall of Fame entry.

retry      2026-10-09T17:28:57Z  HTTP 409 rotation_exhausted (unique_attempts_remaining 0)
practice   mode mastery  started_at 2026-10-09T17:28:57.999Z  (no deadline, practice)
  64 answers (set 1: ELF set; set 2: d02 + d04 as native PE, format pe, d04 architecture x86-64)
  TOTAL 2613 ms
result CORRECT  flag daa37974…0ac4

(The run log for this practice run recorded only the total, not the per-phase timings.)

Time to solve

The Single Solve was completed on 2026-10-09 at 15:34:02 UTC, 47 h 28 min after it started: 24 of 32 answers were in within 2 minutes, the Day 2 config fields came from the Homebase agent about an hour later, and the two format fields took two days. The Time Trial went through first time in 2.1 s; the first Mastery attempt stopped before submitting, and the Mastery was finished at 17:28:59 UTC as untimed practice, so it is unranked.

Takeaways

  • A stripped Go binary still talks. .gopclntab gives every function name and address, and .go.buildinfo gives the toolchain, module path, and target. main.* is the program; the rest is runtime.
  • Find the config by reading the one function that uses it. main.campaign showed the record's layout, which matched Day 3's. One parser then covered both days.
  • Make a decoder prove itself. Accepting a config only when its CRC32 matches means a wrong decode can't produce an answer, and the CRC is itself one of the answers.
  • "architecture" follows the checker, not the language. x86-64 won over Go's amd64, for the ELF and for the Go PE alike.
  • Try the plainest spelling early. Fifteen compound spellings failed before lowercase elf. One precedent (PE_CLI, from a question that spelled out its own format) steered every guess wrong for two days.
  • Compounding pays forward. Byte-identical files (SHA-256 checked) let 17 earlier answers carry over in one submit. One confirmed rule (elf on Mangled) closed the same field in seven challenges.
  • Mastery can change the container. Set 2 swapped ELFs for PE files, so a solver has to branch on the file's bytes, not on the day or the extension.
  • An expired Mastery isn't lost. After rotation_exhausted, practice mode still scores the challenge (no clock, no Hall of Fame).