Tutorial

APK Structure, Decompilation, and Static Analysis

Tear apart an Android APK, the manifest, DEX bytecode, resources, and native libraries, and learn to spot vulnerabilities without running the app.

2 min read beginner
Embedded Android Bug Bounty Static Analysis APK

Prerequisites

  • Completed the Android Bug Bounty Lab Setup tutorial
  • Basic Java or Kotlin reading comprehension

Part 2 of 5 in Android Bug Bounty: Zero to Zero-Day

Table of Contents

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.apk
Archive:  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.RSA

The 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.RSA

The 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.xml

The 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 without android: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 via adb backup; on current releases adb backup is 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.apk

The 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_READABLE and MODE_WORLD_WRITABLE were deprecated in API 17 and made fatal for apps targeting API 24 (Android 7.0) or higher — the call throws SecurityException at 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 whose targetSdkVersion is artificially low. Always check targetSdkVersion from 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 files

Embedded 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-mobsf

Upload 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

  1. Decompile InsecureBankv2 with jadx and find at least three hardcoded credentials or secrets in the source code.

  2. Manifest audit. Pick any app installed on your emulator, pull it with adb shell pm path com.package.name and adb pull, then analyze the AndroidManifest.xml. List all exported components and categorize them by type (activity, service, receiver, provider).

  3. 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?

  4. 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.