zwesty
Write-ups / Day 23 / Script
Day 23 · Fri, Oct 23 · Script

Press Start to Decode

Prey slaughtered

Script triage across 20+ languages, layered encodings (LYR- header), behaviour graphs, ATT&CK mapping, schema adaptation

STTMClaude Homebase Both

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

Challenge ID
script-analysis-day-23-game-resource-loader (+ /time_trial, /mastery)
Solved
2026-10-23: Single Solve CORRECT (09:01:21 ET). The ranked Time Trial crashed in the solver and expired with nothing sent. One untimed retry was CORRECT at 09:13:19; the ranked Mastery was CORRECT at 09:13:51 on its first attempt (after a resume inside that attempt). 12 min 33 s from the first start request (09:01:18 ET) to the Mastery CORRECT
Player
Zwesty (solo), with Claude Code (Opus 5.5): our automated morning solver (a scheduled script on our Kali VM) did the Single Solve and hit the crash on the ranked Time Trial; a Claude helper agent found the new schema, built an adapter for it, ran the checks, and sent the retry and the Mastery

The challenge

"A GDScript resource loader builds its strings in layers, one power-up at a time. Undo the construction, recover the exact indicators, and correlate its stage relationships with the rest of the corpus to find the level that doesn't belong."

Script is cumulative: the Single Solve asked 1,461 questions over one evidence.zip of 157 scripts. Today's new files were four GDScript polyglots (polyglot-gdscript-*.gd), with 44 questions such as:

answer_id Question Format
polyglot-gdscript-downloader-role "Classify the inert source role." enum (benign_utility, downloader, dropper, …)
polyglot-gdscript-downloader-attack Map the decoded behaviour graph to one primary ATT&CK technique (with a stated precedence) ATT&CK ID
polyglot-gdscript-downloader-url "Recover the exact url indicator." exact text
polyglot-gdscript-downloader-steps "Recover the ordered behavior graph using > separators." tokens joined by >
polyglot-gdscript-downloader-obfuscation "Recover the ordered encoding profile." exact text

The timed modes used a different question schema (below): 1,495 questions in the Time Trial, 10,465 in the Mastery (7 sets).

Reading the prompt: what it said we'd need

Clue What it pointed to What I did about it
"A GDScript resource loader" New language: Godot's GDScript, as a polyglot Our existing polyglot readers already covered it; the Single Solve passed
"builds its strings in layers, one power-up at a time" Stacked encodings In the timed modes, the LYR- header names the layers in the order applied
"Undo the construction" Decode those layers last-first A new LYR decoder in our header parsers
"recover the exact indicators" URLs, paths, hosts, file names New indicator, url-host, and path-leaf fields in the timed schema
"correlate its stage relationships with the rest of the corpus" Cross-file links between stages The Python handoff-id comes from the .ps1 precursor whose $NextStage names the file
"the level that doesn't belong" An outlier in the corpus No question asked for it by that name; the cross-file answers covered the relationships asked

Safety

Static analysis only. Nothing was executed. Every script was read as text and decoded in Python; no interpreter, shell, or host for any of the languages was started. The archives stayed on our Kali VM. Indicators below are defanged.

What was in the files

  • Single Solve: evidence.zip, 139,343 bytes, 157 scripts with descriptive names (polyglot-gdscript-downloader.gd, batch-stager.cmd, …) and question IDs built from those names.
  • Time Trial: evidence.zip, 154,625 bytes (ranked) and 156,576 bytes (retry), also 157 files. Fresh content (no SHA-256 matches with the Single) and a new schema:
  • every file renamed to a 6-hex stem (for example 34c1d9.cmd), and question IDs built from it (batch-34c1d9-role);
  • a new field set: for role scripts role, attack, campaign, campaign-token, indicator, steps, plus per-language style (batch, VBScript, JScript: three labels joined by |) and Python carrier and obfuscation pairs; for the 80 polyglot and host files role, attack, steps, stage-count, stage-profile, obfuscation, next-language, campaign, url, file-path, url-host, path-leaf, campaign-token; Bash advanced feature, primitive, campaign, campaign-token; PowerShell url, secondary, tertiary;
  • roles re-drawn (for example four batch uploaders and no batch stager), so it is not a renamed Single;
  • the polyglot and host files carry a new header, HUNTRESS_ANALYSIS_V1:LYR-<layer>_<layer>_…-<payload>, with layers from b64, b32, hex, rev, and frag listed in the order applied. All 80 decode last-first to a v=3|… plan.
  • Mastery: 1,088,904 bytes, 7 sets in the timed schema. A defanged PowerShell example from set 1: URL hxxps://e0776da97f[.]example[.]invalid/6805a0c5cd20[.]dat, secondary update-e0776da97f.dat, tertiary Mozilla/5.0 HuntressLab/4.9.

So the timed variants were the same generator as the Single Solve (157 files each) with a different naming scheme, field set, and header.

Single Solve

The solver got it in two batches. The core sent 1,333 answers (accepted, 128 remaining), then the add-on modules sent 128 (CORRECT). The core noted that some values had been rejected on other questions before; it sent them anyway, as designed, and they were accepted here.

started_at  2026-10-23T13:01:18.581Z
deadline    none (Single Solve)
answers     1,333 core (accepted, remaining 128), then 128 add-on answers
start        370 ms
artifact     304 ms
solve        335 ms
submit      2503 ms   (core 372, add-ons 2129)
TOTAL       3515 ms
result      CORRECT   flag 2f83cf81…c84c   (server stamp 13:01:20.910Z)

