zwesty
Write-ups / Day 2 / Warmups
Day 2 · Fri, Oct 2 · Warmups

Half a Turn

Prey slaughtered

Recognising ROT13 · not trusting output that merely looks valid · per-line mixed encodings · reusing a solver across days

STTMClaude Homebase Both

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

Challenge ID
warmups-day-02 (+ /time_trial, /mastery)
Solved
2026-10-02, all three modes, in 3 min 12 s from the first start (23:39:18 → 23:42:30 UTC)
Player
Zwesty (solo), with Claude Code (Opus 5.5)

The challenge

Thirteen steps forward. Funny how that works out.

The question, word for word Day 1's (A Little Based):

Decode the assigned blob completely. What exact answer slug does its final non-empty decoded line instruct you to submit?

Answer format: word-word-word-NN, trimmed. Mastery: 8 blobs (Day 1 had 3).

Reading the prompt: what it said we'd need

Clue What it pointed to What I did about it
"Thirteen steps forward" ROT13: shift each letter 13 places Add a ROT13 step to Day 1's decoder
"Half a turn" (the title) 13 is half of the alphabet's 26 —
"Funny how that works out" ROT13 is its own inverse: applying it twice gives back the original Never apply ROT13 twice in a row; that would undo it
The question is identical to Day 1's A compounding day: Day 1's Base64 and labels may come back, combined with ROT13 Build on Day 1's solver rather than starting over
Mastery's 8 blobs More variety, more chances for mixing Test mixed chains before any timed run

What was in the files

Every blob decoded to Day 1's familiar sentence: "Nice work decoding this warmup! To solve, submit this exact answer to the Game Master: <slug>". Only the wrapping changed between modes:

Mode Delivered as Raw format
Single Solve blob.txt, 108 B The whole sentence in ROT13
Time Trial blob.txt, 107 B The whole sentence in ROT13
Mastery One zip of blob-01..08.txt (1,891 B) Two labelled lines per blob, with a different encoding on each: 01<TAB> Base64 + 02<TAB> ROT13

The Single Solve blob, raw and decoded:

Avpr jbex qrpbqvat guvf jnezhc! Gb fbyir, fhozvg guvf rknpg nafjre gb gur Tnzr Znfgre: cyhpxl-pybpx-syvag-94

Nice work decoding this warmup! To solve, submit this exact answer to the Game Master: plucky-clock-flint-94

How to spot ROT13 by eye: the word lengths and punctuation survive, so it looks like a sentence in a strange language. Short common words map to fixed tells: gur = "the", gb = "to", guvf = "this".

A Mastery blob, the sentence split mid-word across two encodings:

01<TAB>TmljZSB3b3JrIGRlY29kaW5nIHRoaXMgd2FybXVwISBUbyBzb2x2ZSwgc3VibWl0IHRoaXMgZQ==
02<TAB>knpg nafjre gb gur Tnzr Znfgre: avzoyr-puvzarl-syvag-30
Line Encoding Decodes to
01 Base64 "Nice work decoding this warmup! To solve, submit this e"
02 ROT13 "xact answer to the Game Master: nimble-chimney-flint-30"

The trap: ROT13 output always "looks valid"

Base64 fails loudly on text that isn't Base64. ROT13 never fails: it maps any letters to other letters. Worse, ROT13 of a slug is another slug-shaped string. The raw Single Solve blob already ends in cyhpxl-pybpx-syvag-94, which fits word-word-word-NN perfectly and is wrong.

So the solver doesn't accept the first slug it sees. It only stops when the final line also reads as the instruction, containing "submit" or "answer", and holds exactly one slug.

Single Solve: CORRECT in 15 seconds

Started at 23:39:18 UTC. The blob was downloaded with its SHA-256 matching, ROT13-decoded, and submitted. CORRECT at 23:39:33, flag e90dccd2…58e0.

The solver

