droids3 picoCTF 2019 Solution

Published: April 2, 2026

Description

The app always returns 'don't wanna' no matter what you type. Fix that by editing the bytecode directly: decompile the APK with apktool, swap one method call in the smali, rebuild, sign, and install the patched APK.

Download the APK file.

Install apktool and a JDK (needed for keytool and jarsigner or uber-apk-signer). Have an Android emulator running.

bash
wget <url>/three.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
    Decompile and read the logic
    Observation
    I noticed the APK always returned 'don't wanna' regardless of input, which suggested the app's logic was routing all calls to a dead-end method rather than the real flag method, so I needed to decompile it to locate exactly which method call was responsible.
    Decompile with jadx for readability. In FlagstaffHill.java you will see that getFlag() calls nope(input), which returns the string 'don't wanna'. There is a second method yep(input) that calls the native cilantro() function and returns the real flag, but getFlag() never reaches it. The fix is to redirect that one method call from nope to yep.
    bash
    jadx three.apk -d three_java/
    bash
    cat three_java/sources/com/hellocmu/picoctf/FlagstaffHill.java
    bash
    # Then decompile to smali so you can edit the bytecode:
    bash
    apktool d three.apk --no-res
    What didn't work first

    Tried: Edit the jadx output Java file directly and try to recompile it with javac.

    jadx produces pseudocode for human reading only - it is not valid, compilable Java. jadx output lacks correct imports, uses synthetic field names, and does not map back to DEX. apktool works on its own smali representation; editing jadx output changes nothing in the APK.

    Tried: Run apktool without --no-res and proceed when it throws resource decode errors.

    Without --no-res, apktool tries to decode all resource tables and may fail on certain resource types it cannot reconstruct. The subsequent rebuild step then produces a malformed APK that adb refuses to install. The --no-res flag skips resource decoding so only the smali (bytecode) side is touched.

    Learn more

    Android apps ship as DEX bytecode, not Java source. jadx converts DEX back to readable Java pseudocode - great for analysis. apktool converts DEX to smali, a human-readable assembly language that maps one-to-one with DEX opcodes and can be rebuilt into a valid APK. You need smali (not jadx output) for the edit because apktool can only round-trip its own smali representation.

    The --no-res flag skips resource decoding, which avoids errors when rebuilding apps whose resources apktool cannot perfectly reconstruct.

  2. Step 2
    Patch the smali to call yep instead of nope
    Observation
    I noticed from the jadx decompilation that FlagstaffHill.java contained both a nope() method returning a static string and a yep() method calling the native cilantro() function, and getFlag() only called nope, which suggested a single invoke-static substitution in the smali would redirect execution to the real flag.
    Open three/smali/com/hellocmu/picoctf/FlagstaffHill.smali and find the getFlag() method. You will see an invoke-static instruction that calls nope. Change the word 'nope' to 'yep' in that one line. Everything else in the file stays the same.
    bash
    # View the method in the smali file:
    bash
    grep -n 'nope\|yep' three/smali/com/hellocmu/picoctf/FlagstaffHill.smali
    bash
    # Edit the file - replace the nope call with yep:
    bash
    sed -i 's/->nope(Ljava\/lang\/String;)Ljava\/lang\/String;/->yep(Ljava\/lang\/String;)Ljava\/lang\/String;/' three/smali/com/hellocmu/picoctf/FlagstaffHill.smali
    bash
    # Verify the change:
    bash
    grep -n 'nope\|yep' three/smali/com/hellocmu/picoctf/FlagstaffHill.smali
    What didn't work first

    Tried: Change only the method name in the invoke-static line to 'yep' but leave the descriptor as '->nope(...)' elsewhere in the file.

    smali resolves the full method reference including the method name inside the descriptor string. If any part of the invoke-static line still says 'nope', the DEX verifier sees a call to a method that does not exist and the app crashes with a NoSuchMethodError at runtime. The sed command replaces the full descriptor so the entire reference is consistent.

    Tried: Edit FlagstaffHill.smali to delete the nope() method body instead of redirecting the call.

    Deleting the nope() method causes a DEX verification failure because the class still declares the method in its method table but its implementation is missing. The package manager rejects the APK at install time. The correct fix is a one-word substitution in the caller (getFlag), leaving both nope and yep intact.

    Learn more

    The smali line before the edit looks like:

    invoke-static {p1}, Lcom/hellocmu/picoctf/FlagstaffHill;->nope(Ljava/lang/String;)Ljava/lang/String;

    After the edit it reads:

    invoke-static {p1}, Lcom/hellocmu/picoctf/FlagstaffHill;->yep(Ljava/lang/String;)Ljava/lang/String;

    The type descriptor Ljava/lang/String; uses smali syntax: L for a reference type, slashes instead of dots, and a semicolon terminator. You do not change those because nope and yep have identical signatures.

  3. Step 3
    Rebuild, sign, install, and get the flag
    Observation
    I noticed that apktool strips the original signature during rebuild and Android's package manager requires a valid code-signing certificate to accept any APK, which meant I had to generate a debug keystore and re-sign before adb install would succeed.
    Rebuild the patched APK with apktool, create a debug signing keystore with keytool, sign the new APK with jarsigner (or uber-apk-signer), install it on the emulator, and press the button. Because getFlag() now calls yep() instead of nope(), the app returns the flag string on screen.
    bash
    # Rebuild:
    bash
    apktool b three -o recompiled_three.apk
    bash
    # Create a debug keystore (answer the prompts with anything):
    bash
    keytool -genkey -v -keystore debug.keystore -alias debug -keyalg RSA -keysize 2048 -validity 10000
    bash
    # Sign the APK:
    bash
    jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 -keystore debug.keystore recompiled_three.apk debug
    bash
    # Or with uber-apk-signer (simpler):
    bash
    java -jar uber-apk-signer.jar --apks recompiled_three.apk
    bash
    # Install on the running emulator:
    bash
    adb install recompiled_three.apk
    What didn't work first

    Tried: Install the rebuilt APK with adb without signing it first.

    Android's package manager requires every APK to have a valid JAR signature before installation. An unsigned APK from apktool b produces an INSTALL_PARSE_FAILED_NO_CERTIFICATES error from adb. You must sign with jarsigner or uber-apk-signer using any keystore - even a self-generated debug one - before adb install will succeed.

    Tried: Install the signed APK over the existing original app without uninstalling it first.

    If the original three.apk is already installed, Android compares the signing certificate of the update against the original. Your debug keystore has a different certificate than the original release key, so adb returns INSTALL_FAILED_UPDATE_INCOMPATIBLE. Run 'adb uninstall com.hellocmu.picoctf' first, then install the patched APK fresh.

    Learn more

    Every APK installed on Android must carry a valid code-signing certificate. When you rebuild with apktool the original signature is stripped, so you must re-sign before installation. A self-signed debug certificate is fine for an emulator - it just needs to be consistent across installs (so uninstall the original app first if it is already on the emulator).

    Once installed, open the app and press the Flag button. The patched getFlag() now calls yep(), which internally calls the native cilantro() function compiled into the APK, and the flag appears on screen.

Flag

Reveal flag

picoCTF{tis.but.a.scratch}

The flag is revealed by the patched app on screen after pressing the button - no network call needed, the native cilantro() function embedded in the APK computes it locally.

Key takeaway

Android bytecode patching treats an APK as a mutable artifact: apktool disassembles DEX to editable smali text, a one-line change redirects control flow, and a rebuild produces a new installable package. This technique bypasses any in-app check whose outcome depends solely on which branch executes, including license checks, feature gates, and root detection. The same pattern appears in mobile security research, app auditing, and malware analysis where original source is unavailable.

Related reading

Want more picoCTF 2019 writeups?

Useful tools for Reverse Engineering

What to try next