The challenge
Recover split configuration from DEX and a challenge-owned native library.
One question, answer: "For case MOB-15-… in package com.huntress.mobile.d15.…: Recover split configuration from DEX and a challenge-owned native library. Submit only the exact backend answer code returned for this case." Format: ANS- + exactly 16 uppercase hex characters, normalization exact. Mastery asks the same question five times (set1.answer to set5.answer), one per APK, named delivery N: native_library_secrets_mastery_0N.apk.
Reading the prompt: what it said we'd need
| Clue | What it pointed to | What I did about it |
|---|---|---|
| "a challenge-owned native library" | a . bundled in the APK under lib/, called from Java through JNI |
Listed the APK's members: one lib/. Parsed its ELF sections and symbols, then disassembled the two exported functions |
| "split configuration from DEX and … native library" | part of the answer, or the key to it, in each place | The DEX carries the labels (CODEC = "native", CONTROL = "jni_); the library carries the value. The asset's data carries the same value again |
| "Recover" | read it out, not run it | All static: nothing installed, loaded, or executed |
| "backend answer code returned for this case" | a runtime service | As on every earlier day, the app points at a loopback URL that nothing serves; the code is in the files |
ANS- + 16 uppercase hex, exact |
a strict check | The solver checks the value against this format before sending, and sends it exactly as stored |
The general method (the packaged puzzle asset) is in the Network Config Split write-up.
Safety
Static only, on our Kali VM. Each APK was opened as a zip in Python (python3 -I). The .so was read with readelf, objdump, and strings, and the DEX was turned into smali with baksmali to read its constants. Nothing was installed, loaded, or run, and the library was never dlopened. The APKs and extracted files stayed on the VM.
What was in the files
Single Solve APK (native_library_secrets.apk, 12,772 bytes, sha256 0cf5bc290da521e7…): AndroidManifest.xml, classes.dex, assets/huntress_puzzle.json (94 bytes), lib/x86_64/libarena.so (13,648 bytes), and a FIXTURE- signature. It is 68 bytes bigger than the Day 14 template (12,704).
- The puzzle asset had the new codec:
{"schema":"huntress.mobile.puzzle.v1","day":15,"codec":"native","data":"ANS-E28C…3B8F"}. Sodataalready had the answer's shape. libarena.so: an x86-64 ELF shared object built by GCC 13.3 fromarena.c, not stripped. It exports two functions,huntress_arena_secret(17 bytes) andhuntress_arena_day(15 bytes). The symbol table also names a local 21-byte object,arena_value, at 0x2000, the start of.rodata. The disassembly is tiny:huntress_arena_secret:endbr64; push rbp; mov rbp,rsp; lea rax,[rip+0xff1]; pop rbp; ret. Theleapoints at 0x2000, so the function returns a pointer toarena_value.huntress_arena_day:mov eax,0xf, so it returns 15..rodataholdsANS-E28C…3B8Fand a NUL, the same value as the asset'sdata.- The DEX (
baksmali): the classNativeLibrarySecretsholds the constantsCODEC = "native",CONTROL = "jni_secret", andDAY = 15, anddescribe()returnsDAY-15:jni_secret:challenge-owned JNI shared library.ChallengeActivitybuildshxxp://127.0.0[.]1:19090/mobile/attempt/d15-…/evidence?case_id=…with a fixtureclient_secret=SECRET-…&proof=string. TheSECRET-…value is a decoy for this question: wrong shape, and nothing serves that port.
So the native codec means: the answer is the string the JNI library's secret function returns. Two independent places agreed on it, the asset and the library.
What the timed variants changed: the Time Trial APK had the same template and size (12,772 bytes) with a new case ID, package suffix, and secret. The Mastery was compounding: five deliveries, and only the last was a Day 15 APK.
| Set | APK (sha256 first 16) | Size | Template | Codec |
|---|---|---|---|---|
| 1 | 7db4b9a394588ca3 |
12,704 | Day 3 (intent_) |
base64 |
| 2 | f5fea3c98289fb61 |
12,704 | Day 7 (javascript_) |
caesar, key 3 |
| 3 | a103080f7fb12cf9 |
12,704 | Day 9 (trust_) |
base64 |
| 4 | ffa2e01552ca2be0 |
12,704 | Day 13 (keystore_) |
base64 |
| 5 | e7358b45db0421fa |
12,772 | Day 15 (jni_) |
native, with lib/ |
Single Solve
First try, by our automated morning solver (a scheduled script on our Kali VM): stopped cleanly. It started the attempt at 13:00:40 UTC, fetched and checked the APK, and our Android solver raised unknown puzzle codec 'native' (day 15): no decoder yet. Nothing was sent. That is the designed behaviour for a new codec.
The fix (Claude helper agent, about 10 minutes). After the analysis above, a new solver loads our existing Day 10 Android solver unchanged and adds only the native codec:
- It opens every
lib/<abi>/*.soin the APK in memory with a small pure-Python ELF reader (sections and symbols only). - It finds
huntress_arena_secretand follows its return to the C string: x86-64lea rax,[rip+d], or AArch64adrp x0+add x0, in case a variant ships an ARM build. If it can't follow the function, it falls back to the oneANS--shaped string in.rodata. - Every library and the asset's
datamust agree (whichever are present). A disagreement, two different values, or nothing found stops the run before anything is sent. - Other codecs go straight to the existing solver. A per-set mode lets the solver handle Mastery set by set under a clock.
Tests (12/12 on our Kali VM): synthetic x86-64 and AArch64 .so files built in the test, the .rodata fallback, every stop case, a Mastery delivery N: <file> case with per-set answers, the real Day 15 APK, and all 73 earlier saved Android attempts giving byte-identical answers to the existing solver. A regression rehearsal against the saved Android attempts checked 74: slowest 0.00 s, per-set identical, 73 identical, 1 newly solved, 0 changed. The rehearsal passed.
The answer was then sent through the solver's normal path. Its start reconnected to the same attempt, so started_at is still the morning solver's.
started_at 2026-10-15T13:00:40.194Z deadline none (untimed)
answer ANS-E28C…3B8F
timings start 332 artifact 326 solve 1 ready 0 submit 945 TOTAL 1603 ms
vs started_at: start +603814, artifact +604140, ready +604141, submit +605086 ms
result CORRECT b98e264b…8061 (solved_by.at 13:10:44.574Z)
The value as stored was accepted first time, so there was no wording question.
Time Trial
The challenge list marked both timed routes as supporting ranked starts, so both were started as ranked runs. This one was ranked (hall_of_fame_eligible: True, 31 unique attempts left), unlike Days 13 and 14.
started_at 2026-10-15T13:11:18.755Z deadline 13:12:08.755Z (50 s clock)
answer ANS-6B57…5E4B (native: library and data agree)
timings start 615 artifact 310 solve 1 ready 0 submit 1197 TOTAL 2123 ms
vs started_at: start +113, artifact +423, ready +424, submit +1622 ms
result CORRECT 7508702e…cd27 (solved_by.at 13:11:19.470Z)
Mastery
Five downloads, matched to their questions by the delivery N: rule from Day 10. Four sets used earlier codecs that the existing solver already knew; set 5 used native. All five answers went in one submit. Ranked (hall_of_fame_eligible: True).
started_at 2026-10-15T13:11:28.323Z deadline 13:14:18.323Z (170 s clock)
answers set1 ANS-D7CC…4653 set2 ANS-D4BE…3048 set3 ANS-4040…86B4 set4 ANS-881B…B2C3 set5 ANS-5826…21AF
timings start 797 artifact 453 solve 2 ready 0 submit 758 TOTAL 2011 ms
vs started_at: start +92, artifact +545, ready +547, submit +1305 ms
result CORRECT 600ee1a9…e5ba (solved_by.at 13:11:29.120Z)
No wrong answers, no retries, and no HTTP 429s for this challenge.
Time to solve
All three modes were solved by 09:11:29 ET, 10 min 49 s from the first start to the last CORRECT; the Single Solve went through at 09:10:44, about 10 minutes after the solver stopped on the new codec. The ranked Time Trial and Mastery each went through on the first submit, at +0.7 s and +0.8 s.
Takeaways
- A clean stop beats a guess. The solver's "unknown codec, not submitting" cost ten minutes, not an attempt; the timed modes then went in at well under a second each once the codec existed.
- Two sources that agree are a strong check. The asset's
dataand the library's.rodataheld the same value, so the solver requires them to agree rather than trusting either alone. - A tiny
.soreads like a config file. Exported symbol, onelea, one string.readelfandobjdumpwere enough; no Ghidra and no emulator. - The Mastery compounds the old days. Four of five sets were Day 3, 7, 9, and 13 templates, so keeping every earlier codec working (the 73-attempt identical check) mattered as much as the new one.
- Timed runs were ranked again, after two days of unranked first starts.