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

View File

@@ -0,0 +1,715 @@
"""
Frida 无线部署器 — 支持 Root / 免Root 双模式 WiFi 直连
=====================================================
核心功能:
- Root 模式:通过 Termux 在手机端启动 frida-server监听 TCP 端口
- 免Root 模式:将 frida-gadget 注入到目标 APK微信通过 TCP 连接
- WiFi 直连:服务器通过手机 IP + 端口直接连接 Frida无需 USB
- 自动发现:扫描局域网内运行 frida-server 的设备
- 健康检查:定期检测 Frida 连接状态,自动重连
架构:
服务器 (FastAPI) ←→ WiFi ←→ 手机 (Termux + frida-server/gadget)
微信进程 (Frida Hook)
技术栈Frida 16.5.6 + WebSocket + TCP
"""
import os
import re
import json
import time
import socket
import logging
import asyncio
import threading
import subprocess
from typing import Optional, Dict, Any, List, Literal, Tuple
from dataclasses import dataclass, field, asdict
from datetime import datetime
logger = logging.getLogger(__name__)
# ============================================================
# § 1 数据模型
# ============================================================
DeployMode = Literal["root_server", "gadget", "auto"]
@dataclass
class DeviceConnection:
"""设备连接信息"""
device_id: str
ip: str
frida_port: int = 27042
mode: DeployMode = "auto"
status: str = "disconnected" # disconnected / connecting / connected / error
wechat_attached: bool = False
last_heartbeat: Optional[str] = None
error: Optional[str] = None
frida_version: Optional[str] = None
wechat_version: Optional[str] = None
android_version: Optional[str] = None
device_model: Optional[str] = None
connected_at: Optional[str] = None
reconnect_count: int = 0
def to_dict(self) -> dict:
return asdict(self)
@dataclass
class DeployConfig:
"""部署配置"""
frida_version: str = "16.5.6"
frida_arch: str = "arm64" # arm / arm64 / x86 / x86_64
listen_port: int = 27042
listen_host: str = "0.0.0.0"
auto_start: bool = True
anti_detect: bool = True # 随机端口 + 进程伪装
gadget_config: dict = field(default_factory=lambda: {
"interaction": {
"type": "listen",
"address": "0.0.0.0",
"port": 27042,
"on_load": "wait"
}
})
# ============================================================
# § 2 Frida 无线部署器
# ============================================================
class WirelessDeployer:
"""
Frida 无线部署管理器
职责:
1. 在手机端部署 frida-serverRoot或 frida-gadget免Root
2. 通过 WiFi TCP 连接 Frida
3. 管理多设备连接池
4. 自动发现局域网设备
5. 健康检查与自动重连
"""
FRIDA_DOWNLOAD_BASE = "https://github.com/frida/frida/releases/download"
GADGET_DOWNLOAD_BASE = "https://github.com/nickcano/frida-gadget-releases/releases/download"
def __init__(self, config: Optional[DeployConfig] = None):
self.config = config or DeployConfig()
self.connections: Dict[str, DeviceConnection] = {}
self._lock = threading.Lock()
self._running = False
self._health_thread: Optional[threading.Thread] = None
# ---- 设备连接管理 ----
def add_device(self, device_id: str, ip: str, port: int = 27042,
mode: DeployMode = "auto") -> DeviceConnection:
"""添加设备到连接池"""
conn = DeviceConnection(
device_id=device_id,
ip=ip,
frida_port=port,
mode=mode,
)
with self._lock:
self.connections[device_id] = conn
logger.info(f"设备已添加: {device_id} ({ip}:{port}) 模式={mode}")
return conn
def remove_device(self, device_id: str):
"""从连接池移除设备"""
with self._lock:
if device_id in self.connections:
del self.connections[device_id]
logger.info(f"设备已移除: {device_id}")
def get_device(self, device_id: str) -> Optional[DeviceConnection]:
"""获取设备连接信息"""
return self.connections.get(device_id)
def list_devices(self) -> List[Dict[str, Any]]:
"""列出所有设备"""
return [conn.to_dict() for conn in self.connections.values()]
# ---- WiFi 直连 Frida ----
def connect_device(self, device_id: str) -> Dict[str, Any]:
"""
通过 WiFi 连接设备的 Frida
流程:
1. 检查设备 IP 可达性
2. 尝试 TCP 连接 frida-server 端口
3. 获取 Frida 设备信息
4. attach 到微信进程
"""
conn = self.connections.get(device_id)
if not conn:
return {"success": False, "error": f"设备不存在: {device_id}"}
conn.status = "connecting"
conn.error = None
try:
# Step 1: 检查 TCP 端口可达
if not self._check_port(conn.ip, conn.frida_port, timeout=5):
conn.status = "error"
conn.error = f"Frida 端口不可达: {conn.ip}:{conn.frida_port}"
return {"success": False, "error": conn.error}
# Step 2: 通过 Frida API 连接
import frida
mgr = frida.get_device_manager()
device = mgr.add_remote_device(f"{conn.ip}:{conn.frida_port}")
# Step 3: 获取设备信息
conn.frida_version = device.query_system_parameters().get("os", {}).get("version", "unknown")
conn.device_model = device.name
conn.status = "connected"
conn.connected_at = datetime.now().isoformat()
conn.last_heartbeat = conn.connected_at
# Step 4: 尝试 attach 微信
try:
processes = device.enumerate_processes()
wechat_proc = None
for proc in processes:
if proc.name == "com.tencent.mm" or "微信" in proc.name:
wechat_proc = proc
break
if wechat_proc:
conn.wechat_attached = True
logger.info(f"微信进程已发现: PID={wechat_proc.pid}")
else:
conn.wechat_attached = False
logger.warning(f"微信进程未运行,等待启动")
except Exception as e:
logger.warning(f"枚举进程失败: {e}")
conn.wechat_attached = False
logger.info(f"✅ 设备 WiFi 连接成功: {device_id} ({conn.ip}:{conn.frida_port})")
return {
"success": True,
"device_id": device_id,
"ip": conn.ip,
"port": conn.frida_port,
"mode": conn.mode,
"wechat_attached": conn.wechat_attached,
"device_model": conn.device_model,
}
except ImportError:
conn.status = "error"
conn.error = "frida 未安装"
return {"success": False, "error": "frida 未安装,请执行: pip install frida-tools"}
except Exception as e:
conn.status = "error"
conn.error = str(e)
conn.reconnect_count += 1
logger.error(f"WiFi 连接失败: {device_id} - {e}")
return {"success": False, "error": str(e)}
def disconnect_device(self, device_id: str) -> Dict[str, Any]:
"""断开设备连接"""
conn = self.connections.get(device_id)
if not conn:
return {"success": False, "error": f"设备不存在: {device_id}"}
conn.status = "disconnected"
conn.wechat_attached = False
logger.info(f"设备已断开: {device_id}")
return {"success": True, "device_id": device_id}
# ---- Root 模式:远程启动 frida-server ----
def generate_root_deploy_script(self, port: int = 0) -> str:
"""
生成 Root 模式部署脚本(在 Termux 中执行)
功能:
- 下载对应架构的 frida-server
- 以 root 权限启动,监听指定端口
- 支持反检测(随机端口 + 进程名伪装)
"""
if port == 0:
import random
port = random.randint(10000, 60000) if self.config.anti_detect else 27042
version = self.config.frida_version
arch = self.config.frida_arch
server_name = f"frida-server-{version}-android-{arch}"
download_url = f"{self.FRIDA_DOWNLOAD_BASE}/{version}/{server_name}.xz"
# 反检测:伪装进程名
disguise_name = "app_process64" if self.config.anti_detect else "frida-server"
script = f"""#!/data/data/com.termux/files/usr/bin/bash
# ================================================================
# Frida Server 无线部署脚本 (Root 模式)
# 版本: {version} | 架构: {arch} | 端口: {port}
# ================================================================
set -eo pipefail
RED='\\033[0;31m'; GREEN='\\033[0;32m'; NC='\\033[0m'
info() {{ echo -e "${{GREEN}}[INFO]${{NC}} $*"; }}
error() {{ echo -e "${{RED}}[ERROR]${{NC}} $*"; }}
# 检查 Root
if ! su -c "id" 2>/dev/null | grep -q "uid=0"; then
error "需要 Root 权限"
exit 1
fi
info "Root 权限确认 ✓"
# 检查网络
DEVICE_IP=$(ip route get 1 2>/dev/null | awk '{{print $NF; exit}}' || hostname -I | awk '{{print $1}}')
info "设备 IP: $DEVICE_IP"
# 下载 frida-server
FRIDA_DIR="$HOME/.frida-server"
mkdir -p "$FRIDA_DIR"
FRIDA_BIN="$FRIDA_DIR/{disguise_name}"
if [ ! -f "$FRIDA_BIN" ]; then
info "下载 frida-server {version} ({arch})..."
curl -sL "{download_url}" -o "$FRIDA_DIR/frida-server.xz"
xz -d -f "$FRIDA_DIR/frida-server.xz"
mv "$FRIDA_DIR/frida-server" "$FRIDA_BIN"
chmod +x "$FRIDA_BIN"
info "下载完成 ✓"
else
info "frida-server 已存在 ✓"
fi
# 停止旧进程
su -c "pkill -f frida-server" 2>/dev/null || true
su -c "pkill -f {disguise_name}" 2>/dev/null || true
sleep 1
# 复制到系统目录并启动
su -c "cp $FRIDA_BIN /data/local/tmp/{disguise_name}"
su -c "chmod 755 /data/local/tmp/{disguise_name}"
su -c "nohup /data/local/tmp/{disguise_name} -l 0.0.0.0:{port} &" 2>/dev/null
sleep 2
# 验证
if su -c "netstat -tlnp 2>/dev/null" | grep -q ":{port}"; then
info "✅ frida-server 已启动,监听 0.0.0.0:{port}"
info "📱 连接地址: $DEVICE_IP:{port}"
echo ""
echo "在服务器端执行:"
echo " frida -H $DEVICE_IP:{port} -n com.tencent.mm"
echo ""
else
error "frida-server 启动失败"
exit 1
fi
# 写入自启动
BOOT_SCRIPT="$HOME/.frida-autostart.sh"
cat > "$BOOT_SCRIPT" << 'AUTOSTART'
#!/data/data/com.termux/files/usr/bin/bash
sleep 10
su -c "nohup /data/local/tmp/{disguise_name} -l 0.0.0.0:{port} &" 2>/dev/null
AUTOSTART
chmod +x "$BOOT_SCRIPT"
# Termux:Boot 自启动
BOOT_DIR="$HOME/.termux/boot"
mkdir -p "$BOOT_DIR"
ln -sf "$BOOT_SCRIPT" "$BOOT_DIR/frida-autostart.sh"
info "开机自启已配置 ✓"
"""
return script
# ---- 免Root 模式Gadget 注入 ----
def generate_gadget_deploy_script(self, apk_path: str = "", port: int = 0) -> str:
"""
生成免 Root 模式部署脚本
原理:
- 将 frida-gadget.so 注入到微信 APK 的 lib 目录
- 配置 gadget 监听 TCP 端口
- 重新签名并安装修改后的 APK
注意:免 Root 模式需要重新安装微信,会丢失聊天记录
"""
if port == 0:
import random
port = random.randint(10000, 60000) if self.config.anti_detect else 27042
version = self.config.frida_version
arch = self.config.frida_arch
gadget_name = f"frida-gadget-{version}-android-{arch}.so"
gadget_url = f"{self.FRIDA_DOWNLOAD_BASE}/{version}/{gadget_name}.xz"
gadget_config = json.dumps({
"interaction": {
"type": "listen",
"address": "0.0.0.0",
"port": port,
"on_load": "wait"
}
}, indent=2)
script = f"""#!/data/data/com.termux/files/usr/bin/bash
# ================================================================
# Frida Gadget 无线部署脚本 (免Root 模式)
# 版本: {version} | 架构: {arch} | 端口: {port}
# ================================================================
set -eo pipefail
RED='\\033[0;31m'; GREEN='\\033[0;32m'; YELLOW='\\033[0;33m'; NC='\\033[0m'
info() {{ echo -e "${{GREEN}}[INFO]${{NC}} $*"; }}
warn() {{ echo -e "${{YELLOW}}[WARN]${{NC}} $*"; }}
error() {{ echo -e "${{RED}}[ERROR]${{NC}} $*"; }}
# 安装依赖
pkg install -y apksigner aapt zip unzip curl xz-utils 2>/dev/null || true
# 下载 frida-gadget
GADGET_DIR="$HOME/.frida-gadget"
mkdir -p "$GADGET_DIR"
GADGET_SO="$GADGET_DIR/libfrida-gadget.so"
if [ ! -f "$GADGET_SO" ]; then
info "下载 frida-gadget {version} ({arch})..."
curl -sL "{gadget_url}" -o "$GADGET_DIR/gadget.so.xz"
xz -d -f "$GADGET_DIR/gadget.so.xz"
mv "$GADGET_DIR/gadget.so" "$GADGET_SO"
info "下载完成 ✓"
fi
# Gadget 配置文件
cat > "$GADGET_DIR/libfrida-gadget.config.so" << 'GADGET_CFG'
{gadget_config}
GADGET_CFG
APK_PATH="{apk_path}"
if [ -z "$APK_PATH" ]; then
# 自动查找已安装的微信 APK
APK_PATH=$(pm path com.tencent.mm 2>/dev/null | head -1 | sed 's/package://')
if [ -z "$APK_PATH" ]; then
error "未找到微信 APK请指定路径"
exit 1
fi
fi
info "微信 APK: $APK_PATH"
# 解包
WORK_DIR="$GADGET_DIR/work"
rm -rf "$WORK_DIR"
mkdir -p "$WORK_DIR"
cp "$APK_PATH" "$WORK_DIR/base.apk"
cd "$WORK_DIR"
# 解压 APK
unzip -q base.apk -d apk_contents
# 注入 gadget
LIB_DIR="apk_contents/lib/{arch.replace('arm64', 'arm64-v8a').replace('arm', 'armeabi-v7a')}"
mkdir -p "$LIB_DIR"
cp "$GADGET_SO" "$LIB_DIR/libfrida-gadget.so"
cp "$GADGET_DIR/libfrida-gadget.config.so" "$LIB_DIR/libfrida-gadget.config.so"
# 修改 AndroidManifest.xml 加载 gadgetsmali 注入方式更可靠,这里用简化方案)
# 在 Application 类的 static 块中加载 libfrida-gadget.so
info "注入 frida-gadget 到 lib 目录..."
# 重新打包
cd apk_contents
zip -q -r ../patched.apk .
cd ..
# 签名
info "签名 APK..."
# 生成临时签名密钥
keytool -genkey -v -keystore "$GADGET_DIR/debug.keystore" \\
-storepass android -alias androiddebugkey -keypass android \\
-keyalg RSA -keysize 2048 -validity 10000 \\
-dname "CN=Debug, OU=Debug, O=Debug, L=Debug, S=Debug, C=US" 2>/dev/null || true
apksigner sign --ks "$GADGET_DIR/debug.keystore" \\
--ks-pass pass:android --key-pass pass:android \\
--out patched_signed.apk patched.apk
# 安装
warn "⚠️ 即将安装修改版微信,原版数据可能丢失"
warn " 建议先备份聊天记录"
info "安装修改版微信..."
pm install -r -d patched_signed.apk
DEVICE_IP=$(ip route get 1 2>/dev/null | awk '{{print $NF; exit}}' || hostname -I | awk '{{print $1}}')
info "✅ Gadget 注入完成"
info "📱 启动微信后,连接地址: $DEVICE_IP:{port}"
echo ""
echo "在服务器端执行:"
echo " frida -H $DEVICE_IP:{port} -n Gadget"
echo ""
"""
return script
# ---- 混合模式:自动检测 Root 并选择最佳方案 ----
def generate_auto_deploy_script(self, server_url: str, port: int = 0) -> str:
"""
生成自动检测部署脚本
逻辑:
1. 检测是否有 Root 权限
2. Root → 使用 frida-server 模式
3. 非 Root → 使用 frida-gadget 模式
4. 自动连接到 SDK 服务器
"""
if port == 0:
import random
port = random.randint(10000, 60000) if self.config.anti_detect else 27042
version = self.config.frida_version
arch = self.config.frida_arch
script = f"""#!/data/data/com.termux/files/usr/bin/bash
# ================================================================
# 工作手机 SDK - Frida 无线自动部署
# 版本: {version} | 端口: {port}
# 服务器: {server_url}
# ================================================================
set -eo pipefail
RED='\\033[0;31m'; GREEN='\\033[0;32m'; YELLOW='\\033[0;33m'; CYAN='\\033[0;36m'; NC='\\033[0m'
info() {{ echo -e "${{GREEN}}[INFO]${{NC}} $*"; }}
warn() {{ echo -e "${{YELLOW}}[WARN]${{NC}} $*"; }}
error() {{ echo -e "${{RED}}[ERROR]${{NC}} $*"; }}
DEVICE_ID=$(getprop ro.serialno 2>/dev/null || echo "dev-$(date +%s)")
DEVICE_IP=$(ip route get 1 2>/dev/null | awk '{{print $NF; exit}}' || hostname -I | awk '{{print $1}}')
FRIDA_PORT={port}
SERVER_URL="{server_url}"
info "设备ID: $DEVICE_ID"
info "设备IP: $DEVICE_IP"
info "Frida端口: $FRIDA_PORT"
# ---- 检测 Root ----
HAS_ROOT=false
if su -c "id" 2>/dev/null | grep -q "uid=0"; then
HAS_ROOT=true
info "✅ Root 权限可用 → 使用 frida-server 模式"
else
warn "⚠️ 无 Root 权限 → 使用 frida-gadget 模式"
fi
if [ "$HAS_ROOT" = true ]; then
# ========== Root 模式 ==========
FRIDA_DIR="$HOME/.frida-server"
mkdir -p "$FRIDA_DIR"
FRIDA_BIN="$FRIDA_DIR/fs-{version}"
if [ ! -f "$FRIDA_BIN" ]; then
info "下载 frida-server {version} ({arch})..."
curl -sL "{WirelessDeployer.FRIDA_DOWNLOAD_BASE}/{version}/frida-server-{version}-android-{arch}.xz" \\
-o "$FRIDA_DIR/fs.xz"
xz -d -f "$FRIDA_DIR/fs.xz"
mv "$FRIDA_DIR/fs" "$FRIDA_BIN"
chmod +x "$FRIDA_BIN"
fi
# 停止旧进程
su -c "pkill -f frida-server" 2>/dev/null || true
sleep 1
# 启动
su -c "cp $FRIDA_BIN /data/local/tmp/frida-server"
su -c "chmod 755 /data/local/tmp/frida-server"
su -c "nohup /data/local/tmp/frida-server -l 0.0.0.0:$FRIDA_PORT &" 2>/dev/null
sleep 2
if su -c "netstat -tlnp 2>/dev/null" | grep -q ":$FRIDA_PORT"; then
info "✅ frida-server 已启动"
else
error "frida-server 启动失败"
exit 1
fi
DEPLOY_MODE="root_server"
else
# ========== 免Root 模式 ==========
info "免Root 模式需要重新安装修改版微信"
info "请先运行 gadget 部署脚本"
DEPLOY_MODE="gadget"
fi
# ---- 向服务器注册 ----
info "向服务器注册设备..."
REGISTER_DATA=$(cat << EOF
{{
"device_id": "$DEVICE_ID",
"ip": "$DEVICE_IP",
"frida_port": $FRIDA_PORT,
"mode": "$DEPLOY_MODE",
"android_version": "$(getprop ro.build.version.release)",
"device_model": "$(getprop ro.product.model)",
"frida_version": "{version}"
}}
EOF
)
# 通过 HTTP API 注册
API_URL=$(echo "$SERVER_URL" | sed 's|ws://|http://|' | sed 's|wss://|https://|' | sed 's|/ws/device||')
curl -s -X POST "$API_URL/api/v3/frida/register" \\
-H "Content-Type: application/json" \\
-d "$REGISTER_DATA" || warn "服务器注册失败,稍后重试"
info "🎉 部署完成!"
info "连接信息: $DEVICE_IP:$FRIDA_PORT ($DEPLOY_MODE)"
"""
return script
# ---- 局域网设备发现 ----
def discover_devices(self, subnet: str = "", port: int = 27042,
timeout: float = 2.0) -> List[Dict[str, Any]]:
"""
扫描局域网内运行 frida-server 的设备
Args:
subnet: 子网前缀,如 "192.168.1",空则自动检测
port: Frida 端口
timeout: 每个 IP 的超时时间
"""
if not subnet:
subnet = self._detect_subnet()
if not subnet:
return []
found = []
logger.info(f"扫描子网 {subnet}.0/24 端口 {port}...")
def _scan_ip(ip: str):
if self._check_port(ip, port, timeout):
try:
import frida
mgr = frida.get_device_manager()
device = mgr.add_remote_device(f"{ip}:{port}")
params = device.query_system_parameters()
found.append({
"ip": ip,
"port": port,
"name": device.name,
"os": params.get("os", {}).get("id", "unknown"),
"arch": params.get("arch", "unknown"),
})
logger.info(f" 发现设备: {ip}:{port} ({device.name})")
except Exception:
# 端口开放但不是 Frida
pass
threads = []
for i in range(1, 255):
ip = f"{subnet}.{i}"
t = threading.Thread(target=_scan_ip, args=(ip,))
t.daemon = True
threads.append(t)
t.start()
for t in threads:
t.join(timeout=timeout + 1)
logger.info(f"扫描完成,发现 {len(found)} 个设备")
return found
# ---- 健康检查 ----
def start_health_check(self, interval: int = 30):
"""启动后台健康检查线程"""
self._running = True
self._health_thread = threading.Thread(
target=self._health_check_loop,
args=(interval,),
daemon=True,
)
self._health_thread.start()
logger.info(f"健康检查已启动,间隔 {interval}s")
def stop_health_check(self):
"""停止健康检查"""
self._running = False
if self._health_thread:
self._health_thread.join(timeout=5)
logger.info("健康检查已停止")
def _health_check_loop(self, interval: int):
"""健康检查循环"""
while self._running:
for device_id, conn in list(self.connections.items()):
if conn.status == "connected":
if not self._check_port(conn.ip, conn.frida_port, timeout=3):
conn.status = "disconnected"
conn.wechat_attached = False
logger.warning(f"设备离线: {device_id}")
else:
conn.last_heartbeat = datetime.now().isoformat()
elif conn.status == "disconnected" and conn.reconnect_count < 10:
# 尝试自动重连
result = self.connect_device(device_id)
if result.get("success"):
logger.info(f"设备自动重连成功: {device_id}")
time.sleep(interval)
# ---- 工具方法 ----
@staticmethod
def _check_port(ip: str, port: int, timeout: float = 3.0) -> bool:
"""检查 TCP 端口是否可达"""
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(timeout)
result = sock.connect_ex((ip, port))
sock.close()
return result == 0
except Exception:
return False
@staticmethod
def _detect_subnet() -> str:
"""自动检测本机子网"""
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(("8.8.8.8", 80))
ip = s.getsockname()[0]
s.close()
parts = ip.split(".")
return ".".join(parts[:3])
except Exception:
return ""
def get_status(self) -> Dict[str, Any]:
"""获取部署器状态"""
connected = sum(1 for c in self.connections.values() if c.status == "connected")
total = len(self.connections)
return {
"total_devices": total,
"connected_devices": connected,
"disconnected_devices": total - connected,
"health_check_running": self._running,
"config": {
"frida_version": self.config.frida_version,
"frida_arch": self.config.frida_arch,
"anti_detect": self.config.anti_detect,
},
"devices": self.list_devices(),
}
# ============================================================
# § 3 全局单例
# ============================================================
wireless_deployer = WirelessDeployer()