320 lines
11 KiB
Bash
Executable File
320 lines
11 KiB
Bash
Executable File
#!/bin/bash
|
||
# ============================================================
|
||
# 无 Root 全自动部署脚本
|
||
# 一键完成:检测设备 → 注入 Gadget → 安装 → 连通 → 控制微信
|
||
# ============================================================
|
||
set -euo pipefail
|
||
|
||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||
SDK_DIR="$(dirname "$SCRIPT_DIR")"
|
||
GADGET_SO="$SCRIPT_DIR/frida-gadget-17.8.1-android-arm64.so"
|
||
KEYSTORE="$SCRIPT_DIR/debug.keystore"
|
||
WECHAT_PKG="com.tencent.mm"
|
||
GADGET_PORT=27042
|
||
|
||
GREEN='\033[0;32m'
|
||
YELLOW='\033[1;33m'
|
||
RED='\033[0;31m'
|
||
CYAN='\033[0;36m'
|
||
NC='\033[0m'
|
||
log() { echo -e "${GREEN}[✓]${NC} $1"; }
|
||
warn() { echo -e "${YELLOW}[!]${NC} $1"; }
|
||
err() { echo -e "${RED}[✗]${NC} $1"; exit 1; }
|
||
info() { echo -e "${CYAN}[→]${NC} $1"; }
|
||
|
||
# ─────────────────────────────────────
|
||
# Phase 1: 等待设备
|
||
# ─────────────────────────────────────
|
||
info "Phase 1: 等待 ADB 设备连接..."
|
||
SERIAL=""
|
||
for i in $(seq 1 60); do
|
||
DEV=$(adb devices 2>/dev/null | grep -w "device" | head -1 | awk '{print $1}')
|
||
if [ -n "$DEV" ]; then
|
||
SERIAL="$DEV"
|
||
break
|
||
fi
|
||
printf "."
|
||
sleep 2
|
||
done
|
||
echo ""
|
||
[ -z "$SERIAL" ] && err "等待 120 秒仍未检测到设备,请检查 USB 连接和 USB 调试"
|
||
log "设备已连接: $SERIAL"
|
||
ADB="adb -s $SERIAL"
|
||
|
||
# 设备基本信息
|
||
MODEL=$($ADB shell getprop ro.product.model 2>/dev/null | tr -d '\r')
|
||
ANDROID=$($ADB shell getprop ro.build.version.release 2>/dev/null | tr -d '\r')
|
||
log "型号: $MODEL | Android: $ANDROID"
|
||
|
||
# ─────────────────────────────────────
|
||
# Phase 2: 检查微信
|
||
# ─────────────────────────────────────
|
||
info "Phase 2: 检查微信状态..."
|
||
WX_VER=$($ADB shell dumpsys package $WECHAT_PKG 2>/dev/null | grep versionName | head -1 | awk -F= '{print $2}' | tr -d '\r ')
|
||
[ -z "$WX_VER" ] && err "微信未安装"
|
||
log "微信版本: $WX_VER"
|
||
|
||
# 检查是否已经是注入版
|
||
ALREADY_INJECTED=$($ADB shell "run-as $WECHAT_PKG ls lib/arm64/libfrida-gadget.so 2>/dev/null" 2>/dev/null | grep -c "gadget" || echo "0")
|
||
if [ "$ALREADY_INJECTED" != "0" ]; then
|
||
log "微信已包含 Frida Gadget,跳过注入步骤"
|
||
SKIP_INJECT=1
|
||
else
|
||
SKIP_INJECT=0
|
||
fi
|
||
|
||
# ─────────────────────────────────────
|
||
# Phase 3: 注入 Frida Gadget(如需)
|
||
# ─────────────────────────────────────
|
||
if [ "$SKIP_INJECT" = "0" ]; then
|
||
info "Phase 3: 提取并注入微信 APK..."
|
||
|
||
WORK_DIR="$SCRIPT_DIR/gadget_work"
|
||
rm -rf "$WORK_DIR"
|
||
mkdir -p "$WORK_DIR"
|
||
|
||
# 提取 APK
|
||
APK_PATH=$($ADB shell pm path $WECHAT_PKG 2>/dev/null | head -1 | sed 's/package://' | tr -d '\r\n')
|
||
log "微信 APK: $APK_PATH"
|
||
|
||
info "拉取 APK(可能需要几分钟)..."
|
||
$ADB pull "$APK_PATH" "$WORK_DIR/wechat_original.apk" 2>&1 | tail -1
|
||
APK_SIZE=$(ls -lh "$WORK_DIR/wechat_original.apk" | awk '{print $5}')
|
||
log "APK 大小: $APK_SIZE"
|
||
|
||
# 反编译(只反编译资源,不反编译代码以加快速度)
|
||
info "反编译 APK..."
|
||
apktool d -f -s -o "$WORK_DIR/wechat_dec" "$WORK_DIR/wechat_original.apk" 2>&1 | tail -2
|
||
|
||
# 注入 gadget
|
||
LIB_DIR="$WORK_DIR/wechat_dec/lib/arm64-v8a"
|
||
mkdir -p "$LIB_DIR"
|
||
cp "$GADGET_SO" "$LIB_DIR/libfrida-gadget.so"
|
||
log "frida-gadget.so 已注入到 lib/arm64-v8a/"
|
||
|
||
# Gadget 配置(listen 模式,自动 resume)
|
||
cat > "$LIB_DIR/libfrida-gadget.config.so" << 'EOF'
|
||
{
|
||
"interaction": {
|
||
"type": "listen",
|
||
"address": "0.0.0.0",
|
||
"port": 27042,
|
||
"on_port_conflict": "pick-next",
|
||
"on_load": "resume"
|
||
}
|
||
}
|
||
EOF
|
||
log "Gadget 配置: listen 模式, 端口 $GADGET_PORT"
|
||
|
||
# 修改 AndroidManifest
|
||
MANIFEST="$WORK_DIR/wechat_dec/AndroidManifest.xml"
|
||
sed -i.bak 's/android:extractNativeLibs="false"/android:extractNativeLibs="true"/g' "$MANIFEST" 2>/dev/null || true
|
||
log "AndroidManifest: extractNativeLibs=true"
|
||
|
||
# 在 smali 中注入 loadLibrary
|
||
info "注入 System.loadLibrary(\"frida-gadget\")..."
|
||
# 查找 Application 类
|
||
APP_CLASS=$(grep -oP 'android:name="\K[^"]+(?=.*application)' "$MANIFEST" 2>/dev/null | head -1 || true)
|
||
[ -z "$APP_CLASS" ] && APP_CLASS="com.tencent.mm.app.MMApplication"
|
||
SMALI_REL=$(echo "$APP_CLASS" | sed 's/\./\//g')
|
||
|
||
SMALI_FILE=""
|
||
for d in "$WORK_DIR/wechat_dec"/smali*; do
|
||
[ -f "$d/${SMALI_REL}.smali" ] && SMALI_FILE="$d/${SMALI_REL}.smali" && break
|
||
done
|
||
|
||
if [ -n "$SMALI_FILE" ]; then
|
||
python3 << PYEOF
|
||
import re, sys
|
||
|
||
path = "$SMALI_FILE"
|
||
with open(path, 'r') as f:
|
||
code = f.read()
|
||
|
||
load = ' const-string v0, "frida-gadget"\n invoke-static {v0}, Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V\n'
|
||
|
||
# 注入到 static initializer
|
||
clinit = re.search(r'(\.method\s+.*?static\s+.*?<clinit>.*?\n.*?\.locals\s+\d+\n)', code, re.DOTALL)
|
||
if clinit:
|
||
pos = clinit.end()
|
||
code = code[:pos] + load + code[pos:]
|
||
with open(path, 'w') as f:
|
||
f.write(code)
|
||
print("[✓] 注入到 <clinit>")
|
||
sys.exit(0)
|
||
|
||
# 注入到 attachBaseContext 或 onCreate
|
||
for method_name in ['attachBaseContext', 'onCreate']:
|
||
pat = rf'(\.method\s+.*?{method_name}\(.*?\n(?:.*?\n)*? \.locals\s+\d+\n)'
|
||
m = re.search(pat, code)
|
||
if m:
|
||
pos = m.end()
|
||
code = code[:pos] + load + code[pos:]
|
||
with open(path, 'w') as f:
|
||
f.write(code)
|
||
print(f"[✓] 注入到 {method_name}")
|
||
sys.exit(0)
|
||
|
||
print("[!] 未找到合适注入点,gadget 将通过 native lib 自动加载")
|
||
PYEOF
|
||
else
|
||
warn "未找到 Application smali: $APP_CLASS"
|
||
fi
|
||
|
||
# 重新打包
|
||
info "重新打包 APK..."
|
||
apktool b -o "$WORK_DIR/wechat_unsigned.apk" "$WORK_DIR/wechat_dec" 2>&1 | tail -2
|
||
|
||
# 签名
|
||
info "签名 APK..."
|
||
jarsigner -sigalg SHA256withRSA -digestalg SHA-256 \
|
||
-keystore "$KEYSTORE" -storepass android -keypass android \
|
||
"$WORK_DIR/wechat_unsigned.apk" sdkkey 2>&1 | tail -2
|
||
cp "$WORK_DIR/wechat_unsigned.apk" "$WORK_DIR/wechat_patched.apk"
|
||
PATCHED_SIZE=$(ls -lh "$WORK_DIR/wechat_patched.apk" | awk '{print $5}')
|
||
log "签名完成: $PATCHED_SIZE"
|
||
|
||
# 备份微信数据(重要!)
|
||
info "备份微信数据..."
|
||
$ADB shell "pm clear $WECHAT_PKG 2>/dev/null" || true
|
||
warn "注意:重新安装会清除微信登录状态,需要重新登录"
|
||
|
||
# 卸载 + 安装
|
||
info "卸载原版微信..."
|
||
$ADB shell pm uninstall $WECHAT_PKG 2>&1 || true
|
||
info "安装注入版微信(可能需要几分钟)..."
|
||
$ADB install "$WORK_DIR/wechat_patched.apk" 2>&1 | tail -1
|
||
log "微信安装完成"
|
||
fi
|
||
|
||
# ─────────────────────────────────────
|
||
# Phase 4: 启动微信 + 端口转发
|
||
# ─────────────────────────────────────
|
||
info "Phase 4: 启动微信..."
|
||
$ADB shell am force-stop $WECHAT_PKG 2>/dev/null || true
|
||
sleep 1
|
||
$ADB shell am start -n $WECHAT_PKG/.ui.LauncherUI 2>&1 | tail -1
|
||
log "微信已启动,等待 Gadget 初始化..."
|
||
sleep 8
|
||
|
||
info "设置端口转发..."
|
||
$ADB forward tcp:$GADGET_PORT tcp:$GADGET_PORT 2>&1
|
||
log "端口转发: localhost:$GADGET_PORT → device:$GADGET_PORT"
|
||
|
||
# ─────────────────────────────────────
|
||
# Phase 5: 验证 Frida 连接
|
||
# ─────────────────────────────────────
|
||
info "Phase 5: 验证 Frida Gadget 连接..."
|
||
sleep 3
|
||
|
||
python3 << 'PYEOF'
|
||
import frida
|
||
import time
|
||
import sys
|
||
|
||
host = "127.0.0.1:27042"
|
||
max_retries = 5
|
||
|
||
for attempt in range(max_retries):
|
||
try:
|
||
mgr = frida.get_device_manager()
|
||
device = mgr.add_remote_device(host)
|
||
session = device.attach("Gadget")
|
||
print(f"[✓] Frida Gadget 连接成功(尝试 {attempt+1})")
|
||
|
||
# 加载 Hook 脚本测试
|
||
script = session.create_script('rpc.exports = { ping: function() { return "pong_from_gadget"; } };')
|
||
script.load()
|
||
result = script.exports_sync.ping()
|
||
print(f"[✓] RPC 测试: ping → {result}")
|
||
script.unload()
|
||
session.detach()
|
||
print("[✓] Frida Gadget 工作正常!")
|
||
sys.exit(0)
|
||
except Exception as e:
|
||
print(f"[!] 尝试 {attempt+1}/{max_retries}: {e}")
|
||
time.sleep(3)
|
||
|
||
print("[✗] Frida Gadget 连接失败")
|
||
sys.exit(1)
|
||
PYEOF
|
||
|
||
FRIDA_OK=$?
|
||
|
||
# ─────────────────────────────────────
|
||
# Phase 6: 加载微信 Hook 脚本
|
||
# ─────────────────────────────────────
|
||
if [ "$FRIDA_OK" = "0" ]; then
|
||
info "Phase 6: 加载微信 Hook 脚本..."
|
||
python3 << PYEOF
|
||
import frida, json, sys, os
|
||
|
||
host = "127.0.0.1:$GADGET_PORT"
|
||
script_path = "$SDK_DIR/agent/hook/wechat_hook_v2.js"
|
||
|
||
if not os.path.exists(script_path):
|
||
print(f"[!] Hook 脚本不存在: {script_path}")
|
||
sys.exit(1)
|
||
|
||
with open(script_path, 'r') as f:
|
||
source = f.read()
|
||
|
||
mgr = frida.get_device_manager()
|
||
device = mgr.add_remote_device(host)
|
||
session = device.attach("Gadget")
|
||
|
||
script = session.create_script(source)
|
||
|
||
def on_message(message, data):
|
||
if message['type'] == 'send':
|
||
payload = message.get('payload', {})
|
||
if payload.get('type') == 'hook_event':
|
||
print(f" 事件: {payload.get('event_type', '?')} | {json.dumps(payload.get('data', {}), ensure_ascii=False)[:100]}")
|
||
elif payload.get('type') == 'log':
|
||
print(f" 日志: [{payload.get('tag','')}] {payload.get('message','')}")
|
||
elif message['type'] == 'error':
|
||
print(f" 错误: {message.get('description','')}")
|
||
|
||
script.on('message', on_message)
|
||
script.load()
|
||
|
||
rpc = script.exports_sync
|
||
|
||
try:
|
||
pong = rpc.ping()
|
||
print(f"[✓] Hook 脚本已加载,ping={pong}")
|
||
except Exception as e:
|
||
print(f"[!] ping 失败: {e}")
|
||
|
||
try:
|
||
info = rpc.get_process_info({})
|
||
print(f"[✓] 微信进程: {json.dumps(info, ensure_ascii=False)}")
|
||
except Exception as e:
|
||
print(f"[!] get_process_info: {e}")
|
||
|
||
try:
|
||
status = rpc.get_hook_status({})
|
||
print(f"[✓] Hook 状态: {json.dumps(status, ensure_ascii=False)}")
|
||
except Exception as e:
|
||
print(f"[!] get_hook_status: {e}")
|
||
|
||
print("")
|
||
print("=" * 50)
|
||
print("[✓] 全部完成!微信 Hook 通道已连通")
|
||
print("=" * 50)
|
||
print(f" Frida Gadget: {host}")
|
||
print(f" Hook 脚本: {os.path.basename(script_path)}")
|
||
print(f" 连接模式: 无 Root (Gadget)")
|
||
print(f" SDK 服务端: http://localhost:8899")
|
||
print("=" * 50)
|
||
|
||
script.unload()
|
||
session.detach()
|
||
PYEOF
|
||
else
|
||
warn "Frida 连接未成功,微信可能需要登录后再试"
|
||
fi
|
||
|
||
echo ""
|
||
log "部署完成!"
|