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:
-
Enable USB Debugging in Developer Options.
-
Check your device:
adb devices -
Install the APK:
adb install minions.apk -
Launch the main activity (found in
AndroidManifest.xml):
adb shell am start -n com.example.picoctfimage/.MainActivity
The
AndroidManifest.xmldeclares 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 referenceR.string.Bananaor the numeric ID directly.res/values/public.xmlmaps resource names ↔ IDs. Searching resource files is often the fastest way to find strings in CTF APKs.
Understanding Smali Files
- Smali files are like assembly for the Dalvik VM (the virtual CPU on Android). Most APKs are written in Java, Kotlin, or Swift (high-level languages), compiled to
.classfiles (Java bytecode), then converted into.dexfiles (Dalvik bytecode). * Tools like apktool disassemble.dexinto human-readable.smalifiles. Typical folder structures in smali:
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
-
apktool(for decoding) -
JADX (GUI)for decompiled Java view -
strings.xmlinspection (text editor /grep/Select-String) -
Postman(to test the web endpoint) -
An Android device / emulator(to run the app when needed)
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:

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:
-
Username:johnson -
Password:tricky1990 -
OTP:9673
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:
-
Primary vulnerability:Secret values (credentials and OTP) were hard-coded in the APK resources and code. OTP check happened client-side (R.string.otp_value) which is trivially discoverable by decompilation. -
Secondary step:The server endpoint verify-otp accepted POSTs and returned a JSON containing the first half of the flag — you can query the API directly with Postman or curl once you know the OTP. -
Pro-tip:Always inspect res/values/strings.xml and smali/JADX output for sensitive data. Many CTF mobile challenges intentionally hide secrets in resources.
💡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.