Initial release: automated APK debuggable patching for Android

This commit is contained in:
benjamin-luescher 2026-02-02 16:57:46 +01:00
commit 18f5562aca
9 changed files with 1625 additions and 0 deletions

6
.gitignore vendored Normal file
View file

@ -0,0 +1,6 @@
*.keystore
*_debuggable
apk-disassembled/
apks_*/
.idea/
.claude/

88
CLAUDE.md Normal file
View file

@ -0,0 +1,88 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
Three Bash scripts for making Android APKs debuggable and intercepting traffic:
- **`apk-debuggable.sh`** — End-to-end automation that handles device selection, APK extraction, patching (via `lib/make-debuggable.sh`), and reinstallation. Forwards `--trust-user-certs` to `lib/make-debuggable.sh`.
- **`lib/make-debuggable.sh`** — Core tool that converts release APKs into debuggable versions. It disassembles the APK, patches `AndroidManifest.xml` to set `android:debuggable="true"`, reassembles, and re-signs with a debug keystore. Optionally injects `network_security_config.xml` to trust user CA certs (`--trust-user-certs`).
- **`lib/proxy-setup.sh`** — Starts mitmproxy in Docker, restarts a running Android emulator with HTTP proxy enabled, and installs the mitmproxy CA certificate.
## Usage
```bash
# Automated end-to-end (device → extract → patch → reinstall)
./apk-debuggable.sh <app-name> [--device <serial>] [--keep] [--trust-user-certs] [--proxy]
# Single APK
./lib/make-debuggable.sh <path-to-apk> [output-apk] [--trust-user-certs]
# Split APK directory (contains base.apk + split APKs)
./lib/make-debuggable.sh <directory> [output-directory] [--trust-user-certs]
# Start mitmproxy and restart emulator with proxy
./lib/proxy-setup.sh
# Stop mitmproxy
./lib/proxy-setup.sh --stop
```
There are no build, test, or lint commands.
## Script Architecture
`lib/make-debuggable.sh` is organized into these key functions:
- **`find_android_tools()`** — Auto-discovers Android SDK, apksigner, Java/JDK, keytool, and apktool from standard macOS paths and environment variables (`ANDROID_HOME`, `ANDROID_SDK_ROOT`, `JAVA_HOME`)
- **`ensure_keystore()`** — Generates a debug keystore (`debug-resign.keystore`) if one doesn't exist
- **`sign_apk()`** — Signs an APK using apksigner with the debug keystore
- **`inject_network_security_config()`** — Creates `res/xml/network_security_config.xml` trusting system + user CAs, patches manifest to reference it. Called when `--trust-user-certs` is set.
- **`process_single_apk()`** — Disassembles APK via apktool, patches the manifest with `sed`, optionally injects network security config, reassembles, signs, and verifies
- **`process_split_apks()`** — Handles split APK bundles by processing `base.apk` then signing all splits
- **`main()`** — Entry point that parses `--trust-user-certs` flag, detects input type (file vs directory), and routes accordingly
### `apk-debuggable.sh` Architecture
Automation wrapper that orchestrates the full device-to-device workflow. Each function sets globals consumed by subsequent steps:
- **`parse_args()`** — Parses positional `APP_NAME` + optional `--device`, `--keep`, `--trust-user-certs`, `--proxy` flags (`--proxy` implies `--trust-user-certs`)
- **`find_adb()`** — Discovers `adb` from SDK locations or PATH (same pattern as `find_android_tools()`)
- **`select_device()`** — Parses `adb devices`, skips unauthorized; auto-selects if one device, interactive numbered menu if multiple. Fetches `ro.product.model` for display.
- **`select_package()`** — Runs `adb shell pm list packages | grep -i <name>`; auto-selects if one match, interactive menu if multiple
- **`pull_apks()`** — Gets paths via `adb shell pm path`, pulls each to `apks_<package>/` directory
- **`make_debuggable()`** — Delegates to `./lib/make-debuggable.sh <pull-dir>` (directory mode), forwarding `--trust-user-certs` if set. Output lands in `<pull-dir>_debuggable/`.
- **`install_apks()`** — Uninstalls existing package (non-fatal), then `adb install` or `adb install-multiple` depending on APK count
- **`cleanup()`** — Removes temp directories unless `--keep` flag was set
- **`start_proxy()`** — (when `--proxy`) Starts mitmproxy Docker container with `--set web_password`, pushes CA cert to device, waits for web UI. Always restarts the container fresh to avoid stale state.
Proxy globals: `CONTAINER_NAME="mitmproxy-android"`, `PROXY_PORT=8080`, `WEB_PORT=8081`, `PROXY_PASSWORD="proxy"`, `MITMPROXY_DIR="$HOME/.mitmproxy"`.
Key conventions: all `adb` commands use `-s "$DEVICE_SERIAL"`, all `adb shell` output stripped of `\r` with `tr -d '\r'`, `grep` calls that may match zero use `|| true` to avoid `set -e` abort.
### `lib/proxy-setup.sh` Architecture
Sets up mitmproxy in Docker and configures an Android emulator to route traffic through it:
- **`parse_args()`** — Parses `--stop` and `--port <port>` flags
- **`find_tools()`** — Discovers `adb`, `emulator` binary, and checks Docker availability
- **`stop_proxy()`** — Stops the `mitmproxy-android` Docker container (used with `--stop`)
- **`start_mitmproxy()`** — Runs mitmproxy Docker container (detached, `--rm`, named `mitmproxy-android`), volume-mounts `~/.mitmproxy` for cert persistence. Polls for CA cert file to appear.
- **`find_emulator()`** — Finds running emulators from `adb devices` (entries matching `emulator-*`). Gets AVD name via `adb emu avd name`. Interactive menu if multiple.
- **`restart_emulator()`** — Kills running emulator, waits for it to disappear, relaunches with `-http-proxy http://127.0.0.1:$PROXY_PORT`, waits for boot via `sys.boot_completed`.
- **`install_cert()`** — Pushes `~/.mitmproxy/mitmproxy-ca-cert.cer` to emulator, attempts automated cert install via intent, prints manual fallback instructions.
- **`print_summary()`** — Displays proxy URL, web UI URL, device info, and usage tips.
Key details: Docker container uses `--rm` for auto-cleanup, `~/.mitmproxy` volume persists certs across runs, emulator launched in background with `&`.
## Platform Notes
- **macOS-specific**: Uses `sed -i ''` (BSD sed syntax) and searches macOS paths like `/Applications/Android Studio.app` for bundled JDK
- Requires: Android SDK (build-tools), apktool (`brew install apktool` or jar), Java/JDK
- `lib/proxy-setup.sh` additionally requires Docker and an Android emulator
## Hardcoded Configuration
Keystore credentials are intentionally hardcoded for debug use:
- Keystore: `debug-resign.keystore`, alias: `debug_key`, password: `debugpass123`
- Work directory: `apk-disassembled` (cleaned up after processing)

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Benjamin Lüscher
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

