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_) 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.jsonis the v1 schema:{"schema":"huntress.mobile.puzzle.v1","day":29,"codec":"table","data":"6a7a825f…97a5","key":41}. Thedatais 40 hex characters, so 20 bytes, the same length as anANS-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. Thetablecodec 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.