Our Day 2 solver reuses Day 1's decoder (Base64, hex, Base32, Base85, line labels), loading it by path, and alternates it with a ROT13 step until the final line is a real instruction. It never applies ROT13 twice in a row, and it stops rather than guess.

8 tests, all passing before any timed run: - the real blob; - the ROT13-slug trap; - a Day 1-style blob; - Base64 of ROT13, and ROT13 of Base64; - labels plus Base64 of ROT13 of Base64; - garbage, which it refuses; - Mastery as a zip and as separate files.

One snag along the way: both days' solver files had the same module name, so a plain import found itself and recursed. Loading Day 1's file by its path, under a unique name, fixed it.

Time Trial: 1.56 s of a 5-second clock

Day 2's clocks are tighter: the server's deadline was 5 s, where every Day 1 deadline was 30 s.

started_at 2026-10-02T23:41:24.193Z  deadline 2026-10-02T23:41:29.193Z
  artifact blob.txt 107 bytes sha256 f8018b4f1aea09d1… (checked)
  message: jazzy-garden-dune-29
  start         653 ms
  artifact      348 ms
  solve           0 ms
  submit        560 ms
  TOTAL        1562 ms
result CORRECT  flag c2d67597…a335

Before Mastery, our shared runner got parallel downloads. If 8 blobs arrived as separate files, fetching them one by one through the Homebase relay would take about 3 s, too close to a 5 s clock. Measured live: 8 downloads in 2.0 s in parallel, against about 10.9 s one after another.

Mastery: 1.48 s of 5 s, first attempt

started_at 2026-10-02T23:42:28.802Z  deadline 2026-10-02T23:42:33.802Z
  artifact warmups-day-02-mastery.zip 1891 bytes sha256 91c67c329d4ade03… (checked)
  set1.message: nimble-chimney-flint-30
  …
  set8.message: lucky-sparrow-trail-88
  start         597 ms
  artifact      319 ms
  solve           1 ms
  submit        564 ms
  TOTAL        1481 ms
result CORRECT  flag 30410789…303e

⚠️ An honest note: the answers were right, but partly by luck

The Mastery blobs mixed encodings per line, which the solver wasn't built for. Day 1's decoder couldn't handle the mixed text, so the solver ROT13'd the whole blob. That garbled the Base64 line, but turned the ROT13 line into plain English holding "answer" and the slug. It worked only because the slug sat in the final, ROT13-encoded line. If the slug had been in the Base64 line, the solver would have stopped safely, but the attempt would have expired.

The fix before Day 3: decode each labelled line with its own encoding, join the chunks in label order, and only then look for the instruction.

Update, 2026-10-07: the fix is done, and it mattered. Day 3's Mastery (Hex Appeal) had three lines per blob (Base64, ROT13, hex) with the slug in the hex line every time. The fixed solver passed it first try. See the Hex Appeal write-up.

Time to solve

All three modes were solved on 2026-10-02 by 23:42:30 UTC, 3 min 12 s after the first start. The Single Solve was CORRECT in 15 s, and the Time Trial (1.56 s of 5 s) and the Mastery (1.48 s of 5 s) each went through on the first submit, with the solver extended and the downloads made parallel in between.

Takeaways

  • A decode that can't fail can't be trusted on its own. ROT13 always produces letters. Check that the output means something before accepting it.
  • A pattern match isn't proof. ROT13 of a slug is still slug-shaped. Pair the pattern with a context cue ("submit", "answer").
  • Reuse earlier days' solvers. Compounding days repeat earlier encodings. Building on Day 1 made this a 3-minute challenge.
  • Check how much time the clock actually gives. Day 2 dropped from 30 s to 5 s. Making the downloads parallel before Mastery kept the run well inside the limit.
  • Look at what got the answer right, not just whether it was right. Inspecting the accepted Mastery blobs showed the per-line mixing, a weakness that would have failed in a slightly different layout.