zwesty
Write-ups / Day 29 / Android
Day 29 · Thu, Oct 29 · Android

White Box Key Recovery

Prey slaughtered

APK triage (puzzle asset, DEX strings), recovering an additive key schedule from a known plaintext prefix, the v2 static interpreter for the timed modes

STTMClaude Homebase Both

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

Challenge ID
mobile-android-day-29-white-box-key-recovery (+ /time_trial, /mastery)
Solved
2026-10-29: all three modes CORRECT by 09:07:37.7 ET, 3 min 1.9 s after the solver's Single Solve start (09:04:35.8 ET) and 7 min 37.7 s after the 9 AM release. Our automated morning solver (a scheduled script on our Kali VM) stopped on the Single (new codec table, nothing sent); Claude built the codec and sent the Single on the same attempt (09:07:18.0). Both timed modes were ranked runs, CORRECT on the first submit
Player
Zwesty (solo), with Claude Code (Opus 5.5): the automated solver started the Single and stopped; Claude wrote the codec, solved the Single, and ran both timed modes

The challenge

"Recover an equivalent key operation from challenge-owned table transformations, not a third-party protected binary."

answer_id Question Format
answer "For case MOB-29-2898E911C6 in package com.huntress.mobile.d29.v2898e911c6dc: Recover an equivalent key operation from challenge-owned table transformations, not a third-party protected binary. Submit only the exact backend answer code returned for this case." pattern: "ANS- followed by exactly 16 uppercase hexadecimal characters; no spaces or explanatory text"; exact

The Time Trial asked the same for its own case (MOB-29-93565BAB55); the Mastery asked it once per delivery (13 deliveries, 13 answers), each with its own day's case and package.

Reading the prompt: what it said we'd need

Clue What it pointed to What I did about it
"table transformations" A lookup-table codec in the APK's own data The v1 asset said codec: "table". The DEX class WhiteBoxKeyRecovery only names it (CODEC = "table", CONTROL = "white_box_table") and has no encode method, so the table had to be read from the data itself
"Recover an equivalent key operation" You don't need the real tables, only a function that does the same thing Modelled each byte as plain + key + step × index (mod 256), which reproduces the table exactly
"challenge-owned …, not a third-party protected binary" Scope: this is the challenge's own toy transform, not a commercial white-box product Static reading of the asset and the DEX strings only; nothing to attack beyond the APK
"White Box" (title) White-box crypto: the key is folded into tables so it never appears on its own Here the asset still carried an integer key (41) beside the data; the hidden part was the per-position step
"ANS- followed by exactly 16 uppercase hex" (format) A fixed 4-byte prefix on a 20-byte answer The prefix is a crib: ANS- against the first 4 data bytes gives the key and the step, then every byte decodes

Safety

Static analysis only. Nothing was installed or executed. The APKs were opened as ZIP files on our Kali VM; the puzzle asset was read as JSON and the DEX was disassembled with baksmali to read its strings and class layout. Nothing went near an emulator or the app's loopback backend. The APKs stayed on the VM.

What was in the files

  • Single Solve: white_box_key_recovery.apk, 12,704 bytes. assets/huntress_puzzle.json is the v1 schema: {"schema":"huntress.mobile.puzzle.v1","day":29,"codec":"table","data":"6a7a825f…97a5","key":41}. The data is 40 hex characters, so 20 bytes, the same length as an ANS- code.
  • Time Trial: white_box_key_recovery_time_trial.apk, 12,704 bytes, but a v2 asset (codec: "analysis", day 29): the answer is computed by the app's DEX code, not stored in the asset. The table codec wasn't used.
  • Mastery: 13 deliveries, all v2 analysis, one per earlier control day: 2, 4, 6, 8, 11, 12, 15, 18, 19, 23, 24, 28, and 29. Every APK was 12,704 bytes except delivery 7 (Day 15, 12,772 bytes), which carried a native library, lib/x86_64/libarena.so.

So only the Single used the new codec; both timed modes were v2 puzzles of the kind the lane has answered since Day 25.

Single Solve

The automated solver: stopped, nothing sent

The solver did Metadata and Warmups first, and reached Android at 09:04:35.8 ET. It downloaded and checked the APK, then stopped:

