zwesty
Write-ups / Day 30 / Metadata
Day 30 · Fri, Oct 30 · Metadata

Seeing Double

Prey slaughtered

Hashing real archive bytes rather than trusting a manifest; grouping identical files; tolerating archives with bad CRCs and mismatched header names; the stored-rule overlay our Metadata solver applies

STTMClaude Homebase Both

Flags are shortened to their first 8 and last 4 characters because they're unique per team.

Challenge ID
metadata-forensics-day-30-seeing-double (+ /time_trial, /mastery)
Solved
2026-10-30: all three modes CORRECT by 09:13:28 ET, about 13 min 21 s after the solver's Single Solve start (09:00:07.3 ET). Our automated morning solver (a scheduled script on our Kali VM) stopped on the Single (new field, nothing sent); Claude built the field and sent the Single (CORRECT 09:04:57, first form tried). The ranked Time Trial was CORRECT on its first run (09:06:52.1). The ranked Mastery took four fixes and 28 submit requests inside its one 7 min 25 s clock, then came back CORRECT at 09:13:28, with about 57 s to spare
Player
Zwesty (solo), with Claude Code (Opus 5.5): the automated solver started the Single and stopped; Claude wrote the field, solved the Single, and ran both timed modes

The challenge

"The corpus is too large for manual browsing. Build a relationship map from exact identity, semantic identity, containment, and claimed provenance—then expose the fabricated cluster."

answer_id Question Format
d30-cluster-001-relation-identical-sha256-groups "Determine relation.identical_sha256_groups for cluster-001.zip." (artifact metadata-day-30-evidence.zip -> cluster-001.zip) json: "compact valid JSON array with exact values and order; no Markdown or explanatory text"; case-insensitive

As on every Metadata day, the timed modes asked today's field together with earlier days' fields. The Time Trial had 35 questions, one per field: Days 4–30 plus the evidence-note, roster, and synthesis fields. The Mastery had 14 sets of 35, 490 questions (42 distinct fields, since the roster and evidence-note fields vary by set).

Reading the prompt: what it said we'd need

Clue What it pointed to What I did about it
"exact identity" Byte-identical files: the same SHA-256 The only Day 30 field asked, relation.identical_sha256_groups
"semantic identity, containment, claimed provenance" Same content in other forms, archives inside archives, what a manifest says No question asked for them in any mode. Containment mattered anyway: the field names a zip inside the delivered zip
"claimed provenance" The delivery manifest's own claims (its sha256 and duplicate_ref entries) Not trusted for hashes. Every member's real bytes were hashed; the manifest was used only for ids and order
"expose the fabricated cluster" Tampered archives The Mastery clusters had members with bad CRC-32s and a nested zip whose local and central names differ. Both broke the old inventory (fixes 1 and 2)
"Seeing Double" (title) Duplicates Groups of identical files
"compact valid JSON array with exact values and order" (format) Values and order matter Members listed by manifest id in manifest order; groups ordered by their first member. Two other value forms were ready as fallbacks

Safety

The clusters hold PowerShell scripts (.ps1) and nested zips. Everything was read as bytes on our Kali VM and hashed; nothing was run or opened with its own application.

What was in the files

  • Single Solve: metadata-day-30-evidence.zip, 2,040 bytes, holding cluster-001.zip (stored). That holds delivery-manifest.json (schema huntress.artifact_delivery_manifest.v1: id, filename, path, content_type, family, producer_id, size_bytes, sha256) and 30 files under artifacts/: alpha-01..06.txt, bravo-01..06.ps1, charlie-01..06.json, delta-01..06.png, and echo-01..06.zip. Each family's six files are byte-identical (checked with sha256sum on the real bytes). The manifest marks 01 as text_template and 02–06 as duplicate_ref. Answer: five groups of six manifest ids, [["alpha-01",…,"alpha-06"],["bravo-01",…],…,["echo-01",…]].
  • Time Trial: evidence.zip, 684,038 bytes, 35 questions; its cluster wasn't broken down in our records.
  • Mastery: one zip of 9,355,124 bytes with 14 sets, each in its own evidence-NN.zip, each with its own cluster-001.zip. The clusters used other names: set 1's answer was two groups, of 3 and 6 members (copper-21, indigo-96, willow-57; glacier-72, …, tundra-96). The fabricated parts were here: members whose stored CRC-32 doesn't match their bytes, and a nested zip (raven-77) whose local header says pebcle.txt where the central directory says pebble.txt.

So the timed modes were the Single's template with different names and sizes, plus the tampering, which the Single never showed. And because every set's cluster is named cluster-001.zip, the name alone doesn't pick a set.

Single Solve

The automated solver: stopped, nothing sent

The solver started the Single at 09:00:07.3 ET. Its preparation step sorted the question as a new field. The parser chain couldn't resolve the file ("can't tell which file it's about"), so its best-effort answer was []. The solver had no stored candidates for the slug, so it sent nothing. That stop was correct.

The field

Today's solver builds on the Day 28 Metadata chain and adds the first relation field: - Find the named cluster zip at any depth inside the delivery. - Hash the real bytes of every member except delivery-manifest.json. - Group identical SHA-256s and keep groups of two or more. - List members by manifest id (the file stem when the manifest has no entry), in manifest order, and order the groups by their first member. Output compact JSON.

Its tests: 6/6 OK (the real Single, singletons dropped and mixed groups, real bytes beating a wrong manifest hash, no manifest so stems are used, a nested lookup, and the ranked answer forms).

