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.
apktooldisassembles DEX into.smalifiles you can edit and recompile. This is the layer where you patch behavior without source. - decompiled Java: tools like
jadxlift 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.

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-snapshotBashPush 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 worksBashWalkthrough / 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.apkBash2. 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 | headBashapktool 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, renameBashSearch 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 methodPlaintextThen 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.apkBash5. 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
};
});JavaScriptSpawn the app under instrumentation so hooks land before any early integrity check runs:
frida -U -f com.example.target -l bypass.js --no-pauseBashFor TLS pinning, the fastest route is a maintained universal script:
frida -U -f com.example.target -l frida-multiple-unpinning.js --no-pauseBashfrida-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.*!*'BashCombine 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. Apatched-signed.apkwill 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/mapsforfrida/gum-js-loop, checking default Frida ports (27042/27043), and looking for thefrida-serverprocess 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
CertificatePinnerornetwork_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
- apktool documentation — https://apktool.org/docs/the-basics/intro
- jadx (skylot/jadx) — https://github.com/skylot/jadx
- Frida documentation — https://frida.re/docs/android/
- HackTricks: Android APK reversing — https://book.hacktricks.xyz/mobile-pentesting/android-app-pentesting
- OWASP MASTG — https://mas.owasp.org/MASTG/
- MITRE ATT&CK for Mobile (T1626, T1540, T1407) — https://attack.mitre.org/matrices/mobile/
- Android Play Integrity API — https://developer.android.com/google/play/integrity
You Might Also Like
If you found this useful, these related deep-dives cover adjacent techniques and their defenses:



Comments