Reverse Engineering Android APKs: A Practical Guide for Security Researchers

Reverse engineering Android APKs is the foundation of every mobile security skill. Whether you are doing bug bounty, CTF challenges, or professional pentesting, you will spend more time reading decompiled code than doing anything else. The good news: Android apps decompile well. The bad news: obfuscated apps do not, and you need to know how to handle both.

This guide covers the complete reverse engineering workflow for Android APKs — from initial decompilation to analyzing native libraries — with the tools professionals actually use.

How Android Apps Are Built

Understanding what you are reversing makes the process faster. Android apps are packaged as APK files — ZIP archives containing:

  • classes.dex — compiled Dalvik bytecode (may be multiple: classes2.dex, classes3.dex)
  • AndroidManifest.xml — binary-encoded XML declaring the app structure
  • resources.arsc — compiled resource table
  • res/ — raw resources (layouts, strings, images)
  • lib/ — native libraries (.so files for each ABI)
  • assets/ — raw files bundled with the app (sometimes config, embedded DBs, keys)

The DEX bytecode is compiled from Java or Kotlin source. Decompilers convert it back to Java — not the original source, but something very close. Native libraries (.so files) require a different toolchain because they are compiled ARM/x86 machine code.

Core Tool: jadx

jadx is the best Android APK decompiler available. It converts DEX bytecode directly to readable Java, handles most ProGuard obfuscation gracefully, and includes both a command-line interface and a GUI.

bash — reverse-engineering
# Install jadx (macOS)
$ brew install jadx
 
# Decompile APK to Java source
$ jadx -d output/ app.apk
INFO – loading …
INFO – processing …
INFO – done
[+] Decompiled to output/
 
# Launch GUI for interactive analysis
$ jadx-gui app.apk &

jadx-gui deserves extra attention. Its search functionality is fast across the entire decompiled codebase, it shows cross-references (where is this method called?), and it decodes the binary AndroidManifest into readable XML automatically.

What to Look For First

After decompiling, start with these high-value searches in jadx-gui (Ctrl+Shift+F for global search):

bash — apk-analysis
# High-value searches in decompiled code
$ grep -r “password\|SECRET\|api_key\|TOKEN” output/ -i
output/com/app/BuildConfig.java: String API_KEY = “sk-prod-8×2…”;
output/com/app/Config.java: static String BASE_URL = “http://api.example.com”;
 
$ grep -r ‘exported=”true”‘ output/resources/AndroidManifest.xml
[+] Found exported activity — potential entry point

apktool: When You Need Smali

apktool disassembles APKs to smali — a human-readable representation of Dalvik bytecode. jadx gives you Java; apktool gives you smali. When are they different?

  • When jadx fails to decompile correctly (obfuscated or unusual bytecode)
  • When you need to repackage the app (modify and rebuild)
  • When you need to patch a method — easier to edit smali than fight with jadx output
# Disassemble APK to smali
apktool d app.apk -o app_smali/

# Rebuild after modifications
apktool b app_smali/ -o app_patched.apk

# Sign the rebuilt APK (required for installation)
keytool -genkey -v -keystore test.keystore -alias test -keyalg RSA -keysize 2048 -validity 10000 -storepass testpass -keypass testpass -dname "CN=Test"
apksigner sign --ks test.keystore --ks-pass pass:testpass --out app_signed.apk app_patched.apk

# Install
adb install app_signed.apk

Common patching use case: disable root detection or certificate pinning by replacing a method return value in smali. Find the relevant method in jadx (identifies class and method name), open the same file in smali output, and change the return value or patch the conditional.

Reading the AndroidManifest

The AndroidManifest is the most important file in any app for security analysis. It defines the entire app structure and what parts are accessible to other apps.

# Read decoded manifest (jadx outputs this automatically)
cat output/resources/AndroidManifest.xml

# Key things to look for:
# 1. Exported components (accessible from other apps/ADB)
grep -i 'exported="true"' AndroidManifest.xml

# 2. Intent filters (which intents does each component handle?)
grep -A 5 'intent-filter' AndroidManifest.xml

# 3. Deep links (custom URL schemes)
grep -i 'scheme' AndroidManifest.xml

# 4. Permissions (what can the app do?)
grep 'uses-permission' AndroidManifest.xml

# 5. Backup allowed (can app data be extracted via ADB backup?)
grep 'android:allowBackup' AndroidManifest.xml

A component marked exported="true" with no android:permission attribute can be triggered by any app on the device — including a malicious app, or directly via ADB during testing.

Dealing with Obfuscated Code

ProGuard (and its successor R8) rename classes, methods, and fields to single letters. You will see output like this:

// Obfuscated class - everything renamed to letters
public class a {
    private b c;
    private String d;

    public void a(b bVar, String str) {
        this.c = bVar;
        this.d = str;
    }

    public String b() {
        return a.a(this.d);
    }
}