93
README.md Executable file
View file

@ -0,0 +1,93 @@
# Android Make APK Debuggable
Extracts APKs from a connected Android device, makes them debuggable, and reinstalls — all in one command.
> macOS only — uses BSD `sed` and searches macOS-specific paths for Android Studio and JDK.
## Why?
Release APKs ship without the `android:debuggable` flag, which locks out most development tools. This project patches that flag back in so you can:
- **Inspect layouts and view hierarchies** with [Android Studio's Layout Inspector](https://developer.android.com/studio/debug/layout-inspector) — useful for understanding how a third-party app builds its UI, debugging rendering issues, or reverse-engineering screen flows.
- **Attach a debugger** to a running process via Android Studio's "Attach Debugger to Android Process", allowing you to set breakpoints and step through code in apps you don't have the source for.
- **Intercept HTTPS traffic** with [mitmproxy](https://mitmproxy.org/) (via the `--proxy` flag) — the script also patches the app's network security config to trust user-installed CA certificates, which Android blocks by default since API 24. This lets you inspect API requests, debug authentication flows, or audit data the app sends over the network.
Example: intercepting Wikipedia's API calls with mitmproxy after patching the app with `./apk-debuggable.sh wikipedia --proxy`:
![Intercepting Wikipedia API traffic with mitmproxy](doc/screenshot.png)
## Requirements
| Tool | Purpose | Install |
|------|---------|---------|
| [Android SDK](https://developer.android.com/studio) | `adb`, `apksigner` | Included with [Android Studio](https://developer.android.com/studio) |
| [Java / JDK](https://adoptium.net/) | `keytool` | Bundled with Android Studio, or `brew install --cask temurin` |
| [apktool](https://apktool.org/) | APK disassembly / reassembly | `brew install apktool` |
| [Docker](https://www.docker.com/products/docker-desktop/) | mitmproxy container (`--proxy` only) | [Docker Desktop](https://www.docker.com/products/docker-desktop/) |
## Usage
```bash
# Search for an app by name, extract, patch, and reinstall
./apk-debuggable.sh myapp
# Specify a device if multiple are connected
./apk-debuggable.sh myapp --device emulator-5554
# Keep intermediate files for inspection
./apk-debuggable.sh myapp --keep
# Intercept HTTPS traffic with mitmproxy (requires Docker)
./apk-debuggable.sh myapp --proxy
```
The script will:
1. Find connected devices (interactive menu if multiple)
2. Search for matching packages (interactive menu if multiple)
3. Pull APKs from the device
4. Make them debuggable (via `lib/make-debuggable.sh`)
5. Uninstall the original and install the debuggable version
### Options
| Flag | Description |
|------|-------------|
| `--device <serial>` | Use a specific device (from `adb devices`) |
| `--keep` | Keep intermediate files (pulled APKs and patched APKs) |
| `--trust-user-certs` | Trust user-installed CA certificates (for HTTPS interception) |
| `--proxy` | Start mitmproxy in Docker (implies `--trust-user-certs`, requires Docker) |
## Traffic Interception (mitmproxy)
The `--proxy` flag handles everything — makes the app debuggable, patches it to trust user CA certs, reinstalls it, starts mitmproxy in Docker, and pushes the CA certificate to the device:
```bash
./apk-debuggable.sh myapp --proxy
```
Then install the CA certificate on the device:
1. Open **Settings** → search **"certificate"**
2. Tap **"Install a certificate"** → **"CA certificate"**
3. Tap **"Install anyway"**
4. Select **"mitmproxy-ca-cert.cer"** from internal storage
Open the mitmproxy web UI to inspect traffic:
```
http://localhost:8081
Password: proxy
```
Stop the proxy when done:
```bash
docker stop mitmproxy-android
```
## Advanced / Standalone Usage
The helper scripts in `lib/` can be used independently for more control over individual steps. See [lib/README.md](lib/README.md) for details on:
- **`lib/make-debuggable.sh`** — Patch a single APK or split APK directory to be debuggable
- **`lib/proxy-setup.sh`** — Start mitmproxy and restart an emulator with proxy enabled

486
apk-debuggable.sh Executable file
View file

@ -0,0 +1,486 @@
#!/bin/bash
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Globals set by functions
APP_NAME=""
DEVICE_SERIAL=""
KEEP_FILES=false
TRUST_USER_CERTS=false
PROXY_MODE=false
ADB=""
PACKAGE_NAME=""
PULL_DIR=""
DEBUGGABLE_DIR=""
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# Proxy configuration
CONTAINER_NAME="mitmproxy-android"
PROXY_PORT=8080
WEB_PORT=8081
PROXY_PASSWORD="proxy"
MITMPROXY_DIR="$HOME/.mitmproxy"
print_step() {
echo -e "${GREEN}==>${NC} $1"
}
print_warning() {
echo -e "${YELLOW}Warning:${NC} $1"
}
print_error() {
echo -e "${RED}Error:${NC} $1"
}
print_info() {
echo -e "${BLUE}::${NC} $1"
}
usage() {
echo "Usage: $0 <app-name> [--device <serial>] [--keep] [--trust-user-certs] [--proxy]"
echo ""
echo "Automated end-to-end APK debugging: extracts APKs from a connected"
echo "Android device, makes them debuggable, and reinstalls."
echo ""
echo "Arguments:"
echo " app-name Search term to find the package (e.g., 'myapp')"
echo ""
echo "Options:"
echo " --device <serial> Use a specific device (from 'adb devices')"
echo " --keep Keep intermediate files (pulled APKs and patched APKs)"
echo " --trust-user-certs Trust user-installed CA certificates (for HTTPS interception)"
echo " --proxy Start mitmproxy in Docker for HTTPS traffic interception"
echo " (implies --trust-user-certs, requires Docker)"
echo " --help Show this help message"
echo ""
echo "Examples:"
echo " $0 chrome"
echo " $0 myapp --device emulator-5554"
echo " $0 myapp --keep"
echo " $0 myapp --trust-user-certs"
echo " $0 myapp --proxy"
exit 0
}
parse_args() {
if [[ $# -lt 1 ]]; then
usage
fi
while [[ $# -gt 0 ]]; do
case "$1" in
--help|-h)
usage
;;
--device)
if [[ -z "$2" || "$2" == --* ]]; then
print_error "--device requires a serial number argument"
exit 1
fi
DEVICE_SERIAL="$2"
shift 2
;;
--keep)
KEEP_FILES=true
shift
;;
--trust-user-certs)
TRUST_USER_CERTS=true
shift
;;
--proxy)
PROXY_MODE=true
TRUST_USER_CERTS=true
shift
;;
-*)
print_error "Unknown option: $1"
exit 1
;;
*)
if [[ -z "$APP_NAME" ]]; then
APP_NAME="$1"
else
print_error "Unexpected argument: $1"
exit 1
fi
shift
;;
esac
done
if [[ -z "$APP_NAME" ]]; then
print_error "App name is required"
exit 1
fi
}
find_adb() {
print_step "Searching for adb..."
local sdk_locations=(
"$HOME/Library/Android/sdk"
"/Users/$USER/Library/Android/sdk"
"$ANDROID_HOME"
"$ANDROID_SDK_ROOT"
)
for loc in "${sdk_locations[@]}"; do
if [[ -n "$loc" && -x "$loc/platform-tools/adb" ]]; then
ADB="$loc/platform-tools/adb"
break
fi
done
if [[ -z "$ADB" ]]; then
if command -v adb &> /dev/null; then
ADB="$(command -v adb)"
else
print_error "Could not find adb. Please ensure Android SDK is installed and ANDROID_HOME is set."
exit 1
fi
fi
print_step "Found adb: $ADB"
}
select_device() {
print_step "Looking for connected devices..."
local devices_output
devices_output=$("$ADB" devices 2>&1)
# Parse device lines: serial<tab>state
local serials=()
local models=()
while IFS= read -r line; do
line=$(echo "$line" | tr -d '\r')
# Skip header and empty lines
if [[ "$line" == "List of devices attached" ]] || [[ -z "$line" ]]; then
continue
fi
local serial state
serial=$(echo "$line" | awk '{print $1}')
state=$(echo "$line" | awk '{print $2}')
if [[ "$state" == "device" ]]; then
serials+=("$serial")
local model
model=$("$ADB" -s "$serial" shell getprop ro.product.model 2>/dev/null | tr -d '\r' || echo "unknown")
models+=("$model")
elif [[ "$state" == "unauthorized" ]]; then
print_warning "Device $serial is unauthorized — please accept the USB debugging prompt"
fi
done <<< "$devices_output"
if [[ ${#serials[@]} -eq 0 ]]; then
print_error "No authorized devices found. Connect a device and enable USB debugging."
exit 1
fi
# If --device was specified, validate it
if [[ -n "$DEVICE_SERIAL" ]]; then
local found=false
for s in "${serials[@]}"; do
if [[ "$s" == "$DEVICE_SERIAL" ]]; then
found=true
break
fi
done
if [[ "$found" == false ]]; then
print_error "Device '$DEVICE_SERIAL' not found or not authorized."
echo "Available devices:"
for i in "${!serials[@]}"; do
echo " ${serials[$i]} (${models[$i]})"
done
exit 1
fi
local model
model=$("$ADB" -s "$DEVICE_SERIAL" shell getprop ro.product.model 2>/dev/null | tr -d '\r' || echo "unknown")
print_step "Using specified device: $DEVICE_SERIAL ($model)"
return
fi
# Auto-select if only one device
if [[ ${#serials[@]} -eq 1 ]]; then
DEVICE_SERIAL="${serials[0]}"
print_step "Using device: $DEVICE_SERIAL (${models[0]})"
return
fi
# Interactive menu for multiple devices
echo ""
echo "Multiple devices found:"
for i in "${!serials[@]}"; do
echo " $((i + 1))) ${serials[$i]} (${models[$i]})"
done
echo ""
while true; do
read -rp "Select device [1-${#serials[@]}]: " choice
if [[ "$choice" =~ ^[0-9]+$ ]] && [[ "$choice" -ge 1 ]] && [[ "$choice" -le ${#serials[@]} ]]; then
DEVICE_SERIAL="${serials[$((choice - 1))]}"
print_step "Using device: $DEVICE_SERIAL (${models[$((choice - 1))]})"
return
fi
echo "Invalid selection. Enter a number between 1 and ${#serials[@]}."
done
}
select_package() {
print_step "Searching for packages matching '$APP_NAME'..."
local packages_raw
packages_raw=$("$ADB" -s "$DEVICE_SERIAL" shell pm list packages 2>&1 | tr -d '\r')
local matches=()
while IFS= read -r line; do
[[ -n "$line" ]] && matches+=("$line")
done < <(echo "$packages_raw" | grep -i "$APP_NAME" | sed 's/^package://' || true)
if [[ ${#matches[@]} -eq 0 ]]; then
print_error "No packages found matching '$APP_NAME'"
echo "Try a broader search term, or list all packages with:"
echo " adb -s $DEVICE_SERIAL shell pm list packages"
exit 1
fi
# Auto-select if only one match
if [[ ${#matches[@]} -eq 1 ]]; then
PACKAGE_NAME="${matches[0]}"
print_step "Found package: $PACKAGE_NAME"
return
fi
# Interactive menu for multiple matches
echo ""
echo "Multiple packages found:"
for i in "${!matches[@]}"; do
echo " $((i + 1))) ${matches[$i]}"
done
echo ""
while true; do
read -rp "Select package [1-${#matches[@]}]: " choice
if [[ "$choice" =~ ^[0-9]+$ ]] && [[ "$choice" -ge 1 ]] && [[ "$choice" -le ${#matches[@]} ]]; then
PACKAGE_NAME="${matches[$((choice - 1))]}"
print_step "Selected package: $PACKAGE_NAME"
return
fi
echo "Invalid selection. Enter a number between 1 and ${#matches[@]}."
done
}
pull_apks() {
print_step "Getting APK paths for $PACKAGE_NAME..."
local paths_raw
paths_raw=$("$ADB" -s "$DEVICE_SERIAL" shell pm path "$PACKAGE_NAME" 2>&1 | tr -d '\r')
local apk_paths=()
while IFS= read -r line; do
local path
path=$(echo "$line" | sed 's/^package://')
[[ -n "$path" ]] && apk_paths+=("$path")
done <<< "$paths_raw"
if [[ ${#apk_paths[@]} -eq 0 ]]; then
print_error "Could not find APK paths for $PACKAGE_NAME"
exit 1
fi
print_step "Found ${#apk_paths[@]} APK(s)"
PULL_DIR="apks_${PACKAGE_NAME}"
rm -rf "$PULL_DIR"
mkdir -p "$PULL_DIR"
for apk_path in "${apk_paths[@]}"; do
local apk_name
apk_name=$(basename "$apk_path")
print_info "Pulling $apk_name..."
"$ADB" -s "$DEVICE_SERIAL" pull "$apk_path" "$PULL_DIR/$apk_name"
done
print_step "APKs pulled to $PULL_DIR/"
}
make_debuggable() {
print_step "Making APKs debuggable..."
local make_debuggable_script="$SCRIPT_DIR/lib/make-debuggable.sh"
if [[ ! -x "$make_debuggable_script" ]]; then
print_error "make-debuggable.sh not found or not executable at: $make_debuggable_script"
exit 1
fi
DEBUGGABLE_DIR="${PULL_DIR}_debuggable"
local args=("$PULL_DIR")
[[ "$TRUST_USER_CERTS" == true ]] && args+=("--trust-user-certs")
"$make_debuggable_script" "${args[@]}"
if [[ ! -d "$DEBUGGABLE_DIR" ]]; then
print_error "Expected output directory not found: $DEBUGGABLE_DIR"
exit 1
fi
print_step "Debuggable APKs ready in $DEBUGGABLE_DIR/"
}
install_apks() {
print_step "Uninstalling existing $PACKAGE_NAME..."
"$ADB" -s "$DEVICE_SERIAL" uninstall "$PACKAGE_NAME" || print_warning "Uninstall failed (app may not be installed) — continuing"
local apk_files=("$DEBUGGABLE_DIR"/*.apk)
local apk_count=${#apk_files[@]}
if [[ "$apk_count" -eq 0 ]]; then
print_error "No APK files found in $DEBUGGABLE_DIR"
exit 1
fi
if [[ "$apk_count" -eq 1 ]]; then
print_step "Installing single APK..."
"$ADB" -s "$DEVICE_SERIAL" install "${apk_files[0]}"
else
print_step "Installing $apk_count APKs..."
"$ADB" -s "$DEVICE_SERIAL" install-multiple "${apk_files[@]}"
fi
print_step "Installation complete"
}
cleanup() {
if [[ "$KEEP_FILES" == true ]]; then
print_info "Keeping intermediate files (--keep):"
[[ -d "$PULL_DIR" ]] && print_info " Pulled APKs: $PULL_DIR/"
[[ -d "$DEBUGGABLE_DIR" ]] && print_info " Debuggable APKs: $DEBUGGABLE_DIR/"
return
fi
print_step "Cleaning up temporary files..."
[[ -n "$PULL_DIR" && -d "$PULL_DIR" ]] && rm -rf "$PULL_DIR"
[[ -n "$DEBUGGABLE_DIR" && -d "$DEBUGGABLE_DIR" ]] && rm -rf "$DEBUGGABLE_DIR"
}
start_proxy() {
print_step "Starting mitmproxy..."
if ! command -v docker &> /dev/null; then
print_error "Docker is not installed or not in PATH."
exit 1
fi
if ! docker info &> /dev/null; then
print_error "Docker is not running. Please start Docker and try again."
exit 1
fi
# Stop any existing container to ensure clean state
if docker ps -q --filter "name=$CONTAINER_NAME" | grep -q .; then
print_step "Stopping existing mitmproxy container..."
docker stop "$CONTAINER_NAME" &> /dev/null || true
sleep 1
fi
# Remove stopped container with same name if exists
if docker ps -aq --filter "name=$CONTAINER_NAME" | grep -q .; then
docker rm "$CONTAINER_NAME" &> /dev/null || true
fi
mkdir -p "$MITMPROXY_DIR"
docker run --rm -d \
--name "$CONTAINER_NAME" \
-p "$PROXY_PORT:8080" \
-p "127.0.0.1:$WEB_PORT:8081" \
-v "$MITMPROXY_DIR:/home/mitmproxy/.mitmproxy" \
mitmproxy/mitmproxy \
mitmweb --web-host 0.0.0.0 --set web_password="$PROXY_PASSWORD"
print_step "mitmproxy container started"
# Wait for cert to be generated and push to device
local cert_file="$MITMPROXY_DIR/mitmproxy-ca-cert.cer"
local waited=0
while [[ ! -f "$cert_file" ]]; do
if [[ "$waited" -ge 10 ]]; then
print_warning "Timed out waiting for mitmproxy CA certificate"
break
fi
sleep 1
waited=$((waited + 1))
done
if [[ -f "$cert_file" ]]; then
"$ADB" -s "$DEVICE_SERIAL" push "$cert_file" /sdcard/mitmproxy-ca-cert.cer
print_step "CA certificate pushed to device"
fi
# Wait for web UI to be ready
waited=0
while ! curl -s -o /dev/null "http://localhost:$WEB_PORT" 2>/dev/null; do
if [[ "$waited" -ge 10 ]]; then
print_warning "Timed out waiting for mitmproxy web UI"
break
fi
sleep 1
waited=$((waited + 1))
done
}
main() {
parse_args "$@"
echo ""
echo -e "${GREEN}=== Auto Debug APK ===${NC}"
echo ""
find_adb
select_device
select_package
pull_apks
make_debuggable
install_apks
cleanup
if [[ "$PROXY_MODE" == true ]]; then
echo ""
start_proxy
fi
echo ""
echo -e "${GREEN}=== Done! ===${NC}"
echo ""
echo -e " $PACKAGE_NAME is now debuggable on $DEVICE_SERIAL"
if [[ "$PROXY_MODE" == true ]]; then
echo ""
echo -e " ${GREEN}Proxy:${NC} http://127.0.0.1:$PROXY_PORT"
echo -e " ${GREEN}Web UI:${NC} http://localhost:$WEB_PORT"
echo -e " ${GREEN}Password:${NC} $PROXY_PASSWORD"
echo ""
echo -e " ${YELLOW}Install the mitmproxy CA certificate on the device:${NC}"
echo " 1. Open Settings → search \"certificate\""
echo " 2. Tap \"Install a certificate\" → \"CA certificate\""
echo " 3. Tap \"Install anyway\""
echo " 4. Select \"mitmproxy-ca-cert.cer\" from internal storage"
echo ""
echo " Configure your device to use proxy 10.0.2.2:$PROXY_PORT"
echo " (for emulators, 10.0.2.2 is the host machine)"
echo ""
echo " Stop proxy: docker stop $CONTAINER_NAME"
else
echo ""
echo " To attach a debugger:"
echo " Android Studio → Run → Attach Debugger to Android Process → $PACKAGE_NAME"
fi
}
main "$@"

BIN
doc/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 566 KiB

110
lib/README.md Normal file
View file

@ -0,0 +1,110 @@
# Helper Scripts
These scripts can be used standalone for more control over individual steps. For the automated end-to-end flow, see the [root README](../README.md).
## Requirements
| Tool | Purpose | Install |
|------|---------|---------|
| [Android SDK](https://developer.android.com/studio) | `adb`, `apksigner` | Included with [Android Studio](https://developer.android.com/studio) |
| [Java / JDK](https://adoptium.net/) | `keytool` | Bundled with Android Studio, or `brew install --cask temurin` |
| [apktool](https://apktool.org/) | APK disassembly / reassembly | `brew install apktool` |
| [Docker](https://www.docker.com/products/docker-desktop/) | mitmproxy container (`proxy-setup.sh` only) | [Docker Desktop](https://www.docker.com/products/docker-desktop/) |
| [Android Emulator](https://developer.android.com/studio/run/emulator) | `proxy-setup.sh` only | Included with [Android Studio](https://developer.android.com/studio) |
## `make-debuggable.sh`
Converts release APKs into debuggable versions by disassembling, patching `AndroidManifest.xml`, reassembling, and re-signing with a debug keystore.
### Usage
```bash
# Single APK
./lib/make-debuggable.sh <path-to-apk> [output-apk] [--trust-user-certs]
# Split APK directory (contains base.apk + split APKs)
./lib/make-debuggable.sh <directory> [output-directory] [--trust-user-certs]
```
### Single APK Mode
```bash
./lib/make-debuggable.sh app.apk
# Output: app_debuggable.apk
adb install app_debuggable.apk
```
### Split APK Mode
For apps distributed as split APKs, put all APKs in a directory and pass the directory path:
```bash
./lib/make-debuggable.sh ./my-app-apks
# Output: ./my-app-apks_debuggable/
adb install-multiple ./my-app-apks_debuggable/*.apk
```
The script will:
1. Disassemble `base.apk` with `apktool`
2. Add `android:debuggable="true"` to `AndroidManifest.xml`
3. Reassemble with `apktool`
4. Re-sign all APKs with a debug keystore
### `--trust-user-certs`
Android API 24+ apps only trust system CA certificates by default. This flag injects a `network_security_config.xml` that tells the app to also trust user-installed certificates (like the mitmproxy CA).
```bash
./lib/make-debuggable.sh ./my-app-apks --trust-user-certs
```
## `proxy-setup.sh`
Starts mitmproxy in Docker, restarts a running Android emulator with HTTP proxy enabled, and installs the mitmproxy CA certificate.
### Usage
```bash
# Start proxy and restart emulator with proxy enabled
./lib/proxy-setup.sh
# Use a custom proxy port
./lib/proxy-setup.sh --port 9090
# Stop the proxy
./lib/proxy-setup.sh --stop
```
### Workflow
For a typical interception setup using `proxy-setup.sh` separately:
```bash
# Start proxy and restart emulator with proxy enabled
./lib/proxy-setup.sh
# Make the app trust user-installed CA certs and install it
./apk-debuggable.sh myapp --trust-user-certs
# When done, stop the proxy
./lib/proxy-setup.sh --stop
```
## Troubleshooting
### INSTALL_FAILED_MISSING_SPLIT
The APK requires split APKs. Pull all APKs from the device and use directory mode.
### Signature mismatch
Uninstall the original app before installing the debuggable version:
```bash
adb uninstall <package-id>
```
### apktool not found
```bash
brew install apktool
# or download apktool.jar to the script directory
```

445
lib/make-debuggable.sh Executable file
View file

@ -0,0 +1,445 @@
#!/bin/bash
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Configuration
KEYSTORE_NAME="debug-resign.keystore"
KEYSTORE_ALIAS="debug_key"
KEYSTORE_PASS="debugpass123"
WORK_DIR="apk-disassembled"
TRUST_USER_CERTS=false
print_step() {
echo -e "${GREEN}==>${NC} $1"
}
print_warning() {
echo -e "${YELLOW}Warning:${NC} $1"
}
print_error() {
echo -e "${RED}Error:${NC} $1"
}
# Find Android Studio installation and SDK tools
find_android_tools() {
print_step "Searching for Android SDK tools..."
# Common Android Studio/SDK locations on macOS
local sdk_locations=(
"$HOME/Library/Android/sdk"
"/Users/$USER/Library/Android/sdk"
"$ANDROID_HOME"
"$ANDROID_SDK_ROOT"
)
# Find SDK path
for loc in "${sdk_locations[@]}"; do
if [[ -n "$loc" && -d "$loc/build-tools" ]]; then
ANDROID_SDK="$loc"
break
fi
done
if [[ -z "$ANDROID_SDK" ]]; then
print_error "Could not find Android SDK. Please set ANDROID_HOME or ANDROID_SDK_ROOT environment variable."
exit 1
fi
print_step "Found Android SDK at: $ANDROID_SDK"
# Find latest build-tools version (remove trailing slash)
BUILD_TOOLS_DIR=$(ls -d "$ANDROID_SDK/build-tools"/*/ 2>/dev/null | sort -V | tail -n 1 | sed 's:/*$::')
if [[ -z "$BUILD_TOOLS_DIR" ]]; then
print_error "Could not find build-tools in Android SDK"
exit 1
fi
APKSIGNER="$BUILD_TOOLS_DIR/apksigner"
if [[ ! -f "$APKSIGNER" ]]; then
print_error "apksigner not found at $APKSIGNER"
exit 1
fi
print_step "Found apksigner: $APKSIGNER"
# Find Java from Android Studio's bundled JDK or system
local jdk_locations=(
"/Applications/Android Studio.app/Contents/jbr/Contents/Home"
"/Applications/Android Studio.app/Contents/jre/Contents/Home"
"$JAVA_HOME"
"$(/usr/libexec/java_home 2>/dev/null)"
)
for loc in "${jdk_locations[@]}"; do
if [[ -n "$loc" && -x "$loc/bin/java" ]]; then
JAVA_BIN="$loc/bin/java"
KEYTOOL="$loc/bin/keytool"
break
fi
done
# Fallback to system java/keytool
if [[ -z "$JAVA_BIN" ]]; then
if command -v java &> /dev/null; then
JAVA_BIN="$(which java)"
KEYTOOL="$(which keytool)"
else
print_error "Could not find Java. Please ensure Android Studio or JDK is installed."
exit 1
fi
fi
print_step "Found Java: $JAVA_BIN"
print_step "Found keytool: $KEYTOOL"
# Check for apktool.jar in current directory or common locations
local apktool_jar_locations=(
"./apktool.jar"
"$HOME/apktool/apktool.jar"
"/usr/local/bin/apktool.jar"
)
APKTOOL_JAR=""
for loc in "${apktool_jar_locations[@]}"; do
if [[ -f "$loc" ]]; then
APKTOOL_JAR="$loc"
break
fi
done
if [[ -n "$APKTOOL_JAR" ]]; then
APKTOOL="$JAVA_BIN -jar $APKTOOL_JAR"
print_step "Found apktool: $APKTOOL_JAR (using bundled Java)"
elif command -v apktool &> /dev/null; then
# Use system apktool but override JAVA_HOME
export JAVA_HOME="${JAVA_BIN%/bin/java}"
APKTOOL="apktool"
print_step "Found apktool: apktool (system, using JAVA_HOME=$JAVA_HOME)"
else
print_error "apktool not found. Please either:"
echo " 1. Download apktool.jar to the current directory from https://apktool.org/"
echo " 2. Or run: brew install apktool"
exit 1
fi
}
# Generate keystore if needed
ensure_keystore() {
if [[ ! -f "$KEYSTORE_NAME" ]]; then
print_step "Generating debug keystore..."
"$KEYTOOL" -genkey -v \
-keystore "$KEYSTORE_NAME" \
-alias "$KEYSTORE_ALIAS" \
-keyalg RSA \
-keysize 2048 \
-validity 10000 \
-storepass "$KEYSTORE_PASS" \
-keypass "$KEYSTORE_PASS" \
-dname "CN=Debug, OU=Debug, O=Debug, L=Debug, ST=Debug, C=US"
else
print_step "Using existing keystore: $KEYSTORE_NAME"
fi
}
# Sign an APK
sign_apk() {
local apk="$1"
print_step "Signing: $apk"
"$APKSIGNER" sign \
--ks "$KEYSTORE_NAME" \
--ks-key-alias "$KEYSTORE_ALIAS" \
--ks-pass "pass:$KEYSTORE_PASS" \
--key-pass "pass:$KEYSTORE_PASS" \
"$apk"
}
# Show usage
usage() {
echo "Usage: $0 <path-to-apk-or-directory> [output] [--trust-user-certs]"
echo ""
echo "Makes an APK debuggable by:"
echo " 1. Disassembling the APK"
echo " 2. Adding android:debuggable=\"true\" to AndroidManifest.xml"
echo " 3. Reassembling the APK"
echo " 4. Signing with a debug keystore"
echo ""
echo "Arguments:"
echo " path-to-apk-or-directory"
echo " - Single APK file: processes that APK"
echo " - Directory with split APKs: processes base.apk and re-signs all splits"
echo " output (Optional) Output path (default: <input>_debuggable.apk or <dir>_debuggable/)"
echo ""
echo "Options:"
echo " --trust-user-certs Inject network_security_config.xml to trust user-installed"
echo " CA certificates (required for HTTPS interception on API 24+)"
echo ""
echo "Split APK Support:"
echo " If you have a split APK bundle (base.apk + split_*.apk), put all APKs in a"
echo " directory and pass the directory path. The script will:"
echo " - Make base.apk debuggable"
echo " - Re-sign ALL APKs with the same keystore"
echo " - Output install command for adb install-multiple"
exit 1
}
# Inject network_security_config.xml to trust user-installed CA certs
inject_network_security_config() {
print_step "Injecting network_security_config.xml to trust user CA certificates..."
MANIFEST="$WORK_DIR/AndroidManifest.xml"
local config_content='<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config>
<trust-anchors>
<certificates src="system" />
<certificates src="user" />
</trust-anchors>
</base-config>
</network-security-config>'
if grep -q 'android:networkSecurityConfig' "$MANIFEST"; then
# App already has a network security config — find the referenced file and overwrite it
local ref
ref=$(sed -n 's/.*android:networkSecurityConfig="@xml\/\([^"]*\)".*/\1/p' "$MANIFEST" | head -n 1)
if [[ -n "$ref" ]]; then
print_step "Overwriting existing res/xml/${ref}.xml to trust user CAs"
mkdir -p "$WORK_DIR/res/xml"
echo "$config_content" > "$WORK_DIR/res/xml/${ref}.xml"
else
print_warning "Could not parse existing networkSecurityConfig reference — adding our own"
mkdir -p "$WORK_DIR/res/xml"
echo "$config_content" > "$WORK_DIR/res/xml/network_security_config.xml"
fi
else
# No existing config — create file and add manifest attribute
mkdir -p "$WORK_DIR/res/xml"
echo "$config_content" > "$WORK_DIR/res/xml/network_security_config.xml"
sed -i '' 's/<application/<application android:networkSecurityConfig="@xml\/network_security_config"/' "$MANIFEST"
print_step "Added android:networkSecurityConfig to AndroidManifest.xml"
fi
}
# Process a single APK (make debuggable)
process_single_apk() {
local input_apk="$1"
local output_apk="$2"
# Clean up previous work directory
if [[ -d "$WORK_DIR" ]]; then
print_step "Cleaning up previous work directory..."
rm -rf "$WORK_DIR"
fi
# Step 1: Disassemble
print_step "Disassembling APK..."
$APKTOOL d -f -o "$WORK_DIR" "$input_apk"
# Step 2: Make debuggable
print_step "Making APK debuggable..."
MANIFEST="$WORK_DIR/AndroidManifest.xml"
if [[ ! -f "$MANIFEST" ]]; then
print_error "AndroidManifest.xml not found in disassembled APK"
exit 1
fi
# Check if already debuggable
if grep -q 'android:debuggable="true"' "$MANIFEST"; then
print_warning "APK is already debuggable"
else
# Add debuggable attribute to <application> tag
if grep -q 'android:debuggable="false"' "$MANIFEST"; then
# Replace false with true
sed -i '' 's/android:debuggable="false"/android:debuggable="true"/g' "$MANIFEST"
else
# Add debuggable attribute after <application
sed -i '' 's/<application/<application android:debuggable="true"/g' "$MANIFEST"
fi
print_step "Added android:debuggable=\"true\" to AndroidManifest.xml"
fi
# Step 2b: Inject network security config (if requested)
if [[ "$TRUST_USER_CERTS" == true ]]; then
inject_network_security_config
fi
# Step 2c: Remove AGP-generated locale config if present
# Android Gradle Plugin generates _generated_res_locale_config.xml with attributes
# (e.g. android:defaultLocale) that older aapt2 versions bundled with apktool don't
# support. The file is not needed for the app to function.
local generated_locale="$WORK_DIR/res/xml/_generated_res_locale_config.xml"
if [[ -f "$generated_locale" ]]; then
print_step "Removing _generated_res_locale_config.xml (unsupported by apktool's aapt2)..."
rm "$generated_locale"
# Strip the manifest reference so aapt2 doesn't expect the resource
sed -i '' 's/ android:localeConfig="@xml\/_generated_res_locale_config"//' "$MANIFEST"
# Remove the public.xml resource ID declaration so aapt2 doesn't expect the file
sed -i '' '/_generated_res_locale_config/d' "$WORK_DIR/res/values/public.xml"
fi
# Step 3: Reassemble
print_step "Reassembling APK..."
$APKTOOL b -f -o "$output_apk" "$WORK_DIR"
# Clean up work directory
rm -rf "$WORK_DIR"
}
# Process split APKs directory
process_split_apks() {
local input_dir="$1"
local output_dir="$2"
# Find base.apk
local base_apk=""
if [[ -f "$input_dir/base.apk" ]]; then
base_apk="$input_dir/base.apk"
else
# Look for any APK that might be the base (not starting with split_)
for apk in "$input_dir"/*.apk; do
if [[ -f "$apk" ]] && ! [[ "$(basename "$apk")" =~ ^split_ ]]; then
base_apk="$apk"
break
fi
done
fi
if [[ -z "$base_apk" ]]; then
print_error "Could not find base.apk in directory: $input_dir"
exit 1
fi
print_step "Found base APK: $base_apk"
# Create output directory
mkdir -p "$output_dir"
# Process base APK (make debuggable)
local base_name=$(basename "$base_apk")
process_single_apk "$base_apk" "$output_dir/$base_name"
# Ensure keystore exists
ensure_keystore
# Sign base APK
sign_apk "$output_dir/$base_name"
# Copy and sign all split APKs
for apk in "$input_dir"/*.apk; do
if [[ -f "$apk" ]] && [[ "$apk" != "$base_apk" ]]; then
local apk_name=$(basename "$apk")
print_step "Copying split APK: $apk_name"
cp "$apk" "$output_dir/$apk_name"
sign_apk "$output_dir/$apk_name"
fi
done
# Verify signatures
print_step "Verifying signatures..."
for apk in "$output_dir"/*.apk; do
"$APKSIGNER" verify "$apk"
done
echo ""
echo -e "${GREEN}Success!${NC} Debuggable APKs created in: $output_dir"
echo ""
echo "Install with:"
echo " adb install-multiple $output_dir/*.apk"
}
# Main script
main() {
# Parse arguments: extract --trust-user-certs, treat rest as positional
local positional=()
while [[ $# -gt 0 ]]; do
case "$1" in
--trust-user-certs)
TRUST_USER_CERTS=true
shift
;;
*)
positional+=("$1")
shift
;;
esac
done
if [[ ${#positional[@]} -lt 1 ]]; then
usage
fi
INPUT="${positional[0]}"
OUTPUT_ARG="${positional[1]:-}"
# Find tools first
find_android_tools
if [[ -d "$INPUT" ]]; then
# Directory mode - split APKs
print_step "Directory mode: Processing split APKs"
# Count APKs
apk_count=$(ls -1 "$INPUT"/*.apk 2>/dev/null | wc -l | tr -d ' ')
if [[ "$apk_count" -eq 0 ]]; then
print_error "No APK files found in directory: $INPUT"
exit 1
fi
print_step "Found $apk_count APK(s) in directory"
# Determine output directory
if [[ -n "$OUTPUT_ARG" ]]; then
OUTPUT_DIR="$OUTPUT_ARG"
else
OUTPUT_DIR="${INPUT%/}_debuggable"
fi
process_split_apks "$INPUT" "$OUTPUT_DIR"
elif [[ -f "$INPUT" ]]; then
# Single APK mode
print_step "Single APK mode"
if [[ ! "$INPUT" =~ \.apk$ ]]; then
print_error "Input file must be an APK: $INPUT"
exit 1
fi
# Determine output APK name
if [[ -n "$OUTPUT_ARG" ]]; then
OUTPUT_APK="$OUTPUT_ARG"
else
local basename="${INPUT%.apk}"
OUTPUT_APK="${basename}_debuggable.apk"
fi
print_step "Input APK: $INPUT"
print_step "Output APK: $OUTPUT_APK"
process_single_apk "$INPUT" "$OUTPUT_APK"
# Ensure keystore and sign
ensure_keystore
sign_apk "$OUTPUT_APK"
# Verify signature
print_step "Verifying signature..."
"$APKSIGNER" verify "$OUTPUT_APK"
echo ""
echo -e "${GREEN}Success!${NC} Debuggable APK created: $OUTPUT_APK"
echo ""
echo "Install with: adb install \"$OUTPUT_APK\""
else
print_error "Input not found: $INPUT"
exit 1
fi
}
main "$@"

376
lib/proxy-setup.sh Executable file
View file

@ -0,0 +1,376 @@
#!/bin/bash
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Configuration
CONTAINER_NAME="mitmproxy-android"
PROXY_PORT=8080
WEB_PORT=8081
PROXY_PASSWORD="proxy"
MITMPROXY_DIR="$HOME/.mitmproxy"
ADB=""
EMULATOR_BIN=""
DEVICE_SERIAL=""
AVD_NAME=""
STOP_MODE=false
print_step() {
echo -e "${GREEN}==>${NC} $1"
}
print_warning() {
echo -e "${YELLOW}Warning:${NC} $1"
}
print_error() {
echo -e "${RED}Error:${NC} $1"
}
print_info() {
echo -e "${BLUE}::${NC} $1"
}
usage() {
echo "Usage: $0 [--stop] [--port <port>]"
echo ""
echo "Starts mitmproxy in Docker, restarts a running Android emulator with"
echo "HTTP proxy enabled, and installs the mitmproxy CA certificate."
echo ""
echo "Options:"
echo " --stop Stop the mitmproxy container and exit"
echo " --port <port> Proxy port (default: 8080)"
echo " --help Show this help message"
echo ""
echo "Prerequisites:"
echo " - Docker running"
echo " - An Android emulator currently running"
echo ""
echo "Examples:"
echo " $0 # Start proxy and restart emulator"
echo " $0 --port 9090 # Use a custom proxy port"
echo " $0 --stop # Stop the proxy container"
exit 0
}
parse_args() {
while [[ $# -gt 0 ]]; do
case "$1" in
--help|-h)
usage
;;
--stop)
STOP_MODE=true
shift
;;
--port)
if [[ -z "$2" || "$2" == --* ]]; then
print_error "--port requires a port number argument"
exit 1
fi
PROXY_PORT="$2"
shift 2
;;
-*)
print_error "Unknown option: $1"
exit 1
;;
*)
print_error "Unexpected argument: $1"
exit 1
;;
esac
done
}
find_tools() {
print_step "Searching for required tools..."
# Find adb
local sdk_locations=(
"$HOME/Library/Android/sdk"
"/Users/$USER/Library/Android/sdk"
"$ANDROID_HOME"
"$ANDROID_SDK_ROOT"
)
for loc in "${sdk_locations[@]}"; do
if [[ -n "$loc" && -x "$loc/platform-tools/adb" ]]; then
ADB="$loc/platform-tools/adb"
break
fi
done
if [[ -z "$ADB" ]]; then
if command -v adb &> /dev/null; then
ADB="$(command -v adb)"
else
print_error "Could not find adb. Please ensure Android SDK is installed and ANDROID_HOME is set."
exit 1
fi
fi
print_step "Found adb: $ADB"
# Find emulator binary
for loc in "${sdk_locations[@]}"; do
if [[ -n "$loc" && -x "$loc/emulator/emulator" ]]; then
EMULATOR_BIN="$loc/emulator/emulator"
break
fi
done
if [[ -z "$EMULATOR_BIN" ]]; then
if command -v emulator &> /dev/null; then
EMULATOR_BIN="$(command -v emulator)"
else
print_error "Could not find Android emulator binary. Please ensure Android SDK is installed."
exit 1
fi
fi
print_step "Found emulator: $EMULATOR_BIN"
# Check Docker
if ! command -v docker &> /dev/null; then
print_error "Docker is not installed or not in PATH."
exit 1
fi
if ! docker info &> /dev/null; then
print_error "Docker is not running. Please start Docker and try again."
exit 1
fi
print_step "Docker is available"
}
stop_proxy() {
print_step "Stopping mitmproxy container..."
if docker ps -q --filter "name=$CONTAINER_NAME" | grep -q .; then
docker stop "$CONTAINER_NAME"
print_step "mitmproxy container stopped"
else
print_warning "Container '$CONTAINER_NAME' is not running"
fi
}
start_mitmproxy() {
print_step "Starting mitmproxy..."
# Check if already running
if docker ps -q --filter "name=$CONTAINER_NAME" | grep -q .; then
print_step "mitmproxy container is already running"
return
fi
# Remove stopped container with same name if exists
if docker ps -aq --filter "name=$CONTAINER_NAME" | grep -q .; then
docker rm "$CONTAINER_NAME" &> /dev/null || true
fi
mkdir -p "$MITMPROXY_DIR"
docker run --rm -d \
--name "$CONTAINER_NAME" \
-p "$PROXY_PORT:8080" \
-p "127.0.0.1:$WEB_PORT:8081" \
-v "$MITMPROXY_DIR:/home/mitmproxy/.mitmproxy" \
mitmproxy/mitmproxy \
mitmweb --web-host 0.0.0.0 --set web_password="$PROXY_PASSWORD"
print_step "mitmproxy container started"
# Wait for cert file to be generated
print_step "Waiting for mitmproxy CA certificate..."
local cert_file="$MITMPROXY_DIR/mitmproxy-ca-cert.cer"
local waited=0
while [[ ! -f "$cert_file" ]]; do
if [[ "$waited" -ge 10 ]]; then
print_error "Timed out waiting for mitmproxy certificate at $cert_file"
exit 1
fi
sleep 1
waited=$((waited + 1))
done
print_step "CA certificate ready: $cert_file"
}
find_emulator() {
print_step "Looking for running emulators..."
local devices_output
devices_output=$("$ADB" devices 2>&1)
local serials=()
while IFS= read -r line; do
line=$(echo "$line" | tr -d '\r')
if [[ "$line" == "List of devices attached" ]] || [[ -z "$line" ]]; then
continue
fi
local serial state
serial=$(echo "$line" | awk '{print $1}')
state=$(echo "$line" | awk '{print $2}')
if [[ "$state" == "device" ]] && [[ "$serial" == emulator-* ]]; then
serials+=("$serial")
fi
done <<< "$devices_output"
if [[ ${#serials[@]} -eq 0 ]]; then
print_error "No running emulator found."
echo "Please start an emulator first:"
echo " emulator -list-avds # list available AVDs"
echo " emulator -avd <avd-name> & # start an emulator"
exit 1
fi
# Auto-select if only one emulator
if [[ ${#serials[@]} -eq 1 ]]; then
DEVICE_SERIAL="${serials[0]}"
else
echo ""
echo "Multiple emulators found:"
for i in "${!serials[@]}"; do
echo " $((i + 1))) ${serials[$i]}"
done
echo ""
while true; do
read -rp "Select emulator [1-${#serials[@]}]: " choice
if [[ "$choice" =~ ^[0-9]+$ ]] && [[ "$choice" -ge 1 ]] && [[ "$choice" -le ${#serials[@]} ]]; then
DEVICE_SERIAL="${serials[$((choice - 1))]}"
break
fi
echo "Invalid selection. Enter a number between 1 and ${#serials[@]}."
done
fi
print_step "Using emulator: $DEVICE_SERIAL"
# Get AVD name
AVD_NAME=$("$ADB" -s "$DEVICE_SERIAL" emu avd name 2>/dev/null | head -n 1 | tr -d '\r' || true)
if [[ -z "$AVD_NAME" ]]; then
print_error "Could not determine AVD name for $DEVICE_SERIAL"
exit 1
fi
print_step "AVD name: $AVD_NAME"
}
restart_emulator() {
print_step "Restarting emulator with HTTP proxy..."
# Kill running emulator
print_info "Shutting down emulator $DEVICE_SERIAL..."
"$ADB" -s "$DEVICE_SERIAL" emu kill 2>/dev/null || true
# Wait for emulator to disappear from adb devices
local waited=0
while "$ADB" devices 2>&1 | grep -q "$DEVICE_SERIAL"; do
if [[ "$waited" -ge 30 ]]; then
print_error "Timed out waiting for emulator to shut down"
exit 1
fi
sleep 1
waited=$((waited + 1))
done
print_step "Emulator stopped"
# Launch emulator with proxy
print_info "Starting emulator '$AVD_NAME' with -http-proxy http://127.0.0.1:$PROXY_PORT..."
"$EMULATOR_BIN" -avd "$AVD_NAME" -http-proxy "http://127.0.0.1:$PROXY_PORT" &
# Wait for device to come online
print_info "Waiting for emulator to boot..."
"$ADB" wait-for-device
# Poll for boot completion
waited=0
while true; do
local boot_completed
boot_completed=$("$ADB" shell getprop sys.boot_completed 2>/dev/null | tr -d '\r' || true)
if [[ "$boot_completed" == "1" ]]; then
break
fi
if [[ "$waited" -ge 120 ]]; then
print_error "Timed out waiting for emulator to boot"
exit 1
fi
sleep 2
waited=$((waited + 2))
done
# Update DEVICE_SERIAL to the new emulator instance
DEVICE_SERIAL=$("$ADB" devices 2>&1 | grep 'emulator-' | grep 'device' | awk '{print $1}' | head -n 1 | tr -d '\r')
print_step "Emulator booted: $DEVICE_SERIAL"
}
install_cert() {
print_step "Installing mitmproxy CA certificate..."
local cert_file="$MITMPROXY_DIR/mitmproxy-ca-cert.cer"
if [[ ! -f "$cert_file" ]]; then
print_error "Certificate file not found: $cert_file"
exit 1
fi
"$ADB" -s "$DEVICE_SERIAL" push "$cert_file" /sdcard/mitmproxy-ca-cert.cer
print_step "Certificate pushed to /sdcard/mitmproxy-ca-cert.cer"
# Attempt automated install via cert installer intent
"$ADB" -s "$DEVICE_SERIAL" shell am start \
-n com.android.certinstaller/.CertInstallerMain \
-a android.intent.action.VIEW \
-t application/x-x509-ca-cert \
-d file:///sdcard/mitmproxy-ca-cert.cer 2>/dev/null || true
echo ""
print_warning "If the certificate installer did not open automatically:"
echo " 1. Open Settings → search \"certificate\""
echo " 2. Tap \"Install a certificate\" → \"CA certificate\""
echo " 3. Tap \"Install anyway\""
echo " 4. Select \"mitmproxy-ca-cert.cer\" from internal storage"
}
print_summary() {
echo ""
echo -e "${GREEN}=== Proxy Setup Complete ===${NC}"
echo ""
echo " Proxy: http://127.0.0.1:$PROXY_PORT"
echo " Web UI: http://localhost:$WEB_PORT"
echo " Password: $PROXY_PASSWORD"
echo " Emulator: $DEVICE_SERIAL ($AVD_NAME)"
echo ""
echo " The emulator is configured to route traffic through mitmproxy."
echo " Open the Web UI to inspect HTTP/HTTPS traffic."
echo ""
echo -e " ${YELLOW}Tip:${NC} To intercept HTTPS from apps targeting API 24+, use:"
echo " ./lib/make-debuggable.sh <apk-or-dir> --trust-user-certs"
echo " ./apk-debuggable.sh <app-name> --trust-user-certs"
echo ""
echo " To stop the proxy:"
echo " ./lib/proxy-setup.sh --stop"
}
main() {
parse_args "$@"
if [[ "$STOP_MODE" == true ]]; then
stop_proxy
exit 0
fi
echo ""
echo -e "${GREEN}=== Proxy Setup ===${NC}"
echo ""
find_tools
start_mitmproxy
find_emulator
restart_emulator
install_cert
print_summary
}
main "$@"