Android APK Reversing
Decompile, analyze, patch, and hook Android APKs in CTF reversing challenges.
Android APK Reversing
Android APK reversing challenges ask you to extract a flag from an Android application — usually hidden in logic, encoded in resources, or protected by a license check you need to bypass. The good news: APK decompilation with jadx gives near-original Java source code, making Android reversing significantly easier than native binary reversing.
APK Structure
An APK is a ZIP file. Unzip it to see its contents:
unzip app.apk -d app_contents/
ls app_contents/
# AndroidManifest.xml ← permissions, activities, exported components
# classes.dex ← compiled Dalvik bytecode (main code)
# classes2.dex ← additional DEX files (large apps)
# res/ ← resources (layouts, strings, images)
# assets/ ← raw files bundled with app
# lib/ ← native .so libraries (arm64-v8a, x86, etc.)
# META-INF/ ← signature filesStep-by-Step Analysis
First Recon
file app.apk
strings app.apk | grep -iE "(flag|ctf|secret|key|password)"
# Check strings.xml (often has hardcoded values)
unzip app.apk res/values/strings.xml -d /tmp/
cat /tmp/res/values/strings.xml | grep -i "flag\|key\|secret"Decompile with JADX
JADX decompiles DEX bytecode to readable Java:
sudo apt install jadx
# CLI decompilation
jadx app.apk -d ./output/
# GUI (recommended for browsing)
jadx-gui app.apkIn JADX GUI:
- Navigate to
com.example.app.MainActivity - Search (
Ctrl+F) for:flag,secret,password,key,check,verify - Right-click → Find Usage to trace how a variable flows
- Check
AndroidManifest.xmlfor exported components or unusual permissions
Decompile with apktool (For Smali/Resources)
apktool d app.apk -o app_decoded/
# Key files:
# app_decoded/smali/com/example/app/MainActivity.smali
# app_decoded/res/values/strings.xml
# app_decoded/AndroidManifest.xmlCheck native libraries
# If the app has lib/ directory with .so files
file app_decoded/lib/arm64-v8a/libflag.so
# Analyze with GhidraCheck assets/ for hidden files
ls app_decoded/assets/
strings app_decoded/assets/* | grep -i flagUnderstanding Smali
Smali is the intermediate representation of Dalvik bytecode:
# Java: int result = a + b;
# Smali:
add-int v0, v1, v2 # v0 = v1 + v2
# Java: if (flag.equals("CTF{correct}")) { ... }
# Smali:
const-string v1, "CTF{correct}"
invoke-virtual {v0, v1}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z
move-result v2
if-eqz v2, :cond_0 # if false, jump to cond_0Common patterns:
if-eqz= if equal zero (false) → jumpif-nez= if not equal zero → jumpconst/4 v0, 0x1= set v0 to 1 (true)const/4 v0, 0x0= set v0 to 0 (false)
Patching an APK (Smali Modification)
-
Find the check in Smali — look for
const/4 v0, 0x0followed byreturn v0 -
Change to return true —
const/4 v0, 0x1 -
Repackage:
apktool b app_decoded/ -o app_patched.apk -
Sign the APK:
keytool -genkey -v -keystore my.keystore -alias mykey \ -keyalg RSA -keysize 2048 -validity 10000 jarsigner -verbose -sigalg SHA256withRSA -digestalg SHA-256 \ -keystore my.keystore app_patched.apk mykey -
Install:
adb install -r app_patched.apk
Dynamic Analysis with Frida
Frida lets you hook and modify app behavior at runtime without modifying the APK.
pip3 install frida-tools
# Start frida-server on device
adb push frida-server /data/local/tmp/
adb shell "chmod +x /data/local/tmp/frida-server && /data/local/tmp/frida-server &"
# List running apps
frida-ps -U
# Hook a function
frida -U -n "com.example.app" -l hook.jsExample Frida script — bypass license check:
Java.perform(() => {
let MainActivity = Java.use("com.example.app.MainActivity");
MainActivity.isLicenseValid.implementation = function () {
console.log("[*] isLicenseValid() hooked → returning true");
return true;
};
MainActivity.checkFlag.implementation = function (input) {
console.log("[*] checkFlag called with: " + input);
let result = this.checkFlag(input);
console.log("[*] Result: " + result);
return result;
};
});Hook native library function:
Interceptor.attach(Module.findExportByName("libflag.so", "verifyFlag"), {
onEnter(args) {
console.log("[*] arg0 (string):", args[0].readCString());
},
onLeave(retval) {
retval.replace(1); // force return 1 (success)
},
});Common CTF APK Patterns
| Pattern | Where to Look | Tool |
|---|---|---|
| Hardcoded flag | strings.xml, constant strings in Java | JADX, strings |
| Flag in logic | checkAnswer(), verifyFlag(), comparison | JADX → trace logic |
| Encoded flag | base64/XOR in static initializer | JADX + CyberChef |
| Native library | lib/arm64-v8a/*.so | Ghidra, JADX |
| Certificate pinning bypass | OkHttp, SSLContext classes | Frida |
| Root check bypass | isRooted(), RootBeer class | Frida |
ADB Useful Commands
adb install app.apk
adb pull /data/data/com.example.app/files/flag.txt ./flag.txt
adb shell
adb logcat | grep -i "flag\|secret\|error"
adb forward tcp:8080 tcp:8080Checklist
-
strings app.apk | grep -i flag(quick win) - Check
res/values/strings.xml - Open in JADX-GUI, search for "flag", "secret", "key", "check"
- Read
AndroidManifest.xmlfor exported components - Check native libraries in
lib/with Ghidra - Patch with apktool if logic check needs bypassing
- Use Frida to hook at runtime (no modification needed)
- Check
assets/for hidden files
Last updated on