Initial release: automated APK debuggable patching for Android
This commit is contained in:
commit
18f5562aca
9 changed files with 1625 additions and 0 deletions
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
*.keystore
|
||||||
|
*_debuggable
|
||||||
|
apk-disassembled/
|
||||||
|
apks_*/
|
||||||
|
.idea/
|
||||||
|
.claude/
|
||||||
88
CLAUDE.md
Normal file
88
CLAUDE.md
Normal 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
21
LICENSE
Normal 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
93
README.md
Executable 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`:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 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
486
apk-debuggable.sh
Executable 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
BIN
doc/screenshot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 566 KiB |
110
lib/README.md
Normal file
110
lib/README.md
Normal 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
445
lib/make-debuggable.sh
Executable 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
376
lib/proxy-setup.sh
Executable 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 "$@"
|
||||||
Loading…
Add table
Add a link
Reference in a new issue