Time Trial

Ranked attempt: solver crash, nothing sent

started_at  2026-10-23T13:01:27.781Z
deadline    2026-10-23T13:02:17.781Z   (50 s)
answers     none
result      solver crash: KeyError '34c1d9' in the Day 4 solver's step table (a hex stem it couldn't look up).
            The solver stopped this lane (it doesn't retry); the attempt expired at the deadline.

Why it crashed. Our solver's stem tables (from Day 4, Day 7, …) are keyed on the descriptive file names, and a hex stem can't be looked up. Matching each TT file to its "Single twin" doesn't work either, because the roles were re-drawn. Each file has to be answered from its own decoded plan.

The fix (helper)

The helper built and tested the fix separately, then put it into the solver at 09:14 ET: - A new adapter answers the hex-stem schema file by file. The plans come from our existing per-language parsers (batch, VBScript, Python, Bash, Node, JScript, and the header decoders). Role uses the Time Trial rubric from the steps, cross-checked against the solver's confirmed step tables; ATT&CK comes from the same tables (polyglots: the plan's own values). The field rules: - campaign-token = the campaign after its first hyphen; steps = the plan's steps joined by >. - indicator = the first present of url, upload_url, next_stage, file_path, schedule_path, note_name, host_id. - style: batch graph|control|expansion, VBScript representation|transform|control, JScript function|invocation|property. - Python carrier = carrier|windows_chain, obfuscation = syntax|obfuscation, handoff-id = the $HandoffId of the .ps1 precursor whose $NextStage names the file (same inner zip). - Polyglot url-host = the URL's host, path-leaf = its last path segment; obfuscation = the LYR layer list (it equals the plan's obfuscation_profile on all 80). - PowerShell secondary and tertiary = wallet and mutex, file and user agent, or note and extension, by the confirmed scenario. - Polyglot role and steps disagree inside the plans (the Single's too). The Single accepted the plan's own role and ATT&CK, and the TT question says "role recorded in the decoded analysis descriptor", so the plan values are used. - Routing: a script question set goes to the adapter only when about 90% of the IDs are hex stems, so older sets are unchanged. - Header parsers: gained the LYR decoder; the classic headers are unchanged. - New tests: LYR round trips, ID parsing (old IDs never match), the role rubric.

Checks before sending: - Unit tests: 25 passed. - Every derived field checked against the Day 22 and Day 23 Single answers: 1,035 and 999 fields, 0 mismatches. - Coverage: Single 1,461 of 1,461, Time Trial 1,495 of 1,495, Mastery 10,465 of 10,465. - A regression check against every answer the Game Master had already accepted for Script on Days 20–23 (Single, Time Trial, and Mastery): 0 mismatches.

Untimed retry: CORRECT

We allowed ourselves one untimed retry of a failed first timed run, so the helper used retry in untimed (unranked) mode. All 1,495 answers went in one submit.

started_at  2026-10-23T13:13:17.049Z   (retry, untimed)
deadline    none
answers     1,495
retry        262 ms after started_at
artifact     319 ms
solve         60 ms
submit      1600 ms
TOTAL       1980 ms from the retry reply   (submit +2.24 s after started_at)
result      CORRECT   flag f8888802…b5c2

Mastery

CORRECT on the first attempt, ranked (230 s clock), but not on the first pass. The first pass stopped in the solver before any submit: the handoff-id lookup pooled the .ps1 files from all 7 sets, so a stem could match a precursor from the wrong set. The helper restricted it to the same inner zip, and the same attempt was resumed (the start request returned the same attempt), so no retry was used and nothing wrong was sent.

started_at  2026-10-23T13:13:29.631Z
deadline    2026-10-23T13:17:19.631Z   (230 s)
answers     10,465 (7 sets)
first pass  stopped in the solver before submitting; fixed; resumed at 13:13:47
resume      +19.40 s after started_at
artifact     581 ms
solve        426 ms
submit      1764 ms
TOTAL       2784 ms for the resumed pass   (submit +22.18 s after started_at)
result      CORRECT   flag 8212188a…3995   (server stamp 13:13:50.529Z)

The ranked time includes the fix. About 19 s of the 22.2 s was the first pass, the fix, and the resume.

Time to solve

All three modes were solved by 09:13:51 ET, 13 min 51 s after release (12 min 33 s from the first start request). The Single Solve and the ranked Mastery went through on the first attempt; the ranked Time Trial crashed and expired, and the Time Trial was then solved on an untimed (unranked) retry at 09:13:19 ET after about 8 minutes of fixing.

Takeaways

  • Timed sets can change format from the Single Solve. Same generator, but hex names, a new field set, and a LYR- header. A Single CORRECT didn't prove the timed shape.
  • Answer from each file's own plan, not from a name table. Tables keyed on file names break the moment names are randomised; the decoded plan is the same either way.
  • Route on the question shape, narrowly. Sending a set to the adapter only when ~90% of IDs are hex stems kept every older day's answers unchanged (0 mismatches over Days 20–23).
  • Scope cross-file lookups to the set. Pooling .ps1 files across 7 Mastery sets was a bug only Mastery could show.
  • A clean stop inside a ranked attempt can still be saved. The solver stopped before submitting, so the same attempt could resume; the cost was ~19 s on the clock, not the ranking.