feat: publish workphone SDK deployment and API docs

This commit is contained in:
Manus AI
2026-07-14 18:10:52 +08:00
commit 021d633cc1
534 changed files with 122391 additions and 0 deletions

319
sdk/scripts/setup_noroot.sh Executable file
View File

@@ -0,0 +1,319 @@
#!/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 "部署完成!"