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,434 @@
"""
Frida Manager — 管理 Frida 与微信进程的连接生命周期
职责:
- 连接设备USB / remote / gadget 三种模式)
- attach 到微信进程
- 加载 JS 脚本
- 维护 rpc.exports 代理
- 自动重连
连接模式:
- usb: 需要 Root通过 frida-server 连接(传统方式)
- gadget: 无需 Root通过注入到 APK 中的 frida-gadget 连接
- remote: 通过 TCP 连接远程 frida-server
技术栈Frida 16.5.617.x 在 Android 11 上 Java 桥失效)
"""
import os
import re
import time
import logging
import threading
import subprocess
from typing import Optional, Callable, Dict, Any, Literal
logger = logging.getLogger(__name__)
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
DEFAULT_SCRIPT = os.path.join(SCRIPT_DIR, "wechat_hook_v2.js")
WECHAT_PACKAGE = "com.tencent.mm"
import random as _random
_ANTI_DETECT_PORT_RANGE = (10000, 60000)
GADGET_PORT = _random.randint(*_ANTI_DETECT_PORT_RANGE)
ConnectionMode = Literal["usb", "gadget", "remote"]
_SNAKE_RE = re.compile(r'(?<=[a-z0-9])([A-Z])')
def _random_port() -> int:
return _random.randint(*_ANTI_DETECT_PORT_RANGE)
class FridaManager:
"""Frida 会话管理器,支持 USB / Gadget / Remote 三种连接模式"""
def __init__(
self,
device_serial: Optional[str] = None,
script_path: str = DEFAULT_SCRIPT,
on_event: Optional[Callable[[Dict[str, Any]], None]] = None,
on_detach: Optional[Callable[[str], None]] = None,
auto_reconnect: bool = True,
reconnect_interval: float = 5.0,
mode: ConnectionMode = "gadget",
gadget_host: str = "127.0.0.1",
gadget_port: int = 0,
use_adb_forward: bool = True,
):
if gadget_port == 0:
gadget_port = _random_port()
self.device_serial = device_serial
self.script_path = script_path
self.on_event = on_event
self.on_detach = on_detach
self.auto_reconnect = auto_reconnect
self.reconnect_interval = reconnect_interval
self.mode = mode
self.gadget_host = gadget_host
self.gadget_port = gadget_port
self.use_adb_forward = use_adb_forward
self._device = None
self._session = None
self._script = None
self._rpc = None
self._running = False
self._detaching = False
self._lock = threading.Lock()
@property
def connected(self) -> bool:
return self._session is not None and self._script is not None
@property
def rpc(self):
return self._rpc
def start(self) -> bool:
try:
import frida
except ImportError:
logger.error("frida 未安装,请执行: pip install frida-tools")
return False
with self._lock:
if self.connected:
return True
return self._do_attach()
def _setup_gadget_forward(self):
"""为 Gadget/Remote 模式设置 ADB 端口转发(手机本机 Agent 连 127.0.0.1 时跳过)"""
if not self.use_adb_forward:
logger.info("跳过 ADB forward设备本机 Frida remote")
return
try:
cmd = ["adb"]
if self.device_serial:
cmd += ["-s", self.device_serial]
cmd += ["forward", f"tcp:{self.gadget_port}", f"tcp:{self.gadget_port}"]
subprocess.run(cmd, capture_output=True, timeout=5)
logger.info(f"ADB 端口转发已设置: tcp:{self.gadget_port}")
except Exception as e:
logger.warning(f"端口转发设置失败: {e}")
def _do_attach(self) -> bool:
import frida
try:
if self.mode == "gadget":
self._setup_gadget_forward()
addr = f"{self.gadget_host}:{self.gadget_port}"
mgr = frida.get_device_manager()
self._device = mgr.add_remote_device(addr)
logger.info(f"Frida Gadget 已连接: {addr}")
self._session = self._device.attach("Gadget")
self._session.on("detached", self._on_session_detached)
logger.info("已 attach 到 Gadget 进程(无 Root 模式)")
elif self.mode == "remote":
self._setup_gadget_forward()
addr = f"{self.gadget_host}:{self.gadget_port}"
mgr = frida.get_device_manager()
self._device = mgr.add_remote_device(addr)
logger.info(f"Frida Remote 已连接: {addr}")
pid = self._find_wechat_pid()
if pid is None:
# WX-11 修复:禁用 frida spawnspawn 失败会在 ActivityManager 留下
# (null)pid 幽灵进程记录,阻塞微信后续启动需 reboot。改为 am start
# 温和拉起 + 轮询 attach仍失败则本轮放弃交重连循环稍后重试。
logger.warning("微信未运行am start 温和拉起(禁用 frida spawn 防 AM 幽灵进程 WX-11")
pid = self._launch_wechat_gently()
if pid is None:
logger.warning("微信仍未运行,本轮放弃 attach交重连循环重试 am start不 frida-spawn")
return False
self._session = self._device.attach(pid)
self._session.on("detached", self._on_session_detached)
logger.info(f"已 attach 到微信进程 pid={pid}")
else: # usb (需要 Root)
if self.device_serial:
self._device = frida.get_device(self.device_serial)
else:
self._device = frida.get_usb_device(timeout=10)
logger.info(f"Frida USB 已连接设备: {self._device.name}")
pid = self._find_wechat_pid()
if pid is None:
# WX-11 修复USB 模式同样禁用 frida spawn改 am start 温和拉起。
logger.warning("微信未运行am start 温和拉起(禁用 frida spawn 防 AM 幽灵进程 WX-11")
pid = self._launch_wechat_gently()
if pid is None:
logger.warning("微信仍未运行,本轮放弃 attach不 frida-spawn")
return False
self._session = self._device.attach(pid)
self._session.on("detached", self._on_session_detached)
logger.info(f"已 attach 到微信进程 pid={pid}")
self._load_script()
self._running = True
return True
except Exception as e:
logger.error(f"Frida attach 失败 [{self.mode}]: {e}")
self._do_cleanup()
return False
def _find_wechat_pid(self) -> Optional[int]:
# 手机本机shell pidof无需 adb
if not self.use_adb_forward:
try:
result = subprocess.run(
["pidof", WECHAT_PACKAGE],
capture_output=True, text=True, timeout=5,
)
out = (result.stdout or "").strip()
if out:
return int(out.split()[0])
except Exception:
pass
try:
result = subprocess.run(
["sh", "-c", f"pidof {WECHAT_PACKAGE}"],
capture_output=True, text=True, timeout=5,
)
out = (result.stdout or "").strip()
if out:
return int(out.split()[0])
except Exception:
pass
# 主机 ADB pidofRemote/Gadget 经端口转发时 enumerate 常漏进程)
if self.device_serial and self.use_adb_forward:
for attempt in range(3):
try:
result = subprocess.run(
["adb", "-s", self.device_serial, "shell", "pidof", WECHAT_PACKAGE],
capture_output=True, text=True, timeout=8,
)
out = (result.stdout or "").strip()
if out:
return int(out.split()[0])
except Exception:
pass
if attempt < 2:
time.sleep(2)
try:
for proc in self._device.enumerate_processes():
if proc.name in (WECHAT_PACKAGE, "WeChat") or getattr(proc, 'identifier', '') == WECHAT_PACKAGE:
return proc.pid
except Exception:
pass
try:
for app in self._device.enumerate_applications():
if app.identifier == WECHAT_PACKAGE and app.pid > 0:
return app.pid
except Exception:
pass
try:
result = subprocess.run(
["adb", "-s", self.device_serial or "", "shell", "pidof", WECHAT_PACKAGE],
capture_output=True, text=True, timeout=5,
)
if result.stdout.strip():
return int(result.stdout.strip().split()[0])
except Exception:
pass
return None
def _launch_wechat_gently(self, max_wait: int = 18) -> Optional[int]:
"""温和拉起微信:仅 am start + 轮询 pid绝不用 frida spawn。
WX-11frida spawn 失败会在 ActivityManager 留下 (null)pid 幽灵进程记录,
使微信被视为"正在启动"而拒绝后续 am start最终需 reboot 才能恢复。
故统一改用 am start 温和拉起并轮询,失败返回 None 交重连循环稍后重试。
"""
try:
cmd = ["am", "start", "-n", f"{WECHAT_PACKAGE}/.ui.LauncherUI"]
if self.device_serial:
cmd = ["adb", "-s", self.device_serial, "shell"] + cmd
subprocess.run(cmd, capture_output=True, timeout=15)
except Exception as e:
logger.warning(f"am start 拉起微信失败: {e}")
deadline = time.time() + max_wait
while time.time() < deadline:
pid = self._find_wechat_pid()
if pid:
logger.info(f"微信已温和拉起 pid={pid}")
return pid
time.sleep(2)
return None
def _load_script(self):
path = self.script_path
if not os.path.exists(path):
fallback = os.path.join(SCRIPT_DIR, "wechat_hook_v1.js")
if os.path.exists(fallback):
path = fallback
else:
raise FileNotFoundError(f"Hook 脚本不存在: {path}")
with open(path, "r", encoding="utf-8") as f:
source = f.read()
self._script = self._session.create_script(source)
self._script.on("message", self._on_message)
self._script.load()
# 注意:禁止调用 script.eternalize()。Frida 语义为「eternalize 后即与脚本断开交互」,
# 调用后 exports_sync 失效,所有 RPC 报 "script has been destroyed"。
# 保活改由 _on_session_detached → _reconnect_loopFS-3 detach 重连守护)实现。
self._rpc = self._script.exports_sync
logger.info(f"Hook 脚本已加载: {os.path.basename(path)}")
try:
pong = self._rpc.ping()
logger.info(f"RPC ping: {pong}")
except Exception as e:
logger.warning(f"RPC ping 失败: {e}")
def _on_message(self, message: dict, data):
if message.get("type") == "send":
payload = message.get("payload", {})
msg_type = payload.get("type", "")
if msg_type == "hook_event" and self.on_event:
self.on_event(payload)
elif msg_type == "log":
level = payload.get("level", "info")
tag = payload.get("tag", "hook")
text = payload.get("message", "")
getattr(logger, level, logger.info)(f"[{tag}] {text}")
elif message.get("type") == "error":
logger.error(f"Frida 脚本错误: {message.get('description', '')}")
def _on_session_detached(self, reason: str):
"""Frida 内部线程回调 — 只清理引用,不重复 detach/unload不获取 _lock避免死锁"""
logger.warning(f"Frida 会话断开: {reason}")
self._detaching = True
self._nullify_refs()
self._detaching = False
if self.on_detach:
try:
self.on_detach(reason)
except Exception as e:
logger.warning(f"on_detach 回调异常: {e}")
if self.auto_reconnect and self._running and reason != "application-requested":
threading.Thread(target=self._reconnect_loop, daemon=True).start()
def _reconnect_loop(self):
while self._running and not self.connected:
logger.info(f"尝试重连 Frida{self.reconnect_interval}s 后)...")
time.sleep(self.reconnect_interval)
with self._lock:
if self.connected:
break
try:
if self._do_attach():
logger.info("Frida 重连成功")
break
except Exception as e:
logger.warning(f"重连失败: {e}")
@staticmethod
def _to_snake(name: str) -> str:
return _SNAKE_RE.sub(r'_\1', name).lower()
def call_rpc(self, method: str, params: dict = None) -> Dict[str, Any]:
with self._lock:
if not self.connected or not self._rpc:
return {"success": False, "error": "Frida 未连接"}
rpc = self._rpc
try:
snake = self._to_snake(method)
fn = getattr(rpc, snake, None) or getattr(rpc, method, None)
if fn is None:
return {"success": False, "error": f"RPC 方法不存在: {method} ({snake})"}
result = fn(params) if params else fn({})
if isinstance(result, dict):
result.setdefault("success", True)
return result
return {"success": True, "data": result}
except Exception as e:
logger.error(f"RPC 调用失败 [{method}]: {e}")
return {"success": False, "error": str(e)}
def reload_script(self) -> Dict[str, Any]:
"""重新加载当前 Hook 脚本,不重启微信进程。"""
with self._lock:
try:
if not self._session:
ok = self._do_attach()
return {
"success": ok,
"reloaded": ok,
"script_path": self.script_path,
"method": "attach_and_load",
}
if self._script:
try:
self._script.unload()
except Exception as e:
logger.warning(f"Hook 脚本卸载失败,继续重载: {e}")
self._script = None
self._rpc = None
self._load_script()
return {
"success": True,
"reloaded": True,
"script_path": self.script_path,
"method": "reload_script",
}
except Exception as e:
self._script = None
self._rpc = None
logger.error(f"Hook 脚本重载失败: {e}")
return {
"success": False,
"reloaded": False,
"script_path": self.script_path,
"error": str(e),
}
def stop(self):
self._running = False
with self._lock:
self._do_cleanup()
logger.info("FridaManager 已停止")
def _nullify_refs(self):
"""清空引用(不执行 unload/detach用于 detach 回调内"""
self._script = None
self._session = None
self._rpc = None
def _do_cleanup(self):
"""主动断开时完整清理unload 脚本 + detach 会话)"""
if self._detaching:
return
try:
if self._script:
self._script.unload()
except Exception:
pass
try:
if self._session:
self._session.detach()
except Exception:
pass
self._nullify_refs()
def get_status(self) -> Dict[str, Any]:
return {
"connected": self.connected,
"mode": self.mode,
"device": self._device.name if self._device else None,
"device_serial": self.device_serial,
"script": os.path.basename(self.script_path),
"running": self._running,
"gadget_endpoint": f"{self.gadget_host}:{self.gadget_port}" if self.mode == "gadget" else None,
}