Every app on your phone is making network requests, processing data, and executing logic that you can’t see. As developers, understanding what’s happening behind the scenes isn’t just curiosity — it’s a critical skill for security research, debugging, and building better software.
This post covers my approach to Android reverse engineering using tools I use daily: Frida for runtime instrumentation, mitmproxy for traffic interception, and Jadx for static analysis. Everything here is for educational purposes — understanding how apps work so you can build more secure ones.
The Toolkit #
Before diving in, here’s the stack:
| Tool | Purpose |
|---|---|
| Frida | Dynamic instrumentation — hook functions at runtime |
| mitmproxy | Intercept and inspect HTTPS traffic |
| Jadx | Decompile APKs to readable Java/Kotlin source |
| Objection | Runtime mobile exploration, built on Frida |
| ADB | Android Debug Bridge — communicate with devices |
| Genymotion | Android emulator optimized for testing |
These tools complement each other. Jadx gives you the static picture — what the code looks like. Frida gives you the dynamic picture — what the code actually does at runtime. mitmproxy shows you what’s going over the wire.
Static Analysis with Jadx #
Every RE session starts with static analysis. Before you run anything, understand the codebase.
Decompiling the APK #
Pull the APK from a device or download it, then open it in Jadx:
# Pull APK from connected device
adb shell pm path com.example.app
adb pull /data/app/.../base.apk ./target.apk
# Open in Jadx GUI
jadx-gui target.apkJadx decompiles DEX bytecode back to Java source. It’s not perfect — obfuscated apps produce mangled class names like a.b.c.d — but it’s usually readable enough to understand the architecture.
What to Look For #
When I open an APK in Jadx, I focus on a few things:
1. Network layer — Find the HTTP client configuration. Most apps use OkHttp or Retrofit. Search for OkHttpClient, Interceptor, or Retrofit.Builder:
// Common pattern: custom OkHttp interceptor adding auth headers
public class AuthInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) {
Request original = chain.request();
Request request = original.newBuilder()
.header("Authorization", "Bearer " + getToken())
.header("X-Device-Id", getDeviceId())
.build();
return chain.proceed(request);
}
}This tells you what headers the app sends, how auth works, and what custom interceptors modify requests.
2. Certificate pinning — Search for CertificatePinner, TrustManager, or SSL. If the app pins certificates, you’ll need to bypass this before mitmproxy can intercept traffic.
3. Encryption/signing — Look for request signing logic. Many apps sign API requests with HMAC or similar schemes. Search for Mac.getInstance, MessageDigest, Cipher, or Signature:
// Request signing pattern
public String signRequest(String path, String body, long timestamp) {
String payload = path + body + timestamp;
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(SECRET_KEY, "HmacSHA256"));
byte[] hash = mac.doFinal(payload.getBytes());
return Base64.encodeToString(hash, Base64.NO_WRAP);
}Understanding the signing scheme is critical — without it, modified requests will be rejected by the server.
4. Obfuscation — Check proguard-rules.pro or look for patterns indicating obfuscation tools (ProGuard, R8, DexGuard). Heavy obfuscation means you’ll rely more on dynamic analysis with Frida.
Traffic Interception with mitmproxy #
Once you understand the app’s network layer from static analysis, it’s time to see real traffic.
Setup #
mitmproxy acts as a proxy between the device and the internet. All HTTPS traffic flows through it, and you can inspect, modify, or replay requests.
# Start mitmproxy on your machine
mitmproxy --listen-port 8080
# Or use the web interface
mitmweb --listen-port 8080Configure the Android device to use your machine as a proxy:
# Set proxy on connected device via ADB
adb shell settings put global http_proxy $(hostname -I | awk '{print $1}'):8080
# Install mitmproxy CA certificate on device
# Download from http://mitm.it on the device browserFor apps targeting Android 7+, user-installed CA certificates aren’t trusted by default. You need to install the cert as a system CA, which requires root:
# Convert mitmproxy cert to Android system format
openssl x509 -inform PEM -subject_hash_old \
-in ~/.mitmproxy/mitmproxy-ca-cert.pem | head -1
# Output: c8750f0d (example hash)
cp ~/.mitmproxy/mitmproxy-ca-cert.pem c8750f0d.0
# Push to device system cert store (requires root)
adb root
adb remount
adb push c8750f0d.0 /system/etc/security/cacerts/
adb shell chmod 644 /system/etc/security/cacerts/c8750f0d.0
adb rebootWhat You See #
With mitmproxy running, every API call the app makes is visible:
POST https://api.example.com/v2/feed
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...
X-Device-Id: a1b2c3d4-e5f6-7890
X-Request-Sign: Kx8mNpQ2r1...
Content-Type: application/json
{"page": 1, "limit": 20, "filter": "trending"}You can see the exact headers, request body, response, timing, and status codes. This is invaluable for understanding API contracts that aren’t documented.
Scripting with mitmproxy #
mitmproxy supports Python scripts for automating analysis. For example, logging all API endpoints and their parameters:
# log_endpoints.py
from mitmproxy import http
def response(flow: http.HTTPFlow):
if "api.example.com" in flow.request.pretty_host:
print(f"{flow.request.method} {flow.request.path}")
print(f" Status: {flow.response.status_code}")
print(f" Request size: {len(flow.request.content)} bytes")
print(f" Response size: {len(flow.response.content)} bytes")
print()mitmproxy -s log_endpoints.pyYou can also modify requests on the fly — change parameters, swap tokens, or inject headers to test how the server responds.
Dynamic Instrumentation with Frida #
This is where the real magic happens. Frida lets you inject JavaScript into a running process, hook any function, read and modify arguments, and change return values — all without modifying the APK.
Setup #
Install Frida on your machine and push the Frida server to the device:
# Install Frida CLI tools
pip install frida-tools
# Download frida-server for your device architecture
# Push to device
adb push frida-server-16.x.x-android-arm64 /data/local/tmp/frida-server
adb shell chmod 755 /data/local/tmp/frida-server
# Start frida-server (requires root)
adb shell su -c '/data/local/tmp/frida-server &'Verify it’s running:
frida-ps -U # List processes on USB deviceBypassing SSL Pinning #
The first thing you’ll want to do is bypass certificate pinning so mitmproxy can intercept traffic. This is the most common Frida use case:
// ssl_bypass.js
Java.perform(() => {
// Bypass OkHttp CertificatePinner
const CertificatePinner = Java.use('okhttp3.CertificatePinner')
CertificatePinner.check.overload(
'java.lang.String',
'java.util.List'
).implementation = function (hostname, peerCertificates) {
console.log(`[*] Bypassing pin for: ${hostname}`)
// Do nothing — skip the pin check
}
// Bypass custom TrustManager
const TrustManagerImpl = Java.use('com.android.org.conscrypt.TrustManagerImpl')
TrustManagerImpl.verifyChain.implementation = function () {
console.log('[*] Bypassing TrustManager verification')
return arguments[0] // Return the unverified chain
}
})frida -U -l ssl_bypass.js -f com.example.appThe -f flag spawns the app fresh with Frida attached from the start, which is important for intercepting early network calls.
Hooking Functions #
The real power of Frida is hooking arbitrary functions. Say you found an interesting method in Jadx:
// Found in Jadx: com.example.app.crypto.RequestSigner
public class RequestSigner {
public String sign(String path, String body, long timestamp) {
// ... signing logic
}
}You can hook it to see exactly what’s being signed:
// hook_signer.js
Java.perform(() => {
const Signer = Java.use('com.example.app.crypto.RequestSigner')
Signer.sign.implementation = function (path, body, timestamp) {
console.log('[*] sign() called')
console.log(` path: ${path}`)
console.log(` body: ${body}`)
console.log(` timestamp: ${timestamp}`)
// Call the original method
const result = this.sign(path, body, timestamp)
console.log(` signature: ${result}`)
return result
}
})Every time the app signs a request, you see the inputs and output in your terminal. This is how you reverse-engineer proprietary signing schemes.
Modifying Behavior #
Frida can also change how functions behave:
// Force a function to always return true
Java.perform(() => {
const Auth = Java.use('com.example.app.auth.AuthManager')
Auth.isLoggedIn.implementation = function () {
console.log('[*] isLoggedIn() → forcing true')
return true
}
// Change a parameter before it reaches the original function
Auth.validateToken.implementation = function (token) {
console.log(`[*] Original token: ${token}`)
// Call original with modified token
return this.validateToken('modified_token_here')
}
})Tracing and Discovery #
When you don’t know which function to hook, Frida can trace entire classes:
// trace_class.js — log every method call on a class
Java.perform(() => {
const target = Java.use('com.example.app.api.ApiClient')
const methods = target.class.getDeclaredMethods()
methods.forEach((method) => {
const name = method.getName()
const overloads = target[name].overloads
overloads.forEach((overload) => {
overload.implementation = function () {
const args = Array.from(arguments).map((a) => {
return a ? a.toString() : 'null'
})
console.log(`[*] ${name}(${args.join(', ')})`)
return this[name].apply(this, arguments) // eslint-disable-line prefer-spread
}
})
})
})This is like setting a breakpoint on every method in a class — you see the call sequence, arguments, and can narrow down to the specific function you care about.
Objection: Frida Made Easy #
Objection is a toolkit built on top of Frida that provides common RE tasks as simple commands:
# Start objection on a running app
objection -g com.example.app exploreOnce inside the Objection REPL:
# Disable SSL pinning (one command)
android sslpinning disable
# List all activities
android hooking list activities
# List methods in a class
android hooking list class_methods com.example.app.api.ApiClient
# Hook a method and watch calls
android hooking watch class_method com.example.app.api.ApiClient.sendRequest
# Dump the keystore
android keystore list
# Search for classes containing "Crypto"
android hooking search classes Crypto
# Dump shared preferences
android hooking get_current_activityObjection is perfect for exploration. When you know what you’re looking for, write custom Frida scripts. When you’re exploring, use Objection.
uiautomator2: UI Automation #
Sometimes you need to automate the app’s UI — navigate screens, tap buttons, scroll feeds — while your Frida hooks and mitmproxy capture data.
uiautomator2 is a Python library for Android UI automation:
import uiautomator2 as u2
# Connect to device
d = u2.connect()
# Launch app
d.app_start('com.example.app')
# Wait for element and tap
d(text="Sign In").click()
# Type text
d(resourceId="com.example.app:id/email").set_text("test@example.com")
# Scroll
d.swipe_ext("up", scale=0.8)
# Screenshot
d.screenshot("screen.png")
# Get element info
info = d(resourceId="com.example.app:id/balance").info
print(f"Balance text: {info['text']}")Combined with Frida hooks, this creates a powerful pipeline: automate user flows in the UI while capturing every API call and function invocation happening underneath.
A Complete RE Workflow #
Here’s how I approach reverse engineering an Android app from scratch:
Phase 1: Reconnaissance #
# Pull the APK
adb pull $(adb shell pm path com.example.app | cut -d: -f2) target.apk
# Decompile with Jadx
jadx-gui target.apkIn Jadx, I map out:
- The network layer (HTTP client, interceptors, base URLs)
- Authentication mechanism (token storage, refresh logic)
- Request signing (if any)
- Certificate pinning implementation
- Interesting business logic classes
Phase 2: Traffic Analysis #
# Start mitmproxy
mitmweb --listen-port 8080
# Configure device proxy
adb shell settings put global http_proxy <host_ip>:8080
# If the app uses SSL pinning, bypass with Frida first
frida -U -l ssl_bypass.js -f com.example.appBrowse the app normally and observe the traffic in mitmproxy. Document:
- All API endpoints and their request/response format
- Authentication headers and how tokens are structured
- Any request signing or encryption patterns
- Rate limiting behavior
Phase 3: Deep Dive with Frida #
Based on what I found in Phase 1 and 2, I write targeted Frida hooks:
Java.perform(() => {
// Hook the request signing function found in Jadx
const Signer = Java.use('com.example.app.crypto.RequestSigner')
Signer.sign.implementation = function (path, body, ts) {
console.log(`[sign] ${path}`)
console.log(` body: ${body.substring(0, 100)}`)
console.log(` ts: ${ts}`)
const result = this.sign(path, body, ts)
console.log(` sig: ${result}`)
return result
}
// Hook SharedPreferences to see what's being stored
const SP = Java.use('android.app.SharedPreferencesImpl$EditorImpl')
SP.putString.implementation = function (key, value) {
console.log(`[prefs] ${key} = ${value}`)
return this.putString(key, value)
}
})Phase 4: Documentation #
This is the most important step that most people skip. Document everything:
- API endpoint map with request/response schemas
- Authentication flow diagram
- Request signing algorithm
- Interesting findings and potential vulnerabilities
Without documentation, you’ll forget everything within a week and have to redo the analysis.
Security Lessons #
Reverse engineering apps taught me more about building secure software than any security course. Here are patterns I see repeatedly:
Client-side validation is not validation. Every check that happens on the client can be bypassed with Frida. Price checks, quantity limits, feature flags — if the server doesn’t enforce it, it doesn’t exist.
Hardcoded secrets are always found. API keys, signing secrets, encryption keys embedded in the APK — Frida can dump them at runtime even if they’re obfuscated in the binary. Use server-side key management.
Certificate pinning is a speed bump, not a wall. It takes roughly 30 seconds to bypass standard certificate pinning with Frida or Objection. It’s still worth implementing — it raises the bar — but don’t rely on it as your sole security measure.
Obfuscation slows analysis, it doesn’t prevent it. ProGuard renames classes, but the logic is the same. DexGuard adds more layers, but Frida hooks at the JVM level, bypassing bytecode obfuscation entirely. Focus your security budget on server-side hardening.
The real security boundary is the server. Every request from the client should be treated as potentially malicious. Validate everything server-side: authentication, authorization, input validation, rate limiting, business logic constraints. The client is untrusted territory.
Ethics and Legality #
A note on responsible use. The techniques in this post are powerful — and with power comes responsibility.
Authorized use only. Only reverse engineer apps you own, have permission to test, or are participating in a legitimate bug bounty program for. Unauthorized access to computer systems is illegal in most jurisdictions.
Responsible disclosure. If you find vulnerabilities, report them to the developer through their security contact or bug bounty program. Don’t exploit them, don’t publish exploit code, and give the developer reasonable time to fix issues before any public disclosure.
Educational context. Understanding how apps work makes you a better developer and a better security engineer. The goal is to build more secure software — not to break existing ones.
Wrap Up #
Reverse engineering is a skill that compounds. The more apps you analyze, the faster you recognize patterns — the same OkHttp interceptor setup, the same JWT structure, the same HMAC signing scheme. What takes hours at first becomes minutes with experience.
The tools in this post — Frida, mitmproxy, Jadx, Objection — cover 95% of what you’ll need for Android RE. The remaining 5% is native code analysis (IDA Pro, Ghidra) for apps with C/C++ libraries, which is a topic for another post.
If you’re a developer who has never looked at their own app from the attacker’s perspective, I’d encourage you to try. Install mitmproxy, set up Frida, and see what your app looks like from the outside. You might be surprised.
You can find my full tech stack here.
Thanks for reading!