Three answer forms were ranked: 1. manifest ids, 2. artifacts/… paths, 3. bare filenames. The answer was sent through the solver's Metadata wrapper, and the first form was accepted, so nothing was burned.

started by the solver 09:00:07.3 ET
deadline    none (Single Solve)
answers     1   [["alpha-01",…],["bravo-01",…],["charlie-01",…],["delta-01",…],["echo-01",…]]
result      CORRECT   (09:04:57, Game Master log reply; first form)

The Single's flag and per-step timings weren't recorded.

Before the timed runs: a regression rehearsal against earlier days' puzzles passed (210 saved attempts: 209 identical, 1 newly solved, 0 changed). Following the Day 28 lesson, the Metadata wrapper was pointed at today's solver before the timed runs, at 09:06:50 ET.

Time Trial

CORRECT on the first attempt, ranked (fresh variant).

started_at  09:06:51.3 ET   deadline 09:07:41.3 (50 s clock)
answers     35
start        113 ms after started_at
artifact     331 ms   (684,038 bytes)
solve        213 ms
submit       935 ms   (submit +1,592 ms after started_at)
result      CORRECT   flag 5029fd74…3eeb   (09:06:52.1 ET)

Mastery

Ranked, one attempt, CORRECT inside its clock after four fixes. No retry was spent.

ranked
started_at  09:07:00.8 ET   deadline 09:14:25.8 (7 min 25 s clock)
answers     490 (14 sets × 35)

The four fixes

  1. Bad CRC-32 members made Day 1's inventory raise BadZipFile, so the whole set failed. Such members are now read as stored and listed as CRC mismatches; the hashes are still of the real bytes.
  2. A nested zip with mismatched local and central names (raven-77) hit Day 1's "needs a human" stop. A zip nested inside the delivery is now listed but not expanded (and recorded as a name mismatch); a top-level one still stops. The patch had to reach the chain's own copy of the Day 1 code, which isn't in sys.modules.
  3. registry.hive_root_name fell back to none on every set ("don't know what this question asks"), though the Time Trial had answered it. It is now answered directly: the member the label names, inside the zip the label names, through the hive-root-key-name parser.
  4. Every set read set 1's cluster. The cluster lookup matched cluster-001.zip by name anywhere in the delivery, and every set has one. It now opens the named container (evidence-NN.zip) first.

The submits (from the Game Master log)

The log shows six start requests on the one attempt (09:07:00, :18, :38, 09:08:23, 09:09:58, 09:11:12): each run re-fetches the attempt. Run 1 crashed and sent nothing (fix 1), and run 2 stopped before submitting (fix 2).

Time (ET) Submit Result
09:10:03 Run 3: all 490 (14 hive answers best-effort none) incorrect
09:11:18 Run 4, after fix 3: all 490 incorrect; the reply said wrong_answers: 2 and "243 more wrong answers are free on this timed attempt"
09:11:51 → 09:11:57 Five fields on their own, 14 answers each: zip EOCD total entries, zip central-directory names, hive root, synthesis SHA-256 order, then the relation groups the first four accepted; relation groups incorrect
09:11:58 The remaining 420 answers accepted
09:12:10 → 09:12:28 Relation groups one set at a time (14 requests, all the same value) set 1 accepted; sets 2–14 each incorrect
09:12:45 → 09:12:51 Five other forms on set 2 (artifacts/ paths, other member and group orders) all incorrect
09:13:26 → 09:13:28 After fix 4: all 490 CORRECT

That identical value on all 14 sets was the clue to fix 4.

result      CORRECT   flag b9a1f9cb…d3d0   (09:13:28 ET, Game Master log reply; about 57 s before the deadline)
submits     28 requests on the attempt: 3 full sends, 6 field groups, 14 single sets, 5 form tries

Our working notes at the time undercounted this as 13 submit requests with only sets 2–4 wrong on the relation field; the counts above are from the Game Master log. The free wrong-answer allowance (243 left after run 4) covered all of it, and there was no HTTP 429.

Day 31's solver later built on this one and added one more zip-header tolerance fix.

Time to solve

All three modes were CORRECT by 09:13:28 ET, 13 min 28 s after release: the Single at 09:04:57, about 4 min 50 s after the solver's start, and the ranked Time Trial at 09:06:52.1 on its first run. The ranked Mastery took 6 min 27 s from its start, four fixes, and 28 submit requests, and finished inside its clock with about 57 s to spare, so it stayed ranked.

Takeaways

  • Hash the bytes, not the manifest. The prompt warned about "claimed provenance". The solver never read the manifest's sha256, and the tests include a manifest that lies.
  • Look up by container, not just by name. Every set had a cluster-001.zip. Matching it by name anywhere sent set 1's answer for all 14. Open the named container (evidence-NN.zip) first.
  • Splitting a wrong submit finds the bad field fast. With free wrong answers left, sending field groups on their own isolated the relation field in about a minute. Identical answers on every set then pointed at the lookup.
  • Tampered archives are the "fabricated" part. Bad CRCs and mismatched header names stop a strict inventory. The fix lists them and reads them as stored, without trusting the names.
  • A patch must reach the copy that runs. The chain loads its own copy of the Day 1 code outside sys.modules, so patching the obvious module did nothing.
  • Pointing the wrapper at the new day first worked. The Time Trial ran with the Metadata wrapper already on Day 30 and passed first time.