1 - GhostMode: Reversing a Native Android CTF Library
Overview
GhostMode was the workshop’s first foray into native code. The APK itself was thin — the interesting logic lived entirely in libctflib.so, a stripped ELF64 shared library loaded at runtime. The challenge name already telegraphed the approach: you were not meant to find the flag through static analysis alone.
The fake flag was there to remind you of that.
First Look — The Stub and the Taunt
Decompiling the APK with JADX produced a small FlagManager class with a single JNI method:
1public native String revealFlag(int pin);The implementation was entirely in the native library. Loading libctflib.so into IDA and finding the exported symbol Java_com_example_ctf_1dynamicbypass_FlagManager_revealFlag revealed the structure immediately.
The first thing the function does is call sub_860(a3) — a validator for the PIN passed from Java. If the validator returns false, the function returns a hardcoded string from .rodata:
1"CTF{y0u_c4nt_r34d_m3_fr0m_st4t1c_4n4lys1s}"That is not the flag. That is the challenge author telling you to keep going.
Reversing the PIN Validator — sub_860
sub_860 takes the PIN as an unsigned 32-bit integer and performs a series of nibble-level checks. The decompiled logic translates to:
- The value must be ≤
0xFFFF(16-bit range) (pin & 0xF0F)must equal0x307— low nibble of the low byte must be7, low nibble of the high byte must be3- The four nibbles must sum to
0xE(14) (pin >> 8) ^ (pin & 0xFF)must equal0x24- The second nibble (bits 4–7) must equal
3
Working through the constraints:
- digit0 = 7, digit2 = 3 (from the mask check)
- digit1 = 3 (from the final check)
- digit3 = 14 − 7 − 3 − 3 = 1
Packed together: 0x1337. Verification confirms (0x13 ^ 0x37) = 0x24. The PIN is 4919 — the hexspeak 0x1337 — and the comment left in the decompiled stub already hinted at this: “Value passed to function in APK is 4919”.
The Key Derivation — sub_8C0
With the PIN validated, the function calls sub_8C0(0x1337) to derive a single key byte v7. The function performs a short arithmetic sequence:
1ecx = pin >> 8; // 0x13
2eax = ecx ^ pin; // 0x13 ^ 0x1337 = 0x1324
3edx = eax ^ 0x7E; // 0x1324 ^ 0x7E = 0x135A
4ecx = ecx + pin; // 0x13 + 0x1337 = 0x134A
5bit = (eax >> 7) & 1; // (0x1324 >> 7) = 38, bit = 0
6result = (1 ^ bit) * edx | bit * ecx;With bit = 0: result = 1 * 0x135A | 0 = 0x135A. Truncated to a byte: v7 = 0x5A = 90.
Flag Decryption — The Loop
The function then allocates 37 bytes and runs an 18-iteration loop, pulling characters alternately from two 18-byte arrays (byte_5B0 and byte_590) stored in .rodata:
1byte_5B0 = { 0xBA, 0xC1, 0x29, 0x65, 0xBC, 0xFC, 0xC7, 0x13,
2 0x5A, 0xA8, 0xCE, 0x0F, 0x32, 0xB8, 0xEC, 0x27, 0x46, 0x17 };
3byte_590 = { 0xCE, 0xDB, 0x17, 0x55, 0xC3, 0xE7, 0x3F, 0x38,
4 0xB9, 0xA1, 0x01, 0x0C, 0x7D, 0xDC, 0xE9, 0x4C, 0x5D, 0xBD };Each iteration uses a counter v10 that starts at −1044 and increments by 58 per step, wrapping to zero after exactly 18 steps:
1v9[2*i] = v7 ^ byte_5B0[i] ^ ((v10 - 73) & 0xFF);
2v9[2*i+1] = v7 ^ byte_590[i] ^ ((v10 - 44) & 0xFF);The interleaved XOR of v7, the array bytes, and the counter-derived byte produces a 36-character string:
1CTF{dynam1c_byp4ss_1s_th3_w4y_t0_g0}Why the Fake Flag Works as a Teaching Moment
The fake flag is an elegant design choice. A naive grep over the binary or a quick JADX pass both surface the string immediately — and both tell you exactly nothing useful about the actual mechanism. The challenge forces you to read the conditional: the string is only returned on the failure path of sub_860.
The real flag requires understanding the bit-level PIN constraints, the key derivation, and the loop structure. Each step is self-contained and small — a good introduction to native Android reversing before the workshop moved into harder territory.
Key Techniques
| Technique | Where |
|---|---|
| JNI native bridge | FlagManager.revealFlag(int) delegates entirely to the .so |
| Fake flag as decoy | Hardcoded in .rodata; returned only on PIN validation failure |
| Nibble-level PIN constraint | sub_860 checks bit patterns, nibble sums, and a XOR condition |
| Key byte derivation | sub_8C0 produces a single byte used as a XOR key for decryption |
| Counter-keyed XOR loop | 18-iteration loop interleaves two arrays with a wrapping counter |