Android Pentest Setup: Emulator, adb, Burp & Traffic Interception
This is Chapter 2 of the Mobile & IoT notebook. Chapter 1 built the mental model — what an Android app is, how the stack fits together from the Linux kernel through Binder, ART, the app sandbox and the APK format. This chapter turns that model into a working lab. By the end you will have a device you fully control, a shell into it over adb, a proxy sitting in the middle of its traffic, and — the part that trips up almost everyone the first time — the ability to actually read the app’s HTTPS, not just watch encrypted bytes go by.
Almost everything in the rest of this notebook depends on the environment you build here. Static analysis (Chapter 3) needs an APK you can pull off a device. Dynamic instrumentation with Frida (Chapter 4) needs a rooted device and a working adb. Bypassing pinning (Chapter 5) only makes sense once you understand why interception fails by default. So treat this chapter as infrastructure: get it solid once, and every later chapter is faster.
Who this is for: web and API testers standing up their first mobile rig; bug-bounty hunters who want to poke at a company’s Android app but keep seeing “Network error” the moment Burp is on; CTF players facing an adb-based challenge; and blue-teamers who need to understand exactly what a reviewer does to their app so they can make it harder. You need the Linux fundamentals from earlier notebooks (processes, UIDs, file permissions, iptables) and Chapter 1’s picture of the Android stack. You do not need to have ever written an Android app.
Lawful use: build this lab against emulators, your own devices, and deliberately-vulnerable training apps (DIVA, InsecureBankv2, the OWASP MASTG crackmes, Google’s own test targets). Intercepting or instrumenting an app you have no authorisation to test can breach the app’s terms, local computer-misuse law, and the developer’s rights. Everything below assumes a target you own or are explicitly scoped to assess.
Part 1: What You’re Actually Building
Section titled “Part 1: What You’re Actually Building”Before installing anything, get the shape of the lab clear. A mobile pentest rig has four moving parts, and every problem you hit later is a problem in one of them:
- A device — an emulator or a real phone — that runs the target app. It must be rooted (or root-equivalent) for serious work, because almost every interesting technique needs to read another app’s private files, write to the system CA store, or attach a debugger.
adb— the Android Debug Bridge — your control channel into that device: a shell, a file-transfer pipe, an app installer, and a log tap, all in one tool.- An intercepting proxy — Burp Suite (or mitmproxy / Caido) — that sits between the app and the internet so you can see and modify every request.
- The trust plumbing — the certificate and routing configuration that makes the app send its traffic through your proxy and trust your proxy’s certificate so TLS doesn’t break.
flowchart LR
subgraph Device["Rooted device / emulator"]
App[Target APK]
CA[(System CA store)]
end
App -->|HTTP/HTTPS| Proxy[Burp Suite]
Proxy -->|upstream TLS| Internet[(App backend)]
CA -.trusts.-> Proxy
Host[Your laptop] -->|adb| Device
Host --- Proxy
The single most common failure — the one that eats an afternoon for every new mobile tester — is step 4. You get the proxy running, you point the device at it, and the app just says “no internet” or “certificate error.” That is not a bug; that is Android 7+ working as designed, and Part 10 is entirely about defeating it on a device you control. Keep the diagram above in your head; when something breaks, ask which of the four parts failed.
A note on host OS. Everything here works on Linux, macOS, and Windows/WSL2. Kali is the assumed host because the later tooling (apktool, jadx, Frida, objection) is one apt/pip away, but nothing is Kali-specific. On macOS use Homebrew; on Windows prefer WSL2 for the CLI tools and run the emulator on the Windows side (nested virtualisation for an emulator inside WSL2 is painful).
Part 2: Emulator vs Real Device — Choosing Your Target
Section titled “Part 2: Emulator vs Real Device — Choosing Your Target”You will eventually want both, but start by understanding the trade-offs, because they decide which techniques even work.
| Dimension | AVD emulator (Google APIs) | Genymotion | Physical rooted phone |
|---|---|---|---|
| Root access | Easy (writable system on Google APIs images) | Easy (one-click root toggle) | Hard-to-easy (bootloader unlock + Magisk) |
| Architecture | x86_64 (host) | x86_64 | ARM (real) |
| ARM-only apps | Fail or need ARM translation | Limited | Native |
| Play Integrity / SafetyNet | Fails hard | Fails | Can pass with Magisk + Zygisk + DenyList |
| Hardware (camera, NFC, sensors, telephony) | Simulated / absent | Simulated | Real |
| Performance | Good with KVM/HAXM | Good | Native |
| Cost | Free | Free tier + paid | Cost of a phone |
| Snapshots | Yes | Yes | No |
The decision rules that matter in practice:
- Default to the emulator for the majority of app testing. It is free, disposable, snapshot-able, and trivially rooted. Use a Google APIs system image (not “Google Play”), because the Google APIs images ship with a writable
/systemandadb rootworks — the Play images are locked down like a retail device precisely to stop that. - Use a real device when the app is ARM-only and won’t run under emulation, when it uses hardware you must exercise (camera-based KYC, NFC payments, Bluetooth), or when it uses Play Integrity / hardware attestation that an emulator can never satisfy. Rooting a real phone (unlock bootloader → flash a patched boot image with Magisk) is Chapter 5 territory; for now know that a Pixel is the path of least resistance because its bootloader is officially unlockable.
- Genymotion sits in the middle: an x86 emulator with a friendlier UI, easy root toggle, and GApps installer. Good for teams; the desktop personal edition is free for non-commercial use.
Architecture is the sharp edge. Many production apps ship native libraries (.so files under lib/arm64-v8a/) with no x86 build. On an x86 emulator those apps crash on launch. Modern Android Studio emulator images include ARM-to-x86 translation which helps, but if an app stubbornly won’t run, that’s your cue to move it to a physical ARM device rather than fight the emulator.
Part 3: The Android SDK & Platform-Tools From Scratch
Section titled “Part 3: The Android SDK & Platform-Tools From Scratch”adb, emulator, avdmanager and sdkmanager all come from the Android SDK. You do not need the full Android Studio IDE to get them, though installing Studio is the simplest path and gives you the AVD Manager GUI.
Option A — Android Studio (simplest). Install Studio, open Settings → SDK Manager, and install:
- Android SDK Platform-Tools (this is
adbandfastboot) - Android Emulator
- At least one system image (see Part 6)
Studio drops the SDK at ~/Android/Sdk on Linux, ~/Library/Android/sdk on macOS.
Option B — command-line only (no IDE). Download the “command-line tools” zip, unpack it, and drive everything from sdkmanager:
# Point tooling at a fresh SDK rootexport ANDROID_HOME="$HOME/Android/Sdk"mkdir -p "$ANDROID_HOME/cmdline-tools"# unzip the downloaded commandlinetools-*.zip so the binaries live at:# $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanagerexport PATH="$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools:$ANDROID_HOME/emulator:$PATH"
# Accept licenses and install the core piecesyes | sdkmanager --licensessdkmanager "platform-tools" "emulator" "platforms;android-34"sdkmanager "system-images;android-34;google_apis;x86_64"Option C — distro package (fast, for adb only). On Kali/Debian: sudo apt install adb fastboot. This gives you a working adb/fastboot immediately, which is all you need when the “device” is Genymotion or a physical phone and you install the emulator separately.
Confirm the toolchain:
$ adb versionAndroid Debug Bridge version 1.0.41Version 35.0.2-12147458Installed as /home/kali/Android/Sdk/platform-tools/adb
$ sdkmanager --list_installed Path | Version | Description ------------------------------------------- | ------- | ----------------------- emulator | 35.1.4 | Android Emulator platform-tools | 35.0.2 | Android SDK Platform-Tools platforms;android-34 | 3 | Android SDK Platform 34 system-images;android-34;google_apis;x86_64 | 8 | Google APIs x86_64Pitfall — two adb versions. If you have both the distro adb and the SDK adb on PATH, the daemon and client versions can mismatch and you get adb server version (X) doesn't match this client (Y); killing... in a loop. Pick one. which -a adb shows every copy; keep the SDK one on PATH and remove or shadow the other.
Part 4: adb From Zero — Architecture
Section titled “Part 4: adb From Zero — Architecture”adb is not a single program talking to a phone; it is a three-part system, and understanding the three parts explains every “device not found” you will ever hit.
flowchart LR
Client["adb client<br/>(your CLI command)"] -->|TCP 5037| Server["adb server<br/>(host daemon)"]
Server -->|USB / TCP 5555| DaemonA["adbd<br/>(on device A)"]
Server -->|USB / TCP| DaemonB["adbd<br/>(on device B)"]
- The client is the
adbyou type. It’s short-lived: it sends one command and exits. Everyadb shell,adb install,adb devicesis a fresh client. - The server is a background process on your host, listening on TCP 5037. The first
adbcommand you run silently starts it. It multiplexes: it tracks every connected device and routes client commands to the right one. adbdis the daemon running on the device, as part of the OS. It receives commands from the server and executes them on-device. Its privilege level matters enormously: normally it runs as theshelluser, but on a rooted/eng deviceadb rootrestarts it asroot, which is what lets you read other apps’ data.
Transports. The server reaches adbd over USB or over TCP/IP (network). Emulators are always TCP (they show up as emulator-5554). A physical phone starts on USB and can be switched to TCP with adb tcpip 5555.
Authentication (RSA). The first time a host connects to a real device over USB, the device shows an “Allow USB debugging?” dialog with the host’s RSA key fingerprint. Approve it (tick “always allow”) and the key is stored in /data/misc/adb/adb_keys. This is why a brand-new host can’t just take over any phone plugged into it — the human at the device has to authorise the key. Emulators skip this.
Common lifecycle commands:
adb devices -l # list attached devices with product/model detailadb start-server # explicitly start the host server (usually automatic)adb kill-server # nuke the server; the next command restarts it cleanadb -s emulator-5554 … # target a specific device when several are attachedadb reconnect offline # kick a device stuck in "offline" stateIf adb devices shows a device as unauthorized, you never accepted the RSA prompt (or adb_keys got wiped). If it shows offline, the daemon connection is half-dead — adb kill-server then replug. If it shows nothing for a USB phone, USB debugging isn’t enabled in Developer Options, or it’s a charge-only cable.
Enabling USB debugging on a real device: Settings → About phone → tap Build number seven times to unlock Developer Options, then Settings → System → Developer options → USB debugging. This is muscle memory you’ll repeat on every physical target.
Part 5: adb Command Deep-Dive
Section titled “Part 5: adb Command Deep-Dive”These are the commands you’ll run hundreds of times. Learn them properly now.
5.1 A shell on the device
Section titled “5.1 A shell on the device”adb shell # interactive shell as the 'shell' useradb shell id # uid=2000(shell) gid=2000(shell) ...adb shell getprop ro.build.version.release # Android version, e.g. 14adb shell pm list packages | grep -i target # find an app's package nameadb shell pm path com.example.app # where the APK lives on diskpm (package manager) and am (activity manager) are the two on-device workhorses. pm list packages, pm path, pm dump, and am start/am broadcast (which you’ll weaponise in Chapter 6 against exported components) run inside adb shell.
5.2 Root and remount
Section titled “5.2 Root and remount”adb root # restart adbd as root (works on emulator Google APIs / eng builds)adb unroot # drop back to shell useradb remount # remount /system read-write (needs root + writable image)On a Google APIs emulator, adb root returns restarting adbd as root and a follow-up adb shell id shows uid=0(root). On a retail phone this fails — rooting there means Magisk, covered in Chapter 5.
5.3 Moving files
Section titled “5.3 Moving files”adb push local.apk /data/local/tmp/ # host -> deviceadb pull /data/local/tmp/loot.db ./ # device -> hostadb pull "$(adb shell pm path com.example.app | sed 's/package://')" base.apk/data/local/tmp/ is the universal scratch directory: writable by the shell user, which is where you’ll stage the frida-server binary in Chapter 4 and APKs for install.
5.4 Installing and removing apps
Section titled “5.4 Installing and removing apps”adb install app.apk # installadb install -r app.apk # reinstall keeping dataadb install -t app.apk # allow test-only (debuggable) buildsadb install-multiple base.apk split_config.*.apk # split APKs / app bundlesadb uninstall com.example.appadb shell pm uninstall -k --user 0 com.example.app # remove for user, keep dataSplit APKs are the modern default (Android App Bundles). A single .apk won’t install if the app was published as split; you must pull all the split files (pm path lists several) and install-multiple them. This bites people constantly — the app “won’t install” because they grabbed only base.apk.
5.5 Logs — logcat
Section titled “5.5 Logs — logcat”adb logcat # firehose of system + app logsadb logcat --pid=$(adb shell pidof -s com.example.app) # just this appadb logcat *:E # errors onlyadb logcat | grep -iE "token|password|http|error" # hunt for leaksadb logcat -c # clear the buffer, then reproduceSecurity relevance: apps routinely log secrets in debug builds — session tokens, full request bodies, even passwords. adb logcat during a login flow is often the fastest first finding on an engagement, and it needs no proxy at all. Filter to the app’s PID to cut the noise.
5.6 Port forwarding — the two directions
Section titled “5.6 Port forwarding — the two directions”This pair is the key to proxying, so understand the direction precisely:
adb forward tcp:8000 tcp:8000 # host:8000 -> device:8000 (reach a device port from host)adb reverse tcp:8080 tcp:8080 # device:8080 -> host:8080 (device reaches a host service)adb forwardopens a port on your host that tunnels to a port on the device. Use it to reachfrida-server(device 27042) from your laptop.adb reversemakes the device able to hit a service on your host via127.0.0.1. This is the cleanest way to route app traffic to Burp on your laptop without touching Wi-Fi proxy settings:adb reverse tcp:8080 tcp:8080and point the app at127.0.0.1:8080. Noteadb reverseneedsadbdcooperation and won’t work on very old devices; the Wi-Fi proxy method in Part 9 is the fallback.
5.7 Screens, input, and misc
Section titled “5.7 Screens, input, and misc”adb shell screencap -p /sdcard/s.png && adb pull /sdcard/s.png # screenshotadb shell input tap 500 1200 # simulate a tapadb shell input text "hunter2" # type into a focused fieldadb emu geo fix -122.084 37.422 # spoof GPS on emulatoradb shell settings put global http_proxy 127.0.0.1:8080 # set proxy via settingsadb shell settings delete global http_proxy # clear itPart 6: Building a Rooted Emulator (AVD)
Section titled “Part 6: Building a Rooted Emulator (AVD)”Now stand up the device. The avdmanager tool creates AVDs from system images; emulator boots them.
# List installed system images you can build an AVD fromsdkmanager --list | grep system-images | grep google_apis
# Create an AVD named 'pentest' from the Google APIs imageavdmanager create avd -n pentest \ -k "system-images;android-34;google_apis;x86_64" \ -d "pixel_6"
# Boot it, writable-system so we can modify /system later (CA install!)emulator -avd pentest -writable-system -no-snapshot-loadThe two flags that matter for pentesting:
-writable-systemboots with/systemmountable read-write. Without it,adb remountfails and you cannot drop a CA into the system store the classic way. Always boot pentest AVDs with it.-no-snapshot-loadforces a cold boot, avoiding a stale snapshot that “remembers” a broken state. Handy while you’re configuring.
Other useful boot flags: -http-proxy http://127.0.0.1:8080 (route the whole emulator through Burp at boot), -writable-system -no-snapshot together for a clean modifiable image, -dns-server 8.8.8.8 if DNS misbehaves, -wipe-data to factory-reset the AVD.
Confirm root on the emulator:
$ adb rootrestarting adbd as root$ adb shell iduid=0(root) gid=0(root) groups=0(root) ...$ adb remountremount succeededIf adb root says adbd cannot run as root in production builds, you booted a Google Play image, not Google APIs. Recreate the AVD from a google_apis (not google_apis_playstore) image.
Rooting the emulator further with Magisk (optional now, needed for Chapter 5). For techniques that expect Magisk (root-hiding, Zygisk modules), you patch the emulator’s ramdisk/boot with rootAVD or run a device that already has Magisk. For pure interception and Frida, the built-in adb root on a Google APIs image is enough; we’ll return to Magisk when we need to hide root in Chapter 5.
Part 7: Genymotion & Physical Devices in Brief
Section titled “Part 7: Genymotion & Physical Devices in Brief”Genymotion Desktop. Install VirtualBox (its default hypervisor) or use the QEMU backend, install Genymotion, create a device from its template list (e.g., “Google Pixel 6, Android 13”), and boot it. It exposes adb automatically — adb devices shows it as a networked device (192.168.56.x:5555). Root is a toggle in the device settings; GApps can be flashed via the built-in installer. It’s noticeably snappier to click around than a bare AVD and is popular on teams for that reason.
Physical Pixel (the reference real device). The full flow — which Chapter 5 details — is: enable Developer Options → OEM unlocking → fastboot flashing unlock (wipes the device) → extract boot.img from the factory image → patch it in the Magisk app → fastboot flash boot magisk_patched.img. You end with a rooted phone that can also hide root well enough to pass many integrity checks. For this chapter, if you have a rooted phone already, everything below (Burp, CA install, iptables) applies identically — you just use its real IP for the Wi-Fi proxy instead of the emulator.
Connecting a device over Wi-Fi (Android 11+ wireless debugging):
adb pair 192.168.1.50:37000 # enter the 6-digit code from Developer Optionsadb connect 192.168.1.50:5555Part 8: Burp Suite From Scratch
Section titled “Part 8: Burp Suite From Scratch”Burp Suite is the intercepting proxy at the centre of the rig. If you’ve done web testing you know it; here’s the from-zero version and the mobile-specific bits.
What it is. Burp is a Java application that runs a local HTTP(S) proxy (default 127.0.0.1:8080). Everything that flows through it appears in Proxy → HTTP history; Intercept lets you pause and edit requests in flight; Repeater lets you replay and mutate a request by hand; Intruder automates payload sets. The Community edition is free and sufficient for learning (Repeater + manual proxy); Professional adds the scanner and unthrottled Intruder.
Install on Kali: it’s preinstalled (burpsuite), or sudo apt install burpsuite. Launch, accept defaults, choose a temporary project.
Configure the listener for a device (not just localhost). By default Burp listens only on 127.0.0.1, which a separate device can’t reach. For the Wi-Fi proxy method you must bind it to your LAN interface:
Proxy → Proxy settings → Proxy listeners → Add/Edit → set Bind to port 8080 and Bind to address to All interfaces (or your specific LAN IP). Now the emulator/phone can reach http://<your-laptop-ip>:8080.
sequenceDiagram
participant App as Target app
participant Burp as Burp (proxy)
participant Srv as App backend
App->>Burp: CONNECT api.example.com:443
Burp->>App: 200 Connection established
App->>Burp: TLS ClientHello
Burp-->>App: TLS cert signed by Burp CA
Note over App: Cert validation:<br/>trusted -> continue<br/>untrusted -> abort
App->>Burp: HTTP request (now decrypted for Burp)
Burp->>Srv: new upstream TLS + request
Srv->>Burp: response
Burp->>App: response
That diagram is the whole game. Burp is a TLS man-in-the-middle: it presents the app a certificate that Burp generated, signed by Burp’s CA. The app will only proceed if it trusts that CA — which is exactly the problem Part 10 solves. Get Burp’s CA now: browse to http://burp from a proxied browser, or Proxy → Proxy settings → Import/export CA certificate → Certificate in DER format, and save cacert.der.
Alternatives worth knowing. mitmproxy (free, scriptable in Python, superb for CLI/CI and for writing quick request-rewriting addons), Caido (a modern Burp-like written in Rust). The interception theory is identical across all three; pick Burp to follow along.
Part 9: Wiring the App’s Traffic Into Burp
Section titled “Part 9: Wiring the App’s Traffic Into Burp”Two reliable methods. Use whichever fits.
Method A — Wi-Fi / global proxy (proxy-aware apps)
Section titled “Method A — Wi-Fi / global proxy (proxy-aware apps)”Set the device’s HTTP proxy to your laptop:
# Emulator, at boot:emulator -avd pentest -writable-system -http-proxy http://127.0.0.1:8080# Or at runtime, via settings:adb shell settings put global http_proxy 192.168.1.100:8080# Clear it later:adb shell settings delete global http_proxyOr set it in the GUI: Wi-Fi → long-press network → Modify → Advanced → Proxy → Manual → host = your laptop IP, port = 8080. Now open any app that respects the system proxy and you’ll see traffic in Burp’s HTTP history (HTTP will be readable immediately; HTTPS will fail cert validation until Part 10).
Caveat: the global proxy only affects apps that honour it. Many apps using low-level sockets, gRPC, or their own networking ignore http_proxy. Those need Method B or the iptables approach in Part 12.
Method B — adb reverse (cleanest for emulator)
Section titled “Method B — adb reverse (cleanest for emulator)”adb reverse tcp:8080 tcp:8080Now 127.0.0.1:8080 on the device is your laptop’s Burp. Set the device proxy to 127.0.0.1:8080 (or configure the app to use it). This avoids IP/firewall issues entirely and survives Wi-Fi changes — great for emulators.
Verify the pipe before chasing cert errors. Point the device browser at http://example.com. If it shows in Burp’s HTTP history, routing works and any remaining failure is a certificate problem, not a routing one. Diagnosing routing vs trust separately saves hours.
Part 10: The TLS Problem — Why Interception Breaks on Modern Android
Section titled “Part 10: The TLS Problem — Why Interception Breaks on Modern Android”Here is the wall every new mobile tester hits. You’ve wired the proxy, HTTP works, but HTTPS apps show “network error.” The cause is a deliberate 2016 security change.
Two trust stores. Android has two CA trust stores:
- System store — CAs baked into the OS at
/system/etc/security/cacerts/. Trusted by everything. - User store — CAs the user installs via Settings → Security → Encryption & credentials → Install a certificate. Stored under
/data/misc/user/0/cacerts-added/.
Android 6 and earlier (API 23-): apps trusted the user store by default. So you installed Burp’s CA as a user cert and every app trusted it. Easy.
Android 7+ (API 24+): apps no longer trust user-added CAs by default. Since Nougat, an app trusts user CAs only if it opts in via a Network Security Config. Most apps don’t. So your user-installed Burp CA is simply ignored, and HTTPS validation fails. This is the single most important fact in mobile interception.
flowchart TD
A[App makes HTTPS call] --> B{targetSdk and NSC?}
B -->|"targetSdk >= 24, no NSC override"| C[Trust SYSTEM store only]
B -->|"NSC opts into user certs"| D[Trust USER store too]
C --> E{Burp CA in system store?}
E -->|no| F[TLS fails: untrusted cert]
E -->|yes| G[Interception works]
D --> G
The Network Security Config (NSC). Introduced in Android 7, declared in AndroidManifest.xml via android:networkSecurityConfig="@xml/network_security_config". An app can:
- restrict trust to only certain CAs,
- pin specific certificates/public keys (
<pin-set>), which defeats even a system-store CA — that’s Chapter 5, - or (rarely, in debug builds) opt into user certs with
<debug-overrides><trust-anchors><certificates src="user"/>.
A production app that pins, or simply uses the default (system-only) trust, cannot be intercepted by a user-installed cert. The reliable fix on a device you control is to put Burp’s CA into the system store, so the app trusts it exactly as it would a real CA. That’s Part 11. (Pinning survives even that, and is defeated at runtime in Chapter 5.)
For your own app / a debuggable build, the developer-friendly path is to add an NSC that trusts user certs in debug, so testers don’t need root:
<network-security-config> <debug-overrides> <trust-anchors> <certificates src="user"/> </trust-anchors> </debug-overrides></network-security-config>You can also patch this in to a target APK during static repackaging (Chapter 3): add the NSC, set android:networkSecurityConfig, and add android:debuggable="true", then re-sign. But repackaging trips pinning and integrity checks, so the system-store route is usually cleaner.
Part 11: Installing Burp’s CA Into the System Store
Section titled “Part 11: Installing Burp’s CA Into the System Store”Three approaches, from simplest to most robust. All require root and a device you own.
First, convert Burp’s DER cert to the PEM/hashed name Android expects. Android names system CAs by the subject hash with a .0 suffix.
# Export Burp CA as DER (Proxy settings -> Import/export CA -> DER), then:openssl x509 -inform DER -in cacert.der -out cacert.pem # DER -> PEM# Compute the old-style subject hash Android uses as the filename:HASH=$(openssl x509 -inform PEM -subject_hash_old -in cacert.pem | head -1)echo "$HASH" # e.g. 9a5ba575cp cacert.pem "$HASH.0"Approach 1 — remount /system and drop the file (emulator / eng device)
Section titled “Approach 1 — remount /system and drop the file (emulator / eng device)”adb rootadb remount # /system now read-writeadb push 9a5ba575.0 /system/etc/security/cacerts/adb shell chmod 644 /system/etc/security/cacerts/9a5ba575.0adb shell chown root:root /system/etc/security/cacerts/9a5ba575.0adb rebootAfter reboot the CA appears under Settings → Security → Trusted credentials → System, and apps trust it. This is the classic method and works great on -writable-system emulators.
Android 10+ wrinkle: newer images use a read-only /system even with root, and some use an APEX//apex/com.android.conscrypt overlay for CA certs. If adb remount fails, mount a tmpfs over the cacerts dir instead:
adb shell su -c ' mkdir -p /data/local/tmp/cacerts cp /system/etc/security/cacerts/* /data/local/tmp/cacerts/ cp /sdcard/9a5ba575.0 /data/local/tmp/cacerts/ mount -t tmpfs tmpfs /system/etc/security/cacerts cp /data/local/tmp/cacerts/* /system/etc/security/cacerts/ chown root:root /system/etc/security/cacerts/* chmod 644 /system/etc/security/cacerts/* restorecon -v /system/etc/security/cacerts/*'This overlays a writable copy of the system cacerts containing your CA, without persistently modifying the real partition (it resets on reboot — a feature for a shared device).
Approach 2 — Magisk module (persistent, survives reboot, hides better)
Section titled “Approach 2 — Magisk module (persistent, survives reboot, hides better)”On a Magisk-rooted device, community modules such as “AlwaysTrustUserCerts” / “MagiskTrustUserCerts” copy user-installed CAs into a system-store overlay at boot. Workflow: install Burp CA as a user cert normally, install the module, reboot — the module promotes it to system trust via Magisk’s systemless overlay. Clean, reversible (disable the module), and doesn’t touch the real partition. This is the preferred method on real phones.
Approach 3 — patch the APK’s Network Security Config (no device root needed)
Section titled “Approach 3 — patch the APK’s Network Security Config (no device root needed)”If you can’t root the device but can repackage the app (Chapter 3), decompile with apktool, add a network_security_config.xml trusting user certs, reference it and set debuggable, rebuild and re-sign. Then a user-installed Burp CA is trusted by that one app. Downsides: breaks pinning and any signature/integrity check, and won’t work on apps that detect tampering.
Verify success: with the CA in the system store, reopen the target app. HTTPS requests now populate Burp’s HTTP history in cleartext. If they still fail, the app is pinning — jump to Chapter 5.
Part 12: Intercepting Non-Proxy-Aware Apps
Section titled “Part 12: Intercepting Non-Proxy-Aware Apps”Some apps ignore the system proxy entirely (raw sockets, gRPC, custom stacks). The proxy is running and trusted, but nothing shows up. You have two options.
Transparent redirection with iptables (on the device)
Section titled “Transparent redirection with iptables (on the device)”Force all the device’s outbound TCP to your proxy at the packet level, bypassing the app’s proxy-ignorance:
adb shell su -c ' iptables -t nat -A OUTPUT -p tcp --dport 80 -j DNAT --to-destination 192.168.1.100:8080 iptables -t nat -A OUTPUT -p tcp --dport 443 -j DNAT --to-destination 192.168.1.100:8080'Then enable Burp’s invisible proxying (Proxy settings → listener → Request handling → Support invisible proxying), because the redirected connections arrive without a CONNECT/absolute URL — Burp must infer the host from the TLS SNI / Host header. This catches apps that never asked about a proxy.
mitmproxy in transparent mode
Section titled “mitmproxy in transparent mode”mitmproxy --mode transparent plus the same iptables redirection does the identical job with a scriptable Python core, which is handy when you want to auto-rewrite a header on every request.
When even this fails, the app is doing certificate pinning or using non-TLS transport (e.g., a custom protocol, or QUIC/HTTP3 over UDP which your TCP redirect misses). Pinning → Chapter 5 (Frida unpinning). QUIC → force HTTP/2 by blocking UDP/443 so the app falls back to TCP TLS you can intercept:
adb shell su -c 'iptables -A OUTPUT -p udp --dport 443 -j REJECT'Part 13: Detection & Defense Angle
Section titled “Part 13: Detection & Defense Angle”Everything above is what a reviewer does; the defensive mirror is what a mobile app team should ship to make it hard. Fold these into a threat model rather than treating any single one as a silver bullet — a rooted device defeats all of them eventually, but together they raise the cost.
- Certificate pinning. Pin the server’s leaf or intermediate public key in the Network Security Config (
<pin-set>) or via a library (OkHttpCertificatePinner, TrustKit). This defeats system-store CA injection outright and forces the tester to a runtime bypass (Frida), which is noisier and easier to detect. Pin the intermediate, not just the leaf, so certificate rotation doesn’t brick the app. - Root & emulator detection. Check for
su, Magisk paths,test-keysbuild tags, known emulator properties (ro.kernel.qemu, genericBuild.FINGERPRINT), and the presence offrida-serverports. None are foolproof, but they push the attacker to spend time on Chapter 5’s hiding techniques. - Play Integrity / hardware attestation. The strongest signal: server-side verification of a Google-signed device attestation. An emulator and most rooted phones fail it. Enforce it server-side for sensitive actions, not just client-side, so a Frida hook on the client verdict doesn’t win.
- No secrets in logs. Strip request/response logging and token dumps from release builds (
if (BuildConfig.DEBUG)guards, ProGuard/R8 stripping). Theadb logcatfinding in Part 5 should return nothing on a release build. - Detect the debug/tamper state server-side. Ship
android:debuggable="false"in release, verify the APK signature at runtime, and — most importantly — don’t trust the client. Every control above is client-side and therefore ultimately bypassable on a device the attacker owns; the durable defense is server-side authorization and rate-limiting.
IR use case: if you’re investigating a suspected malicious app, the same lab is your analysis rig — install into a snapshot emulator, proxy its traffic to see C2 endpoints, logcat for behaviour, and revert the snapshot when done. The offensive lab and the malware-triage lab are the same lab.
Final Revision / Summary
Section titled “Final Revision / Summary”- A mobile rig is four parts: a rooted device,
adb, an intercepting proxy, and the trust plumbing. Every failure is one of those four; diagnose which before flailing. adbis client → host server (TCP 5037) → on-deviceadbd.unauthorized/offline/missing device each map to a specific one of those links.adb root+adb remounton a Google APIs emulator gives you root and a writable/system.- Learn the command core cold:
shell,push/pull,install(-r/-t/install-multiplefor splits),logcat(filter by PID for secret-hunting), and the direction-sensitive pairforward(host→device) vsreverse(device→host, the clean way to reach Burp). - Build the AVD with
-writable-systemfrom a google_apis (not playstore) image. Genymotion for a friendlier UX; a rooted Pixel when you need ARM/hardware/Play Integrity. - Burp is a TLS MITM: it works only if the app trusts Burp’s CA.
- The Android 7 change is the crux: apps stopped trusting user-installed CAs. Put Burp’s CA in the system store (remount/push, tmpfs overlay, or a Magisk module) so the app trusts it. Pinning still beats that — deferred to Chapter 5.
- Non-proxy-aware apps: iptables DNAT + Burp invisible proxying, or mitmproxy transparent mode. Kill UDP/443 to defeat QUIC fallback.
Cheat Sheet / Quick Reference
Section titled “Cheat Sheet / Quick Reference”# --- Device / adb ---adb devices -l # list devicesadb kill-server; adb start-server # reset host daemonadb root; adb remount # root + writable /system (emulator)adb shell pm list packages | grep X # find package nameadb shell pm path com.pkg # locate APK(s) (watch for splits)adb pull "$(adb shell pm path com.pkg|sed 's/package://')" base.apk
# --- Files / install / logs ---adb push f /data/local/tmp/ # host -> deviceadb pull /path/loot ./ # device -> hostadb install -r -t app.apk # (re)install, allow test buildadb install-multiple base.apk split_*.apkadb logcat --pid=$(adb shell pidof -s com.pkg) | grep -iE 'token|http'
# --- Proxy routing ---adb reverse tcp:8080 tcp:8080 # device 127.0.0.1:8080 -> host Burpadb shell settings put global http_proxy 192.168.1.100:8080adb shell settings delete global http_proxy
# --- Emulator ---avdmanager create avd -n pentest -k "system-images;android-34;google_apis;x86_64" -d pixel_6emulator -avd pentest -writable-system -no-snapshot-load
# --- Burp CA -> system store ---openssl x509 -inform DER -in cacert.der -out cacert.pemHASH=$(openssl x509 -subject_hash_old -in cacert.pem | head -1); cp cacert.pem "$HASH.0"adb push "$HASH.0" /system/etc/security/cacerts/ && adb shell chmod 644 /system/etc/security/cacerts/$HASH.0adb reboot
# --- Transparent intercept ---adb shell su -c 'iptables -t nat -A OUTPUT -p tcp --dport 443 -j DNAT --to-destination 192.168.1.100:8080'# Burp: enable "Support invisible proxying"Trust-store decision, in one line: Android 6 and earlier -> user cert is enough. Android 7+ -> system-store cert. Pinning -> runtime bypass (Ch5).
Practice Labs & Resources
Section titled “Practice Labs & Resources”- InsecureBankv2 — a deliberately-vulnerable Android app; perfect first target for standing up the whole rig, proxying its login, and reading tokens in
logcat. - DIVA (Damn Insecure and Vulnerable App) — small challenges that exercise adb, logcat leaks, and traffic interception.
- OWASP MASTG “crackmes” and the MASTG “Android Basic Security Testing” chapter — the canonical walkthroughs for exactly this setup (adb, emulator, CA install), worth cross-reading.
- HackTheBox / TryHackMe mobile rooms (e.g., THM “Android Hacking 101”, HTB mobile challenges) — end-to-end labs that assume a working emulator + Burp rig.
- Google’s AOSP emulator docs — authoritative reference for
avdmanager/emulatorflags. - PortSwigger Web Security Academy — not mobile-specific, but the Burp fundamentals (Repeater, Intruder, interception) transfer directly; do these if Burp is new to you.
In the next chapter we move from watching the app to taking it apart statically — pulling the APK, decompiling it with apktool and jadx, and running it through MobSF to map its attack surface before we ever run it.
Discussion
Questions, corrections, or something to add? Comment with your GitHub account.