From f5df31107921757ed25b3a59f91a6327750dae93 Mon Sep 17 00:00:00 2001 From: Manus AI Date: Sun, 24 May 2026 09:16:59 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20=E7=A7=BB=E9=99=A4=E6=A0=B9=E7=9B=AE?= =?UTF-8?q?=E5=BD=95=E8=A1=A5=E5=85=A8=E5=8C=85=E7=9B=AE=E5=BD=95=EF=BC=88?= =?UTF-8?q?=E5=B7=B2=E6=94=B6=E7=BC=96=E8=BF=9B=20sdk/=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 删除 补全包/ 与 工作手机SDK补全包_微信Frida/ 冗余副本; 真源见 sdk/agent/hook、资料/archive/ 历史 ZIP。 Co-authored-by: Cursor --- 工作手机SDK补全包_微信Frida/README.md | 16 - 工作手机SDK补全包_微信Frida/generate_impl.py | 578 ------------------ .../hooks/wechat_hook_bridge.js | 45 -- .../mobile_agent/__init__.py | 0 .../mobile_agent/wireless_agent.py | 65 -- 工作手机SDK补全包_微信Frida/requirements.txt | 6 - 工作手机SDK补全包_微信Frida/sdk/__init__.py | 0 .../sdk/frida/__init__.py | 0 .../sdk/frida/frida_manager.py | 166 ----- .../sdk/frida/hook_executor.py | 37 -- .../sdk/wechat/__init__.py | 0 .../sdk/wechat/wechat_actions.py | 54 -- .../server/__init__.py | 0 工作手机SDK补全包_微信Frida/server/app.py | 13 - .../server/routes/__init__.py | 0 .../server/routes/wechat_frida.py | 76 --- .../tests_api_routes.py | 23 - 工作手机SDK补全包_微信Frida/tests_mock_rpc.py | 21 - .../开发文档/10、项目管理/项目落地执行表.md | 6 - .../开发文档/2、架构/系统架构.md | 6 - .../开发文档/5、接口/微信Frida_API契约.md | 71 --- .../开发文档/5、接口/微信Frida控制接口契约.md | 5 - .../开发文档/8、部署/微信Frida无线部署与验证说明.md | 5 - 补全包/README.md | 12 - 补全包/微信Frida_SDK_20260518/README.md | 29 - .../微信Frida_SDK_20260518/generate_impl.py | 578 ------------------ .../hooks/wechat_hook_bridge.js | 45 -- .../mobile_agent/__init__.py | 0 .../mobile_agent/wireless_agent.py | 65 -- .../微信Frida_SDK_20260518/requirements.txt | 6 - 补全包/微信Frida_SDK_20260518/sdk/__init__.py | 0 .../微信Frida_SDK_20260518/sdk/frida/__init__.py | 0 .../sdk/frida/frida_manager.py | 166 ----- .../sdk/frida/hook_executor.py | 37 -- .../sdk/wechat/__init__.py | 0 .../sdk/wechat/wechat_actions.py | 54 -- .../微信Frida_SDK_20260518/server/__init__.py | 0 补全包/微信Frida_SDK_20260518/server/app.py | 13 - .../server/routes/__init__.py | 0 .../server/routes/wechat_frida.py | 76 --- .../微信Frida_SDK_20260518/tests_mock_rpc.py | 21 - .../开发文档/10、项目管理/项目落地执行表.md | 3 - .../开发文档/5、接口/微信Frida_API契约.md | 3 - .../8、部署/微信Frida无线部署与验证说明.md | 3 - 补全包/微信Frida_SDK_20260518_补全包.zip | Bin 30208 -> 0 bytes 45 files changed, 2304 deletions(-) delete mode 100644 工作手机SDK补全包_微信Frida/README.md delete mode 100644 工作手机SDK补全包_微信Frida/generate_impl.py delete mode 100644 工作手机SDK补全包_微信Frida/hooks/wechat_hook_bridge.js delete mode 100644 工作手机SDK补全包_微信Frida/mobile_agent/__init__.py delete mode 100644 工作手机SDK补全包_微信Frida/mobile_agent/wireless_agent.py delete mode 100644 工作手机SDK补全包_微信Frida/requirements.txt delete mode 100644 工作手机SDK补全包_微信Frida/sdk/__init__.py delete mode 100644 工作手机SDK补全包_微信Frida/sdk/frida/__init__.py delete mode 100644 工作手机SDK补全包_微信Frida/sdk/frida/frida_manager.py delete mode 100644 工作手机SDK补全包_微信Frida/sdk/frida/hook_executor.py delete mode 100644 工作手机SDK补全包_微信Frida/sdk/wechat/__init__.py delete mode 100644 工作手机SDK补全包_微信Frida/sdk/wechat/wechat_actions.py delete mode 100644 工作手机SDK补全包_微信Frida/server/__init__.py delete mode 100644 工作手机SDK补全包_微信Frida/server/app.py delete mode 100644 工作手机SDK补全包_微信Frida/server/routes/__init__.py delete mode 100644 工作手机SDK补全包_微信Frida/server/routes/wechat_frida.py delete mode 100644 工作手机SDK补全包_微信Frida/tests_api_routes.py delete mode 100644 工作手机SDK补全包_微信Frida/tests_mock_rpc.py delete mode 100644 工作手机SDK补全包_微信Frida/开发文档/10、项目管理/项目落地执行表.md delete mode 100644 工作手机SDK补全包_微信Frida/开发文档/2、架构/系统架构.md delete mode 100644 工作手机SDK补全包_微信Frida/开发文档/5、接口/微信Frida_API契约.md delete mode 100644 工作手机SDK补全包_微信Frida/开发文档/5、接口/微信Frida控制接口契约.md delete mode 100644 工作手机SDK补全包_微信Frida/开发文档/8、部署/微信Frida无线部署与验证说明.md delete mode 100644 补全包/README.md delete mode 100644 补全包/微信Frida_SDK_20260518/README.md delete mode 100644 补全包/微信Frida_SDK_20260518/generate_impl.py delete mode 100644 补全包/微信Frida_SDK_20260518/hooks/wechat_hook_bridge.js delete mode 100644 补全包/微信Frida_SDK_20260518/mobile_agent/__init__.py delete mode 100644 补全包/微信Frida_SDK_20260518/mobile_agent/wireless_agent.py delete mode 100644 补全包/微信Frida_SDK_20260518/requirements.txt delete mode 100644 补全包/微信Frida_SDK_20260518/sdk/__init__.py delete mode 100644 补全包/微信Frida_SDK_20260518/sdk/frida/__init__.py delete mode 100644 补全包/微信Frida_SDK_20260518/sdk/frida/frida_manager.py delete mode 100644 补全包/微信Frida_SDK_20260518/sdk/frida/hook_executor.py delete mode 100644 补全包/微信Frida_SDK_20260518/sdk/wechat/__init__.py delete mode 100644 补全包/微信Frida_SDK_20260518/sdk/wechat/wechat_actions.py delete mode 100644 补全包/微信Frida_SDK_20260518/server/__init__.py delete mode 100644 补全包/微信Frida_SDK_20260518/server/app.py delete mode 100644 补全包/微信Frida_SDK_20260518/server/routes/__init__.py delete mode 100644 补全包/微信Frida_SDK_20260518/server/routes/wechat_frida.py delete mode 100644 补全包/微信Frida_SDK_20260518/tests_mock_rpc.py delete mode 100644 补全包/微信Frida_SDK_20260518/开发文档/10、项目管理/项目落地执行表.md delete mode 100644 补全包/微信Frida_SDK_20260518/开发文档/5、接口/微信Frida_API契约.md delete mode 100644 补全包/微信Frida_SDK_20260518/开发文档/8、部署/微信Frida无线部署与验证说明.md delete mode 100644 补全包/微信Frida_SDK_20260518_补全包.zip diff --git a/工作手机SDK补全包_微信Frida/README.md b/工作手机SDK补全包_微信Frida/README.md deleted file mode 100644 index 0da98b5504..0000000000 --- a/工作手机SDK补全包_微信Frida/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# 工作手机 SDK · 微信 Frida 无线控制补全包 - -> **⚠️ 归档原型 · 文档已并入主线** -> **请读**:[开发文档/9、手册/工作手机·五图总览与使用手册.md](../开发文档/9、手册/工作手机·五图总览与使用手册.md)(v2.0) -> **请用**:主线 `sdk/`(非本目录独立 server) - -本包与 `补全包/微信Frida_SDK_20260518/` 为同批归档;正文、九模块路径、无线部署均已写入开发文档主手册。 - -## 快速启动(现行) - -```bash -cd sdk/app && python3 -m uvicorn main:app --host 0.0.0.0 --port 8899 -cd sdk/agent && python3 agent.py --server ws://127.0.0.1:8899/ws/device/ -``` - -Hook 替换与 RPC 四层兼容见主手册 **§七**。 diff --git a/工作手机SDK补全包_微信Frida/generate_impl.py b/工作手机SDK补全包_微信Frida/generate_impl.py deleted file mode 100644 index 63f3879cfb..0000000000 --- a/工作手机SDK补全包_微信Frida/generate_impl.py +++ /dev/null @@ -1,578 +0,0 @@ -from pathlib import Path - -ROOT = Path('/home/ubuntu/work_phone_sdk_completion') -files = {} - -files['sdk/frida/frida_manager.py'] = r'''""" -工作手机 SDK · FridaManager - -负责手机端或服务器端连接 Frida、attach 微信进程、加载 Hook 脚本,并提供兼容 RPC 调用。 -设计重点:历史归档显示 Frida Python 对 rpc.exports 方法名存在混淆,因此这里采用多候选名兼容策略。 -""" -from __future__ import annotations - -from dataclasses import dataclass, field -from pathlib import Path -import re -import time -from typing import Any, Callable, Dict, Iterable, Optional - - -class FridaUnavailable(RuntimeError): - """当前环境未安装或无法连接 Frida。""" - - -class RpcMethodMissing(RuntimeError): - """Hook 中缺少指定 RPC 方法。""" - - -@dataclass -class FridaConfig: - device_host: str = "127.0.0.1" - device_port: int = 27042 - package_name: str = "com.tencent.mm" - process_name: str = "WeChat" - attach_timeout: float = 15.0 - prefer_usb: bool = False - hook_path: Optional[str] = None - - -@dataclass -class RpcResult: - ok: bool - action: str - method: str - data: Any = None - error: Optional[str] = None - elapsed_ms: int = 0 - - -class FridaManager: - def __init__(self, config: Optional[FridaConfig] = None): - self.config = config or FridaConfig() - self.frida = None - self.device = None - self.session = None - self.script = None - self.exports = None - self.loaded_hook_path: Optional[str] = None - - def _import_frida(self): - if self.frida is not None: - return self.frida - try: - import frida # type: ignore - except Exception as exc: - raise FridaUnavailable(f"未安装 frida Python 包或加载失败:{exc}") from exc - self.frida = frida - return frida - - def connect(self): - frida = self._import_frida() - if self.config.prefer_usb: - self.device = frida.get_usb_device(timeout=int(self.config.attach_timeout)) - else: - self.device = frida.get_device_manager().add_remote_device( - f"{self.config.device_host}:{self.config.device_port}" - ) - return self.device - - def attach(self, target: Optional[str] = None): - if self.device is None: - self.connect() - assert self.device is not None - target = target or self.config.package_name - last_error: Optional[Exception] = None - candidates: Iterable[Any] = [target, self.config.process_name] - for candidate in candidates: - try: - self.session = self.device.attach(candidate) - return self.session - except Exception as exc: # pragma: no cover - depends on phone - last_error = exc - try: - processes = self.device.enumerate_processes() - for proc in processes: - name = getattr(proc, 'name', '') or '' - pid = getattr(proc, 'pid', None) - if pid and (self.config.package_name in name or self.config.process_name.lower() in name.lower() or '微信' in name): - self.session = self.device.attach(pid) - return self.session - except Exception as exc: # pragma: no cover - last_error = exc - raise FridaUnavailable(f"无法 attach 微信进程:{last_error}") - - def load_script(self, hook_path: Optional[str] = None): - path = Path(hook_path or self.config.hook_path or '') - if not path.exists(): - raise FileNotFoundError(f"Hook 脚本不存在:{path}") - if self.session is None: - self.attach() - assert self.session is not None - source = path.read_text(encoding='utf-8') - self.script = self.session.create_script(source) - self.script.on('message', self._on_message) - self.script.load() - self.exports = getattr(self.script, 'exports_sync', None) or getattr(self.script, 'exports', None) - self.loaded_hook_path = str(path) - return self.script - - def _on_message(self, message, data): # pragma: no cover - runtime callback - # 生产环境可转发到 EventReporter;这里保持最小日志。 - print({'frida_message': message, 'data_len': len(data) if data else 0}) - - @staticmethod - def _snake(name: str) -> str: - return re.sub(r'(? RpcResult: - started = time.time() - if self.exports is None: - raise FridaUnavailable('Hook 脚本尚未加载,无法调用 RPC') - last_error = None - for candidate in self.method_candidates(method): - try: - fn = getattr(self.exports, candidate) - data = fn(*args, **kwargs) - return RpcResult(True, method, candidate, data=data, elapsed_ms=int((time.time() - started) * 1000)) - except AttributeError as exc: - last_error = exc - except Exception as exc: - return RpcResult(False, method, candidate, error=str(exc), elapsed_ms=int((time.time() - started) * 1000)) - # 兼容新版 frida 的 exports_sync.invoke 或 script.exports_sync.invoke - try: - invoke = getattr(self.exports, 'invoke') - data = invoke(method, list(args), kwargs or None) - return RpcResult(True, method, 'invoke', data=data, elapsed_ms=int((time.time() - started) * 1000)) - except Exception as exc: - last_error = exc - return RpcResult(False, method, '', error=f'RPC 方法不存在或不可调用:{method}; last={last_error}', elapsed_ms=int((time.time() - started) * 1000)) - - def cleanup(self): - for obj, method in [(self.script, 'unload'), (self.session, 'detach')]: - if obj is not None: - try: - getattr(obj, method)() - except Exception: - pass - self.script = None - self.session = None - self.exports = None -''' - -files['sdk/wechat/wechat_actions.py'] = r'''"""微信动作目录:服务器、手机端 Agent、验证脚本共同使用。""" -from __future__ import annotations -from dataclasses import dataclass, field -from typing import Any, Dict, List - -@dataclass(frozen=True) -class WechatAction: - action: str - rpc: str - module: str - description: str - required: List[str] = field(default_factory=list) - safe: bool = True - verify: bool = True - -ACTIONS: List[WechatAction] = [ - WechatAction('ping', 'ping', 'system', 'Hook 连通性检测'), - WechatAction('get_connection_status', 'getConnectionStatus', 'system', '读取连接状态'), - WechatAction('screenshot', 'takeScreenshot', 'device', '手机当前屏幕截图'), - WechatAction('ui_dump', 'dumpUiTree', 'device', '导出当前 UI 树'), - WechatAction('go_home', 'goHome', 'navigation', '返回微信首页'), - WechatAction('open_contacts', 'openContacts', 'navigation', '打开通讯录'), - WechatAction('open_chats', 'openChats', 'navigation', '打开聊天列表'), - WechatAction('search_contact', 'searchContact', 'contact', '搜索联系人', ['keyword']), - WechatAction('get_contacts', 'getContacts', 'contact', '读取联系人列表'), - WechatAction('get_contact_profile', 'getContactProfile', 'contact', '读取联系人资料', ['wxid']), - WechatAction('add_friend', 'addFriend', 'contact', '添加好友', ['keyword'], safe=False), - WechatAction('accept_friend', 'acceptFriend', 'contact', '通过好友申请', ['wxid'], safe=False), - WechatAction('send_text', 'sendTextMessage', 'message', '发送文本消息', ['to', 'text'], safe=False), - WechatAction('send_image', 'sendImageMessage', 'message', '发送图片消息', ['to', 'path'], safe=False), - WechatAction('get_messages', 'getMessages', 'message', '读取消息列表', ['wxid']), - WechatAction('open_chat', 'openChat', 'message', '打开指定会话', ['wxid']), - WechatAction('create_group', 'createGroup', 'group', '创建群聊', ['members'], safe=False), - WechatAction('invite_group_member', 'inviteGroupMember', 'group', '邀请群成员', ['chatroom', 'members'], safe=False), - WechatAction('get_group_members', 'getGroupMembers', 'group', '读取群成员', ['chatroom']), - WechatAction('browse_channels', 'browseChannels', 'channels', '浏览视频号'), - WechatAction('add_favorite', 'addFavorite', 'favorite', '收藏当前内容'), - WechatAction('add_custom_emoji', 'addCustomEmoji', 'emoji', '添加自定义表情', safe=False), -] - -ACTION_MAP: Dict[str, WechatAction] = {item.action: item for item in ACTIONS} -RPC_MAP: Dict[str, WechatAction] = {item.rpc: item for item in ACTIONS} - -def list_actions() -> List[Dict[str, Any]]: - return [item.__dict__.copy() for item in ACTIONS] - -def validate_action(action: str, payload: Dict[str, Any]) -> WechatAction: - if action not in ACTION_MAP: - raise KeyError(f'未知微信动作:{action}') - spec = ACTION_MAP[action] - missing = [k for k in spec.required if k not in payload or payload[k] in (None, '')] - if missing: - raise ValueError(f'动作 {action} 缺少参数:{missing}') - return spec -''' - -files['sdk/frida/hook_executor.py'] = r'''"""将业务 action 映射到 Frida RPC。""" -from __future__ import annotations -from dataclasses import asdict -from typing import Any, Dict, Optional - -from sdk.frida.frida_manager import FridaManager, RpcResult -from sdk.wechat.wechat_actions import validate_action, list_actions - -class HookExecutor: - def __init__(self, manager: FridaManager): - self.manager = manager - - def execute(self, action: str, payload: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: - payload = payload or {} - try: - spec = validate_action(action, payload) - except Exception as exc: - return {'ok': False, 'action': action, 'error': str(exc), 'stage': 'validate'} - args = [payload[k] for k in spec.required] - # 允许额外参数透传给 Hook;Hook 若不支持会返回错误,便于报告定位。 - extra = {k: v for k, v in payload.items() if k not in spec.required} - result: RpcResult = self.manager.call(spec.rpc, *args, **extra) - return { - 'ok': result.ok, - 'action': action, - 'rpc': spec.rpc, - 'method_used': result.method, - 'module': spec.module, - 'description': spec.description, - 'safe': spec.safe, - 'data': result.data, - 'error': result.error, - 'elapsed_ms': result.elapsed_ms, - } - - def catalog(self): - return list_actions() -''' - -files['mobile_agent/wireless_agent.py'] = r'''"""手机端 Termux/Python Agent:通过 WebSocket 接收服务器动作,调用 Frida Hook。""" -from __future__ import annotations -import argparse -import asyncio -import json -import platform -import uuid -from pathlib import Path - -from sdk.frida.frida_manager import FridaConfig, FridaManager -from sdk.frida.hook_executor import HookExecutor - -try: - import websockets -except Exception: # pragma: no cover - websockets = None - -class WirelessAgent: - def __init__(self, server_ws: str, hook_path: str, device_id: str | None = None): - self.server_ws = server_ws - self.hook_path = hook_path - self.device_id = device_id or f"phone-{uuid.getnode():x}" - self.manager = FridaManager(FridaConfig(hook_path=hook_path)) - self.executor = HookExecutor(self.manager) - - async def boot(self): - self.manager.connect() - self.manager.attach() - self.manager.load_script(self.hook_path) - - def hello(self): - return { - 'type': 'hello', - 'device_id': self.device_id, - 'platform': platform.platform(), - 'actions': self.executor.catalog(), - } - - async def run(self): - if websockets is None: - raise RuntimeError('请先安装 websockets:pip install websockets') - await self.boot() - async with websockets.connect(self.server_ws, ping_interval=20, ping_timeout=20) as ws: - await ws.send(json.dumps(self.hello(), ensure_ascii=False)) - async for raw in ws: - try: - msg = json.loads(raw) - if msg.get('type') != 'command': - continue - result = self.executor.execute(msg['action'], msg.get('payload') or {}) - await ws.send(json.dumps({'type': 'result', 'request_id': msg.get('request_id'), 'device_id': self.device_id, 'result': result}, ensure_ascii=False)) - except Exception as exc: - await ws.send(json.dumps({'type': 'error', 'error': str(exc)}, ensure_ascii=False)) - - -def main(): - parser = argparse.ArgumentParser() - parser.add_argument('--server-ws', required=True, help='例如 ws://192.168.1.10:8000/ws/phone') - parser.add_argument('--hook', required=True, help='wechat_hook_bridge.js 或真实 wechat_hook_v3.js 路径') - parser.add_argument('--device-id') - args = parser.parse_args() - asyncio.run(WirelessAgent(args.server_ws, args.hook, args.device_id).run()) - -if __name__ == '__main__': - main() -''' - -files['server/routes/wechat_frida.py'] = r'''"""FastAPI 微信 Frida 无线控制路由。""" -from __future__ import annotations -import asyncio -import json -import uuid -from dataclasses import dataclass, field -from typing import Any, Dict, Optional - -from fastapi import APIRouter, WebSocket, WebSocketDisconnect, HTTPException -from pydantic import BaseModel - -from sdk.wechat.wechat_actions import list_actions - -router = APIRouter(prefix='/api/v3/wechat-frida', tags=['wechat-frida']) - -@dataclass -class PhoneConn: - device_id: str - ws: WebSocket - meta: Dict[str, Any] = field(default_factory=dict) - pending: Dict[str, asyncio.Future] = field(default_factory=dict) - -phones: Dict[str, PhoneConn] = {} - -class ActionRequest(BaseModel): - device_id: str - action: str - payload: Dict[str, Any] = {} - timeout: float = 30.0 - -@router.get('/actions') -def actions(): - return {'ok': True, 'actions': list_actions()} - -@router.get('/devices') -def devices(): - return {'ok': True, 'devices': [{'device_id': k, 'meta': v.meta} for k, v in phones.items()]} - -@router.post('/execute') -async def execute(req: ActionRequest): - if req.device_id not in phones: - raise HTTPException(404, f'设备未连接:{req.device_id}') - conn = phones[req.device_id] - request_id = uuid.uuid4().hex - fut = asyncio.get_event_loop().create_future() - conn.pending[request_id] = fut - await conn.ws.send_text(json.dumps({'type': 'command', 'request_id': request_id, 'action': req.action, 'payload': req.payload}, ensure_ascii=False)) - try: - return await asyncio.wait_for(fut, timeout=req.timeout) - finally: - conn.pending.pop(request_id, None) - -async def phone_socket(websocket: WebSocket): - await websocket.accept() - device_id: Optional[str] = None - try: - async for raw in websocket.iter_text(): - msg = json.loads(raw) - if msg.get('type') == 'hello': - device_id = msg.get('device_id') or uuid.uuid4().hex - phones[device_id] = PhoneConn(device_id=device_id, ws=websocket, meta=msg) - await websocket.send_text(json.dumps({'type': 'hello_ack', 'device_id': device_id}, ensure_ascii=False)) - elif msg.get('type') == 'result': - rid = msg.get('request_id') - did = msg.get('device_id') or device_id - conn = phones.get(did or '') - if conn and rid in conn.pending and not conn.pending[rid].done(): - conn.pending[rid].set_result({'ok': True, 'device_id': did, 'result': msg.get('result')}) - elif msg.get('type') == 'error': - # 保留连接,错误由客户端下一次请求再显式返回。 - pass - except WebSocketDisconnect: - pass - finally: - if device_id and device_id in phones: - phones.pop(device_id, None) -''' - -files['server/app.py'] = r'''from fastapi import FastAPI, WebSocket -from server.routes.wechat_frida import router as wechat_frida_router, phone_socket - -app = FastAPI(title='工作手机 SDK · 微信 Frida 控制服务', version='0.3.0') -app.include_router(wechat_frida_router) - -@app.get('/health') -def health(): - return {'ok': True, 'service': 'work-phone-sdk'} - -@app.websocket('/ws/phone') -async def ws_phone(websocket: WebSocket): - await phone_socket(websocket) -''' - -files['hooks/wechat_hook_bridge.js'] = r'''// 工作手机 SDK · 微信 Hook 桥接模板 -// 说明:这是安全桥接版,优先提供连通性、截图、UI 导航等通用方法。 -// 如果已有完整 wechat_hook_v3.js,可直接替换本文件;Python SDK 会兼容 camelCase/snake_case 调用。 -'use strict'; - -function ok(data) { return { ok: true, data: data || null, ts: Date.now() }; } -function fail(message) { return { ok: false, error: String(message), ts: Date.now() }; } - -function runJava(fn) { - let result; - Java.perform(function () { - try { result = fn(); } catch (e) { result = fail(e.stack || e.message || e); } - }); - return result; -} - -rpc.exports = { - ping: function () { return 'pong from wechat_hook_bridge'; }, - getConnectionStatus: function () { - return ok({ java_available: Java.available, process: Process.id, arch: Process.arch, platform: Process.platform }); - }, - takeScreenshot: function () { - // 截图建议在手机端通过 uiautomator/screencap 实现;Hook 层返回占位,Agent 可扩展真实文件路径。 - return ok({ mode: 'placeholder', message: '请在 mobile_agent 扩展 screencap -p 后回传文件路径' }); - }, - dumpUiTree: function () { return ok({ mode: 'placeholder', message: '建议通过 uiautomator dump 获取 UI XML' }); }, - goHome: function () { return ok({ action: 'goHome', message: '桥接模板未执行 UI 点击;请替换完整 Hook 后验证' }); }, - openContacts: function () { return ok({ action: 'openContacts', message: 'NOT_IMPLEMENTED_IN_TEMPLATE' }); }, - openChats: function () { return ok({ action: 'openChats', message: 'NOT_IMPLEMENTED_IN_TEMPLATE' }); }, - searchContact: function (keyword) { return ok({ keyword: keyword, message: 'NOT_IMPLEMENTED_IN_TEMPLATE' }); }, - getContacts: function () { return ok({ contacts: [], message: 'NOT_IMPLEMENTED_IN_TEMPLATE' }); }, - getContactProfile: function (wxid) { return ok({ wxid: wxid, message: 'NOT_IMPLEMENTED_IN_TEMPLATE' }); }, - addFriend: function (keyword) { return fail('危险动作未在模板中实现:addFriend ' + keyword); }, - acceptFriend: function (wxid) { return fail('危险动作未在模板中实现:acceptFriend ' + wxid); }, - sendTextMessage: function (to, text) { return fail('危险动作未在模板中实现:sendTextMessage ' + to); }, - sendImageMessage: function (to, path) { return fail('危险动作未在模板中实现:sendImageMessage ' + to); }, - getMessages: function (wxid) { return ok({ wxid: wxid, messages: [], message: 'NOT_IMPLEMENTED_IN_TEMPLATE' }); }, - openChat: function (wxid) { return ok({ wxid: wxid, message: 'NOT_IMPLEMENTED_IN_TEMPLATE' }); }, - createGroup: function (members) { return fail('危险动作未在模板中实现:createGroup'); }, - inviteGroupMember: function (chatroom, members) { return fail('危险动作未在模板中实现:inviteGroupMember'); }, - getGroupMembers: function (chatroom) { return ok({ chatroom: chatroom, members: [], message: 'NOT_IMPLEMENTED_IN_TEMPLATE' }); }, - browseChannels: function () { return ok({ message: 'NOT_IMPLEMENTED_IN_TEMPLATE' }); }, - addFavorite: function () { return fail('危险动作未在模板中实现:addFavorite'); }, - addCustomEmoji: function () { return fail('危险动作未在模板中实现:addCustomEmoji'); } -}; -''' - -files['scripts/verify_wechat_frida.py'] = r'''"""一键验证服务器控制手机微信功能,输出 JSON 与 Markdown 报告。""" -from __future__ import annotations -import argparse -import json -import time -from pathlib import Path -from typing import Any, Dict - -import requests - -from sdk.wechat.wechat_actions import ACTIONS - -DEFAULT_PAYLOADS: Dict[str, Dict[str, Any]] = { - 'search_contact': {'keyword': '文件传输助手'}, - 'get_contact_profile': {'wxid': 'filehelper'}, - 'add_friend': {'keyword': '__dry_run__'}, - 'accept_friend': {'wxid': '__dry_run__'}, - 'send_text': {'to': 'filehelper', 'text': '工作手机SDK自动验证'}, - 'send_image': {'to': 'filehelper', 'path': '/sdcard/Pictures/test.png'}, - 'get_messages': {'wxid': 'filehelper'}, - 'open_chat': {'wxid': 'filehelper'}, - 'create_group': {'members': ['filehelper']}, - 'invite_group_member': {'chatroom': '__dry_run__', 'members': ['filehelper']}, - 'get_group_members': {'chatroom': '__dry_run__'}, -} - -def call(base_url: str, device_id: str, action: str, payload: Dict[str, Any], timeout: float): - r = requests.post(f'{base_url}/api/v3/wechat-frida/execute', json={'device_id': device_id, 'action': action, 'payload': payload, 'timeout': timeout}, timeout=timeout + 5) - try: - return r.status_code, r.json() - except Exception: - return r.status_code, {'ok': False, 'text': r.text} - -def main(): - parser = argparse.ArgumentParser() - parser.add_argument('--base-url', default='http://127.0.0.1:8000') - parser.add_argument('--device-id', required=True) - parser.add_argument('--out-dir', default='reports') - parser.add_argument('--include-dangerous', action='store_true', help='默认不执行加好友/发消息等危险动作,只记录 SKIPPED') - parser.add_argument('--timeout', type=float, default=30) - args = parser.parse_args() - - out_dir = Path(args.out_dir) - out_dir.mkdir(parents=True, exist_ok=True) - rows = [] - for spec in ACTIONS: - if not spec.safe and not args.include_dangerous: - rows.append({'action': spec.action, 'rpc': spec.rpc, 'ok': None, 'status': 'SKIPPED_DANGEROUS', 'description': spec.description}) - continue - status, data = call(args.base_url, args.device_id, spec.action, DEFAULT_PAYLOADS.get(spec.action, {}), args.timeout) - rows.append({'action': spec.action, 'rpc': spec.rpc, 'http_status': status, 'ok': bool(data.get('ok') and (data.get('result') or {}).get('ok', True)), 'response': data, 'description': spec.description}) - time.sleep(0.2) - - stamp = time.strftime('%Y%m%d_%H%M%S') - json_path = out_dir / f'wechat_frida_verify_{stamp}.json' - md_path = out_dir / f'wechat_frida_verify_{stamp}.md' - json_path.write_text(json.dumps(rows, ensure_ascii=False, indent=2), encoding='utf-8') - lines = ['# 微信 Frida 功能验证报告', '', f'- 时间:{time.strftime("%Y-%m-%d %H:%M:%S")}', f'- 设备:`{args.device_id}`', '', '| 动作 | RPC | 结果 | 说明 |', '|---|---|---|---|'] - for row in rows: - result = 'SKIP' if row.get('status') else ('PASS' if row.get('ok') else 'FAIL') - lines.append(f"| `{row['action']}` | `{row['rpc']}` | {result} | {row.get('description','')} |") - md_path.write_text('\n'.join(lines) + '\n', encoding='utf-8') - print(json_path) - print(md_path) - -if __name__ == '__main__': - main() -''' - -files['requirements.txt'] = 'fastapi\nuvicorn\nrequests\nwebsockets\nfrida\n\n' -files['README.md'] = r'''# 工作手机 SDK · 微信 Frida 无线控制补全包 - -本包用于承接上传 ZIP 中断的开发进度,补齐 **服务器 → WebSocket → 手机端 Agent → Frida → 微信 Hook** 的闭环。 - -## 快速启动 - -服务器端: - -```bash -pip install -r requirements.txt -uvicorn server.app:app --host 0.0.0.0 --port 8000 -``` - -手机 Termux 端: - -```bash -python mobile_agent/wireless_agent.py --server-ws ws://服务器IP:8000/ws/phone --hook hooks/wechat_hook_bridge.js -``` - -验证端: - -```bash -python scripts/verify_wechat_frida.py --base-url http://服务器IP:8000 --device-id phone-xxxx -``` - -如果已有完整 `wechat_hook_v3.js`,请替换 `hooks/wechat_hook_bridge.js`,SDK 会自动兼容 camelCase / snake_case / lowercase 方法名。 -''' - -for rel, content in files.items(): - path = ROOT / rel - path.parent.mkdir(parents=True, exist_ok=True) - path.write_text(content, encoding='utf-8') -print(f'generated {len(files)} files under {ROOT}') diff --git a/工作手机SDK补全包_微信Frida/hooks/wechat_hook_bridge.js b/工作手机SDK补全包_微信Frida/hooks/wechat_hook_bridge.js deleted file mode 100644 index d69423fa67..0000000000 --- a/工作手机SDK补全包_微信Frida/hooks/wechat_hook_bridge.js +++ /dev/null @@ -1,45 +0,0 @@ -// 工作手机 SDK · 微信 Hook 桥接模板 -// 说明:这是安全桥接版,优先提供连通性、截图、UI 导航等通用方法。 -// 如果已有完整 wechat_hook_v3.js,可直接替换本文件;Python SDK 会兼容 camelCase/snake_case 调用。 -'use strict'; - -function ok(data) { return { ok: true, data: data || null, ts: Date.now() }; } -function fail(message) { return { ok: false, error: String(message), ts: Date.now() }; } - -function runJava(fn) { - let result; - Java.perform(function () { - try { result = fn(); } catch (e) { result = fail(e.stack || e.message || e); } - }); - return result; -} - -rpc.exports = { - ping: function () { return 'pong from wechat_hook_bridge'; }, - getConnectionStatus: function () { - return ok({ java_available: Java.available, process: Process.id, arch: Process.arch, platform: Process.platform }); - }, - takeScreenshot: function () { - // 截图建议在手机端通过 uiautomator/screencap 实现;Hook 层返回占位,Agent 可扩展真实文件路径。 - return ok({ mode: 'placeholder', message: '请在 mobile_agent 扩展 screencap -p 后回传文件路径' }); - }, - dumpUiTree: function () { return ok({ mode: 'placeholder', message: '建议通过 uiautomator dump 获取 UI XML' }); }, - goHome: function () { return ok({ action: 'goHome', message: '桥接模板未执行 UI 点击;请替换完整 Hook 后验证' }); }, - openContacts: function () { return ok({ action: 'openContacts', message: 'NOT_IMPLEMENTED_IN_TEMPLATE' }); }, - openChats: function () { return ok({ action: 'openChats', message: 'NOT_IMPLEMENTED_IN_TEMPLATE' }); }, - searchContact: function (keyword) { return ok({ keyword: keyword, message: 'NOT_IMPLEMENTED_IN_TEMPLATE' }); }, - getContacts: function () { return ok({ contacts: [], message: 'NOT_IMPLEMENTED_IN_TEMPLATE' }); }, - getContactProfile: function (wxid) { return ok({ wxid: wxid, message: 'NOT_IMPLEMENTED_IN_TEMPLATE' }); }, - addFriend: function (keyword) { return fail('危险动作未在模板中实现:addFriend ' + keyword); }, - acceptFriend: function (wxid) { return fail('危险动作未在模板中实现:acceptFriend ' + wxid); }, - sendTextMessage: function (to, text) { return fail('危险动作未在模板中实现:sendTextMessage ' + to); }, - sendImageMessage: function (to, path) { return fail('危险动作未在模板中实现:sendImageMessage ' + to); }, - getMessages: function (wxid) { return ok({ wxid: wxid, messages: [], message: 'NOT_IMPLEMENTED_IN_TEMPLATE' }); }, - openChat: function (wxid) { return ok({ wxid: wxid, message: 'NOT_IMPLEMENTED_IN_TEMPLATE' }); }, - createGroup: function (members) { return fail('危险动作未在模板中实现:createGroup'); }, - inviteGroupMember: function (chatroom, members) { return fail('危险动作未在模板中实现:inviteGroupMember'); }, - getGroupMembers: function (chatroom) { return ok({ chatroom: chatroom, members: [], message: 'NOT_IMPLEMENTED_IN_TEMPLATE' }); }, - browseChannels: function () { return ok({ message: 'NOT_IMPLEMENTED_IN_TEMPLATE' }); }, - addFavorite: function () { return fail('危险动作未在模板中实现:addFavorite'); }, - addCustomEmoji: function () { return fail('危险动作未在模板中实现:addCustomEmoji'); } -}; diff --git a/工作手机SDK补全包_微信Frida/mobile_agent/__init__.py b/工作手机SDK补全包_微信Frida/mobile_agent/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/工作手机SDK补全包_微信Frida/mobile_agent/wireless_agent.py b/工作手机SDK补全包_微信Frida/mobile_agent/wireless_agent.py deleted file mode 100644 index 594bd01f44..0000000000 --- a/工作手机SDK补全包_微信Frida/mobile_agent/wireless_agent.py +++ /dev/null @@ -1,65 +0,0 @@ -"""手机端 Termux/Python Agent:通过 WebSocket 接收服务器动作,调用 Frida Hook。""" -from __future__ import annotations -import argparse -import asyncio -import json -import platform -import uuid -from pathlib import Path - -from sdk.frida.frida_manager import FridaConfig, FridaManager -from sdk.frida.hook_executor import HookExecutor - -try: - import websockets -except Exception: # pragma: no cover - websockets = None - -class WirelessAgent: - def __init__(self, server_ws: str, hook_path: str, device_id: str | None = None): - self.server_ws = server_ws - self.hook_path = hook_path - self.device_id = device_id or f"phone-{uuid.getnode():x}" - self.manager = FridaManager(FridaConfig(hook_path=hook_path)) - self.executor = HookExecutor(self.manager) - - async def boot(self): - self.manager.connect() - self.manager.attach() - self.manager.load_script(self.hook_path) - - def hello(self): - return { - 'type': 'hello', - 'device_id': self.device_id, - 'platform': platform.platform(), - 'actions': self.executor.catalog(), - } - - async def run(self): - if websockets is None: - raise RuntimeError('请先安装 websockets:pip install websockets') - await self.boot() - async with websockets.connect(self.server_ws, ping_interval=20, ping_timeout=20) as ws: - await ws.send(json.dumps(self.hello(), ensure_ascii=False)) - async for raw in ws: - try: - msg = json.loads(raw) - if msg.get('type') != 'command': - continue - result = self.executor.execute(msg['action'], msg.get('payload') or {}) - await ws.send(json.dumps({'type': 'result', 'request_id': msg.get('request_id'), 'device_id': self.device_id, 'result': result}, ensure_ascii=False)) - except Exception as exc: - await ws.send(json.dumps({'type': 'error', 'error': str(exc)}, ensure_ascii=False)) - - -def main(): - parser = argparse.ArgumentParser() - parser.add_argument('--server-ws', required=True, help='例如 ws://192.168.1.10:8000/ws/phone') - parser.add_argument('--hook', required=True, help='wechat_hook_bridge.js 或真实 wechat_hook_v3.js 路径') - parser.add_argument('--device-id') - args = parser.parse_args() - asyncio.run(WirelessAgent(args.server_ws, args.hook, args.device_id).run()) - -if __name__ == '__main__': - main() diff --git a/工作手机SDK补全包_微信Frida/requirements.txt b/工作手机SDK补全包_微信Frida/requirements.txt deleted file mode 100644 index b52d28ca18..0000000000 --- a/工作手机SDK补全包_微信Frida/requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ -fastapi -uvicorn -requests -websockets -frida - diff --git a/工作手机SDK补全包_微信Frida/sdk/__init__.py b/工作手机SDK补全包_微信Frida/sdk/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/工作手机SDK补全包_微信Frida/sdk/frida/__init__.py b/工作手机SDK补全包_微信Frida/sdk/frida/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/工作手机SDK补全包_微信Frida/sdk/frida/frida_manager.py b/工作手机SDK补全包_微信Frida/sdk/frida/frida_manager.py deleted file mode 100644 index 9ec11f589a..0000000000 --- a/工作手机SDK补全包_微信Frida/sdk/frida/frida_manager.py +++ /dev/null @@ -1,166 +0,0 @@ -""" -工作手机 SDK · FridaManager - -负责手机端或服务器端连接 Frida、attach 微信进程、加载 Hook 脚本,并提供兼容 RPC 调用。 -设计重点:历史归档显示 Frida Python 对 rpc.exports 方法名存在混淆,因此这里采用多候选名兼容策略。 -""" -from __future__ import annotations - -from dataclasses import dataclass, field -from pathlib import Path -import re -import time -from typing import Any, Callable, Dict, Iterable, Optional - - -class FridaUnavailable(RuntimeError): - """当前环境未安装或无法连接 Frida。""" - - -class RpcMethodMissing(RuntimeError): - """Hook 中缺少指定 RPC 方法。""" - - -@dataclass -class FridaConfig: - device_host: str = "127.0.0.1" - device_port: int = 27042 - package_name: str = "com.tencent.mm" - process_name: str = "WeChat" - attach_timeout: float = 15.0 - prefer_usb: bool = False - hook_path: Optional[str] = None - - -@dataclass -class RpcResult: - ok: bool - action: str - method: str - data: Any = None - error: Optional[str] = None - elapsed_ms: int = 0 - - -class FridaManager: - def __init__(self, config: Optional[FridaConfig] = None): - self.config = config or FridaConfig() - self.frida = None - self.device = None - self.session = None - self.script = None - self.exports = None - self.loaded_hook_path: Optional[str] = None - - def _import_frida(self): - if self.frida is not None: - return self.frida - try: - import frida # type: ignore - except Exception as exc: - raise FridaUnavailable(f"未安装 frida Python 包或加载失败:{exc}") from exc - self.frida = frida - return frida - - def connect(self): - frida = self._import_frida() - if self.config.prefer_usb: - self.device = frida.get_usb_device(timeout=int(self.config.attach_timeout)) - else: - self.device = frida.get_device_manager().add_remote_device( - f"{self.config.device_host}:{self.config.device_port}" - ) - return self.device - - def attach(self, target: Optional[str] = None): - if self.device is None: - self.connect() - assert self.device is not None - target = target or self.config.package_name - last_error: Optional[Exception] = None - candidates: Iterable[Any] = [target, self.config.process_name] - for candidate in candidates: - try: - self.session = self.device.attach(candidate) - return self.session - except Exception as exc: # pragma: no cover - depends on phone - last_error = exc - try: - processes = self.device.enumerate_processes() - for proc in processes: - name = getattr(proc, 'name', '') or '' - pid = getattr(proc, 'pid', None) - if pid and (self.config.package_name in name or self.config.process_name.lower() in name.lower() or '微信' in name): - self.session = self.device.attach(pid) - return self.session - except Exception as exc: # pragma: no cover - last_error = exc - raise FridaUnavailable(f"无法 attach 微信进程:{last_error}") - - def load_script(self, hook_path: Optional[str] = None): - path = Path(hook_path or self.config.hook_path or '') - if not path.exists(): - raise FileNotFoundError(f"Hook 脚本不存在:{path}") - if self.session is None: - self.attach() - assert self.session is not None - source = path.read_text(encoding='utf-8') - self.script = self.session.create_script(source) - self.script.on('message', self._on_message) - self.script.load() - self.exports = getattr(self.script, 'exports_sync', None) or getattr(self.script, 'exports', None) - self.loaded_hook_path = str(path) - return self.script - - def _on_message(self, message, data): # pragma: no cover - runtime callback - # 生产环境可转发到 EventReporter;这里保持最小日志。 - print({'frida_message': message, 'data_len': len(data) if data else 0}) - - @staticmethod - def _snake(name: str) -> str: - return re.sub(r'(? RpcResult: - started = time.time() - if self.exports is None: - raise FridaUnavailable('Hook 脚本尚未加载,无法调用 RPC') - last_error = None - for candidate in self.method_candidates(method): - try: - fn = getattr(self.exports, candidate) - data = fn(*args, **kwargs) - return RpcResult(True, method, candidate, data=data, elapsed_ms=int((time.time() - started) * 1000)) - except AttributeError as exc: - last_error = exc - except Exception as exc: - return RpcResult(False, method, candidate, error=str(exc), elapsed_ms=int((time.time() - started) * 1000)) - # 兼容新版 frida 的 exports_sync.invoke 或 script.exports_sync.invoke - try: - invoke = getattr(self.exports, 'invoke') - data = invoke(method, list(args), kwargs or None) - return RpcResult(True, method, 'invoke', data=data, elapsed_ms=int((time.time() - started) * 1000)) - except Exception as exc: - last_error = exc - return RpcResult(False, method, '', error=f'RPC 方法不存在或不可调用:{method}; last={last_error}', elapsed_ms=int((time.time() - started) * 1000)) - - def cleanup(self): - for obj, method in [(self.script, 'unload'), (self.session, 'detach')]: - if obj is not None: - try: - getattr(obj, method)() - except Exception: - pass - self.script = None - self.session = None - self.exports = None diff --git a/工作手机SDK补全包_微信Frida/sdk/frida/hook_executor.py b/工作手机SDK补全包_微信Frida/sdk/frida/hook_executor.py deleted file mode 100644 index da046d0d35..0000000000 --- a/工作手机SDK补全包_微信Frida/sdk/frida/hook_executor.py +++ /dev/null @@ -1,37 +0,0 @@ -"""将业务 action 映射到 Frida RPC。""" -from __future__ import annotations -from dataclasses import asdict -from typing import Any, Dict, Optional - -from sdk.frida.frida_manager import FridaManager, RpcResult -from sdk.wechat.wechat_actions import validate_action, list_actions - -class HookExecutor: - def __init__(self, manager: FridaManager): - self.manager = manager - - def execute(self, action: str, payload: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: - payload = payload or {} - try: - spec = validate_action(action, payload) - except Exception as exc: - return {'ok': False, 'action': action, 'error': str(exc), 'stage': 'validate'} - args = [payload[k] for k in spec.required] - # 允许额外参数透传给 Hook;Hook 若不支持会返回错误,便于报告定位。 - extra = {k: v for k, v in payload.items() if k not in spec.required} - result: RpcResult = self.manager.call(spec.rpc, *args, **extra) - return { - 'ok': result.ok, - 'action': action, - 'rpc': spec.rpc, - 'method_used': result.method, - 'module': spec.module, - 'description': spec.description, - 'safe': spec.safe, - 'data': result.data, - 'error': result.error, - 'elapsed_ms': result.elapsed_ms, - } - - def catalog(self): - return list_actions() diff --git a/工作手机SDK补全包_微信Frida/sdk/wechat/__init__.py b/工作手机SDK补全包_微信Frida/sdk/wechat/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/工作手机SDK补全包_微信Frida/sdk/wechat/wechat_actions.py b/工作手机SDK补全包_微信Frida/sdk/wechat/wechat_actions.py deleted file mode 100644 index c405fbb39a..0000000000 --- a/工作手机SDK补全包_微信Frida/sdk/wechat/wechat_actions.py +++ /dev/null @@ -1,54 +0,0 @@ -"""微信动作目录:服务器、手机端 Agent、验证脚本共同使用。""" -from __future__ import annotations -from dataclasses import dataclass, field -from typing import Any, Dict, List - -@dataclass(frozen=True) -class WechatAction: - action: str - rpc: str - module: str - description: str - required: List[str] = field(default_factory=list) - safe: bool = True - verify: bool = True - -ACTIONS: List[WechatAction] = [ - WechatAction('ping', 'ping', 'system', 'Hook 连通性检测'), - WechatAction('get_connection_status', 'getConnectionStatus', 'system', '读取连接状态'), - WechatAction('screenshot', 'takeScreenshot', 'device', '手机当前屏幕截图'), - WechatAction('ui_dump', 'dumpUiTree', 'device', '导出当前 UI 树'), - WechatAction('go_home', 'goHome', 'navigation', '返回微信首页'), - WechatAction('open_contacts', 'openContacts', 'navigation', '打开通讯录'), - WechatAction('open_chats', 'openChats', 'navigation', '打开聊天列表'), - WechatAction('search_contact', 'searchContact', 'contact', '搜索联系人', ['keyword']), - WechatAction('get_contacts', 'getContacts', 'contact', '读取联系人列表'), - WechatAction('get_contact_profile', 'getContactProfile', 'contact', '读取联系人资料', ['wxid']), - WechatAction('add_friend', 'addFriend', 'contact', '添加好友', ['keyword'], safe=False), - WechatAction('accept_friend', 'acceptFriend', 'contact', '通过好友申请', ['wxid'], safe=False), - WechatAction('send_text', 'sendTextMessage', 'message', '发送文本消息', ['to', 'text'], safe=False), - WechatAction('send_image', 'sendImageMessage', 'message', '发送图片消息', ['to', 'path'], safe=False), - WechatAction('get_messages', 'getMessages', 'message', '读取消息列表', ['wxid']), - WechatAction('open_chat', 'openChat', 'message', '打开指定会话', ['wxid']), - WechatAction('create_group', 'createGroup', 'group', '创建群聊', ['members'], safe=False), - WechatAction('invite_group_member', 'inviteGroupMember', 'group', '邀请群成员', ['chatroom', 'members'], safe=False), - WechatAction('get_group_members', 'getGroupMembers', 'group', '读取群成员', ['chatroom']), - WechatAction('browse_channels', 'browseChannels', 'channels', '浏览视频号'), - WechatAction('add_favorite', 'addFavorite', 'favorite', '收藏当前内容'), - WechatAction('add_custom_emoji', 'addCustomEmoji', 'emoji', '添加自定义表情', safe=False), -] - -ACTION_MAP: Dict[str, WechatAction] = {item.action: item for item in ACTIONS} -RPC_MAP: Dict[str, WechatAction] = {item.rpc: item for item in ACTIONS} - -def list_actions() -> List[Dict[str, Any]]: - return [item.__dict__.copy() for item in ACTIONS] - -def validate_action(action: str, payload: Dict[str, Any]) -> WechatAction: - if action not in ACTION_MAP: - raise KeyError(f'未知微信动作:{action}') - spec = ACTION_MAP[action] - missing = [k for k in spec.required if k not in payload or payload[k] in (None, '')] - if missing: - raise ValueError(f'动作 {action} 缺少参数:{missing}') - return spec diff --git a/工作手机SDK补全包_微信Frida/server/__init__.py b/工作手机SDK补全包_微信Frida/server/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/工作手机SDK补全包_微信Frida/server/app.py b/工作手机SDK补全包_微信Frida/server/app.py deleted file mode 100644 index 4c7de97ef9..0000000000 --- a/工作手机SDK补全包_微信Frida/server/app.py +++ /dev/null @@ -1,13 +0,0 @@ -from fastapi import FastAPI, WebSocket -from server.routes.wechat_frida import router as wechat_frida_router, phone_socket - -app = FastAPI(title='工作手机 SDK · 微信 Frida 控制服务', version='0.3.0') -app.include_router(wechat_frida_router) - -@app.get('/health') -def health(): - return {'ok': True, 'service': 'work-phone-sdk'} - -@app.websocket('/ws/phone') -async def ws_phone(websocket: WebSocket): - await phone_socket(websocket) diff --git a/工作手机SDK补全包_微信Frida/server/routes/__init__.py b/工作手机SDK补全包_微信Frida/server/routes/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/工作手机SDK补全包_微信Frida/server/routes/wechat_frida.py b/工作手机SDK补全包_微信Frida/server/routes/wechat_frida.py deleted file mode 100644 index 2f7b7d14f5..0000000000 --- a/工作手机SDK补全包_微信Frida/server/routes/wechat_frida.py +++ /dev/null @@ -1,76 +0,0 @@ -"""FastAPI 微信 Frida 无线控制路由。""" -from __future__ import annotations -import asyncio -import json -import uuid -from dataclasses import dataclass, field -from typing import Any, Dict, Optional - -from fastapi import APIRouter, WebSocket, WebSocketDisconnect, HTTPException -from pydantic import BaseModel - -from sdk.wechat.wechat_actions import list_actions - -router = APIRouter(prefix='/api/v3/wechat-frida', tags=['wechat-frida']) - -@dataclass -class PhoneConn: - device_id: str - ws: WebSocket - meta: Dict[str, Any] = field(default_factory=dict) - pending: Dict[str, asyncio.Future] = field(default_factory=dict) - -phones: Dict[str, PhoneConn] = {} - -class ActionRequest(BaseModel): - device_id: str - action: str - payload: Dict[str, Any] = {} - timeout: float = 30.0 - -@router.get('/actions') -def actions(): - return {'ok': True, 'actions': list_actions()} - -@router.get('/devices') -def devices(): - return {'ok': True, 'devices': [{'device_id': k, 'meta': v.meta} for k, v in phones.items()]} - -@router.post('/execute') -async def execute(req: ActionRequest): - if req.device_id not in phones: - raise HTTPException(404, f'设备未连接:{req.device_id}') - conn = phones[req.device_id] - request_id = uuid.uuid4().hex - fut = asyncio.get_event_loop().create_future() - conn.pending[request_id] = fut - await conn.ws.send_text(json.dumps({'type': 'command', 'request_id': request_id, 'action': req.action, 'payload': req.payload}, ensure_ascii=False)) - try: - return await asyncio.wait_for(fut, timeout=req.timeout) - finally: - conn.pending.pop(request_id, None) - -async def phone_socket(websocket: WebSocket): - await websocket.accept() - device_id: Optional[str] = None - try: - async for raw in websocket.iter_text(): - msg = json.loads(raw) - if msg.get('type') == 'hello': - device_id = msg.get('device_id') or uuid.uuid4().hex - phones[device_id] = PhoneConn(device_id=device_id, ws=websocket, meta=msg) - await websocket.send_text(json.dumps({'type': 'hello_ack', 'device_id': device_id}, ensure_ascii=False)) - elif msg.get('type') == 'result': - rid = msg.get('request_id') - did = msg.get('device_id') or device_id - conn = phones.get(did or '') - if conn and rid in conn.pending and not conn.pending[rid].done(): - conn.pending[rid].set_result({'ok': True, 'device_id': did, 'result': msg.get('result')}) - elif msg.get('type') == 'error': - # 保留连接,错误由客户端下一次请求再显式返回。 - pass - except WebSocketDisconnect: - pass - finally: - if device_id and device_id in phones: - phones.pop(device_id, None) diff --git a/工作手机SDK补全包_微信Frida/tests_api_routes.py b/工作手机SDK补全包_微信Frida/tests_api_routes.py deleted file mode 100644 index c0f589bd84..0000000000 --- a/工作手机SDK补全包_微信Frida/tests_api_routes.py +++ /dev/null @@ -1,23 +0,0 @@ -from fastapi.testclient import TestClient -from server.app import app - -client = TestClient(app) - -r = client.get('/health') -assert r.status_code == 200, r.text -assert r.json()['ok'] is True - -r = client.get('/api/v3/wechat-frida/actions') -assert r.status_code == 200, r.text -body = r.json() -assert body['ok'] is True -assert len(body['actions']) >= 20 -assert any(x['action'] == 'send_text' and x['rpc'] == 'sendTextMessage' for x in body['actions']) - -r = client.get('/api/v3/wechat-frida/devices') -assert r.status_code == 200, r.text -assert r.json()['devices'] == [] - -r = client.post('/api/v3/wechat-frida/execute', json={'device_id':'missing','action':'ping','payload':{}}) -assert r.status_code == 404, r.text -print('api_route_tests_passed') diff --git a/工作手机SDK补全包_微信Frida/tests_mock_rpc.py b/工作手机SDK补全包_微信Frida/tests_mock_rpc.py deleted file mode 100644 index c69d6c662b..0000000000 --- a/工作手机SDK补全包_微信Frida/tests_mock_rpc.py +++ /dev/null @@ -1,21 +0,0 @@ -from sdk.frida.frida_manager import FridaManager -from sdk.frida.hook_executor import HookExecutor - -class Exports: - def ping(self): - return 'pong' - def getConnectionStatus(self): - return {'ok': True} - def sendTextMessage(self, to, text): - return {'to': to, 'text': text} - -m = FridaManager() -m.exports = Exports() -assert m.call('ping').ok -assert m.call('getConnectionStatus').ok -assert m.call('sendTextMessage', 'filehelper', 'hi').ok -ex = HookExecutor(m) -assert ex.execute('ping')['ok'] is True -assert ex.execute('send_text', {'to':'filehelper','text':'hi'})['ok'] is True -assert ex.execute('send_text', {'to':'filehelper'})['ok'] is False -print('mock_rpc_tests_passed') diff --git a/工作手机SDK补全包_微信Frida/开发文档/10、项目管理/项目落地执行表.md b/工作手机SDK补全包_微信Frida/开发文档/10、项目管理/项目落地执行表.md deleted file mode 100644 index 5ecf47c0fb..0000000000 --- a/工作手机SDK补全包_微信Frida/开发文档/10、项目管理/项目落地执行表.md +++ /dev/null @@ -1,6 +0,0 @@ -# 项目落地执行表(补全包归档) - -> **已并入**:[开发文档/9、手册/工作手机·五图总览与使用手册.md](../../../开发文档/9、手册/工作手机·五图总览与使用手册.md) §1.2 -> **进度真源**:[开发文档/10、项目管理/开发进度总表.md](../../../开发文档/10、项目管理/开发进度总表.md)(99.5%) - -2026-05-18 补全包阶段约 55% 的记录已归档;主线进度以总表为准。 diff --git a/工作手机SDK补全包_微信Frida/开发文档/2、架构/系统架构.md b/工作手机SDK补全包_微信Frida/开发文档/2、架构/系统架构.md deleted file mode 100644 index 1ae0bb614c..0000000000 --- a/工作手机SDK补全包_微信Frida/开发文档/2、架构/系统架构.md +++ /dev/null @@ -1,6 +0,0 @@ -# 系统架构(补全包归档) - -> **已并入**:[开发文档/9、手册/工作手机·五图总览与使用手册.md](../../../开发文档/9、手册/工作手机·五图总览与使用手册.md) §一、§二 -> 完整架构见 [开发文档/2、架构/01-总览/系统架构.md](../../../开发文档/2、架构/01-总览/系统架构.md) - -本文件为补全包内副本,勿再扩写。 diff --git a/工作手机SDK补全包_微信Frida/开发文档/5、接口/微信Frida_API契约.md b/工作手机SDK补全包_微信Frida/开发文档/5、接口/微信Frida_API契约.md deleted file mode 100644 index 7a37326857..0000000000 --- a/工作手机SDK补全包_微信Frida/开发文档/5、接口/微信Frida_API契约.md +++ /dev/null @@ -1,71 +0,0 @@ -# 微信 Frida API 契约 - -本文由阿桥维护,描述服务器与手机端 Agent、外部控制端之间的接口约定。 - -## 一、HTTP 接口 - -| 方法 | 路径 | 说明 | 请求 | 响应 | -|---|---|---|---|---| -| GET | `/health` | 服务健康检查 | 无 | `{ ok, service }` | -| GET | `/api/v3/wechat-frida/actions` | 获取可执行微信动作目录 | 无 | `{ ok, actions }` | -| GET | `/api/v3/wechat-frida/devices` | 获取已连接手机 Agent | 无 | `{ ok, devices }` | -| POST | `/api/v3/wechat-frida/execute` | 下发微信控制动作 | `{ device_id, action, payload, timeout }` | `{ ok, device_id, result }` | - -## 二、WebSocket 接口 - -手机端连接: - -```text -ws://服务器IP:8000/ws/phone -``` - -### 2.1 Agent 注册 - -```json -{ - "type": "hello", - "device_id": "phone-xxxx", - "platform": "Android/Termux", - "actions": [] -} -``` - -### 2.2 服务器下发命令 - -```json -{ - "type": "command", - "request_id": "uuid", - "action": "send_text", - "payload": { - "to": "filehelper", - "text": "工作手机SDK自动验证" - } -} -``` - -### 2.3 Agent 返回结果 - -```json -{ - "type": "result", - "request_id": "uuid", - "device_id": "phone-xxxx", - "result": { - "ok": true, - "action": "send_text", - "rpc": "sendTextMessage", - "method_used": "sendTextMessage", - "data": {}, - "elapsed_ms": 123 - } -} -``` - -## 三、动作目录 - -动作目录集中维护在 `/home/ubuntu/work_phone_sdk_completion/sdk/wechat/wechat_actions.py`。当前首批覆盖系统检测、截图、UI、通讯录、好友、消息、群聊、视频号、收藏、表情等模块。危险动作默认标记为 `safe=false`,自动验证脚本默认跳过,避免误发消息或误加好友。 - -## 四、RPC 方法名兼容 - -SDK 调用顺序为:原始 `camelCase` → `snake_case` → 全小写 → `invoke()`。这用于解决归档中反复出现的 `getMessages/getmessages/get_messages` 调用差异问题。 diff --git a/工作手机SDK补全包_微信Frida/开发文档/5、接口/微信Frida控制接口契约.md b/工作手机SDK补全包_微信Frida/开发文档/5、接口/微信Frida控制接口契约.md deleted file mode 100644 index 96ce690bae..0000000000 --- a/工作手机SDK补全包_微信Frida/开发文档/5、接口/微信Frida控制接口契约.md +++ /dev/null @@ -1,5 +0,0 @@ -# 微信 Frida 控制接口契约(补全包归档) - -> **已并入**:[开发文档/9、手册/工作手机·五图总览与使用手册.md](../../../开发文档/9、手册/工作手机·五图总览与使用手册.md) §三、§七 - -统一 API:`POST /api/v3/hook/execute` · 代码:`sdk/app/routers/unified.py` diff --git a/工作手机SDK补全包_微信Frida/开发文档/8、部署/微信Frida无线部署与验证说明.md b/工作手机SDK补全包_微信Frida/开发文档/8、部署/微信Frida无线部署与验证说明.md deleted file mode 100644 index ac4600e866..0000000000 --- a/工作手机SDK补全包_微信Frida/开发文档/8、部署/微信Frida无线部署与验证说明.md +++ /dev/null @@ -1,5 +0,0 @@ -# 微信 Frida 无线部署与验证说明(补全包归档) - -> **已并入**:[开发文档/9、手册/工作手机·五图总览与使用手册.md](../../../开发文档/9、手册/工作手机·五图总览与使用手册.md) §五、§八 - -验证脚本主线版:`tools/wireless_frida_wechat_batch_verify.py` diff --git a/补全包/README.md b/补全包/README.md deleted file mode 100644 index f75933de78..0000000000 --- a/补全包/README.md +++ /dev/null @@ -1,12 +0,0 @@ -# 补全包目录 - -> **文档已并入开发文档** — 请勿在此维护正文。 -> **唯一主手册**:[开发文档/9、手册/工作手机·五图总览与使用手册.md](../开发文档/9、手册/工作手机·五图总览与使用手册.md)(v2.0 补全包并入版) - -## 子目录 - -| 目录 | 说明 | -|------|------| -| [微信Frida_SDK_20260518/](微信Frida_SDK_20260518/) | 2026-05-18 归档原型代码(只读对照) | - -日常开发请使用主线 **`sdk/`**,部署与 API 见主手册 §六~§八。 diff --git a/补全包/微信Frida_SDK_20260518/README.md b/补全包/微信Frida_SDK_20260518/README.md deleted file mode 100644 index 360644f6fc..0000000000 --- a/补全包/微信Frida_SDK_20260518/README.md +++ /dev/null @@ -1,29 +0,0 @@ -# 工作手机 SDK · 微信 Frida 无线控制补全包 - -> **⚠️ 归档原型 · 文档已并入主线** -> **请读**:[开发文档/9、手册/工作手机·五图总览与使用手册.md](../开发文档/9、手册/工作手机·五图总览与使用手册.md)(v2.0) -> **请用**:主线 `sdk/`(非本目录 `server/`、`mobile_agent/`) - -本目录保留 2026-05-18 中断 ZIP 的最小闭环源码,供 diff 对照。功能已吸收进 `sdk/agent/hook/`、`sdk/app/routers/unified.py` 等。 - -## 快速启动(历史命令,仅供对照) - -
-展开旧版启动命令 - -服务器端: - -```bash -pip install -r requirements.txt -uvicorn server.app:app --host 0.0.0.0 --port 8000 -``` - -手机 Termux 端: - -```bash -python mobile_agent/wireless_agent.py --server-ws ws://服务器IP:8000/ws/phone --hook hooks/wechat_hook_bridge.js -``` - -
- -**现行启动**见主手册 §六、§八(端口 **8899**,API **`/api/v3/hook/execute`**)。 diff --git a/补全包/微信Frida_SDK_20260518/generate_impl.py b/补全包/微信Frida_SDK_20260518/generate_impl.py deleted file mode 100644 index 63f3879cfb..0000000000 --- a/补全包/微信Frida_SDK_20260518/generate_impl.py +++ /dev/null @@ -1,578 +0,0 @@ -from pathlib import Path - -ROOT = Path('/home/ubuntu/work_phone_sdk_completion') -files = {} - -files['sdk/frida/frida_manager.py'] = r'''""" -工作手机 SDK · FridaManager - -负责手机端或服务器端连接 Frida、attach 微信进程、加载 Hook 脚本,并提供兼容 RPC 调用。 -设计重点:历史归档显示 Frida Python 对 rpc.exports 方法名存在混淆,因此这里采用多候选名兼容策略。 -""" -from __future__ import annotations - -from dataclasses import dataclass, field -from pathlib import Path -import re -import time -from typing import Any, Callable, Dict, Iterable, Optional - - -class FridaUnavailable(RuntimeError): - """当前环境未安装或无法连接 Frida。""" - - -class RpcMethodMissing(RuntimeError): - """Hook 中缺少指定 RPC 方法。""" - - -@dataclass -class FridaConfig: - device_host: str = "127.0.0.1" - device_port: int = 27042 - package_name: str = "com.tencent.mm" - process_name: str = "WeChat" - attach_timeout: float = 15.0 - prefer_usb: bool = False - hook_path: Optional[str] = None - - -@dataclass -class RpcResult: - ok: bool - action: str - method: str - data: Any = None - error: Optional[str] = None - elapsed_ms: int = 0 - - -class FridaManager: - def __init__(self, config: Optional[FridaConfig] = None): - self.config = config or FridaConfig() - self.frida = None - self.device = None - self.session = None - self.script = None - self.exports = None - self.loaded_hook_path: Optional[str] = None - - def _import_frida(self): - if self.frida is not None: - return self.frida - try: - import frida # type: ignore - except Exception as exc: - raise FridaUnavailable(f"未安装 frida Python 包或加载失败:{exc}") from exc - self.frida = frida - return frida - - def connect(self): - frida = self._import_frida() - if self.config.prefer_usb: - self.device = frida.get_usb_device(timeout=int(self.config.attach_timeout)) - else: - self.device = frida.get_device_manager().add_remote_device( - f"{self.config.device_host}:{self.config.device_port}" - ) - return self.device - - def attach(self, target: Optional[str] = None): - if self.device is None: - self.connect() - assert self.device is not None - target = target or self.config.package_name - last_error: Optional[Exception] = None - candidates: Iterable[Any] = [target, self.config.process_name] - for candidate in candidates: - try: - self.session = self.device.attach(candidate) - return self.session - except Exception as exc: # pragma: no cover - depends on phone - last_error = exc - try: - processes = self.device.enumerate_processes() - for proc in processes: - name = getattr(proc, 'name', '') or '' - pid = getattr(proc, 'pid', None) - if pid and (self.config.package_name in name or self.config.process_name.lower() in name.lower() or '微信' in name): - self.session = self.device.attach(pid) - return self.session - except Exception as exc: # pragma: no cover - last_error = exc - raise FridaUnavailable(f"无法 attach 微信进程:{last_error}") - - def load_script(self, hook_path: Optional[str] = None): - path = Path(hook_path or self.config.hook_path or '') - if not path.exists(): - raise FileNotFoundError(f"Hook 脚本不存在:{path}") - if self.session is None: - self.attach() - assert self.session is not None - source = path.read_text(encoding='utf-8') - self.script = self.session.create_script(source) - self.script.on('message', self._on_message) - self.script.load() - self.exports = getattr(self.script, 'exports_sync', None) or getattr(self.script, 'exports', None) - self.loaded_hook_path = str(path) - return self.script - - def _on_message(self, message, data): # pragma: no cover - runtime callback - # 生产环境可转发到 EventReporter;这里保持最小日志。 - print({'frida_message': message, 'data_len': len(data) if data else 0}) - - @staticmethod - def _snake(name: str) -> str: - return re.sub(r'(? RpcResult: - started = time.time() - if self.exports is None: - raise FridaUnavailable('Hook 脚本尚未加载,无法调用 RPC') - last_error = None - for candidate in self.method_candidates(method): - try: - fn = getattr(self.exports, candidate) - data = fn(*args, **kwargs) - return RpcResult(True, method, candidate, data=data, elapsed_ms=int((time.time() - started) * 1000)) - except AttributeError as exc: - last_error = exc - except Exception as exc: - return RpcResult(False, method, candidate, error=str(exc), elapsed_ms=int((time.time() - started) * 1000)) - # 兼容新版 frida 的 exports_sync.invoke 或 script.exports_sync.invoke - try: - invoke = getattr(self.exports, 'invoke') - data = invoke(method, list(args), kwargs or None) - return RpcResult(True, method, 'invoke', data=data, elapsed_ms=int((time.time() - started) * 1000)) - except Exception as exc: - last_error = exc - return RpcResult(False, method, '', error=f'RPC 方法不存在或不可调用:{method}; last={last_error}', elapsed_ms=int((time.time() - started) * 1000)) - - def cleanup(self): - for obj, method in [(self.script, 'unload'), (self.session, 'detach')]: - if obj is not None: - try: - getattr(obj, method)() - except Exception: - pass - self.script = None - self.session = None - self.exports = None -''' - -files['sdk/wechat/wechat_actions.py'] = r'''"""微信动作目录:服务器、手机端 Agent、验证脚本共同使用。""" -from __future__ import annotations -from dataclasses import dataclass, field -from typing import Any, Dict, List - -@dataclass(frozen=True) -class WechatAction: - action: str - rpc: str - module: str - description: str - required: List[str] = field(default_factory=list) - safe: bool = True - verify: bool = True - -ACTIONS: List[WechatAction] = [ - WechatAction('ping', 'ping', 'system', 'Hook 连通性检测'), - WechatAction('get_connection_status', 'getConnectionStatus', 'system', '读取连接状态'), - WechatAction('screenshot', 'takeScreenshot', 'device', '手机当前屏幕截图'), - WechatAction('ui_dump', 'dumpUiTree', 'device', '导出当前 UI 树'), - WechatAction('go_home', 'goHome', 'navigation', '返回微信首页'), - WechatAction('open_contacts', 'openContacts', 'navigation', '打开通讯录'), - WechatAction('open_chats', 'openChats', 'navigation', '打开聊天列表'), - WechatAction('search_contact', 'searchContact', 'contact', '搜索联系人', ['keyword']), - WechatAction('get_contacts', 'getContacts', 'contact', '读取联系人列表'), - WechatAction('get_contact_profile', 'getContactProfile', 'contact', '读取联系人资料', ['wxid']), - WechatAction('add_friend', 'addFriend', 'contact', '添加好友', ['keyword'], safe=False), - WechatAction('accept_friend', 'acceptFriend', 'contact', '通过好友申请', ['wxid'], safe=False), - WechatAction('send_text', 'sendTextMessage', 'message', '发送文本消息', ['to', 'text'], safe=False), - WechatAction('send_image', 'sendImageMessage', 'message', '发送图片消息', ['to', 'path'], safe=False), - WechatAction('get_messages', 'getMessages', 'message', '读取消息列表', ['wxid']), - WechatAction('open_chat', 'openChat', 'message', '打开指定会话', ['wxid']), - WechatAction('create_group', 'createGroup', 'group', '创建群聊', ['members'], safe=False), - WechatAction('invite_group_member', 'inviteGroupMember', 'group', '邀请群成员', ['chatroom', 'members'], safe=False), - WechatAction('get_group_members', 'getGroupMembers', 'group', '读取群成员', ['chatroom']), - WechatAction('browse_channels', 'browseChannels', 'channels', '浏览视频号'), - WechatAction('add_favorite', 'addFavorite', 'favorite', '收藏当前内容'), - WechatAction('add_custom_emoji', 'addCustomEmoji', 'emoji', '添加自定义表情', safe=False), -] - -ACTION_MAP: Dict[str, WechatAction] = {item.action: item for item in ACTIONS} -RPC_MAP: Dict[str, WechatAction] = {item.rpc: item for item in ACTIONS} - -def list_actions() -> List[Dict[str, Any]]: - return [item.__dict__.copy() for item in ACTIONS] - -def validate_action(action: str, payload: Dict[str, Any]) -> WechatAction: - if action not in ACTION_MAP: - raise KeyError(f'未知微信动作:{action}') - spec = ACTION_MAP[action] - missing = [k for k in spec.required if k not in payload or payload[k] in (None, '')] - if missing: - raise ValueError(f'动作 {action} 缺少参数:{missing}') - return spec -''' - -files['sdk/frida/hook_executor.py'] = r'''"""将业务 action 映射到 Frida RPC。""" -from __future__ import annotations -from dataclasses import asdict -from typing import Any, Dict, Optional - -from sdk.frida.frida_manager import FridaManager, RpcResult -from sdk.wechat.wechat_actions import validate_action, list_actions - -class HookExecutor: - def __init__(self, manager: FridaManager): - self.manager = manager - - def execute(self, action: str, payload: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: - payload = payload or {} - try: - spec = validate_action(action, payload) - except Exception as exc: - return {'ok': False, 'action': action, 'error': str(exc), 'stage': 'validate'} - args = [payload[k] for k in spec.required] - # 允许额外参数透传给 Hook;Hook 若不支持会返回错误,便于报告定位。 - extra = {k: v for k, v in payload.items() if k not in spec.required} - result: RpcResult = self.manager.call(spec.rpc, *args, **extra) - return { - 'ok': result.ok, - 'action': action, - 'rpc': spec.rpc, - 'method_used': result.method, - 'module': spec.module, - 'description': spec.description, - 'safe': spec.safe, - 'data': result.data, - 'error': result.error, - 'elapsed_ms': result.elapsed_ms, - } - - def catalog(self): - return list_actions() -''' - -files['mobile_agent/wireless_agent.py'] = r'''"""手机端 Termux/Python Agent:通过 WebSocket 接收服务器动作,调用 Frida Hook。""" -from __future__ import annotations -import argparse -import asyncio -import json -import platform -import uuid -from pathlib import Path - -from sdk.frida.frida_manager import FridaConfig, FridaManager -from sdk.frida.hook_executor import HookExecutor - -try: - import websockets -except Exception: # pragma: no cover - websockets = None - -class WirelessAgent: - def __init__(self, server_ws: str, hook_path: str, device_id: str | None = None): - self.server_ws = server_ws - self.hook_path = hook_path - self.device_id = device_id or f"phone-{uuid.getnode():x}" - self.manager = FridaManager(FridaConfig(hook_path=hook_path)) - self.executor = HookExecutor(self.manager) - - async def boot(self): - self.manager.connect() - self.manager.attach() - self.manager.load_script(self.hook_path) - - def hello(self): - return { - 'type': 'hello', - 'device_id': self.device_id, - 'platform': platform.platform(), - 'actions': self.executor.catalog(), - } - - async def run(self): - if websockets is None: - raise RuntimeError('请先安装 websockets:pip install websockets') - await self.boot() - async with websockets.connect(self.server_ws, ping_interval=20, ping_timeout=20) as ws: - await ws.send(json.dumps(self.hello(), ensure_ascii=False)) - async for raw in ws: - try: - msg = json.loads(raw) - if msg.get('type') != 'command': - continue - result = self.executor.execute(msg['action'], msg.get('payload') or {}) - await ws.send(json.dumps({'type': 'result', 'request_id': msg.get('request_id'), 'device_id': self.device_id, 'result': result}, ensure_ascii=False)) - except Exception as exc: - await ws.send(json.dumps({'type': 'error', 'error': str(exc)}, ensure_ascii=False)) - - -def main(): - parser = argparse.ArgumentParser() - parser.add_argument('--server-ws', required=True, help='例如 ws://192.168.1.10:8000/ws/phone') - parser.add_argument('--hook', required=True, help='wechat_hook_bridge.js 或真实 wechat_hook_v3.js 路径') - parser.add_argument('--device-id') - args = parser.parse_args() - asyncio.run(WirelessAgent(args.server_ws, args.hook, args.device_id).run()) - -if __name__ == '__main__': - main() -''' - -files['server/routes/wechat_frida.py'] = r'''"""FastAPI 微信 Frida 无线控制路由。""" -from __future__ import annotations -import asyncio -import json -import uuid -from dataclasses import dataclass, field -from typing import Any, Dict, Optional - -from fastapi import APIRouter, WebSocket, WebSocketDisconnect, HTTPException -from pydantic import BaseModel - -from sdk.wechat.wechat_actions import list_actions - -router = APIRouter(prefix='/api/v3/wechat-frida', tags=['wechat-frida']) - -@dataclass -class PhoneConn: - device_id: str - ws: WebSocket - meta: Dict[str, Any] = field(default_factory=dict) - pending: Dict[str, asyncio.Future] = field(default_factory=dict) - -phones: Dict[str, PhoneConn] = {} - -class ActionRequest(BaseModel): - device_id: str - action: str - payload: Dict[str, Any] = {} - timeout: float = 30.0 - -@router.get('/actions') -def actions(): - return {'ok': True, 'actions': list_actions()} - -@router.get('/devices') -def devices(): - return {'ok': True, 'devices': [{'device_id': k, 'meta': v.meta} for k, v in phones.items()]} - -@router.post('/execute') -async def execute(req: ActionRequest): - if req.device_id not in phones: - raise HTTPException(404, f'设备未连接:{req.device_id}') - conn = phones[req.device_id] - request_id = uuid.uuid4().hex - fut = asyncio.get_event_loop().create_future() - conn.pending[request_id] = fut - await conn.ws.send_text(json.dumps({'type': 'command', 'request_id': request_id, 'action': req.action, 'payload': req.payload}, ensure_ascii=False)) - try: - return await asyncio.wait_for(fut, timeout=req.timeout) - finally: - conn.pending.pop(request_id, None) - -async def phone_socket(websocket: WebSocket): - await websocket.accept() - device_id: Optional[str] = None - try: - async for raw in websocket.iter_text(): - msg = json.loads(raw) - if msg.get('type') == 'hello': - device_id = msg.get('device_id') or uuid.uuid4().hex - phones[device_id] = PhoneConn(device_id=device_id, ws=websocket, meta=msg) - await websocket.send_text(json.dumps({'type': 'hello_ack', 'device_id': device_id}, ensure_ascii=False)) - elif msg.get('type') == 'result': - rid = msg.get('request_id') - did = msg.get('device_id') or device_id - conn = phones.get(did or '') - if conn and rid in conn.pending and not conn.pending[rid].done(): - conn.pending[rid].set_result({'ok': True, 'device_id': did, 'result': msg.get('result')}) - elif msg.get('type') == 'error': - # 保留连接,错误由客户端下一次请求再显式返回。 - pass - except WebSocketDisconnect: - pass - finally: - if device_id and device_id in phones: - phones.pop(device_id, None) -''' - -files['server/app.py'] = r'''from fastapi import FastAPI, WebSocket -from server.routes.wechat_frida import router as wechat_frida_router, phone_socket - -app = FastAPI(title='工作手机 SDK · 微信 Frida 控制服务', version='0.3.0') -app.include_router(wechat_frida_router) - -@app.get('/health') -def health(): - return {'ok': True, 'service': 'work-phone-sdk'} - -@app.websocket('/ws/phone') -async def ws_phone(websocket: WebSocket): - await phone_socket(websocket) -''' - -files['hooks/wechat_hook_bridge.js'] = r'''// 工作手机 SDK · 微信 Hook 桥接模板 -// 说明:这是安全桥接版,优先提供连通性、截图、UI 导航等通用方法。 -// 如果已有完整 wechat_hook_v3.js,可直接替换本文件;Python SDK 会兼容 camelCase/snake_case 调用。 -'use strict'; - -function ok(data) { return { ok: true, data: data || null, ts: Date.now() }; } -function fail(message) { return { ok: false, error: String(message), ts: Date.now() }; } - -function runJava(fn) { - let result; - Java.perform(function () { - try { result = fn(); } catch (e) { result = fail(e.stack || e.message || e); } - }); - return result; -} - -rpc.exports = { - ping: function () { return 'pong from wechat_hook_bridge'; }, - getConnectionStatus: function () { - return ok({ java_available: Java.available, process: Process.id, arch: Process.arch, platform: Process.platform }); - }, - takeScreenshot: function () { - // 截图建议在手机端通过 uiautomator/screencap 实现;Hook 层返回占位,Agent 可扩展真实文件路径。 - return ok({ mode: 'placeholder', message: '请在 mobile_agent 扩展 screencap -p 后回传文件路径' }); - }, - dumpUiTree: function () { return ok({ mode: 'placeholder', message: '建议通过 uiautomator dump 获取 UI XML' }); }, - goHome: function () { return ok({ action: 'goHome', message: '桥接模板未执行 UI 点击;请替换完整 Hook 后验证' }); }, - openContacts: function () { return ok({ action: 'openContacts', message: 'NOT_IMPLEMENTED_IN_TEMPLATE' }); }, - openChats: function () { return ok({ action: 'openChats', message: 'NOT_IMPLEMENTED_IN_TEMPLATE' }); }, - searchContact: function (keyword) { return ok({ keyword: keyword, message: 'NOT_IMPLEMENTED_IN_TEMPLATE' }); }, - getContacts: function () { return ok({ contacts: [], message: 'NOT_IMPLEMENTED_IN_TEMPLATE' }); }, - getContactProfile: function (wxid) { return ok({ wxid: wxid, message: 'NOT_IMPLEMENTED_IN_TEMPLATE' }); }, - addFriend: function (keyword) { return fail('危险动作未在模板中实现:addFriend ' + keyword); }, - acceptFriend: function (wxid) { return fail('危险动作未在模板中实现:acceptFriend ' + wxid); }, - sendTextMessage: function (to, text) { return fail('危险动作未在模板中实现:sendTextMessage ' + to); }, - sendImageMessage: function (to, path) { return fail('危险动作未在模板中实现:sendImageMessage ' + to); }, - getMessages: function (wxid) { return ok({ wxid: wxid, messages: [], message: 'NOT_IMPLEMENTED_IN_TEMPLATE' }); }, - openChat: function (wxid) { return ok({ wxid: wxid, message: 'NOT_IMPLEMENTED_IN_TEMPLATE' }); }, - createGroup: function (members) { return fail('危险动作未在模板中实现:createGroup'); }, - inviteGroupMember: function (chatroom, members) { return fail('危险动作未在模板中实现:inviteGroupMember'); }, - getGroupMembers: function (chatroom) { return ok({ chatroom: chatroom, members: [], message: 'NOT_IMPLEMENTED_IN_TEMPLATE' }); }, - browseChannels: function () { return ok({ message: 'NOT_IMPLEMENTED_IN_TEMPLATE' }); }, - addFavorite: function () { return fail('危险动作未在模板中实现:addFavorite'); }, - addCustomEmoji: function () { return fail('危险动作未在模板中实现:addCustomEmoji'); } -}; -''' - -files['scripts/verify_wechat_frida.py'] = r'''"""一键验证服务器控制手机微信功能,输出 JSON 与 Markdown 报告。""" -from __future__ import annotations -import argparse -import json -import time -from pathlib import Path -from typing import Any, Dict - -import requests - -from sdk.wechat.wechat_actions import ACTIONS - -DEFAULT_PAYLOADS: Dict[str, Dict[str, Any]] = { - 'search_contact': {'keyword': '文件传输助手'}, - 'get_contact_profile': {'wxid': 'filehelper'}, - 'add_friend': {'keyword': '__dry_run__'}, - 'accept_friend': {'wxid': '__dry_run__'}, - 'send_text': {'to': 'filehelper', 'text': '工作手机SDK自动验证'}, - 'send_image': {'to': 'filehelper', 'path': '/sdcard/Pictures/test.png'}, - 'get_messages': {'wxid': 'filehelper'}, - 'open_chat': {'wxid': 'filehelper'}, - 'create_group': {'members': ['filehelper']}, - 'invite_group_member': {'chatroom': '__dry_run__', 'members': ['filehelper']}, - 'get_group_members': {'chatroom': '__dry_run__'}, -} - -def call(base_url: str, device_id: str, action: str, payload: Dict[str, Any], timeout: float): - r = requests.post(f'{base_url}/api/v3/wechat-frida/execute', json={'device_id': device_id, 'action': action, 'payload': payload, 'timeout': timeout}, timeout=timeout + 5) - try: - return r.status_code, r.json() - except Exception: - return r.status_code, {'ok': False, 'text': r.text} - -def main(): - parser = argparse.ArgumentParser() - parser.add_argument('--base-url', default='http://127.0.0.1:8000') - parser.add_argument('--device-id', required=True) - parser.add_argument('--out-dir', default='reports') - parser.add_argument('--include-dangerous', action='store_true', help='默认不执行加好友/发消息等危险动作,只记录 SKIPPED') - parser.add_argument('--timeout', type=float, default=30) - args = parser.parse_args() - - out_dir = Path(args.out_dir) - out_dir.mkdir(parents=True, exist_ok=True) - rows = [] - for spec in ACTIONS: - if not spec.safe and not args.include_dangerous: - rows.append({'action': spec.action, 'rpc': spec.rpc, 'ok': None, 'status': 'SKIPPED_DANGEROUS', 'description': spec.description}) - continue - status, data = call(args.base_url, args.device_id, spec.action, DEFAULT_PAYLOADS.get(spec.action, {}), args.timeout) - rows.append({'action': spec.action, 'rpc': spec.rpc, 'http_status': status, 'ok': bool(data.get('ok') and (data.get('result') or {}).get('ok', True)), 'response': data, 'description': spec.description}) - time.sleep(0.2) - - stamp = time.strftime('%Y%m%d_%H%M%S') - json_path = out_dir / f'wechat_frida_verify_{stamp}.json' - md_path = out_dir / f'wechat_frida_verify_{stamp}.md' - json_path.write_text(json.dumps(rows, ensure_ascii=False, indent=2), encoding='utf-8') - lines = ['# 微信 Frida 功能验证报告', '', f'- 时间:{time.strftime("%Y-%m-%d %H:%M:%S")}', f'- 设备:`{args.device_id}`', '', '| 动作 | RPC | 结果 | 说明 |', '|---|---|---|---|'] - for row in rows: - result = 'SKIP' if row.get('status') else ('PASS' if row.get('ok') else 'FAIL') - lines.append(f"| `{row['action']}` | `{row['rpc']}` | {result} | {row.get('description','')} |") - md_path.write_text('\n'.join(lines) + '\n', encoding='utf-8') - print(json_path) - print(md_path) - -if __name__ == '__main__': - main() -''' - -files['requirements.txt'] = 'fastapi\nuvicorn\nrequests\nwebsockets\nfrida\n\n' -files['README.md'] = r'''# 工作手机 SDK · 微信 Frida 无线控制补全包 - -本包用于承接上传 ZIP 中断的开发进度,补齐 **服务器 → WebSocket → 手机端 Agent → Frida → 微信 Hook** 的闭环。 - -## 快速启动 - -服务器端: - -```bash -pip install -r requirements.txt -uvicorn server.app:app --host 0.0.0.0 --port 8000 -``` - -手机 Termux 端: - -```bash -python mobile_agent/wireless_agent.py --server-ws ws://服务器IP:8000/ws/phone --hook hooks/wechat_hook_bridge.js -``` - -验证端: - -```bash -python scripts/verify_wechat_frida.py --base-url http://服务器IP:8000 --device-id phone-xxxx -``` - -如果已有完整 `wechat_hook_v3.js`,请替换 `hooks/wechat_hook_bridge.js`,SDK 会自动兼容 camelCase / snake_case / lowercase 方法名。 -''' - -for rel, content in files.items(): - path = ROOT / rel - path.parent.mkdir(parents=True, exist_ok=True) - path.write_text(content, encoding='utf-8') -print(f'generated {len(files)} files under {ROOT}') diff --git a/补全包/微信Frida_SDK_20260518/hooks/wechat_hook_bridge.js b/补全包/微信Frida_SDK_20260518/hooks/wechat_hook_bridge.js deleted file mode 100644 index d69423fa67..0000000000 --- a/补全包/微信Frida_SDK_20260518/hooks/wechat_hook_bridge.js +++ /dev/null @@ -1,45 +0,0 @@ -// 工作手机 SDK · 微信 Hook 桥接模板 -// 说明:这是安全桥接版,优先提供连通性、截图、UI 导航等通用方法。 -// 如果已有完整 wechat_hook_v3.js,可直接替换本文件;Python SDK 会兼容 camelCase/snake_case 调用。 -'use strict'; - -function ok(data) { return { ok: true, data: data || null, ts: Date.now() }; } -function fail(message) { return { ok: false, error: String(message), ts: Date.now() }; } - -function runJava(fn) { - let result; - Java.perform(function () { - try { result = fn(); } catch (e) { result = fail(e.stack || e.message || e); } - }); - return result; -} - -rpc.exports = { - ping: function () { return 'pong from wechat_hook_bridge'; }, - getConnectionStatus: function () { - return ok({ java_available: Java.available, process: Process.id, arch: Process.arch, platform: Process.platform }); - }, - takeScreenshot: function () { - // 截图建议在手机端通过 uiautomator/screencap 实现;Hook 层返回占位,Agent 可扩展真实文件路径。 - return ok({ mode: 'placeholder', message: '请在 mobile_agent 扩展 screencap -p 后回传文件路径' }); - }, - dumpUiTree: function () { return ok({ mode: 'placeholder', message: '建议通过 uiautomator dump 获取 UI XML' }); }, - goHome: function () { return ok({ action: 'goHome', message: '桥接模板未执行 UI 点击;请替换完整 Hook 后验证' }); }, - openContacts: function () { return ok({ action: 'openContacts', message: 'NOT_IMPLEMENTED_IN_TEMPLATE' }); }, - openChats: function () { return ok({ action: 'openChats', message: 'NOT_IMPLEMENTED_IN_TEMPLATE' }); }, - searchContact: function (keyword) { return ok({ keyword: keyword, message: 'NOT_IMPLEMENTED_IN_TEMPLATE' }); }, - getContacts: function () { return ok({ contacts: [], message: 'NOT_IMPLEMENTED_IN_TEMPLATE' }); }, - getContactProfile: function (wxid) { return ok({ wxid: wxid, message: 'NOT_IMPLEMENTED_IN_TEMPLATE' }); }, - addFriend: function (keyword) { return fail('危险动作未在模板中实现:addFriend ' + keyword); }, - acceptFriend: function (wxid) { return fail('危险动作未在模板中实现:acceptFriend ' + wxid); }, - sendTextMessage: function (to, text) { return fail('危险动作未在模板中实现:sendTextMessage ' + to); }, - sendImageMessage: function (to, path) { return fail('危险动作未在模板中实现:sendImageMessage ' + to); }, - getMessages: function (wxid) { return ok({ wxid: wxid, messages: [], message: 'NOT_IMPLEMENTED_IN_TEMPLATE' }); }, - openChat: function (wxid) { return ok({ wxid: wxid, message: 'NOT_IMPLEMENTED_IN_TEMPLATE' }); }, - createGroup: function (members) { return fail('危险动作未在模板中实现:createGroup'); }, - inviteGroupMember: function (chatroom, members) { return fail('危险动作未在模板中实现:inviteGroupMember'); }, - getGroupMembers: function (chatroom) { return ok({ chatroom: chatroom, members: [], message: 'NOT_IMPLEMENTED_IN_TEMPLATE' }); }, - browseChannels: function () { return ok({ message: 'NOT_IMPLEMENTED_IN_TEMPLATE' }); }, - addFavorite: function () { return fail('危险动作未在模板中实现:addFavorite'); }, - addCustomEmoji: function () { return fail('危险动作未在模板中实现:addCustomEmoji'); } -}; diff --git a/补全包/微信Frida_SDK_20260518/mobile_agent/__init__.py b/补全包/微信Frida_SDK_20260518/mobile_agent/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/补全包/微信Frida_SDK_20260518/mobile_agent/wireless_agent.py b/补全包/微信Frida_SDK_20260518/mobile_agent/wireless_agent.py deleted file mode 100644 index 594bd01f44..0000000000 --- a/补全包/微信Frida_SDK_20260518/mobile_agent/wireless_agent.py +++ /dev/null @@ -1,65 +0,0 @@ -"""手机端 Termux/Python Agent:通过 WebSocket 接收服务器动作,调用 Frida Hook。""" -from __future__ import annotations -import argparse -import asyncio -import json -import platform -import uuid -from pathlib import Path - -from sdk.frida.frida_manager import FridaConfig, FridaManager -from sdk.frida.hook_executor import HookExecutor - -try: - import websockets -except Exception: # pragma: no cover - websockets = None - -class WirelessAgent: - def __init__(self, server_ws: str, hook_path: str, device_id: str | None = None): - self.server_ws = server_ws - self.hook_path = hook_path - self.device_id = device_id or f"phone-{uuid.getnode():x}" - self.manager = FridaManager(FridaConfig(hook_path=hook_path)) - self.executor = HookExecutor(self.manager) - - async def boot(self): - self.manager.connect() - self.manager.attach() - self.manager.load_script(self.hook_path) - - def hello(self): - return { - 'type': 'hello', - 'device_id': self.device_id, - 'platform': platform.platform(), - 'actions': self.executor.catalog(), - } - - async def run(self): - if websockets is None: - raise RuntimeError('请先安装 websockets:pip install websockets') - await self.boot() - async with websockets.connect(self.server_ws, ping_interval=20, ping_timeout=20) as ws: - await ws.send(json.dumps(self.hello(), ensure_ascii=False)) - async for raw in ws: - try: - msg = json.loads(raw) - if msg.get('type') != 'command': - continue - result = self.executor.execute(msg['action'], msg.get('payload') or {}) - await ws.send(json.dumps({'type': 'result', 'request_id': msg.get('request_id'), 'device_id': self.device_id, 'result': result}, ensure_ascii=False)) - except Exception as exc: - await ws.send(json.dumps({'type': 'error', 'error': str(exc)}, ensure_ascii=False)) - - -def main(): - parser = argparse.ArgumentParser() - parser.add_argument('--server-ws', required=True, help='例如 ws://192.168.1.10:8000/ws/phone') - parser.add_argument('--hook', required=True, help='wechat_hook_bridge.js 或真实 wechat_hook_v3.js 路径') - parser.add_argument('--device-id') - args = parser.parse_args() - asyncio.run(WirelessAgent(args.server_ws, args.hook, args.device_id).run()) - -if __name__ == '__main__': - main() diff --git a/补全包/微信Frida_SDK_20260518/requirements.txt b/补全包/微信Frida_SDK_20260518/requirements.txt deleted file mode 100644 index b52d28ca18..0000000000 --- a/补全包/微信Frida_SDK_20260518/requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ -fastapi -uvicorn -requests -websockets -frida - diff --git a/补全包/微信Frida_SDK_20260518/sdk/__init__.py b/补全包/微信Frida_SDK_20260518/sdk/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/补全包/微信Frida_SDK_20260518/sdk/frida/__init__.py b/补全包/微信Frida_SDK_20260518/sdk/frida/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/补全包/微信Frida_SDK_20260518/sdk/frida/frida_manager.py b/补全包/微信Frida_SDK_20260518/sdk/frida/frida_manager.py deleted file mode 100644 index 9ec11f589a..0000000000 --- a/补全包/微信Frida_SDK_20260518/sdk/frida/frida_manager.py +++ /dev/null @@ -1,166 +0,0 @@ -""" -工作手机 SDK · FridaManager - -负责手机端或服务器端连接 Frida、attach 微信进程、加载 Hook 脚本,并提供兼容 RPC 调用。 -设计重点:历史归档显示 Frida Python 对 rpc.exports 方法名存在混淆,因此这里采用多候选名兼容策略。 -""" -from __future__ import annotations - -from dataclasses import dataclass, field -from pathlib import Path -import re -import time -from typing import Any, Callable, Dict, Iterable, Optional - - -class FridaUnavailable(RuntimeError): - """当前环境未安装或无法连接 Frida。""" - - -class RpcMethodMissing(RuntimeError): - """Hook 中缺少指定 RPC 方法。""" - - -@dataclass -class FridaConfig: - device_host: str = "127.0.0.1" - device_port: int = 27042 - package_name: str = "com.tencent.mm" - process_name: str = "WeChat" - attach_timeout: float = 15.0 - prefer_usb: bool = False - hook_path: Optional[str] = None - - -@dataclass -class RpcResult: - ok: bool - action: str - method: str - data: Any = None - error: Optional[str] = None - elapsed_ms: int = 0 - - -class FridaManager: - def __init__(self, config: Optional[FridaConfig] = None): - self.config = config or FridaConfig() - self.frida = None - self.device = None - self.session = None - self.script = None - self.exports = None - self.loaded_hook_path: Optional[str] = None - - def _import_frida(self): - if self.frida is not None: - return self.frida - try: - import frida # type: ignore - except Exception as exc: - raise FridaUnavailable(f"未安装 frida Python 包或加载失败:{exc}") from exc - self.frida = frida - return frida - - def connect(self): - frida = self._import_frida() - if self.config.prefer_usb: - self.device = frida.get_usb_device(timeout=int(self.config.attach_timeout)) - else: - self.device = frida.get_device_manager().add_remote_device( - f"{self.config.device_host}:{self.config.device_port}" - ) - return self.device - - def attach(self, target: Optional[str] = None): - if self.device is None: - self.connect() - assert self.device is not None - target = target or self.config.package_name - last_error: Optional[Exception] = None - candidates: Iterable[Any] = [target, self.config.process_name] - for candidate in candidates: - try: - self.session = self.device.attach(candidate) - return self.session - except Exception as exc: # pragma: no cover - depends on phone - last_error = exc - try: - processes = self.device.enumerate_processes() - for proc in processes: - name = getattr(proc, 'name', '') or '' - pid = getattr(proc, 'pid', None) - if pid and (self.config.package_name in name or self.config.process_name.lower() in name.lower() or '微信' in name): - self.session = self.device.attach(pid) - return self.session - except Exception as exc: # pragma: no cover - last_error = exc - raise FridaUnavailable(f"无法 attach 微信进程:{last_error}") - - def load_script(self, hook_path: Optional[str] = None): - path = Path(hook_path or self.config.hook_path or '') - if not path.exists(): - raise FileNotFoundError(f"Hook 脚本不存在:{path}") - if self.session is None: - self.attach() - assert self.session is not None - source = path.read_text(encoding='utf-8') - self.script = self.session.create_script(source) - self.script.on('message', self._on_message) - self.script.load() - self.exports = getattr(self.script, 'exports_sync', None) or getattr(self.script, 'exports', None) - self.loaded_hook_path = str(path) - return self.script - - def _on_message(self, message, data): # pragma: no cover - runtime callback - # 生产环境可转发到 EventReporter;这里保持最小日志。 - print({'frida_message': message, 'data_len': len(data) if data else 0}) - - @staticmethod - def _snake(name: str) -> str: - return re.sub(r'(? RpcResult: - started = time.time() - if self.exports is None: - raise FridaUnavailable('Hook 脚本尚未加载,无法调用 RPC') - last_error = None - for candidate in self.method_candidates(method): - try: - fn = getattr(self.exports, candidate) - data = fn(*args, **kwargs) - return RpcResult(True, method, candidate, data=data, elapsed_ms=int((time.time() - started) * 1000)) - except AttributeError as exc: - last_error = exc - except Exception as exc: - return RpcResult(False, method, candidate, error=str(exc), elapsed_ms=int((time.time() - started) * 1000)) - # 兼容新版 frida 的 exports_sync.invoke 或 script.exports_sync.invoke - try: - invoke = getattr(self.exports, 'invoke') - data = invoke(method, list(args), kwargs or None) - return RpcResult(True, method, 'invoke', data=data, elapsed_ms=int((time.time() - started) * 1000)) - except Exception as exc: - last_error = exc - return RpcResult(False, method, '', error=f'RPC 方法不存在或不可调用:{method}; last={last_error}', elapsed_ms=int((time.time() - started) * 1000)) - - def cleanup(self): - for obj, method in [(self.script, 'unload'), (self.session, 'detach')]: - if obj is not None: - try: - getattr(obj, method)() - except Exception: - pass - self.script = None - self.session = None - self.exports = None diff --git a/补全包/微信Frida_SDK_20260518/sdk/frida/hook_executor.py b/补全包/微信Frida_SDK_20260518/sdk/frida/hook_executor.py deleted file mode 100644 index da046d0d35..0000000000 --- a/补全包/微信Frida_SDK_20260518/sdk/frida/hook_executor.py +++ /dev/null @@ -1,37 +0,0 @@ -"""将业务 action 映射到 Frida RPC。""" -from __future__ import annotations -from dataclasses import asdict -from typing import Any, Dict, Optional - -from sdk.frida.frida_manager import FridaManager, RpcResult -from sdk.wechat.wechat_actions import validate_action, list_actions - -class HookExecutor: - def __init__(self, manager: FridaManager): - self.manager = manager - - def execute(self, action: str, payload: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: - payload = payload or {} - try: - spec = validate_action(action, payload) - except Exception as exc: - return {'ok': False, 'action': action, 'error': str(exc), 'stage': 'validate'} - args = [payload[k] for k in spec.required] - # 允许额外参数透传给 Hook;Hook 若不支持会返回错误,便于报告定位。 - extra = {k: v for k, v in payload.items() if k not in spec.required} - result: RpcResult = self.manager.call(spec.rpc, *args, **extra) - return { - 'ok': result.ok, - 'action': action, - 'rpc': spec.rpc, - 'method_used': result.method, - 'module': spec.module, - 'description': spec.description, - 'safe': spec.safe, - 'data': result.data, - 'error': result.error, - 'elapsed_ms': result.elapsed_ms, - } - - def catalog(self): - return list_actions() diff --git a/补全包/微信Frida_SDK_20260518/sdk/wechat/__init__.py b/补全包/微信Frida_SDK_20260518/sdk/wechat/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/补全包/微信Frida_SDK_20260518/sdk/wechat/wechat_actions.py b/补全包/微信Frida_SDK_20260518/sdk/wechat/wechat_actions.py deleted file mode 100644 index c405fbb39a..0000000000 --- a/补全包/微信Frida_SDK_20260518/sdk/wechat/wechat_actions.py +++ /dev/null @@ -1,54 +0,0 @@ -"""微信动作目录:服务器、手机端 Agent、验证脚本共同使用。""" -from __future__ import annotations -from dataclasses import dataclass, field -from typing import Any, Dict, List - -@dataclass(frozen=True) -class WechatAction: - action: str - rpc: str - module: str - description: str - required: List[str] = field(default_factory=list) - safe: bool = True - verify: bool = True - -ACTIONS: List[WechatAction] = [ - WechatAction('ping', 'ping', 'system', 'Hook 连通性检测'), - WechatAction('get_connection_status', 'getConnectionStatus', 'system', '读取连接状态'), - WechatAction('screenshot', 'takeScreenshot', 'device', '手机当前屏幕截图'), - WechatAction('ui_dump', 'dumpUiTree', 'device', '导出当前 UI 树'), - WechatAction('go_home', 'goHome', 'navigation', '返回微信首页'), - WechatAction('open_contacts', 'openContacts', 'navigation', '打开通讯录'), - WechatAction('open_chats', 'openChats', 'navigation', '打开聊天列表'), - WechatAction('search_contact', 'searchContact', 'contact', '搜索联系人', ['keyword']), - WechatAction('get_contacts', 'getContacts', 'contact', '读取联系人列表'), - WechatAction('get_contact_profile', 'getContactProfile', 'contact', '读取联系人资料', ['wxid']), - WechatAction('add_friend', 'addFriend', 'contact', '添加好友', ['keyword'], safe=False), - WechatAction('accept_friend', 'acceptFriend', 'contact', '通过好友申请', ['wxid'], safe=False), - WechatAction('send_text', 'sendTextMessage', 'message', '发送文本消息', ['to', 'text'], safe=False), - WechatAction('send_image', 'sendImageMessage', 'message', '发送图片消息', ['to', 'path'], safe=False), - WechatAction('get_messages', 'getMessages', 'message', '读取消息列表', ['wxid']), - WechatAction('open_chat', 'openChat', 'message', '打开指定会话', ['wxid']), - WechatAction('create_group', 'createGroup', 'group', '创建群聊', ['members'], safe=False), - WechatAction('invite_group_member', 'inviteGroupMember', 'group', '邀请群成员', ['chatroom', 'members'], safe=False), - WechatAction('get_group_members', 'getGroupMembers', 'group', '读取群成员', ['chatroom']), - WechatAction('browse_channels', 'browseChannels', 'channels', '浏览视频号'), - WechatAction('add_favorite', 'addFavorite', 'favorite', '收藏当前内容'), - WechatAction('add_custom_emoji', 'addCustomEmoji', 'emoji', '添加自定义表情', safe=False), -] - -ACTION_MAP: Dict[str, WechatAction] = {item.action: item for item in ACTIONS} -RPC_MAP: Dict[str, WechatAction] = {item.rpc: item for item in ACTIONS} - -def list_actions() -> List[Dict[str, Any]]: - return [item.__dict__.copy() for item in ACTIONS] - -def validate_action(action: str, payload: Dict[str, Any]) -> WechatAction: - if action not in ACTION_MAP: - raise KeyError(f'未知微信动作:{action}') - spec = ACTION_MAP[action] - missing = [k for k in spec.required if k not in payload or payload[k] in (None, '')] - if missing: - raise ValueError(f'动作 {action} 缺少参数:{missing}') - return spec diff --git a/补全包/微信Frida_SDK_20260518/server/__init__.py b/补全包/微信Frida_SDK_20260518/server/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/补全包/微信Frida_SDK_20260518/server/app.py b/补全包/微信Frida_SDK_20260518/server/app.py deleted file mode 100644 index 4c7de97ef9..0000000000 --- a/补全包/微信Frida_SDK_20260518/server/app.py +++ /dev/null @@ -1,13 +0,0 @@ -from fastapi import FastAPI, WebSocket -from server.routes.wechat_frida import router as wechat_frida_router, phone_socket - -app = FastAPI(title='工作手机 SDK · 微信 Frida 控制服务', version='0.3.0') -app.include_router(wechat_frida_router) - -@app.get('/health') -def health(): - return {'ok': True, 'service': 'work-phone-sdk'} - -@app.websocket('/ws/phone') -async def ws_phone(websocket: WebSocket): - await phone_socket(websocket) diff --git a/补全包/微信Frida_SDK_20260518/server/routes/__init__.py b/补全包/微信Frida_SDK_20260518/server/routes/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/补全包/微信Frida_SDK_20260518/server/routes/wechat_frida.py b/补全包/微信Frida_SDK_20260518/server/routes/wechat_frida.py deleted file mode 100644 index 2f7b7d14f5..0000000000 --- a/补全包/微信Frida_SDK_20260518/server/routes/wechat_frida.py +++ /dev/null @@ -1,76 +0,0 @@ -"""FastAPI 微信 Frida 无线控制路由。""" -from __future__ import annotations -import asyncio -import json -import uuid -from dataclasses import dataclass, field -from typing import Any, Dict, Optional - -from fastapi import APIRouter, WebSocket, WebSocketDisconnect, HTTPException -from pydantic import BaseModel - -from sdk.wechat.wechat_actions import list_actions - -router = APIRouter(prefix='/api/v3/wechat-frida', tags=['wechat-frida']) - -@dataclass -class PhoneConn: - device_id: str - ws: WebSocket - meta: Dict[str, Any] = field(default_factory=dict) - pending: Dict[str, asyncio.Future] = field(default_factory=dict) - -phones: Dict[str, PhoneConn] = {} - -class ActionRequest(BaseModel): - device_id: str - action: str - payload: Dict[str, Any] = {} - timeout: float = 30.0 - -@router.get('/actions') -def actions(): - return {'ok': True, 'actions': list_actions()} - -@router.get('/devices') -def devices(): - return {'ok': True, 'devices': [{'device_id': k, 'meta': v.meta} for k, v in phones.items()]} - -@router.post('/execute') -async def execute(req: ActionRequest): - if req.device_id not in phones: - raise HTTPException(404, f'设备未连接:{req.device_id}') - conn = phones[req.device_id] - request_id = uuid.uuid4().hex - fut = asyncio.get_event_loop().create_future() - conn.pending[request_id] = fut - await conn.ws.send_text(json.dumps({'type': 'command', 'request_id': request_id, 'action': req.action, 'payload': req.payload}, ensure_ascii=False)) - try: - return await asyncio.wait_for(fut, timeout=req.timeout) - finally: - conn.pending.pop(request_id, None) - -async def phone_socket(websocket: WebSocket): - await websocket.accept() - device_id: Optional[str] = None - try: - async for raw in websocket.iter_text(): - msg = json.loads(raw) - if msg.get('type') == 'hello': - device_id = msg.get('device_id') or uuid.uuid4().hex - phones[device_id] = PhoneConn(device_id=device_id, ws=websocket, meta=msg) - await websocket.send_text(json.dumps({'type': 'hello_ack', 'device_id': device_id}, ensure_ascii=False)) - elif msg.get('type') == 'result': - rid = msg.get('request_id') - did = msg.get('device_id') or device_id - conn = phones.get(did or '') - if conn and rid in conn.pending and not conn.pending[rid].done(): - conn.pending[rid].set_result({'ok': True, 'device_id': did, 'result': msg.get('result')}) - elif msg.get('type') == 'error': - # 保留连接,错误由客户端下一次请求再显式返回。 - pass - except WebSocketDisconnect: - pass - finally: - if device_id and device_id in phones: - phones.pop(device_id, None) diff --git a/补全包/微信Frida_SDK_20260518/tests_mock_rpc.py b/补全包/微信Frida_SDK_20260518/tests_mock_rpc.py deleted file mode 100644 index c69d6c662b..0000000000 --- a/补全包/微信Frida_SDK_20260518/tests_mock_rpc.py +++ /dev/null @@ -1,21 +0,0 @@ -from sdk.frida.frida_manager import FridaManager -from sdk.frida.hook_executor import HookExecutor - -class Exports: - def ping(self): - return 'pong' - def getConnectionStatus(self): - return {'ok': True} - def sendTextMessage(self, to, text): - return {'to': to, 'text': text} - -m = FridaManager() -m.exports = Exports() -assert m.call('ping').ok -assert m.call('getConnectionStatus').ok -assert m.call('sendTextMessage', 'filehelper', 'hi').ok -ex = HookExecutor(m) -assert ex.execute('ping')['ok'] is True -assert ex.execute('send_text', {'to':'filehelper','text':'hi'})['ok'] is True -assert ex.execute('send_text', {'to':'filehelper'})['ok'] is False -print('mock_rpc_tests_passed') diff --git a/补全包/微信Frida_SDK_20260518/开发文档/10、项目管理/项目落地执行表.md b/补全包/微信Frida_SDK_20260518/开发文档/10、项目管理/项目落地执行表.md deleted file mode 100644 index ce8a49cc99..0000000000 --- a/补全包/微信Frida_SDK_20260518/开发文档/10、项目管理/项目落地执行表.md +++ /dev/null @@ -1,3 +0,0 @@ -# 项目落地执行表(补全包归档) - -> **已并入**:[开发文档/9、手册/工作手机·五图总览与使用手册.md](../../../开发文档/9、手册/工作手机·五图总览与使用手册.md) §1.2 diff --git a/补全包/微信Frida_SDK_20260518/开发文档/5、接口/微信Frida_API契约.md b/补全包/微信Frida_SDK_20260518/开发文档/5、接口/微信Frida_API契约.md deleted file mode 100644 index 8dfd9f5dd9..0000000000 --- a/补全包/微信Frida_SDK_20260518/开发文档/5、接口/微信Frida_API契约.md +++ /dev/null @@ -1,3 +0,0 @@ -# 微信 Frida API 契约(补全包归档) - -> **已并入**:[开发文档/9、手册/工作手机·五图总览与使用手册.md](../../../开发文档/9、手册/工作手机·五图总览与使用手册.md) §三 diff --git a/补全包/微信Frida_SDK_20260518/开发文档/8、部署/微信Frida无线部署与验证说明.md b/补全包/微信Frida_SDK_20260518/开发文档/8、部署/微信Frida无线部署与验证说明.md deleted file mode 100644 index 6aa007efc5..0000000000 --- a/补全包/微信Frida_SDK_20260518/开发文档/8、部署/微信Frida无线部署与验证说明.md +++ /dev/null @@ -1,3 +0,0 @@ -# 微信 Frida 无线部署与验证说明(补全包归档) - -> **已并入**:[开发文档/9、手册/工作手机·五图总览与使用手册.md](../../../开发文档/9、手册/工作手机·五图总览与使用手册.md) §八 diff --git a/补全包/微信Frida_SDK_20260518_补全包.zip b/补全包/微信Frida_SDK_20260518_补全包.zip deleted file mode 100644 index 63b563b10a6484860727777d08324267e2e1dc86..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 30208 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