Strategies for navigating obfuscated code:

  • Follow strings — hardcoded strings survive obfuscation. Search for interesting strings and trace backward to find what code uses them.
  • Follow method names — class names get obfuscated but Android framework methods do not. onCreate, onResume, onClick remain intact.
  • Rename in jadx-gui — when you understand what a class does, rename it in jadx-gui (right-click → Rename). This persists across the session and makes navigation much easier.
  • Dynamic tracing — attach Frida and trace all method calls in a package. The runtime shows you which obfuscated methods are invoked during specific actions.

Native Library Analysis with Ghidra

When apps implement security logic in native code (.so files in the lib/ directory), you need a different tool. Ghidra is the standard — it is free, developed by the NSA, and handles ARM/AArch64 binaries well.

# Extract native libraries from APK
unzip app.apk "lib/*" -d app_libs/

# Libraries are organized by ABI
ls app_libs/lib/
# arm64-v8a/  armeabi-v7a/  x86/  x86_64/

# Use arm64-v8a for modern devices
file app_libs/lib/arm64-v8a/libnative.so

In Ghidra:

  1. Create a new project, import the .so file
  2. Let auto-analysis run (takes 1-5 minutes depending on library size)
  3. Check the Symbol Tree for exported functions — these are callable from Java via JNI
  4. Search Functions for names containing “check”, “verify”, “validate”, “pin”, “ssl”, “cert”
  5. Use the Decompiler view to read C pseudocode for complex functions

JNI functions follow a naming pattern: Java_com_package_name_ClassName_methodName. These are the bridge between the Java layer (visible in jadx) and the native implementation. Start your analysis here.

Dynamic Analysis with Frida

Static analysis tells you what the code should do. Dynamic analysis tells you what it actually does at runtime. For encrypted strings, runtime values, and control flow decisions that depend on device state, you need Frida.

# Trace all method calls in a package (useful for obfuscated code)
frida-trace -U -f com.target.app -j "com.target.app.*!*"

# Hook a specific method and log arguments
frida -U -f com.target.app -l hook.js

# hook.js - example: log calls to any method named "verify"
Java.perform(function() {
    Java.enumerateLoadedClasses({
        onMatch: function(className) {
            try {
                var clazz = Java.use(className);
                var methods = clazz.class.getDeclaredMethods();
                methods.forEach(function(method) {
                    if (method.getName().toLowerCase().includes("verify") ||
                        method.getName().toLowerCase().includes("check") ||
                        method.getName().toLowerCase().includes("validate")) {
                        console.log("[*] Found: " + className + "." + method.getName());
                    }
                });
            } catch(e) {}
        },
        onComplete: function() {}
    });
});

Practical Workflow: Analyzing an Unknown App

Here is the systematic approach to analyzing any Android APK from scratch:

  1. Structure scanunzip -l app.apk to understand what is inside. Note any native libraries, unusual asset files, multiple DEX files.
  2. Manifest review — decompile with apktool, read AndroidManifest. Document all exported components, permissions, and deep link schemes.
  3. Java decompilation — open in jadx-gui, search for high-value patterns (strings, dangerous methods). Map the app architecture: Activities, Services, networking classes.
  4. Network layer — find how the app makes HTTP requests. Is it OkHttp? Retrofit? Custom? Where are API endpoints defined? What authentication is used?
  5. Data storage — find SharedPreferences, SQLite databases, file storage. What is stored and is any of it sensitive?
  6. Native analysis — if .so files exist, identify JNI functions and analyze security-relevant ones in Ghidra.
  7. Dynamic confirmation — attach Frida, confirm your static analysis hypotheses, capture runtime values for encrypted or computed strings.

Using Djini.ai for API Attack Surface

Once you understand the app structure from static analysis, the API endpoints you identified need to be tested at runtime. Djini.ai takes the list of endpoints you found (from hardcoded strings, network configurations, decompiled API client code) and automatically tests for authentication bypass, authorization issues, and injection vulnerabilities across the entire surface area.

The combination works well in practice: jadx for static reconnaissance and endpoint discovery, Djini.ai for automated API security testing, Frida for dynamic confirmation of specific findings.

Practice Targets

The fastest way to get good at Android reverse engineering is repetition on real apps. Start with intentionally vulnerable apps before moving to real targets:

  • OWASP MSTG Crackmes — progressively harder APKs, explicit challenge of reverse engineering the secret
  • InjuredAndroid — 18+ flags, each requiring different analysis techniques
  • Mobile Hacking Lab free labs — structured challenges with real vulnerable applications, guided reverse engineering objectives
  • HackTheBox Android challenges — unguided challenges with real-world obfuscation and complexity

After 20-30 practice APKs, the pattern recognition kicks in. You will stop spending time on structural exploration and start spending time on the actual security-relevant code. That efficiency is when real penetration testing becomes productive.


Go hands-on with Android reverse engineering. Mobile Hacking Lab labs give you real vulnerable apps to analyze, structured objectives, and a browser-based environment — no setup required. Start free at mobilehackinglab.com.