""" 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.6(17.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 spawn(spawn 失败会在 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 pidof(Remote/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-11:frida 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_loop(FS-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, }