Android APK Reverse Engineering: From APK to Source
Complete guide to decompiling, analyzing, and patching Android applications using jadx, apktool, and smali - covering APK structure, native libraries, manifest analysis, common security flaws, and the full patch-rebuild-resign workflow.
What’s Actually Inside an APK
An APK is just a ZIP archive (with a slightly stricter signing structure than ordinary ZIPs). Before any tooling runs, knowing the layout helps you target the right artefact:
target.apk
├── AndroidManifest.xml ← binary XML - needs apktool to decode
├── classes.dex ← Dalvik bytecode - primary application code
├── classes2.dex, ... ← MultiDex split files
├── resources.arsc ← compiled resources
├── res/
│ ├── drawable*/ ← images
│ ├── layout/ ← UI layouts (binary XML)
│ ├── raw/ ← misc files (configs, certs, models)
│ └── values*/ ← strings, styles, colors
├── assets/ ← raw bundled files (Flutter snapshots, ML models)
├── lib/
│ ├── armeabi-v7a/*.so ← native code per ABI
│ ├── arm64-v8a/*.so
│ ├── x86/, x86_64/
├── META-INF/
│ ├── MANIFEST.MF, *.SF ← v1 (JAR) signing
│ ├── *.RSA / *.DSA / *.EC ← signing certificate
│ └── (v2/v3 sig blocks live in APK Signing Block, not META-INF)
└── stamp-cert-sha256 ← Play Store metadata
Every assessment touches multiple layers - Java source from DEX, native code from lib/, configuration from res/raw and assets/, and authorisation logic from the manifest. Skipping any layer means missing findings.
Step 1: Decompile with jadx
jadx -d output/ target.apk
jadx decompiles DEX bytecode to readable Java source. Search for: API endpoints, hardcoded credentials, encryption keys, certificate pinning implementations, and root detection logic.
jadx Practical Flags
# Continue past errors instead of aborting on the first failed class
jadx --no-debug-info --show-bad-code -d output/ target.apk
# GUI version, much better for navigation and dynamic search
jadx-gui target.apk
# Deobfuscate Proguard-renamed identifiers (heuristic, but catches many)
jadx --deobf --deobf-min 3 --deobf-rewrite-cfg -d output/ target.apk
# Limit decompilation to specific package - much faster on huge apps
jadx --classes-only -d output/ target.apk
What to Search For First
Open jadx-gui, hit Search (Ctrl+Shift+F), and run these regex / keyword passes in order:
http(s)?:// ← API endpoints, embedded URLs
[A-Za-z0-9+/]{40,}=* ← base64 blobs (often encrypted constants)
[0-9a-fA-F]{32,} ← hex constants (keys, hashes, IVs)
"AES" ← cipher selection - leads to key material
SecretKeySpec | KeyGenerator ← runtime-generated keys
SharedPreferences ← potentially insecure local storage
PRAGMA key ← SQLCipher-encrypted databases
api_key | apiKey | API_KEY ← obvious credential constants
debuggable | DEBUG ← debug-mode toggles still in production
TrustManager | HostnameVerifier ← weakened TLS validation
SSLContext\.getInstance ← custom TLS - pinning candidates
addJavascriptInterface ← WebView-to-native bridge - RCE candidate
loadUrl\("javascript: ← JS injection in WebViews
Runtime.exec | ProcessBuilder ← command execution
File.exists.*\/system\/xbin\/su ← root detection
The patterns above identify ~80% of high-impact findings before you even start dynamic analysis.
When jadx Output Looks Wrong
If decompiled methods are full of // JADX WARNING: ... and unreadable, the app likely uses:
- R8 with full mode optimisation - heavy inlining and method merging.
- DexGuard / DashO / Promon Shield - commercial obfuscators with control-flow flattening, string encryption, and reflection-only API access.
- Native-code logic invoked via JNI - the Java side is a thin shim; real logic is in
lib/*.so.
Fallback: open the same DEX in CFR (cfr_0.152.jar), Procyon, or Bytecode Viewer - different decompilers fail on different patterns and side-by-side comparison usually recovers most methods.
Step 2: Disassemble with apktool
apktool d target.apk -o disassembled/
apktool produces smali (Dalvik assembly) and decoded resources. Use this when you need to modify and rebuild the APK.
Why Smali, Not Java
You cannot patch jadx-decompiled Java back into a working APK - the decompilation is lossy. Patching happens at the smali (DEX assembly) layer because smali round-trips losslessly: assemble → DEX → assemble → smali yields identical results.
Smali at a Glance
A snippet of smali looks like:
.method public checkRoot()Z
.registers 4
const-string v0, "/system/xbin/su"
new-instance v1, Ljava/io/File;
invoke-direct {v1, v0}, Ljava/io/File;-><init>(Ljava/lang/String;)V
invoke-virtual {v1}, Ljava/io/File;->exists()Z
move-result v2
return v2
.end method
The corresponding Java is:
public boolean checkRoot() {
File f = new File("/system/xbin/su");
return f.exists();
}
Common Smali Patches
To force the function to always return false, replace its body:
.method public checkRoot()Z
.registers 1
const/4 v0, 0x0 # 0 = false
return v0
.end method
To always return true:
const/4 v0, 0x1
return v0
To skip a code path:
# Original
invoke-direct {p0}, Lcom/app/RootChecker;->isRooted()Z
move-result v0
if-eqz v0, :cond_safe
# rooted path
# Patched - invert the condition
invoke-direct {p0}, Lcom/app/RootChecker;->isRooted()Z
move-result v0
if-nez v0, :cond_safe # NEZ instead of EQZ
Pulling Out Native Libraries
unzip target.apk lib/arm64-v8a/libnative.so -d native/
file native/lib/arm64-v8a/libnative.so
# ELF 64-bit LSB shared object, ARM aarch64
# Disassemble in Ghidra / IDA / Binary Ninja / radare2
r2 -A native/lib/arm64-v8a/libnative.so
When the Java side is a thin wrapper around JNI calls, all the interesting logic - anti-tamper, key derivation, certificate pinning, license validation - lives here. Modern obfuscators like O-LLVM layer on bogus control flow, instruction substitution, and string encryption; budget time accordingly.
Step 3: Analyze the Manifest
AndroidManifest.xml reveals: exported components (activities, services, receivers), permissions, intent filters, backup settings, and debuggable flags.
High-Value Manifest Findings
<!-- ✕ debuggable in production -->
<application android:debuggable="true">
<!-- ✕ allowBackup leaks SharedPrefs/databases via adb backup -->
<application android:allowBackup="true">
<!-- ⚠ exported component with no permission gate -->
<activity android:name=".AdminActivity" android:exported="true"/>
<!-- ⚠ deeplink without intent verification -->
<activity android:name=".LinkHandler">
<intent-filter android:autoVerify="false">
<action android:name="android.intent.action.VIEW"/>
<data android:scheme="https" android:host="example.com"/>
</intent-filter>
</activity>
<!-- ⚠ usesCleartextTraffic permits HTTP -->
<application android:usesCleartextTraffic="true">
<!-- ⚠ network_security_config that disables pinning in debug only -->
<application android:networkSecurityConfig="@xml/network_security_config">
network_security_config.xml Worth Special Attention
<network-security-config>
<base-config cleartextTrafficPermitted="false">
<trust-anchors>
<certificates src="system"/>
</trust-anchors>
</base-config>
<domain-config>
<domain includeSubdomains="true">api.example.com</domain>
<pin-set>
<pin digest="SHA-256">AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</pin>
<pin digest="SHA-256">BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=</pin>
</pin-set>
</domain-config>
<debug-overrides>
<trust-anchors>
<certificates src="user"/>
</trust-anchors>
</debug-overrides>
</network-security-config>
If <debug-overrides> trusts user certificates and the app is debuggable=true (or you can patch it to be), Burp’s user-installed CA is automatically trusted.
Exported Component Auditing
Every exported="true" component is callable by other (potentially malicious) apps on the device. Check:
- Activities: can they be launched without authentication? (e.g.
AdminActivity,WebViewActivityaccepting URL extras) - Services: do they expose sensitive operations via Intents?
- BroadcastReceivers: do they trust extras without sender verification?
- ContentProviders: are read/write permissions enforced? Path-based permissions misconfigured?
adb shell am start -n com.victim/.AdminActivity is the simplest verification.
Step 4: Identify Security Issues
| What to Look For | Where |
|---|---|
| Hardcoded API keys | strings.xml, BuildConfig.java, native libs |
| Insecure storage | SharedPreferences, SQLite, file storage |
| Weak crypto | Custom implementations, ECB mode, hardcoded IVs |
| Certificate pinning | network_security_config.xml, OkHttp CertificatePinner |
| Root detection | Checking /system/xbin/su, SafetyNet, Play Integrity |
The expanded checklist I run on every assessment:
| Category | Specific check |
|---|---|
| Storage | SharedPreferences in plaintext (use EncryptedSharedPreferences); SQLite without SQLCipher; files in getExternalFilesDir() (world-readable); SD card writes; Keystore key without setUserAuthenticationRequired |
| Crypto | Hardcoded keys/IVs in source; ECB mode; static IV across all encryptions; custom MAC implementations; SHA-1 for password hashing; PBKDF2 with iteration count < 10000; key derived from a constant string |
| Networking | Cleartext HTTP allowed; TrustManager overridden to no-op; HostnameVerifier.verify returns true unconditionally; pinning bypassable by network_security_config user override |
| Authentication | Biometric flag trusted client-side; session token stored without expiry; logout doesn’t invalidate token server-side; predictable session identifiers |
| WebView | setJavaScriptEnabled(true) + addJavascriptInterface + loads remote URLs; setAllowFileAccess(true) with file:// loadable; mixed-content allowed |
| IPC | Exported components without permission; broadcast receivers acting on extras without validation; ContentProviders with overly broad URI permissions |
| Deeplinks | URL parameters reflected into WebViews; deeplink handlers performing privileged actions without confirmation; URL schemes without autoVerify/HTTPS App Links |
| Logging | Sensitive data in Log.d/i/e; reachable in production via logcat with READ_LOGS (pre-API 17) or via adb logcat |
| Dynamic loading | DexClassLoader from network-loaded sources; native library loaded from external storage |
Static-Analysis Tooling
Beyond manual review:
- MobSF - best automated first-pass; flags ~70% of common issues.
- APKLeaks - fast secret hunter (API keys, tokens, URLs).
- Quark-Engine - risk scoring per “malicious behaviour” rule set.
- androguard - programmatic API for custom analysis.
- semgrep with mobsfscan rules - pattern-based code scanning with mobile-specific rules.
Step 5: Patch and Rebuild
Modify smali code to bypass checks, rebuild with apktool b, sign with apksigner, install with adb install.
The Full Patch / Rebuild / Resign Cycle
# 1. Decompile
apktool d target.apk -o work/
# 2. Edit smali (e.g., neutralise root detection)
$EDITOR work/smali/com/example/security/RootDetector.smali
# 3. Make the app debuggable + force network_security_config to trust user CA
sed -i 's|<application |<application android:debuggable="true" |' work/AndroidManifest.xml
# 4. Rebuild
apktool b work/ -o patched-unsigned.apk
# 5. Generate a debug keystore (once)
keytool -genkey -v -keystore debug.keystore -alias androiddebugkey \
-keyalg RSA -keysize 2048 -validity 10000 \
-dname "CN=Android Debug,O=Android,C=US" -storepass android -keypass android
# 6. Sign with apksigner (the modern, post-v2 way)
apksigner sign --ks debug.keystore --ks-pass pass:android \
--ks-key-alias androiddebugkey --key-pass pass:android \
--out patched.apk patched-unsigned.apk
# 7. Verify
apksigner verify --verbose patched.apk
# 8. Install
adb install -r -t patched.apk
Common Rebuild Failures and Fixes
- “resource ID is invalid” during apktool b → run
apktool empty-framework-dir --forceand re-decode withapktool if framework-res.apkfirst. - “Couldn’t find android-XX target” → install Android SDK platforms matching the app’s
targetSdkVersion. - App installs but crashes immediately → almost always a smali edit registered the wrong number of registers (
.registers Ndirective). Check the modified method’s local count. - App detects modification at runtime → the APK signature has changed, and the app verifies it. Hook
PackageManager.getPackageInfoto return the original signature, or patch out the verification function. - “INSTALL_PARSE_FAILED_NO_CERTIFICATES” on Android 11+ → the v2 signing scheme is required; use
apksigner(not legacyjarsigner).
apk-mitm for the Common Case
If your only goal is to bypass network_security_config pinning, apk-mitm automates the entire decompile → patch → rebuild → sign cycle with a single command:
apk-mitm target.apk
# Output: target-patched.apk - installable, with permissive nsc and v1+v2 sigs
Worth knowing for fast triage; doesn’t replace manual smali editing for non-trivial bypasses.
Native Library Reverse Engineering
For the increasing number of apps that move sensitive logic into JNI:
# Identify what's exported
nm -D --defined-only lib/arm64-v8a/libnative.so | grep ' T '
# JNI functions are named Java_com_package_Class_method
# e.g. Java_com_example_NativeLib_decryptKey
# Disassemble the function in Ghidra/IDA, look for:
# - JNIEnv* calls (NewStringUTF, GetStringUTFChars) - string handling
# - native crypto (AES, ChaCha20) - key derivation
# - inline obfuscation patterns from O-LLVM (bogus control flow, opaque predicates)
For dynamic analysis, hook native exports with Frida directly:
Interceptor.attach(Module.findExportByName('libnative.so', 'Java_com_example_NativeLib_decryptKey'), {
onEnter: function (args) {
console.log('[+] decryptKey called, JNIEnv=' + args[0]);
},
onLeave: function (ret) {
// ret is a jstring; convert via Java.use('java.lang.String')
}
});
Building a Repeatable Workflow
A repeatable assessment kit I keep:
apk-mitmfor fast pinning bypass during initial triage.apktool+ smali patches for targeted bypasses (root, anti-debug, anti-Frida).jadx-guifor static review with my saved search patterns.MobSFrunning in the background for first-pass automated findings.Frida+Objectionfor runtime hooking once the app launches.- Burp Suite or mitmproxy for network capture.
- Ghidra for native libraries.
A first-time assessment of a moderately complex app takes 1-2 days. With the workflow above, every subsequent app of similar shape collapses to half a day.
APK reverse engineering is the foundation of Android security testing. Master jadx for analysis and apktool for modification - they complement each other. Native libraries and runtime instrumentation are the next frontiers, but the static workflow you build here is the lever for everything else.