Initial release: automated APK debuggable patching for Android
This commit is contained in:
commit
18f5562aca
9 changed files with 1625 additions and 0 deletions
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