commit 18f5562acaa9c8021171590eac4354542908749b Author: benjamin-luescher Date: Mon Feb 2 16:57:46 2026 +0100 Initial release: automated APK debuggable patching for Android diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ac99d66 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +*.keystore +*_debuggable +apk-disassembled/ +apks_*/ +.idea/ +.claude/ \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..dada203 --- /dev/null +++ b/CLAUDE.md @@ -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 [--device ] [--keep] [--trust-user-certs] [--proxy] + +# Single APK +./lib/make-debuggable.sh [output-apk] [--trust-user-certs] + +# Split APK directory (contains base.apk + split APKs) +./lib/make-debuggable.sh [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 `; auto-selects if one match, interactive menu if multiple +- **`pull_apks()`** — Gets paths via `adb shell pm path`, pulls each to `apks_/` directory +- **`make_debuggable()`** — Delegates to `./lib/make-debuggable.sh ` (directory mode), forwarding `--trust-user-certs` if set. Output lands in `_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 ` 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) \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6cd671a --- /dev/null +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md new file mode 100755 index 0000000..e77cdd5 --- /dev/null +++ b/README.md @@ -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 ` | 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 diff --git a/apk-debuggable.sh b/apk-debuggable.sh new file mode 100755 index 0000000..355ab2e --- /dev/null +++ b/apk-debuggable.sh @@ -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 [--device ] [--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 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: serialstate + 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 "$@" diff --git a/doc/screenshot.png b/doc/screenshot.png new file mode 100644 index 0000000..4693dac Binary files /dev/null and b/doc/screenshot.png differ diff --git a/lib/README.md b/lib/README.md new file mode 100644 index 0000000..bbd9cc9 --- /dev/null +++ b/lib/README.md @@ -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 [output-apk] [--trust-user-certs] + +# Split APK directory (contains base.apk + split APKs) +./lib/make-debuggable.sh [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 +``` + +### apktool not found +```bash +brew install apktool +# or download apktool.jar to the script directory +``` diff --git a/lib/make-debuggable.sh b/lib/make-debuggable.sh new file mode 100755 index 0000000..8031d47 --- /dev/null +++ b/lib/make-debuggable.sh @@ -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 [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: _debuggable.apk or _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=' + + + + + + + +' + + 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/ 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 /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 "$@" diff --git a/lib/proxy-setup.sh b/lib/proxy-setup.sh new file mode 100755 index 0000000..93c08c0 --- /dev/null +++ b/lib/proxy-setup.sh @@ -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 ]" + 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 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 & # 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 --trust-user-certs" + echo " ./apk-debuggable.sh --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 "$@"