Reversing Android Apps: apktool, jadx, and Frida in Practice

Mobile API OSINT
Time it takes to read this article 6 minutes.

Disclaimer: This article is for education and authorized security testing only. Reverse engineering, repackaging, or runtime-manipulating an application you do not own or do not have explicit written permission to test may violate the DMCA, the Computer Fraud and Abuse Act, app-store terms, and local law. Always work against your own builds or apps inside a documented engagement scope.

Introduction

Android apps ship as APKs (or split AABs delivered as APK sets) that bundle compiled Dalvik bytecode, native libraries, and resources. Because the bytecode retains far more structure than stripped native binaries, an Android client is one of the most accessible attack surfaces in a mobile assessment: hardcoded API keys, weak certificate pinning, client-side authorization, and trivially bypassable root or tamper checks all surface quickly. This post walks through the three tools that form the backbone of nearly every Android reversing workflow — apktool for resource and smali disassembly, jadx for readable Java decompilation, and Frida for runtime instrumentation — and closes with the blue-team controls that blunt them.

How It Works / Background

An APK is a ZIP archive. Inside, classes.dex (and classes2.dex, …) hold Dalvik Executable bytecode. That bytecode can be represented in two useful ways:

  • smali: a human-readable assembly syntax for DEX. apktool disassembles DEX into .smali files you can edit and recompile. This is the layer where you patch behavior without source.
  • decompiled Java: tools like jadx lift DEX back to approximate Java. Great for reading logic, terrible for round-tripping (you usually cannot recompile jadx output cleanly).

Frida takes a different path entirely. Instead of modifying the app on disk, it injects a JavaScript engine into the running process and lets you hook methods — intercepting arguments, return values, and control flow at runtime. This avoids re-signing the APK and survives many integrity checks that only validate on-disk files.

Reversing Android Apps: apktool, jadx, and Frida in Practice diagram 1

The diagram shows the two divergent strategies: static patching (smali edit then recompile) versus dynamic instrumentation (Frida hooks against the live process), both converging on a modified-behavior outcome.

Prerequisites / Lab Setup

You need a rooted device or an emulator. Use a Google APIs (non-Play) AVD so you can write to the system partition:

# Tools
brew install apktool jadx          # macOS; or grab JARs from GitHub releases
pip install frida-tools            # frida + frida-ps + frida-trace
sdkmanager "platform-tools" "emulator"

# Spin up a writable emulator
avdmanager create avd -n pentest -k "system-images;android-30;google_apis;x86_64"
emulator -avd pentest -writable-system -no-snapshot
Bash

Push and start the matching frida-server on the device (architecture must match the AVD/device):

adb root && adb remount
adb push frida-server-16.x.x-android-x86_64 /data/local/tmp/frida-server
adb shell "chmod 755 /data/local/tmp/frida-server"
adb shell "/data/local/tmp/frida-server &"
frida-ps -U | head           # -U = USB/emulator; confirms the bridge works
Bash

Walkthrough / PoC

1. Pull and unpack the target

# Find the package, then pull every split APK
adb shell pm list packages | grep target
adb shell pm path com.example.target
adb pull /data/app/.../base.apk ./base.apk
Bash

2. Decode to smali and resources with apktool

apktool d base.apk -o target_src
# Inspect AndroidManifest.xml, network_security_config.xml, and smali/
grep -R "https://" target_src/res target_src/smali | head
Bash

apktool d gives you a decoded AndroidManifest.xml (no longer binary XML), the resource tree, and a smali/ directory. This is where you look for the network security configuration, debuggable flags, and exported components.

3. Read the logic with jadx

For comprehension, jadx is faster than reading smali:

jadx -d target_java base.apk          # batch decompile to ./target_java
jadx-gui base.apk                      # interactive: search, xref, rename
Bash

Search for sinks like Cipher.getInstance, SharedPreferences, OkHttpClient, or strings such as BEGIN CERTIFICATE. jadx's xref ("Find Usage") quickly maps where a hardcoded secret or a checkRoot() method is called.

4. Static patch: defeat a root check via smali

Suppose jadx shows RootBeer.isRooted() gating the app. Locate it in smali and force the return value. In smali, returning false for a boolean method looks like:

.method public isRooted()Z
    .locals 1
    const/4 v0, 0x0      # 0x0 = false
    return v0
.end method
Plaintext

Then recompile, sign, and install:

apktool b target_src -o patched.apk
# Generate a debug keystore once, then sign with apksigner (zipalign first)
zipalign -p 4 patched.apk patched-aligned.apk
apksigner sign --ks ~/.android/debug.keystore \
  --ks-pass pass:android --out patched-signed.apk patched-aligned.apk
adb install -r patched-signed.apk
Bash

5. Dynamic alternative: hook the same check with Frida

No re-signing required. A Frida script that flips the root check and dumps an outgoing token might look like:

Java.perform(function () {
  // 1. Defeat the root check
  var RB = Java.use("com.scottyab.rootbeer.RootBeer");
  RB.isRooted.implementation = function () {
    console.log("[*] isRooted() called -> forcing false");
    return false;
  };

  // 2. Inspect a method's arguments at runtime
  var Auth = Java.use("com.example.target.AuthManager");
  Auth.signRequest.overload("java.lang.String").implementation = function (body) {
    console.log("[+] signRequest body: " + body);
    var out = this.signRequest(body);   // call original
    console.log("[+] signature: " + out);
    return out;                          // tamper here if needed
  };
});
JavaScript

Spawn the app under instrumentation so hooks land before any early integrity check runs:

frida -U -f com.example.target -l bypass.js --no-pause
Bash

For TLS pinning, the fastest route is a maintained universal script:

frida -U -f com.example.target -l frida-multiple-unpinning.js --no-pause
Bash

frida-trace is useful for discovery — it auto-generates stubs for matching methods so you can watch traffic without writing handlers by hand:

frida-trace -U -f com.example.target -j 'javax.net.ssl.*!*'
Bash

Combine Frida-disabled pinning with an intercepting proxy (Burp/mitmproxy) to read the now-decrypted API traffic — exactly where mobile work bleeds into API recon and broader mobile OSINT methodology.

Detection & Defense (Blue Team)

Client-side hardening cannot make an app uncrackable, but it raises cost and produces signals you can act on. Weight these equally with the offensive steps above.

  • Don't trust the client. Treat every API request as hostile. Enforce authorization, rate limits, and anomaly detection server-side. Any secret shipped in the APK is already compromised; rotate keys server-side and scope them tightly.
  • Tamper / re-sign detection. Verify the APK signing certificate at runtime via PackageManager.getPackageInfo(..., GET_SIGNING_CERTIFICATES) and compare against a known hash. A patched-signed.apk will carry a different (debug) certificate. Use the Play Integrity API for an attestation backed by Google rather than self-checks alone.
  • Anti-instrumentation. Detect Frida by scanning /proc/self/maps for frida / gum-js-loop, checking default Frida ports (27042/27043), and looking for the frida-server process or named pipes. Combine with native (NDK) checks so the detection itself is harder to hook.
  • Make static analysis expensive. Ship release builds through R8/ProGuard with aggressive obfuscation; keep secrets out of strings and resources; consider native code or commercial RASP for high-value logic. None of this stops a determined reverser — it buys time and breaks low-effort tooling.
  • Robust certificate pinning. Pin via the OkHttp CertificatePinner or network_security_config.xml, but assume pinning will be bypassed and back it with server-side mutual TLS or signed-request validation.
  • Telemetry. This maps to MITRE ATT&CK for Mobile — T1626 (Abuse Elevation Control / root), T1540 (Code Injection), and T1407 (Download New Code at Runtime). Forward integrity-failure and instrumentation-detection events to your backend so a spike in tampered clients is visible, and fail closed (degrade or block) rather than only logging.

Conclusion

apktool, jadx, and Frida cover the full Android reversing loop: disassemble to smali and recompile, read decompiled Java for comprehension, and hook the live process when on-disk patching is inconvenient or blocked. Static patching wins when you need a redistributable artifact; Frida wins for speed and for defeating checks that only validate files at rest. From the defensive side, the durable lesson is the same one that holds for every thick client: the device belongs to the user, so security decisions must live on the server, with client-side hardening serving as a tripwire rather than a wall.

References

You Might Also Like

If you found this useful, these related deep-dives cover adjacent techniques and their defenses:

Comments

Copied title and URL