STOP: unknown puzzle codec 'table' (day 29): fallback found 0 ANS readings []; not trusted; not submitting

Its fallback tries bounded one- and two-layer transforms; none gave an ANS- string, so nothing was sent. That stop was correct.

The codec (Claude)

Laying ANS- over the first four data bytes:

i data byte plaintext data − plain (mod 256) − key 41
0 6a (106) A (65) 41 0
1 7a (122) N (78) 44 3
2 82 (130) S (83) 47 6
3 5f (95) - (45) 50 9

So data[i] = (plain[i] + key + 3·i) mod 256: byte 0 gives the key exactly, and the step is 3 per position.

Today's solver builds on the Day 28 Android solver and adds table for v1 assets only: - Checks the integer key against byte 0 and stops if ANS- doesn't give it exactly. - Derives the step from byte 1, then requires bytes 1–3 to agree on that one step. The step is never hard-coded. - Decodes every byte with it, requires ASCII, and checks the result against the question's format. - Anything that doesn't fit stops cleanly ("layout not modelled") rather than guess. Every other codec, schema, and .apks bundle goes down the Day 28 path unchanged.

Its tests: 9/9 OK (the real saved Single, the format check, a synthetic key and step, a wrong key that stops, a prefix that isn't linear, bad data, a value that doesn't fit the format, a Day 28 packet asset unchanged, and the per-set flag). A regression check against every answer the Game Master had already accepted for Android: 558 reproduced, 0 mismatched, 0 stopped.

The decoded answer was ANS-AB1D…D98C (20 characters, fits the format). Claude sent it on the solver's own attempt (a start request resumes it).

started by the solver 09:04:35.8 ET; resumed by Claude
deadline    none (Single Solve)
answers     1   ANS-AB1D…D98C
start → artifact   288 ms
solve                1 ms
submit             856 ms   (submit +162,743 ms after started_at: the solver's start, then the build)
result      CORRECT   flag 98456d7e…e3e4   (09:07:18.0 ET, first try)

Time Trial

CORRECT on the first attempt, ranked (fresh variant). The v2 asset went to the Day 25 static interpreter in the chain, unchanged.

started_at  09:07:27.4 ET   deadline 09:08:17.4 (50 s clock)
answers     1   (v2 analysis, day 29)
start        100 ms after started_at
artifact     298 ms
solve          2 ms
submit      1660 ms   (submit +2,060 ms after started_at)
result      CORRECT   flag ed9447c1…c93b   (09:07:27.9 ET)

Mastery

CORRECT on the first attempt, ranked.

started_at  09:07:36.4 ET   deadline 09:14:26.4 (6 min 50 s clock)
answers     13 deliveries, all v2 analysis (delivery 7 with a native library)
start        122 ms after started_at
artifact     805 ms
solve        291 ms
submit       695 ms   (submit +1,913 ms after started_at)
result      CORRECT   flag 7676b010…c962   (09:07:37.7 ET)

The native-guard fallback wasn't needed. We had a fallback ready in case the Mastery came back partial on a native cmp/jcc guard, a problem seen on an earlier day. Delivery 7 was the native kind, and the live evaluator answered it.

After the timed runs: a regression rehearsal against the Day 28 solver passed (203 identical, 1 newly solved, 0 changed), and today's solver became our Android solver for later days.

Time to solve

All three modes were solved by 09:07:37.7 ET, 7 min 37.7 s after release and 3 min 1.9 s after the solver's Single Solve start; the Single took 2 min 42.2 s from that start. Both timed modes were ranked and went through on the first submit, in 0.5 s and 1.3 s by the server's stamps.

Takeaways

  • A known prefix is a key-recovery tool. The answer format promised ANS-; four bytes of known plaintext gave both the key and the step. The same move works on any file with a fixed magic.
  • Derive the parameter, check it, then stop on anything else. The step isn't hard-coded, so another variant with a different step decodes too; one that isn't linear stops instead of guessing.
  • "White box" here was a toy. The table was a plain additive schedule. Reading the data first was faster than looking for an encoder the DEX didn't have.
  • The new codec only matters for the Single. Both timed modes were v2 puzzles the Day 25 interpreter already handled. So a new v1 codec costs only the Single, and a quick, strict decoder is the right size of fix.