$ cat writeup.md…
$ cat writeup.md…
d3c2026
Task: An Android APK gates a game and its encrypted flag behind a 64-hex token, obfuscated AArch64 JNI code, and an encrypted MNN model. Solution: Decrypt the native payload, solve its verifier symbolically, reconstruct callback state, and reimplement the AES KDF.
$ cat /etc/rate-limit
Rate limit reached (20 reads/hour per IP). Showing preview only — full content returns at the next hour roll-over.
Enter the token and start playing the game.
The supplied app-debug.apk presents a handwriting-driven hexadecimal token entry screen. The objective is to recover the accepted token and reproduce the native flag-reveal path without relying on Android gameplay.
Unpacking the APK and opening its DEX files in JADX immediately established the high-level control flow:
MainActivity collects exactly 64 hexadecimal characters and calls FlagNative.nativeVerifyInput().GameActivity.FlagNative.nativeRevealFlag().FlagNative loads c++_shared, MNN, and d3llvm, so the real verification and reveal logic is native.MnnTouchClassifier uses a 64-position, 4-by-16 input state, while ModelAssets loads assets/model/touch_model.mnn.enc.The loader's JNI_OnLoad at libd3llvm.so VA 0xfd7c validates and loads libd3llvm_payload.so, makes its .text writable, decrypts it, flushes the AArch64 instruction cache, restores execute permissions, resolves Payload_OnLoad, and invokes it.
The payload's .signinfo structure begins at file offset 0x52b0. Its D3SGN2 metadata gives encrypted .text offset 0x11c20, length 0x2d7cc, a 16-byte nonce, and a 32-byte key. The loader generates each 32-byte keystream block as:
SHA256(key32 || nonce16 || LE64(counter))
XORing those blocks over the encrypted region produces valid AArch64 code. The decrypted JNI registration table at VAs 0x408c8..0x40950 maps nativeVerifyInput to 0x15938 and nativeRevealFlag to 0x15954; those wrappers lead to OLLVM-style flattened dispatchers at 0x292c4 and 0x2a444.
The verifier accepts only 64 hexadecimal characters. On success, it copies the 64 input bytes to global 0x43180, stores related state at 0x431c8, sets byte 0x431d0, and unlocks a mutex. The reveal routine consumes this verifier-produced state, so patching only the success byte is insufficient.
...
$ grep --similar