This tutorial teaches you to take apart an Android application and read its source code without running it. Static analysis is the first step in any bug bounty engagement: you learn what the app does, what it exposes, and where to focus your dynamic testing. By the end you’ll be comfortable navigating decompiled code and spotting common vulnerability patterns.
What’s Inside an APK?
An APK is a ZIP archive with a specific structure. You can verify this with unzip -l:
unzip -l target.apkArchive: target.apk
Length Date Time Name
--------- ---------- ----- ----
1org 2026-01-15 10:23 AndroidManifest.xml
3847292 2026-01-15 10:23 classes.dex
294821 2026-01-15 10:23 classes2.dex
12083 2026-01-15 10:23 resources.arsc
0 2026-01-15 10:23 res/
483291 2026-01-15 10:23 lib/arm64-v8a/libnative.so
0 2026-01-15 10:23 assets/
2948 2026-01-15 10:23 META-INF/CERT.RSAThe key components:
target.apk (ZIP archive)
├── AndroidManifest.xml Binary XML — app metadata, components, permissions
├── classes.dex Dalvik bytecode — compiled Java/Kotlin code
├── classes2.dex Overflow DEX (multidex apps)
├── resources.arsc Compiled resource table
├── res/ Layouts, drawables, XML configs
│ ├── layout/
│ ├── xml/ Network security config, preferences
│ └── values/ strings.xml, arrays.xml
├── lib/ Native libraries per architecture
│ ├── arm64-v8a/
│ ├── armeabi-v7a/
│ └── x86_64/
├── assets/ Raw files bundled with the app
└── META-INF/ Signing information
├── MANIFEST.MF
├── CERT.SF
└── CERT.RSAThe AndroidManifest.xml inside the ZIP is binary-encoded: you can’t read it with a text editor. That’s where apktool comes in.
The Android Manifest
Decode the APK with apktool to get a human-readable manifest:
apktool d target.apk -o target_decoded/
cat target_decoded/AndroidManifest.xmlThe manifest is the single most important file for attack surface analysis. Here’s what to look for:
Exported Components
Components with android:exported="true" are accessible to other apps, including an attacker’s app:
<!-- Dangerous: admin panel accessible to any app -->
<activity
android:name=".admin.AdminPanelActivity"
android:exported="true" />
<!-- Dangerous: service with no permission requirement -->
<service
android:name=".sync.DataSyncService"
android:exported="true" />Warning
Implicit Exports
Any component with an
<intent-filter>is implicitly exported, even withoutandroid:exported="true". On apps targeting Android 12+ (API 31), this must be declared explicitly, but older apps still have this behavior.
Debuggable and Backup Flags
<!-- Each of these can matter during review -->
<application
android:debuggable="true"
android:allowBackup="true"
android:usesCleartextTraffic="true">debuggable="true": allows attaching a debugger to the app’s process. Should never ship in production.allowBackup="true": opts the app into Android backup/restore flows. On older Android versions this could expose app data viaadb backup; on current releasesadb backupis deprecated/removed, so treat this as contextual signal rather than a universal data-extraction primitive.usesCleartextTraffic="true": allows HTTP (unencrypted) connections.
Permissions and Protection Levels
<!-- Custom permission with weak protection -->
<permission
android:name="com.target.app.ADMIN_ACCESS"
android:protectionLevel="normal" />Protection levels matter: normal permissions are granted automatically at install. dangerous requires user approval. signature restricts to apps signed with the same key. If a sensitive operation is guarded by a normal permission, any app can access it.
Decompiling DEX to Java with jadx
jadx recovers Java source from DEX bytecode:
# CLI — decompile to a directory
jadx -d output/ target.apk
# GUI — interactive browsing
jadx-gui target.apkThe output mirrors a Java project structure:
output/
├── sources/
│ └── com/
│ └── target/
│ └── app/
│ ├── MainActivity.java
│ ├── api/
│ │ ├── ApiClient.java
│ │ └── AuthService.java
│ ├── admin/
│ │ └── AdminPanelActivity.java
│ └── utils/
│ └── CryptoHelper.java
└── resources/
├── AndroidManifest.xml
└── res/Note
Obfuscation
Most production apps use ProGuard or R8 to shrink and obfuscate code. You’ll see single-letter class names (
a.b.c), renamed methods, and encrypted strings. This slows analysis but doesn’t stop it: the control flow and API calls are still visible. jadx handles obfuscated code well; just expect more detective work.
Static Analysis Checklist
Once you have decompiled source, search systematically for these vulnerability patterns. Each entry includes a grep pattern and an example of what the vulnerable code looks like.
Hardcoded Secrets
grep -ri "api_key\|apikey\|secret\|password\|token\|bearer" output/sources/// Vulnerable: hardcoded API key
public static final String API_KEY = "AIzaSyD-XXXXXXXXXXXXXXXXXXXXXXXXXXXX";
private static final String DB_PASSWORD = "supersecret123";Insecure Cryptography
grep -ri "ECB\|DES\|MD5\|hardcoded.*iv\|static.*iv" output/sources/// Vulnerable: ECB mode (no diffusion — identical plaintext blocks produce identical ciphertext)
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
// Vulnerable: static IV (defeats the purpose of CBC mode)
byte[] iv = "1234567890123456".getBytes();
IvParameterSpec ivSpec = new IvParameterSpec(iv);SQL Injection
grep -rn "rawQuery\|execSQL" output/sources/ | grep -i "+"// Vulnerable: string concatenation in SQL query
String query = "SELECT * FROM users WHERE name = '" + userInput + "'";
db.rawQuery(query, null);Sensitive Data in Logs
grep -rn "Log\.\(d\|e\|i\|v\|w\)" output/sources/ | grep -i "token\|password\|session\|key"// Vulnerable: logging auth tokens
Log.d("Auth", "User token: " + authToken);WebView Misconfigurations
grep -rn "setJavaScriptEnabled\|addJavascriptInterface\|setAllowFileAccess" output/sources/// Vulnerable: JavaScript enabled with a JavaScript interface
webView.getSettings().setJavaScriptEnabled(true);
webView.addJavascriptInterface(new BridgeObject(), "AndroidBridge");Insecure Network Configuration
grep -rn "http://\|TrustManager\|AllowAllHostnameVerifier\|ALLOW_ALL" output/sources/// Vulnerable: trust manager that accepts any certificate
TrustManager[] trustAll = new TrustManager[] {
new X509TrustManager() {
public void checkClientTrusted(X509Certificate[] chain, String type) {}
public void checkServerTrusted(X509Certificate[] chain, String type) {}
public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; }
}
};Insecure File Permissions
grep -rn "MODE_WORLD_READABLE\|MODE_WORLD_WRITABLE\|openFileOutput" output/sources/// Vulnerable: world-readable file
FileOutputStream fos = openFileOutput("config.txt", Context.MODE_WORLD_READABLE);Note
Modern targets crash here
MODE_WORLD_READABLEandMODE_WORLD_WRITABLEwere deprecated in API 17 and made fatal for apps targeting API 24 (Android 7.0) or higher — the call throwsSecurityExceptionat runtime. Finding these constants in a current APK still indicates a real bug class, but the call site is usually in a stale module gated behind a feature flag, an SDK component the developer didn’t realise still ships with these flags, or an app whosetargetSdkVersionis artificially low. Always checktargetSdkVersionfrom the manifest before triaging severity.
Resources and Assets
Don’t skip the non-code parts of the APK.
String Resources
cat output/resources/res/values/strings.xml | grep -i "url\|endpoint\|api\|server\|host"Hidden API endpoints, staging server URLs, and debug flags often live in strings.xml or build_config.xml.
Network Security Config
cat output/resources/res/xml/network_security_config.xml<!-- Finding: cleartext traffic allowed to specific domains -->
<network-security-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">api.target.com</domain>
</domain-config>
<!-- Finding: custom trust anchor (user-installed CAs trusted) -->
<base-config>
<trust-anchors>
<certificates src="user" />
<certificates src="system" />
</trust-anchors>
</base-config>
</network-security-config>If <certificates src="user" /> is present, the app trusts user-installed CA certificates, which means Burp interception works without certificate pinning bypass.
Embedded Databases and Configs
ls output/resources/assets/
# Look for: .db, .sqlite, .json, .xml, .properties filesEmbedded SQLite databases, JSON configs, and properties files sometimes contain credentials, encryption keys, or internal API documentation.
Native Libraries
List the native libraries and their architectures:
unzip -l target.apk | grep "\.so$"Quick triage without Ghidra:
# Use the libraries extracted by apktool
find target_decoded/lib -name "*.so"
# List exported JNI symbols
readelf --dyn-syms target_decoded/lib/arm64-v8a/libnative.so | grep "Java_"
# Extract readable strings
strings target_decoded/lib/arm64-v8a/libnative.so | grep -i "http\|key\|pass\|error\|/api/"If you only used jadx and do not have target_decoded/, extract the APK first with apktool or unzip so the lib/<arch>/ directory exists on disk. JNI functions follow the naming pattern Java_com_package_Class_method. If you find exported JNI functions, note them: Tutorial 5 covers native code reversing in depth with Ghidra.
Automated Scanning with MobSF
Mobile Security Framework (MobSF) automates many of the checks above:
# Run MobSF with Docker
docker run -it --rm -p 8000:8000 opensecurity/mobile-security-framework-mobsfUpload an APK through the web interface at http://localhost:8000. MobSF produces a report covering:
- Manifest analysis (exported components, permissions, flags)
- Code analysis (hardcoded secrets, crypto issues, SQL injection)
- Network security configuration review
- Binary analysis of native libraries
Note
MobSF vs Manual Analysis
MobSF is excellent for initial triage and catching low-hanging fruit. But it produces false positives, misses context-dependent bugs, and can’t follow complex logic flows. Use it as a starting point, not a replacement for manual review. The bugs that pay well in bug bounties are almost always the ones automated tools miss.
Exercises
-
Decompile InsecureBankv2 with jadx and find at least three hardcoded credentials or secrets in the source code.
-
Manifest audit. Pick any app installed on your emulator, pull it with
adb shell pm path com.package.nameandadb pull, then analyze the AndroidManifest.xml. List all exported components and categorize them by type (activity, service, receiver, provider). -
Find the network security config in a decompiled APK. Does it allow cleartext traffic? Does it trust user-installed CA certificates? What domains are in scope?
-
MobSF comparison. Run MobSF against a sample APK. Compare its automated findings to your manual analysis: what did it catch that you missed? What did you find that it didn’t?
What’s next
In the next tutorial, Dynamic Analysis with Frida, we’ll move from reading code to running it: hooking methods at runtime, bypassing security controls, and observing the app’s behavior live on a device.