I recently enjoyed picoCTF’s picoMini by CMU Africa, particularly the reverse engineering challenges.

Here’s a detailed walkthrough of the challenges I solved.

M1n10n’5_53cr37


We were given an APK file. The goal was to “unpack” and inspect the app’s source to find interesting strings.

Decompiling the APK


I used apktool to decode the APK:

apktool d minions.apk -o decoded_app

Later, I explored the app in JADX GUI and ran it on my phone using ADB.

Using ADB (Android Debug Bridge)


ADB is a versatile command-line tool that lets you communicate with Android devices.

It consists of three components:

Client: Sends commands from your development machine (adb command in terminal).

Daemon (adbd): Runs on the device to execute commands.

Server: Manages communication between the client and daemon.

To get started:

  1. Enable USB Debugging in Developer Options.

  2. Check your device: adb devices

  3. Install the APK: adb install minions.apk

  4. Launch the main activity (found in AndroidManifest.xml):
    adb shell am start -n com.example.picoctfimage/.MainActivity

The AndroidManifest.xml declares components, activities, and intents, and is mandatory for signing and is mandatory for signing and distributing an APK. Checking it helps identify activity names and entry points.

Observing the App


Running the app on the phone, I saw a UI that mentioned:

“The banana value is interesting.” Clearly, “banana” was a hint.”

Searching for the Banana String


Using JADX, I searched for "Banana" in the com directory. One method, C0547R, looked like it contained numeric resource initialization. APK resources are usually stored in res/values/*.xml. Searching for Banana in PowerShell:

PS C:\decoded_app> Select-String -Path res\values\*.xml -Pattern "Banana"

Output:

res\values\public.xml:3780: 
<public type="string" name="Banana" id="0x7f0f0000" /> 
res\values\strings.xml:3: 
<string name="Banana">OBUWG32DKRDHWMLUL53TI43OG5PWQNDSMRPXK3TSGR3DG3BRNY4V65DIGNPW2MDCGFWDGX3DGBSDG7I=</string>`

💡Note: Android resource IDs are numeric in code (e.g., 0x7f0f0000). Decompiled Java code code may reference R.string.Banana or the numeric ID directly. res/values/public.xml maps resource names ↔ IDs. Searching resource files is often the fastest way to find strings in CTF APKs.

Understanding Smali Files


com/google/firebase/... → Firebase SDK 
androidx/... → Android libraries 
com/example/... → Developer’s own code

Decoding the String


The value of Banana looked like Base64 but gave gibberish when decoded. Trying Base32 yielded the flag:

picoCTF{<redacted-hehe>}

🏁 Takeaways


Resource IDs: Numeric IDs in code often correspond to human-readable strings in res/values/*.xml.

Smali: Acts as an assembly-like layer for the Dalvik VM; essential for low-level APK analysis and patching.

JADX vs Smali: Use JADX for readability, smali for accuracy and obfuscated code.

Decoding: Always check multiple encodings (Base32, Base64, hex).


Pico Bank


This challenge was hard not in a sense of actual reverse engineering but in sense of my lack of common sense haha!

Decompiling the APK


Tools used

I used apktool to decode the APK:

apktool d minions.apk -o decoded_app

Inside the Login class (viewed in JADX) it checks hard-coded credentials:

usernamepassword

okay we see this logic:

public void onClick(View v) {
    String username = Login.this.usernameEditText.getText().toString();
    String password = Login.this.passwordEditText.getText().toString();
    if ("johnson".equals(username) && "tricky1990".equals(password)) {
                    Intent intent = new Intent(Login.this, (Class<?>) OTP.class);
                    Login.this.startActivity(intent);
                    Login.this.finish();
                    return;
    }
    Toast.makeText(Login.this, "Incorrect credentials", 0).show();
}

now, double clicking on member OTP, we get another class of OTP:

Understanding Code

public class OTP extends AppCompatActivity {
    private EditText otpDigit1;
    private EditText otpDigit2;
    private EditText otpDigit3;
    private EditText otpDigit4;
    private RequestQueue requestQueue;
    private Button submitOtpButton;
...
}

and,

String otp = otpDigit1.getText().toString()
           + otpDigit2.getText().toString()
           + otpDigit3.getText().toString()
           + otpDigit4.getText().toString();
verifyOtp(otp);
...

and,

String endpoint = "your server url/verify-otp";
if (getResources().getString(R.string.otp_value).equals(otp)) {
    Intent intent = new Intent(this, MainActivity.class);
    startActivity(intent);
    finish();
} else {
    Toast.makeText(this, "Invalid OTP", 0).show();
}
...
// build JSON and send POST; onResponse checks response.getBoolean("success")
// and if true extracts "flag" and "hint" from the JSON and starts MainActivity with extras

here, im seeing its checking our OTP (otp) input against some hardcoded otp value (otp_value):

so,

PS C:\pico-decoded> Select-String -Path res\values\*.xml -Pattern "otp_value"

res\values\public.xml:4051:    <public type="string" name="otp_value" id="0x7f0f009b" />
res\values\strings.xml:158:    <string name="otp_value">9673</string>

Result:

Exploit Startegy

The source used your server url/verify-otp as the POST endpoint. I was initially confused about which server to call. Instead of trying to brute-force random URLs inside the APK, I checked the Pico Bank web app to see what endpoint the site exposes.

and web app showed me:

Cannot GET /verify-otp

That suggested /verify-otp exists but expects a POST (common with REST APIs). So I switched to Postman and POSTed a JSON body:

lets open postman:

okay its working, its time to pass that otp in body parameter we found before:

we got half of the flag and for second half it asking for running the apk, by previous steps I ran the apk in my mobile:

hint is look at transaction history, well i do see unsual binary number as amount so choosing first 4 binary number i got pico and eventually the first half of the flag:

picoCTF{...redacted---

🏁Takeaways:


💡What I did wrong (and how to do better): I initially tried to brute-force or re-sign/rebuild the APK to force server behavior. That’s overcomplicating things, first check resources and strings, then test the backend with the correct HTTP verb. Always try the simplest approach (readable resource files and decompiled code) before heavy-handed dynamic approaches.