Mirror Garden
The input to enter into the app's "whisper" field is:
Challenge
Something is growing in the mirror garden, and only one leaf remembers.
Attachment: mirror-garden.apk
Flag format: isfcr{...}
Result
isfcr{m1rr0r_g4rden_bl00ms_@t_m1dn1ght_7f3a}The input to enter into the app's "whisper" field is:
L.]^ooq(LlU^(a]#APK Inspection
Listing the archive revealed a DEX file and a small ARM64 native library:
unzip -l mirror-garden.apk
jadx -d /tmp/opencode/mirror-jadx mirror-garden.apk
unzip mirror-garden.apk 'lib/*' -d /tmp/opencode/mirror-nativeThe important files are:
classes.dexlib/arm64-v8a/libgarden.soone/sr/garden/MainActivity.javain the decompiled output- 1,024 classes named
one/sr/garden/leaf/L0000throughL1023
MainActivity loads the native library and declares:
private native String bloom(byte[] input);When the user presses Water, the app converts the entered text to bytes and calls bloom. There is no flag validation in the activity itself; the native routine determines which leaf to invoke and processes its output.
Finding the Remembering Leaf
The native library exports:
Java_one_sr_garden_MainActivity_bloomIts entry point is at virtual address 0x1b28. Disassembling the ARM64 code with Capstone shows an early call to an internal state-generation routine at 0x2374.
The caller takes the first 32-bit word of the generated state and masks it with 0x3ff:
leaf_index = first_state_word & 0x3ffIt then formats the class name as:
one/sr/garden/leaf/L%04dRather than analyze all 1,024 leaf classes, we can emulate the state-generation routine with Unicorn. Loading the ELF's PT_LOAD segments at their virtual addresses, setting up a stack and an output buffer, and running 0x2374 produces:
657db61a964a04d3a54b416f0243ab7b
aaf6b1146475155b2b6faa6e3f56b86d
1784d10160343a78ca7a31c665f12b33
f244a1f8a01c651e5173856718388c8eThe first word is little-endian:
0x1ab67d65 & 0x3ff = 357The remembering leaf is therefore L0357. This was also confirmed by intercepting the native routine's JNI FindClass call during emulation.
Solving the Leaf
Each leaf exposes a static method with the signature:
public static byte[] verify(byte[] input)The selected leaf rejects inputs that are not exactly 16 bytes long. It interprets the input bytes as unsigned values, computes a series of arithmetic expressions, and compares the results with constants. A failed comparison returns null.
The expressions disguise familiar operations. For example:
(a | b) - (a & b) = a ^ b
(a ^ b) + 2 * (a & b) = a + bThe code also uses multiplication, 32-bit masks, and rotations represented as shifts and ORs:
((value << r) | (value >>> (32 - r))) & 0xffffffffTo solve these checks, represent the 16 input bytes as Z3 bit-vectors constrained to 0..255, evaluate the decompiled assignments symbolically, and assert that every checked expression equals its expected constant.
The recovery script uses 64-bit bit-vectors to match the Java long intermediates and retains the explicit 0xffffffff masks. For these expressions, the right-shift operands are nonnegative masked values, so translating their unsigned Java shifts to bit-vector right shifts does not change the result.
Z3 finds:
L.]^ooq(LlU^(a]#After the checks pass, the leaf XORs a fixed 48-byte array with the repeating 16-byte input:
for (int i = 0; i < 48; i++) {
output[i] = (byte) (encoded[i] ^ input[i & 15]);
}For the recovered whisper, the resulting payload is:
85d3d1dfacd6abaa42931f9a7b26b7366
9decf40a7e42467c996e931baac6e8d6
ce5ef43f22bcbd14fafbfc3360b589bThis payload is not the flag. It must still pass through the native routine.
Native Authentication and Decryption
The native code calls L0357.verify through JNI and requires a 48-byte result. It XORs that result with the first 48 bytes of the generated native state, then performs hash-based key derivation, authentication, and decryption. The library contains the domain-separation strings:
garden/e/v1
garden/m/v1To recover the final output without an Android device, emulate the complete bloom routine and provide minimal implementations for the JNI and libc calls it makes:
snprintfformats the selected leaf class name.memcpycopies buffers inside the emulated address space.FindClassandGetStaticMethodIDreturn placeholder handles after checking the requested class and method.CallStaticObjectMethodreturns a handle for the independently computed leaf payload.- The array access functions expose that payload and its length.
ReleaseByteArrayElementsandDeleteLocalRefare no-ops in this harness.NewStringUTFcaptures the final native output.
The emulated library follows its normal authentication check before reaching the successful decryption path. The captured string is:
isfcr{m1rr0r_g4rden_bl00ms_@t_m1dn1ght_7f3a}Verification
The symbolic result was checked independently by compiling the decompiled L0357.java and calling its actual Java verify method with the recovered whisper. The method returned the expected 48-byte payload rather than null.
javac -d /tmp/opencode/mirror-jadx \
/tmp/opencode/mirror-jadx/sources/one/sr/garden/leaf/L0357.java \
/tmp/opencode/MirrorVerify.java
java -cp /tmp/opencode/mirror-jadx MirrorVerifyThe output confirmed:
Java leaf verification passed: 85d3d1dfacd6abaa42931f9a7b26b73669decf40a7e42467c996e931baac6e8d6ce5ef43f22bcbd14fafbfc3360b589bThe complete recovery script from this solve is available locally at /tmp/opencode/mirror_solve.py. It uses pyelftools, unicorn, and z3-solver, together with the JADX output at the paths above:
python /tmp/opencode/mirror_solve.pyVerification was performed using Java execution and ARM64 native emulation, not by launching the APK on an Android device.