droids4 picoCTF 2019 Solution

Published: April 2, 2026

Description

The hardest Android challenge. Multiple layers of protection in droids4.apk.

Download the APK file.

bash
wget <url>/droids4.apk

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
  1. Step 1
    Static analysis: decompile and map the app
    Observation
    I noticed this was the hardest droids challenge with multiple protection layers described in the prompt, which suggested starting with broad static analysis via jadx to map all obfuscation techniques (name mangling, string assembly, native calls) before attempting any targeted attack.
    Start with jadx decompilation. Understand the app structure: where is the password validation? Does it involve native code, obfuscated strings, reflection, or runtime class loading?
    bash
    jadx droids4.apk -d droids4_java/
    bash
    unzip droids4.apk -d droids4_raw/
    bash
    ls droids4_raw/
    What didn't work first

    Tried: Searching getFlag() for a hardcoded string literal and finding nothing readable

    The password is assembled at runtime through character-offset arithmetic: each character is produced by adding an integer offset to a char literal, so no plaintext 'alphabetsoup' appears in the decompiled output. You have to manually evaluate each arithmetic expression in the jadx output and concatenate the resulting characters to reconstruct the password.

    Tried: Trying to patch out the password check in smali and skip straight to the flag display

    The flag is only produced by the native cardamom() method, which is only called after the correct password is verified. Bypassing the check branch in smali still requires the native call to run with the right internal state, so the flag never appears. The correct approach is to reconstruct the password from the character arithmetic, enter it at runtime, and let cardamom() return the actual flag string.

    Learn more

    Advanced Android protections may include: ProGuard/R8 obfuscation (class/method name mangling), string encryption (strings decrypted at runtime), reflection (dynamic method calls), DexGuard (commercial obfuscator), and APK split/multi-dex.

    Start by finding the entry point (MainActivity), then trace all function calls from the button click handler inward to find the validation logic.

    In droids4, the getFlag() method in FlagstaffHill builds a password string through character-offset arithmetic: it adds integer offsets to character literals to assemble each character at runtime. Trace this arithmetic in the decompiled Java to reconstruct the plaintext password: alphabetsoup. Once you have the password, the smali patch replaces the getFlag() return value with a call to the native cardamom() method defined in FlagstaffHill, which is what actually produces the flag when the correct password is entered.

  2. Step 2
    Analyze native libraries
    Observation
    I noticed the APK contained a lib/ directory with .so files after unzipping, which suggested the flag validation logic was partially or fully implemented in native JNI code that jadx alone could not fully decompile.
    Unzip the APK and examine any .so files in the lib/ directory. Load them in Ghidra and look for the JNI entry points. The flag validation may be entirely in native code.
    bash
    ls droids4_raw/lib/
    bash
    file droids4_raw/lib/x86/*.so
    bash
    strings droids4_raw/lib/x86/*.so | grep -i pico
    What didn't work first

    Tried: Running strings on the .so and grepping for 'pico' expecting to find the flag directly

    The flag is not stored as a static string inside the native library - it is computed by cardamom() at runtime using values derived from the correct password input. The strings scan shows you the JNI method names and some constants but not the final flag. You need to supply the reconstructed password to trigger the native function and capture its return value.

    Tried: Loading only the arm64-v8a .so in Ghidra and not finding the expected JNI exports

    Ghidra may load the wrong architecture slice if you pick the wrong lib/ subdirectory. The APK ships separate .so files under lib/x86/, lib/armeabi-v7a/, and lib/arm64-v8a/. The emulator or test device ABI determines which one is actually loaded at runtime. Analyze the slice matching your runtime ABI (typically x86 for AVD emulators) to ensure the function signatures match what you observe with Frida.

    Learn more

    Even in obfuscated native code, string literals are often stored in plain text in the .rodata section. The strings command extracts printable sequences of 4+ characters from any file, which can quickly reveal hardcoded flags or keys.

  3. Step 3
    Dynamic analysis with Frida
    Observation
    I noticed that the native cardamom() method computes the flag at runtime and static analysis alone could not reveal the final output, which suggested using Frida to hook live method calls and capture the actual flag value returned during execution.
    If static analysis is insufficient, use Frida to hook key methods at runtime. Hook the final comparison or any method that receives the flag string. Use frida-trace to automatically trace all method calls.
    bash
    frida-trace -U -f com.hellocmu.picoctf -j '*!check*'
    bash
    frida-trace -U -f com.hellocmu.picoctf -j '*!*' -J '*!equals*'
    What didn't work first

    Tried: Attaching frida-trace with -I flag instead of -j to hook Java methods

    The -I flag in frida-trace is for including native module patterns (e.g. libc!strcmp), not for Java class method matching. Java method hooks require the -j flag with the 'Class!method' glob syntax. Using -I on a Java class name pattern produces no hooks and no output, silently failing without any error message.

    Tried: Spawning frida-trace with -f but seeing 'unable to find process' because frida-server is not running on the device

    frida-trace in USB mode (-U) requires frida-server to be running on the Android device or emulator as root (adb shell, then su, then ./frida-server &). Without it, the frida client cannot attach to any process. The alternative is to use the frida gadget embedded in a repackaged APK, but that requires a separate patching workflow.

    Learn more

    frida-trace automatically generates hook scripts for matched methods and prints their arguments. The -j flag matches Java methods by class and method name pattern. This is useful when you need to discover which method to hook without reading all the source first.

    For APK patching: use apktool to decompile, modify the smali to bypass the check or log the expected value, recompile with apktool b, sign with a debug certificate, and install the patched APK.

Flag

Reveal flag

picoCTF{not.particularly.silly}

Combine static analysis (jadx + Ghidra) with dynamic analysis (Frida hooks) to extract the flag through multiple protection layers.

Key takeaway

Layered Android protections, such as name mangling, runtime string assembly, and native JNI validation, are designed to make static analysis harder but not impossible. The standard response is to combine static and dynamic analysis: decompile to map control flow, then instrument the running process with Frida to observe actual values at the comparison point. This static-then-dynamic workflow applies directly to commercial mobile app audits, anti-cheat research, and malware triage where full source is unavailable.

Related reading

Want more picoCTF 2019 writeups?

Useful tools for Reverse Engineering

What to try next