feat: Add support for local APKs, multi-device workflows, and anti-tampering detection
This change introduces the ability to patch local APK files or directories, support for separate source and target devices, and detection of common anti-tampering libraries. Key changes: - **Local APK Support**: Added `--apk <path>` flag to use local `.apk` files or split-APK directories instead of pulling from a device. - **Two-Device Workflow**: Added `--source <serial>` flag to pull an APK from one device (e.g., a Play Store emulator) and install the patched version on another (e.g., a `userdebug` emulator). - **Anti-Tampering Detection**: The patching script now scans for known integrity-protection libraries (e.g., PairIP, DexGuard, Bangcle) and issues a warning if detected. - **Improved Disassembly**: Introduced a `--no-res` optimization when user certificate trust is not required, avoiding common `apktool` resource decoding errors. - **Package Name Extraction**: Integrated `aapt2` to automatically detect package names from local APK files for cleaner uninstalls. - **Enhanced Device Selection**: Updated the interactive menu to handle source/target selection and filter unauthorized devices more effectively. - **Documentation**: Updated `README.md` and `CLAUDE.md` with new usage examples and information regarding anti-tampering limitations.
This commit is contained in:
parent
0c5c835263
commit
86d8393dd4
4 changed files with 339 additions and 32 deletions
13
CLAUDE.md
13
CLAUDE.md
|
|
@ -15,6 +15,9 @@ Three Bash scripts for making Android APKs debuggable and intercepting traffic:
|
||||||
# Automated end-to-end (device → extract → patch → reinstall)
|
# Automated end-to-end (device → extract → patch → reinstall)
|
||||||
./apk-debuggable.sh <app-name> [--device <serial>] [--keep] [--trust-user-certs] [--proxy]
|
./apk-debuggable.sh <app-name> [--device <serial>] [--keep] [--trust-user-certs] [--proxy]
|
||||||
|
|
||||||
|
# Use a local APK file or split-APK directory
|
||||||
|
./apk-debuggable.sh --apk <path> [--device <serial>] [--keep] [--trust-user-certs] [--proxy]
|
||||||
|
|
||||||
# Single APK
|
# Single APK
|
||||||
./lib/make-debuggable.sh <path-to-apk> [output-apk] [--trust-user-certs]
|
./lib/make-debuggable.sh <path-to-apk> [output-apk] [--trust-user-certs]
|
||||||
|
|
||||||
|
|
@ -46,18 +49,22 @@ There are no build, test, or lint commands.
|
||||||
|
|
||||||
Automation wrapper that orchestrates the full device-to-device workflow. Each function sets globals consumed by subsequent steps:
|
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`)
|
- **`parse_args()`** — Parses positional `APP_NAME` + optional `--apk`, `--device`, `--keep`, `--trust-user-certs`, `--proxy` flags (`--proxy` implies `--trust-user-certs`). Validates that exactly one of `APP_NAME` or `--apk` is provided.
|
||||||
- **`find_adb()`** — Discovers `adb` from SDK locations or PATH (same pattern as `find_android_tools()`)
|
- **`find_adb()`** — Discovers `adb` from SDK locations or PATH (same pattern as `find_android_tools()`)
|
||||||
|
- **`find_aapt2()`** — Discovers `aapt2` from latest `build-tools/*/aapt2` in SDK locations, fallback to `command -v aapt2`. Used only in `--apk` mode for package name extraction.
|
||||||
- **`select_device()`** — Parses `adb devices`, skips unauthorized; auto-selects if one device, interactive numbered menu if multiple. Fetches `ro.product.model` for display.
|
- **`select_device()`** — Parses `adb devices`, skips unauthorized; auto-selects if one device, interactive numbered menu if multiple. Fetches `ro.product.model` for display.
|
||||||
|
- **`prepare_local_apk()`** — (when `--apk`) For directory input, uses the path as-is as `PULL_DIR`. For single APK, copies to temp dir `apks_local_<basename>`. Extracts `PACKAGE_NAME` via `aapt2 dump badging` (soft failure if aapt2 unavailable).
|
||||||
- **`select_package()`** — Runs `adb shell pm list packages | grep -i <name>`; auto-selects if one match, interactive menu if multiple
|
- **`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
|
- **`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/`.
|
- **`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
|
- **`install_apks()`** — Uninstalls existing package if `PACKAGE_NAME` is known (non-fatal), then `adb install` or `adb install-multiple` depending on APK count
|
||||||
- **`cleanup()`** — Removes temp directories unless `--keep` flag was set
|
- **`cleanup()`** — Removes temp directories unless `--keep` flag was set. Never deletes a user-provided `--apk` directory.
|
||||||
- **`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.
|
- **`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"`.
|
Proxy globals: `CONTAINER_NAME="mitmproxy-android"`, `PROXY_PORT=8080`, `WEB_PORT=8081`, `PROXY_PASSWORD="proxy"`, `MITMPROXY_DIR="$HOME/.mitmproxy"`.
|
||||||
|
|
||||||
|
Main flow: `find_adb → select_device → [prepare_local_apk | select_package + pull_apks] → make_debuggable → install_apks → cleanup`
|
||||||
|
|
||||||
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.
|
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
|
### `lib/proxy-setup.sh` Architecture
|
||||||
|
|
|
||||||
15
README.md
15
README.md
|
|
@ -39,6 +39,12 @@ Example: intercepting Wikipedia's API calls with mitmproxy after patching the ap
|
||||||
|
|
||||||
# Intercept HTTPS traffic with mitmproxy (requires Docker)
|
# Intercept HTTPS traffic with mitmproxy (requires Docker)
|
||||||
./apk-debuggable.sh myapp --proxy
|
./apk-debuggable.sh myapp --proxy
|
||||||
|
|
||||||
|
# Use a local APK file (useful for emulators without Play Store)
|
||||||
|
./apk-debuggable.sh --apk ./some-app.apk --device emulator-5554
|
||||||
|
|
||||||
|
# Use a local split-APK directory
|
||||||
|
./apk-debuggable.sh --apk ./split-apks/ --proxy
|
||||||
```
|
```
|
||||||
|
|
||||||
The script will:
|
The script will:
|
||||||
|
|
@ -52,6 +58,7 @@ The script will:
|
||||||
|
|
||||||
| Flag | Description |
|
| Flag | Description |
|
||||||
|------|-------------|
|
|------|-------------|
|
||||||
|
| `--apk <path>` | Use a local APK file or split-APK directory instead of pulling from the device |
|
||||||
| `--device <serial>` | Use a specific device (from `adb devices`) |
|
| `--device <serial>` | Use a specific device (from `adb devices`) |
|
||||||
| `--keep` | Keep intermediate files (pulled APKs and patched APKs) |
|
| `--keep` | Keep intermediate files (pulled APKs and patched APKs) |
|
||||||
| `--trust-user-certs` | Trust user-installed CA certificates (for HTTPS interception) |
|
| `--trust-user-certs` | Trust user-installed CA certificates (for HTTPS interception) |
|
||||||
|
|
@ -85,6 +92,14 @@ Stop the proxy when done:
|
||||||
docker stop mitmproxy-android
|
docker stop mitmproxy-android
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
|
||||||
|
**Anti-tampering protection** — Some apps include native integrity-checking libraries (e.g. PairIP, DexGuard) that detect APK modifications and crash on launch. The script detects known anti-tampering libraries and warns you, but it cannot bypass them.
|
||||||
|
|
||||||
|
**Workaround:** Use a **"Google APIs" emulator image** (not "Google Play") — these are `userdebug` builds where all apps are debuggable by default (`ro.debuggable=1`), no APK patching needed. Since these images don't include the Play Store, use `--apk` to install a local APK: `./apk-debuggable.sh --apk ./app.apk --device emulator-5554`.
|
||||||
|
|
||||||
|
**Certificate pinning** — Apps that pin specific server certificates (common in banking apps) will reject connections even with the CA installed. Bypassing pinning requires tools like [Frida](https://frida.re/), which is out of scope.
|
||||||
|
|
||||||
## Advanced / Standalone Usage
|
## 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:
|
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:
|
||||||
|
|
|
||||||
|
|
@ -12,14 +12,18 @@ NC='\033[0m' # No Color
|
||||||
# Globals set by functions
|
# Globals set by functions
|
||||||
APP_NAME=""
|
APP_NAME=""
|
||||||
DEVICE_SERIAL=""
|
DEVICE_SERIAL=""
|
||||||
|
SOURCE_SERIAL=""
|
||||||
KEEP_FILES=false
|
KEEP_FILES=false
|
||||||
TRUST_USER_CERTS=false
|
TRUST_USER_CERTS=false
|
||||||
PROXY_MODE=false
|
PROXY_MODE=false
|
||||||
ADB=""
|
ADB=""
|
||||||
|
AAPT2=""
|
||||||
PACKAGE_NAME=""
|
PACKAGE_NAME=""
|
||||||
PULL_DIR=""
|
PULL_DIR=""
|
||||||
DEBUGGABLE_DIR=""
|
DEBUGGABLE_DIR=""
|
||||||
PROXY_HOST=""
|
PROXY_HOST=""
|
||||||
|
LOCAL_APK_PATH=""
|
||||||
|
LOCAL_APK_IS_TEMP=false
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
|
||||||
# Proxy configuration
|
# Proxy configuration
|
||||||
|
|
@ -46,7 +50,8 @@ print_info() {
|
||||||
}
|
}
|
||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
echo "Usage: $0 <app-name> [--device <serial>] [--keep] [--trust-user-certs] [--proxy]"
|
echo "Usage: $0 <app-name> [--device <serial>] [--source <serial>] [--keep] [--trust-user-certs] [--proxy]"
|
||||||
|
echo " $0 --apk <path> [--device <serial>] [--keep] [--trust-user-certs] [--proxy]"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Automated end-to-end APK debugging: extracts APKs from a connected"
|
echo "Automated end-to-end APK debugging: extracts APKs from a connected"
|
||||||
echo "Android device, makes them debuggable, and reinstalls."
|
echo "Android device, makes them debuggable, and reinstalls."
|
||||||
|
|
@ -55,7 +60,12 @@ usage() {
|
||||||
echo " app-name Search term to find the package (e.g., 'myapp')"
|
echo " app-name Search term to find the package (e.g., 'myapp')"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Options:"
|
echo "Options:"
|
||||||
echo " --device <serial> Use a specific device (from 'adb devices')"
|
echo " --apk <path> Use a local APK file or split-APK directory instead of"
|
||||||
|
echo " pulling from the device"
|
||||||
|
echo " --device <serial> Target device for installation (from 'adb devices')"
|
||||||
|
echo " --source <serial> Pull APK from this device instead of the target device."
|
||||||
|
echo " Useful for grabbing apps from a Play Store emulator and"
|
||||||
|
echo " installing on a non-Play-Store emulator."
|
||||||
echo " --keep Keep intermediate files (pulled APKs and patched APKs)"
|
echo " --keep Keep intermediate files (pulled APKs and patched APKs)"
|
||||||
echo " --trust-user-certs Trust user-installed CA certificates (for HTTPS interception)"
|
echo " --trust-user-certs Trust user-installed CA certificates (for HTTPS interception)"
|
||||||
echo " --proxy Start mitmproxy in Docker for HTTPS traffic interception"
|
echo " --proxy Start mitmproxy in Docker for HTTPS traffic interception"
|
||||||
|
|
@ -65,9 +75,12 @@ usage() {
|
||||||
echo "Examples:"
|
echo "Examples:"
|
||||||
echo " $0 chrome"
|
echo " $0 chrome"
|
||||||
echo " $0 myapp --device emulator-5554"
|
echo " $0 myapp --device emulator-5554"
|
||||||
|
echo " $0 myapp --source emulator-5554 --device emulator-5556"
|
||||||
echo " $0 myapp --keep"
|
echo " $0 myapp --keep"
|
||||||
echo " $0 myapp --trust-user-certs"
|
echo " $0 myapp --trust-user-certs"
|
||||||
echo " $0 myapp --proxy"
|
echo " $0 myapp --proxy"
|
||||||
|
echo " $0 --apk ./some-app.apk --device emulator-5554"
|
||||||
|
echo " $0 --apk ./split-apks/ --proxy"
|
||||||
exit 0
|
exit 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -81,6 +94,18 @@ parse_args() {
|
||||||
--help|-h)
|
--help|-h)
|
||||||
usage
|
usage
|
||||||
;;
|
;;
|
||||||
|
--apk)
|
||||||
|
if [[ -z "$2" || "$2" == --* ]]; then
|
||||||
|
print_error "--apk requires a file or directory path"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
LOCAL_APK_PATH="$2"
|
||||||
|
if [[ ! -e "$LOCAL_APK_PATH" ]]; then
|
||||||
|
print_error "Path does not exist: $LOCAL_APK_PATH"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
--device)
|
--device)
|
||||||
if [[ -z "$2" || "$2" == --* ]]; then
|
if [[ -z "$2" || "$2" == --* ]]; then
|
||||||
print_error "--device requires a serial number argument"
|
print_error "--device requires a serial number argument"
|
||||||
|
|
@ -89,6 +114,14 @@ parse_args() {
|
||||||
DEVICE_SERIAL="$2"
|
DEVICE_SERIAL="$2"
|
||||||
shift 2
|
shift 2
|
||||||
;;
|
;;
|
||||||
|
--source)
|
||||||
|
if [[ -z "$2" || "$2" == --* ]]; then
|
||||||
|
print_error "--source requires a serial number argument"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
SOURCE_SERIAL="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
--keep)
|
--keep)
|
||||||
KEEP_FILES=true
|
KEEP_FILES=true
|
||||||
shift
|
shift
|
||||||
|
|
@ -118,8 +151,23 @@ parse_args() {
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
if [[ -z "$APP_NAME" ]]; then
|
if [[ -z "$APP_NAME" && -z "$LOCAL_APK_PATH" ]]; then
|
||||||
print_error "App name is required"
|
print_error "App name or --apk <path> is required"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$APP_NAME" && -n "$LOCAL_APK_PATH" ]]; then
|
||||||
|
print_error "Cannot use both app name and --apk at the same time"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$SOURCE_SERIAL" && -n "$LOCAL_APK_PATH" ]]; then
|
||||||
|
print_error "Cannot use both --source and --apk at the same time"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$SOURCE_SERIAL" && -z "$APP_NAME" ]]; then
|
||||||
|
print_error "--source requires an app name to search for on the source device"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
@ -151,6 +199,73 @@ find_adb() {
|
||||||
fi
|
fi
|
||||||
|
|
||||||
print_step "Found adb: $ADB"
|
print_step "Found adb: $ADB"
|
||||||
|
|
||||||
|
# Ensure adb server is running and has discovered all devices
|
||||||
|
"$ADB" start-server 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
find_aapt2() {
|
||||||
|
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" && -d "$loc/build-tools" ]]; then
|
||||||
|
# Find latest build-tools version with aapt2
|
||||||
|
local latest
|
||||||
|
latest=$(ls -1 "$loc/build-tools" 2>/dev/null | sort -V | tail -1)
|
||||||
|
if [[ -n "$latest" && -x "$loc/build-tools/$latest/aapt2" ]]; then
|
||||||
|
AAPT2="$loc/build-tools/$latest/aapt2"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -z "$AAPT2" ]]; then
|
||||||
|
if command -v aapt2 &> /dev/null; then
|
||||||
|
AAPT2="$(command -v aapt2)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
prepare_local_apk() {
|
||||||
|
print_step "Preparing local APK: $LOCAL_APK_PATH"
|
||||||
|
|
||||||
|
if [[ -d "$LOCAL_APK_PATH" ]]; then
|
||||||
|
# Directory input — use as-is
|
||||||
|
PULL_DIR="$LOCAL_APK_PATH"
|
||||||
|
# Find base.apk for package name extraction
|
||||||
|
local base_apk="$PULL_DIR/base.apk"
|
||||||
|
if [[ ! -f "$base_apk" ]]; then
|
||||||
|
# Try to find any APK
|
||||||
|
base_apk=$(ls "$PULL_DIR"/*.apk 2>/dev/null | head -1)
|
||||||
|
fi
|
||||||
|
if [[ -n "$base_apk" && -n "$AAPT2" ]]; then
|
||||||
|
PACKAGE_NAME=$("$AAPT2" dump badging "$base_apk" 2>/dev/null | grep "^package:" | sed "s/.*name='//" | sed "s/'.*//" || true)
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# Single APK — copy to temp dir
|
||||||
|
local basename
|
||||||
|
basename=$(basename "$LOCAL_APK_PATH" .apk)
|
||||||
|
PULL_DIR="apks_local_${basename}"
|
||||||
|
LOCAL_APK_IS_TEMP=true
|
||||||
|
rm -rf "$PULL_DIR"
|
||||||
|
mkdir -p "$PULL_DIR"
|
||||||
|
cp "$LOCAL_APK_PATH" "$PULL_DIR/"
|
||||||
|
if [[ -n "$AAPT2" ]]; then
|
||||||
|
PACKAGE_NAME=$("$AAPT2" dump badging "$LOCAL_APK_PATH" 2>/dev/null | grep "^package:" | sed "s/.*name='//" | sed "s/'.*//" || true)
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$PACKAGE_NAME" ]]; then
|
||||||
|
print_step "Detected package: $PACKAGE_NAME"
|
||||||
|
else
|
||||||
|
print_warning "Could not detect package name (aapt2 not found or extraction failed)"
|
||||||
|
print_info "The app will be installed but the previous version won't be uninstalled automatically"
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
select_device() {
|
select_device() {
|
||||||
|
|
@ -174,7 +289,7 @@ select_device() {
|
||||||
if [[ "$state" == "device" ]]; then
|
if [[ "$state" == "device" ]]; then
|
||||||
serials+=("$serial")
|
serials+=("$serial")
|
||||||
local model
|
local model
|
||||||
model=$("$ADB" -s "$serial" shell getprop ro.product.model 2>/dev/null | tr -d '\r' || echo "unknown")
|
model=$("$ADB" -s "$serial" shell getprop ro.product.model < /dev/null 2>/dev/null | tr -d '\r' || echo "unknown")
|
||||||
models+=("$model")
|
models+=("$model")
|
||||||
elif [[ "$state" == "unauthorized" ]]; then
|
elif [[ "$state" == "unauthorized" ]]; then
|
||||||
print_warning "Device $serial is unauthorized — please accept the USB debugging prompt"
|
print_warning "Device $serial is unauthorized — please accept the USB debugging prompt"
|
||||||
|
|
@ -186,7 +301,27 @@ select_device() {
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# If --device was specified, validate it
|
# Validate --source if specified
|
||||||
|
if [[ -n "$SOURCE_SERIAL" ]]; then
|
||||||
|
local source_found=false
|
||||||
|
for i in "${!serials[@]}"; do
|
||||||
|
if [[ "${serials[$i]}" == "$SOURCE_SERIAL" ]]; then
|
||||||
|
source_found=true
|
||||||
|
print_step "Source device: $SOURCE_SERIAL (${models[$i]})"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [[ "$source_found" == false ]]; then
|
||||||
|
print_error "Source device '$SOURCE_SERIAL' not found or not authorized."
|
||||||
|
echo "Available devices:"
|
||||||
|
for i in "${!serials[@]}"; do
|
||||||
|
echo " ${serials[$i]} (${models[$i]})"
|
||||||
|
done
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Validate --device if specified
|
||||||
if [[ -n "$DEVICE_SERIAL" ]]; then
|
if [[ -n "$DEVICE_SERIAL" ]]; then
|
||||||
local found=false
|
local found=false
|
||||||
for s in "${serials[@]}"; do
|
for s in "${serials[@]}"; do
|
||||||
|
|
@ -205,32 +340,102 @@ select_device() {
|
||||||
fi
|
fi
|
||||||
local model
|
local model
|
||||||
model=$("$ADB" -s "$DEVICE_SERIAL" shell getprop ro.product.model 2>/dev/null | tr -d '\r' || echo "unknown")
|
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)"
|
if [[ -n "$SOURCE_SERIAL" ]]; then
|
||||||
|
print_step "Target device: $DEVICE_SERIAL ($model)"
|
||||||
|
else
|
||||||
|
print_step "Using device: $DEVICE_SERIAL ($model)"
|
||||||
|
fi
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Auto-select if only one device
|
# Auto-select if only one device
|
||||||
if [[ ${#serials[@]} -eq 1 ]]; then
|
if [[ ${#serials[@]} -eq 1 ]]; then
|
||||||
DEVICE_SERIAL="${serials[0]}"
|
DEVICE_SERIAL="${serials[0]}"
|
||||||
print_step "Using device: $DEVICE_SERIAL (${models[0]})"
|
if [[ -n "$SOURCE_SERIAL" ]]; then
|
||||||
|
print_step "Target device: $DEVICE_SERIAL (${models[0]})"
|
||||||
|
else
|
||||||
|
print_step "Using device: $DEVICE_SERIAL (${models[0]})"
|
||||||
|
fi
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Interactive menu for multiple devices
|
# Multiple devices available, no --device specified.
|
||||||
echo ""
|
# If pulling from device (APP_NAME mode) and no --source, prompt for source first.
|
||||||
echo "Multiple devices found:"
|
if [[ -n "$APP_NAME" && -z "$SOURCE_SERIAL" ]]; then
|
||||||
|
echo ""
|
||||||
|
echo "Multiple devices found. Select source device (pull APK from):"
|
||||||
|
for i in "${!serials[@]}"; do
|
||||||
|
echo " $((i + 1))) ${serials[$i]} (${models[$i]})"
|
||||||
|
done
|
||||||
|
echo ""
|
||||||
|
while true; do
|
||||||
|
read -rp "Source [1-${#serials[@]}]: " choice
|
||||||
|
if [[ "$choice" =~ ^[0-9]+$ ]] && [[ "$choice" -ge 1 ]] && [[ "$choice" -le ${#serials[@]} ]]; then
|
||||||
|
SOURCE_SERIAL="${serials[$((choice - 1))]}"
|
||||||
|
print_step "Source device: $SOURCE_SERIAL (${models[$((choice - 1))]})"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
echo "Invalid selection. Enter a number between 1 and ${#serials[@]}."
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Build target candidate list (exclude source in two-device mode)
|
||||||
|
local target_serials=()
|
||||||
|
local target_models=()
|
||||||
for i in "${!serials[@]}"; do
|
for i in "${!serials[@]}"; do
|
||||||
echo " $((i + 1))) ${serials[$i]} (${models[$i]})"
|
if [[ -n "$SOURCE_SERIAL" && "${serials[$i]}" == "$SOURCE_SERIAL" ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
target_serials+=("${serials[$i]}")
|
||||||
|
target_models+=("${models[$i]}")
|
||||||
|
done
|
||||||
|
|
||||||
|
# If no candidates after filtering (only source device connected), error
|
||||||
|
if [[ -n "$SOURCE_SERIAL" && ${#target_serials[@]} -eq 0 ]]; then
|
||||||
|
print_error "No target device found. Connect a second device/emulator to install on."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Fall back to full list if no source filtering happened
|
||||||
|
if [[ ${#target_serials[@]} -eq 0 ]]; then
|
||||||
|
target_serials=("${serials[@]}")
|
||||||
|
target_models=("${models[@]}")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Auto-select if only one target candidate
|
||||||
|
if [[ ${#target_serials[@]} -eq 1 ]]; then
|
||||||
|
DEVICE_SERIAL="${target_serials[0]}"
|
||||||
|
if [[ -n "$SOURCE_SERIAL" ]]; then
|
||||||
|
print_step "Target device: $DEVICE_SERIAL (${target_models[0]})"
|
||||||
|
else
|
||||||
|
print_step "Using device: $DEVICE_SERIAL (${target_models[0]})"
|
||||||
|
fi
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Interactive menu for target device
|
||||||
|
echo ""
|
||||||
|
if [[ -n "$SOURCE_SERIAL" ]]; then
|
||||||
|
echo "Select target device (install on):"
|
||||||
|
else
|
||||||
|
echo "Multiple devices found:"
|
||||||
|
fi
|
||||||
|
for i in "${!target_serials[@]}"; do
|
||||||
|
echo " $((i + 1))) ${target_serials[$i]} (${target_models[$i]})"
|
||||||
done
|
done
|
||||||
echo ""
|
echo ""
|
||||||
while true; do
|
while true; do
|
||||||
read -rp "Select device [1-${#serials[@]}]: " choice
|
read -rp "Select device [1-${#target_serials[@]}]: " choice
|
||||||
if [[ "$choice" =~ ^[0-9]+$ ]] && [[ "$choice" -ge 1 ]] && [[ "$choice" -le ${#serials[@]} ]]; then
|
if [[ "$choice" =~ ^[0-9]+$ ]] && [[ "$choice" -ge 1 ]] && [[ "$choice" -le ${#target_serials[@]} ]]; then
|
||||||
DEVICE_SERIAL="${serials[$((choice - 1))]}"
|
DEVICE_SERIAL="${target_serials[$((choice - 1))]}"
|
||||||
print_step "Using device: $DEVICE_SERIAL (${models[$((choice - 1))]})"
|
if [[ -n "$SOURCE_SERIAL" ]]; then
|
||||||
|
print_step "Target device: $DEVICE_SERIAL (${target_models[$((choice - 1))]})"
|
||||||
|
else
|
||||||
|
print_step "Using device: $DEVICE_SERIAL (${target_models[$((choice - 1))]})"
|
||||||
|
fi
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
echo "Invalid selection. Enter a number between 1 and ${#serials[@]}."
|
echo "Invalid selection. Enter a number between 1 and ${#target_serials[@]}."
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -238,7 +443,7 @@ select_package() {
|
||||||
print_step "Searching for packages matching '$APP_NAME'..."
|
print_step "Searching for packages matching '$APP_NAME'..."
|
||||||
|
|
||||||
local packages_raw
|
local packages_raw
|
||||||
packages_raw=$("$ADB" -s "$DEVICE_SERIAL" shell pm list packages 2>&1 | tr -d '\r')
|
packages_raw=$("$ADB" -s "$SOURCE_SERIAL" shell pm list packages 2>&1 | tr -d '\r')
|
||||||
|
|
||||||
local matches=()
|
local matches=()
|
||||||
while IFS= read -r line; do
|
while IFS= read -r line; do
|
||||||
|
|
@ -248,7 +453,7 @@ select_package() {
|
||||||
if [[ ${#matches[@]} -eq 0 ]]; then
|
if [[ ${#matches[@]} -eq 0 ]]; then
|
||||||
print_error "No packages found matching '$APP_NAME'"
|
print_error "No packages found matching '$APP_NAME'"
|
||||||
echo "Try a broader search term, or list all packages with:"
|
echo "Try a broader search term, or list all packages with:"
|
||||||
echo " adb -s $DEVICE_SERIAL shell pm list packages"
|
echo " adb -s $SOURCE_SERIAL shell pm list packages"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
@ -281,7 +486,7 @@ pull_apks() {
|
||||||
print_step "Getting APK paths for $PACKAGE_NAME..."
|
print_step "Getting APK paths for $PACKAGE_NAME..."
|
||||||
|
|
||||||
local paths_raw
|
local paths_raw
|
||||||
paths_raw=$("$ADB" -s "$DEVICE_SERIAL" shell pm path "$PACKAGE_NAME" 2>&1 | tr -d '\r')
|
paths_raw=$("$ADB" -s "$SOURCE_SERIAL" shell pm path "$PACKAGE_NAME" 2>&1 | tr -d '\r')
|
||||||
|
|
||||||
local apk_paths=()
|
local apk_paths=()
|
||||||
while IFS= read -r line; do
|
while IFS= read -r line; do
|
||||||
|
|
@ -305,7 +510,7 @@ pull_apks() {
|
||||||
local apk_name
|
local apk_name
|
||||||
apk_name=$(basename "$apk_path")
|
apk_name=$(basename "$apk_path")
|
||||||
print_info "Pulling $apk_name..."
|
print_info "Pulling $apk_name..."
|
||||||
"$ADB" -s "$DEVICE_SERIAL" pull "$apk_path" "$PULL_DIR/$apk_name"
|
"$ADB" -s "$SOURCE_SERIAL" pull "$apk_path" "$PULL_DIR/$apk_name"
|
||||||
done
|
done
|
||||||
|
|
||||||
print_step "APKs pulled to $PULL_DIR/"
|
print_step "APKs pulled to $PULL_DIR/"
|
||||||
|
|
@ -335,8 +540,10 @@ make_debuggable() {
|
||||||
}
|
}
|
||||||
|
|
||||||
install_apks() {
|
install_apks() {
|
||||||
print_step "Uninstalling existing $PACKAGE_NAME..."
|
if [[ -n "$PACKAGE_NAME" ]]; then
|
||||||
"$ADB" -s "$DEVICE_SERIAL" uninstall "$PACKAGE_NAME" || print_warning "Uninstall failed (app may not be installed) — continuing"
|
print_step "Uninstalling existing $PACKAGE_NAME..."
|
||||||
|
"$ADB" -s "$DEVICE_SERIAL" uninstall "$PACKAGE_NAME" || print_warning "Uninstall failed (app may not be installed) — continuing"
|
||||||
|
fi
|
||||||
|
|
||||||
local apk_files=("$DEBUGGABLE_DIR"/*.apk)
|
local apk_files=("$DEBUGGABLE_DIR"/*.apk)
|
||||||
local apk_count=${#apk_files[@]}
|
local apk_count=${#apk_files[@]}
|
||||||
|
|
@ -366,7 +573,13 @@ cleanup() {
|
||||||
fi
|
fi
|
||||||
|
|
||||||
print_step "Cleaning up temporary files..."
|
print_step "Cleaning up temporary files..."
|
||||||
[[ -n "$PULL_DIR" && -d "$PULL_DIR" ]] && rm -rf "$PULL_DIR"
|
# Only delete PULL_DIR if we created it (not a user-provided directory)
|
||||||
|
if [[ -n "$LOCAL_APK_PATH" && -d "$LOCAL_APK_PATH" ]]; then
|
||||||
|
# User provided a directory — don't delete it
|
||||||
|
:
|
||||||
|
else
|
||||||
|
[[ -n "$PULL_DIR" && -d "$PULL_DIR" ]] && rm -rf "$PULL_DIR"
|
||||||
|
fi
|
||||||
[[ -n "$DEBUGGABLE_DIR" && -d "$DEBUGGABLE_DIR" ]] && rm -rf "$DEBUGGABLE_DIR"
|
[[ -n "$DEBUGGABLE_DIR" && -d "$DEBUGGABLE_DIR" ]] && rm -rf "$DEBUGGABLE_DIR"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -463,8 +676,20 @@ main() {
|
||||||
|
|
||||||
find_adb
|
find_adb
|
||||||
select_device
|
select_device
|
||||||
select_package
|
|
||||||
pull_apks
|
# Default source to target when not in two-device mode
|
||||||
|
if [[ -z "$SOURCE_SERIAL" ]]; then
|
||||||
|
SOURCE_SERIAL="$DEVICE_SERIAL"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$LOCAL_APK_PATH" ]]; then
|
||||||
|
find_aapt2
|
||||||
|
prepare_local_apk
|
||||||
|
else
|
||||||
|
select_package
|
||||||
|
pull_apks
|
||||||
|
fi
|
||||||
|
|
||||||
make_debuggable
|
make_debuggable
|
||||||
install_apks
|
install_apks
|
||||||
cleanup
|
cleanup
|
||||||
|
|
@ -477,7 +702,15 @@ main() {
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${GREEN}=== Done! ===${NC}"
|
echo -e "${GREEN}=== Done! ===${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " $PACKAGE_NAME is now debuggable on $DEVICE_SERIAL"
|
if [[ -n "$PACKAGE_NAME" ]]; then
|
||||||
|
echo -e " $PACKAGE_NAME is now debuggable on $DEVICE_SERIAL"
|
||||||
|
else
|
||||||
|
echo -e " App is now debuggable on $DEVICE_SERIAL"
|
||||||
|
fi
|
||||||
|
if [[ "$SOURCE_SERIAL" != "$DEVICE_SERIAL" ]]; then
|
||||||
|
echo -e " ${BLUE}Pulled from:${NC} $SOURCE_SERIAL"
|
||||||
|
echo -e " ${BLUE}Installed on:${NC} $DEVICE_SERIAL"
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ "$PROXY_MODE" == true ]]; then
|
if [[ "$PROXY_MODE" == true ]]; then
|
||||||
echo ""
|
echo ""
|
||||||
|
|
@ -498,7 +731,7 @@ main() {
|
||||||
else
|
else
|
||||||
echo ""
|
echo ""
|
||||||
echo " To attach a debugger:"
|
echo " To attach a debugger:"
|
||||||
echo " Android Studio → Run → Attach Debugger to Android Process → $PACKAGE_NAME"
|
echo " Android Studio → Run → Attach Debugger to Android Process${PACKAGE_NAME:+ → $PACKAGE_NAME}"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -225,6 +225,47 @@ inject_network_security_config() {
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Check for anti-tampering / integrity-protection libraries
|
||||||
|
check_anti_tampering() {
|
||||||
|
local found=()
|
||||||
|
|
||||||
|
# Library filename:protection name pairs (Bash 3.2 compatible — no associative arrays)
|
||||||
|
local pairs=(
|
||||||
|
"libpairipcore.so:PairIP"
|
||||||
|
"libDexHelper.so:DexGuard"
|
||||||
|
"libDexHelper-x86.so:DexGuard"
|
||||||
|
"libjiagu.so:360 Jiagu"
|
||||||
|
"libjiagu_art.so:360 Jiagu"
|
||||||
|
"libsecexe.so:Bangcle"
|
||||||
|
"libsecmain.so:Bangcle"
|
||||||
|
"libtosprotection.so:Tencent Legu"
|
||||||
|
"libexec.so:Baidu"
|
||||||
|
"libexecmain.so:Baidu"
|
||||||
|
)
|
||||||
|
|
||||||
|
for pair in "${pairs[@]}"; do
|
||||||
|
local lib_name="${pair%%:*}"
|
||||||
|
local protection="${pair#*:}"
|
||||||
|
if find "$WORK_DIR/lib" -name "$lib_name" 2>/dev/null | grep -q .; then
|
||||||
|
found+=("$protection ($lib_name)")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ ${#found[@]} -gt 0 ]]; then
|
||||||
|
echo ""
|
||||||
|
print_warning "Anti-tampering protection detected!"
|
||||||
|
echo -e " ${YELLOW}This app uses native integrity checks that will likely crash${NC}"
|
||||||
|
echo -e " ${YELLOW}after patching because the APK signature has changed.${NC}"
|
||||||
|
echo ""
|
||||||
|
for item in "${found[@]}"; do
|
||||||
|
echo -e " ${RED}•${NC} $item"
|
||||||
|
done
|
||||||
|
echo ""
|
||||||
|
echo -e " The patched APK will still be created, but the app may crash on launch."
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
# Process a single APK (make debuggable)
|
# Process a single APK (make debuggable)
|
||||||
process_single_apk() {
|
process_single_apk() {
|
||||||
local input_apk="$1"
|
local input_apk="$1"
|
||||||
|
|
@ -238,7 +279,18 @@ process_single_apk() {
|
||||||
|
|
||||||
# Step 1: Disassemble
|
# Step 1: Disassemble
|
||||||
print_step "Disassembling APK..."
|
print_step "Disassembling APK..."
|
||||||
$APKTOOL d -f -o "$WORK_DIR" "$input_apk"
|
if [[ "$TRUST_USER_CERTS" == true ]]; then
|
||||||
|
# Full resource decode needed so we can inject/overwrite network_security_config.xml
|
||||||
|
$APKTOOL d -f -o "$WORK_DIR" "$input_apk"
|
||||||
|
else
|
||||||
|
# --no-res: skip resource decoding — we only need AndroidManifest.xml.
|
||||||
|
# This avoids aapt2 "private resource" errors (e.g. android:style/TextAppearance.*)
|
||||||
|
# that occur when the app references private Android framework styles/resources.
|
||||||
|
$APKTOOL d -f --no-res -o "$WORK_DIR" "$input_apk"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Step 1b: Check for anti-tampering protections
|
||||||
|
check_anti_tampering
|
||||||
|
|
||||||
# Step 2: Make debuggable
|
# Step 2: Make debuggable
|
||||||
print_step "Making APK debuggable..."
|
print_step "Making APK debuggable..."
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue