716 lines
24 KiB
Python
716 lines
24 KiB
Python
"""
|
||
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-server(Root)或 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 加载 gadget(smali 注入方式更可靠,这里用简化方案)
|
||
# 在 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()
|