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

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 "$@"