""" 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, ): 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._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 模式设置 ADB 端口转发""" 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 and self.device_serial: logger.warning("微信未运行,尝试 adb 启动后 attach") try: subprocess.run( ["adb", "-s", self.device_serial, "shell", "am", "start", "-n", f"{WECHAT_PACKAGE}/.ui.LauncherUI"], capture_output=True, timeout=15, ) time.sleep(5) pid = self._find_wechat_pid() except Exception as launch_err: logger.warning(f"adb 启动微信失败: {launch_err}") if pid is None: logger.warning("微信未运行,Remote 模式尝试 spawn 启动") try: pid = self._device.spawn([WECHAT_PACKAGE]) self._device.resume(pid) time.sleep(3) except Exception as spawn_err: logger.error(f"微信 spawn 失败: {spawn_err}") 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: logger.warning("微信未运行,尝试 spawn 启动") pid = self._device.spawn([WECHAT_PACKAGE]) self._device.resume(pid) time.sleep(3) 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]: # 优先 ADB pidof(Remote/Gadget 经端口转发时 enumerate 常漏进程) if self.device_serial: 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 _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() 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 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, }