feat: 补齐微信Frida手机SDK闭环
This commit is contained in:
26
工作手机SDK补全包_微信Frida/README.md
Normal file
26
工作手机SDK补全包_微信Frida/README.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# 工作手机 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 方法名。
|
||||
578
工作手机SDK补全包_微信Frida/generate_impl.py
Normal file
578
工作手机SDK补全包_微信Frida/generate_impl.py
Normal file
@@ -0,0 +1,578 @@
|
||||
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'(?<!^)(?=[A-Z])', '_', name).lower()
|
||||
|
||||
@classmethod
|
||||
def method_candidates(cls, name: str):
|
||||
snake = cls._snake(name)
|
||||
lower = name.lower()
|
||||
candidates = [name, snake, lower]
|
||||
seen = set()
|
||||
for item in candidates:
|
||||
if item and item not in seen:
|
||||
seen.add(item)
|
||||
yield item
|
||||
|
||||
def call(self, method: str, *args, **kwargs) -> 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}')
|
||||
45
工作手机SDK补全包_微信Frida/hooks/wechat_hook_bridge.js
Normal file
45
工作手机SDK补全包_微信Frida/hooks/wechat_hook_bridge.js
Normal file
@@ -0,0 +1,45 @@
|
||||
// 工作手机 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'); }
|
||||
};
|
||||
0
工作手机SDK补全包_微信Frida/mobile_agent/__init__.py
Normal file
0
工作手机SDK补全包_微信Frida/mobile_agent/__init__.py
Normal file
65
工作手机SDK补全包_微信Frida/mobile_agent/wireless_agent.py
Normal file
65
工作手机SDK补全包_微信Frida/mobile_agent/wireless_agent.py
Normal file
@@ -0,0 +1,65 @@
|
||||
"""手机端 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()
|
||||
6
工作手机SDK补全包_微信Frida/requirements.txt
Normal file
6
工作手机SDK补全包_微信Frida/requirements.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
fastapi
|
||||
uvicorn
|
||||
requests
|
||||
websockets
|
||||
frida
|
||||
|
||||
0
工作手机SDK补全包_微信Frida/sdk/__init__.py
Normal file
0
工作手机SDK补全包_微信Frida/sdk/__init__.py
Normal file
0
工作手机SDK补全包_微信Frida/sdk/frida/__init__.py
Normal file
0
工作手机SDK补全包_微信Frida/sdk/frida/__init__.py
Normal file
166
工作手机SDK补全包_微信Frida/sdk/frida/frida_manager.py
Normal file
166
工作手机SDK补全包_微信Frida/sdk/frida/frida_manager.py
Normal file
@@ -0,0 +1,166 @@
|
||||
"""
|
||||
工作手机 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'(?<!^)(?=[A-Z])', '_', name).lower()
|
||||
|
||||
@classmethod
|
||||
def method_candidates(cls, name: str):
|
||||
snake = cls._snake(name)
|
||||
lower = name.lower()
|
||||
candidates = [name, snake, lower]
|
||||
seen = set()
|
||||
for item in candidates:
|
||||
if item and item not in seen:
|
||||
seen.add(item)
|
||||
yield item
|
||||
|
||||
def call(self, method: str, *args, **kwargs) -> 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
|
||||
37
工作手机SDK补全包_微信Frida/sdk/frida/hook_executor.py
Normal file
37
工作手机SDK补全包_微信Frida/sdk/frida/hook_executor.py
Normal file
@@ -0,0 +1,37 @@
|
||||
"""将业务 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()
|
||||
0
工作手机SDK补全包_微信Frida/sdk/wechat/__init__.py
Normal file
0
工作手机SDK补全包_微信Frida/sdk/wechat/__init__.py
Normal file
54
工作手机SDK补全包_微信Frida/sdk/wechat/wechat_actions.py
Normal file
54
工作手机SDK补全包_微信Frida/sdk/wechat/wechat_actions.py
Normal file
@@ -0,0 +1,54 @@
|
||||
"""微信动作目录:服务器、手机端 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
|
||||
0
工作手机SDK补全包_微信Frida/server/__init__.py
Normal file
0
工作手机SDK补全包_微信Frida/server/__init__.py
Normal file
13
工作手机SDK补全包_微信Frida/server/app.py
Normal file
13
工作手机SDK补全包_微信Frida/server/app.py
Normal file
@@ -0,0 +1,13 @@
|
||||
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)
|
||||
0
工作手机SDK补全包_微信Frida/server/routes/__init__.py
Normal file
0
工作手机SDK补全包_微信Frida/server/routes/__init__.py
Normal file
76
工作手机SDK补全包_微信Frida/server/routes/wechat_frida.py
Normal file
76
工作手机SDK补全包_微信Frida/server/routes/wechat_frida.py
Normal file
@@ -0,0 +1,76 @@
|
||||
"""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)
|
||||
23
工作手机SDK补全包_微信Frida/tests_api_routes.py
Normal file
23
工作手机SDK补全包_微信Frida/tests_api_routes.py
Normal file
@@ -0,0 +1,23 @@
|
||||
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')
|
||||
21
工作手机SDK补全包_微信Frida/tests_mock_rpc.py
Normal file
21
工作手机SDK补全包_微信Frida/tests_mock_rpc.py
Normal file
@@ -0,0 +1,21 @@
|
||||
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')
|
||||
12
工作手机SDK补全包_微信Frida/开发文档/10、项目管理/项目落地执行表.md
Normal file
12
工作手机SDK补全包_微信Frida/开发文档/10、项目管理/项目落地执行表.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# 项目落地执行表
|
||||
|
||||
> 项目:工作手机 · 微信 / 手机端 / SDK / Frida 补全
|
||||
> 本轮时间:2026-05-18
|
||||
> 当前总进度:约 55%
|
||||
> 当前阶段进度:实现阶段约 70%
|
||||
|
||||
| 时间 | 人设 | 用户原话整理与扫描 | 执行目标 | 已完成结果 | 生成/修改文件 | 本地同步状态 | GitHub同步状态 | 阻塞与下一步 |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| 2026-05-18 08:58 | 阿表 | 用户要求继续上传 ZIP 的进度,完成微信、手机、SDK、Frida。重点不是反复测试,而是把服务器能控制手机微信的功能开发出来,并逐项验证。 | 解析归档,恢复历史进度,明确 Frida RPC 与微信 Hook 缺口。 | 已确认历史进度:Frida 连接曾成功,微信 PID 可见;Hook 方法约 112 个;核心问题是 RPC 方法名兼容和源码缺失。 | `/home/ubuntu/karuo_zip_work/工作手机_微信Frida_SDK续跑任务清单.md` | 尚未同步到本地项目,桌面终端连接曾中断,稍后尝试写入挂载目录。 | 尚未同步;未发现可安全使用的 GitHub Connector。 | 继续生成 SDK/Agent/Hook/验证脚本。 |
|
||||
| 2026-05-18 09:05 | 阿机/阿桥 | 继续补齐服务器到手机端再到 Frida Hook 的闭环,并保留后续真机验证入口。 | 重建最小可运行代码包。 | 已生成 FastAPI 路由、WebSocket 手机端 Agent、FridaManager、HookExecutor、微信动作目录、Hook 桥接模板、自动验证脚本。 | `/home/ubuntu/work_phone_sdk_completion/server/app.py`、`/home/ubuntu/work_phone_sdk_completion/server/routes/wechat_frida.py`、`/home/ubuntu/work_phone_sdk_completion/mobile_agent/wireless_agent.py`、`/home/ubuntu/work_phone_sdk_completion/sdk/frida/frida_manager.py`、`/home/ubuntu/work_phone_sdk_completion/sdk/frida/hook_executor.py`、`/home/ubuntu/work_phone_sdk_completion/sdk/wechat/wechat_actions.py`、`/home/ubuntu/work_phone_sdk_completion/hooks/wechat_hook_bridge.js`、`/home/ubuntu/work_phone_sdk_completion/scripts/verify_wechat_frida.py` | 尚未同步到本地项目,待复制。 | 尚未同步;需要用户启用 GitHub 集成或提供仓库。 | 运行语法检查和 Mock RPC 测试。 |
|
||||
| 2026-05-18 09:08 | 阿端/阿表 | 要求每次迭代更新开发文档,方便 Manus/Cursor/卡若AI 继续开发。 | 写入部署说明和验证流程。 | 已完成部署文档,包含服务器启动、手机 Termux 启动、设备查看、危险动作验证开关、历史问题修复点。 | `/home/ubuntu/work_phone_sdk_completion/开发文档/8、部署/微信Frida无线部署与验证说明.md`、`/home/ubuntu/work_phone_sdk_completion/README.md` | 尚未同步到本地项目,待复制。 | 尚未同步;需要仓库入口。 | 打包并尝试同步到本地挂载目录。 |
|
||||
43
工作手机SDK补全包_微信Frida/开发文档/2、架构/系统架构.md
Normal file
43
工作手机SDK补全包_微信Frida/开发文档/2、架构/系统架构.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# 工作手机 SDK · 微信 Frida 控制架构
|
||||
|
||||
## 一、目标
|
||||
|
||||
本轮目标是承接归档 ZIP 的历史进度,补齐 **服务器可以控制一台手机内微信功能** 的最小可运行闭环。由于上传归档中真实 Hook 源码存在缺失,本轮重点不是伪造验证结论,而是把 SDK、手机端 Agent、服务端路由、验证脚本和开发文档全部补齐,等桌面端或手机端连接恢复后即可一键验证并生成报告。
|
||||
|
||||
## 二、架构链路
|
||||
|
||||
```text
|
||||
运营服务器 / 控制台
|
||||
↓ HTTP POST /api/v3/wechat-frida/execute
|
||||
FastAPI 服务
|
||||
↓ WebSocket command
|
||||
手机端 WirelessAgent(Termux/Python)
|
||||
↓ FridaManager.attach + load_script
|
||||
frida-server
|
||||
↓ rpc.exports
|
||||
微信进程 / Hook JS
|
||||
↓ result
|
||||
验证报告 JSON + Markdown
|
||||
```
|
||||
|
||||
## 三、模块拆解
|
||||
|
||||
| 编号 | 模块 | 职责 | 代码路径 | 状态 |
|
||||
|---|---|---|---|---|
|
||||
| F01 | Frida 连接管理 | 连接远端 frida-server、attach 微信、加载 Hook、释放会话 | `/home/ubuntu/work_phone_sdk_completion/sdk/frida/frida_manager.py` | ✅ 已补齐 |
|
||||
| F02 | RPC 兼容调用 | 兼容 camelCase、snake_case、lowercase、invoke 调用方式 | `/home/ubuntu/work_phone_sdk_completion/sdk/frida/frida_manager.py` | ✅ 已补齐 |
|
||||
| F03 | 微信动作目录 | 维护 action、rpc、参数、危险动作、安全验证标记 | `/home/ubuntu/work_phone_sdk_completion/sdk/wechat/wechat_actions.py` | ✅ 已补齐 |
|
||||
| F04 | Hook 执行器 | 将业务动作映射到 RPC 并返回统一结构 | `/home/ubuntu/work_phone_sdk_completion/sdk/frida/hook_executor.py` | ✅ 已补齐 |
|
||||
| F05 | 手机 Agent | WebSocket 接入服务器并执行 Frida 指令 | `/home/ubuntu/work_phone_sdk_completion/mobile_agent/wireless_agent.py` | ✅ 已补齐 |
|
||||
| F06 | 服务端 API | 设备注册、动作下发、结果回传 | `/home/ubuntu/work_phone_sdk_completion/server/routes/wechat_frida.py` | ✅ 已补齐 |
|
||||
| F07 | Hook 桥接模板 | 提供 ping、连接状态、截图/UI 占位与完整 Hook 替换入口 | `/home/ubuntu/work_phone_sdk_completion/hooks/wechat_hook_bridge.js` | ✅ 已补齐 |
|
||||
| F08 | 自动验证 | 批量验证功能并输出 JSON/Markdown 报告 | `/home/ubuntu/work_phone_sdk_completion/scripts/verify_wechat_frida.py` | ✅ 已补齐 |
|
||||
| F09 | 真机截图与逐项证明 | 手机连接恢复后执行验证并收集截图 | `/home/ubuntu/work_phone_sdk_completion/reports/` | ⏳ 待真机连接 |
|
||||
|
||||
## 四、关键设计决定
|
||||
|
||||
本轮把历史争议最大的 Frida RPC 方法名问题下沉到 `FridaManager.call()` 里解决。调用时优先使用 Hook 里常见的原始 `camelCase` 名称,例如 `getMessages`;若失败,再尝试 `get_messages`、`getmessages` 和 `invoke()`。这样后续不管真实 Hook 是 `wechat_hook_v2.js` 还是 `wechat_hook_v3.js`,SDK 侧都能尽量稳定调用并记录实际命中的 `method_used`。
|
||||
|
||||
## 五、真机验证阻塞
|
||||
|
||||
历史记录显示之前曾连上微信进程并看到 PID,但本轮桌面端会话在归档恢复时已断开,且归档中的核心源码不完整。因此当前不能声明“微信好友添加已真机成功”。本轮交付的是可运行工具链,下一步需要将补全包同步到手机或项目本地,再运行验证脚本生成逐项结果和截图。
|
||||
71
工作手机SDK补全包_微信Frida/开发文档/5、接口/微信Frida_API契约.md
Normal file
71
工作手机SDK补全包_微信Frida/开发文档/5、接口/微信Frida_API契约.md
Normal file
@@ -0,0 +1,71 @@
|
||||
# 微信 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` 调用差异问题。
|
||||
64
工作手机SDK补全包_微信Frida/开发文档/5、接口/微信Frida控制接口契约.md
Normal file
64
工作手机SDK补全包_微信Frida/开发文档/5、接口/微信Frida控制接口契约.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# 微信 Frida 控制接口契约
|
||||
|
||||
本文记录本轮补齐的 **服务器控制手机微信** API 契约。路径均为绝对路径,方便 Manus、Cursor 或卡若AI 后续接力。
|
||||
|
||||
## 一、HTTP 接口
|
||||
|
||||
| 方法 | 路径 | 说明 | 代码路径 |
|
||||
|---|---|---|---|
|
||||
| GET | `/health` | 服务健康检查 | `/home/ubuntu/work_phone_sdk_completion/server/app.py` |
|
||||
| GET | `/api/v3/wechat-frida/actions` | 返回微信动作目录、RPC 名称、参数、危险动作标记 | `/home/ubuntu/work_phone_sdk_completion/server/routes/wechat_frida.py` |
|
||||
| GET | `/api/v3/wechat-frida/devices` | 返回已连接手机 Agent 列表 | `/home/ubuntu/work_phone_sdk_completion/server/routes/wechat_frida.py` |
|
||||
| POST | `/api/v3/wechat-frida/execute` | 向指定手机下发微信动作,并等待执行结果 | `/home/ubuntu/work_phone_sdk_completion/server/routes/wechat_frida.py` |
|
||||
|
||||
## 二、WebSocket 接口
|
||||
|
||||
| 方向 | 路径/类型 | 说明 |
|
||||
|---|---|---|
|
||||
| 手机 → 服务器 | `ws://服务器IP:8000/ws/phone` | 手机端 Agent 建立连接。 |
|
||||
| 手机 → 服务器 | `hello` | 上报 `device_id`、平台、动作目录。 |
|
||||
| 服务器 → 手机 | `command` | 下发 `request_id`、`action`、`payload`。 |
|
||||
| 手机 → 服务器 | `result` | 回传 `request_id`、`device_id`、结构化执行结果。 |
|
||||
|
||||
## 三、执行请求格式
|
||||
|
||||
```json
|
||||
{
|
||||
"device_id": "phone-xxxx",
|
||||
"action": "send_text",
|
||||
"payload": {
|
||||
"to": "filehelper",
|
||||
"text": "工作手机SDK自动验证"
|
||||
},
|
||||
"timeout": 30
|
||||
}
|
||||
```
|
||||
|
||||
## 四、执行响应格式
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"device_id": "phone-xxxx",
|
||||
"result": {
|
||||
"ok": true,
|
||||
"action": "send_text",
|
||||
"rpc": "sendTextMessage",
|
||||
"method_used": "sendTextMessage",
|
||||
"module": "message",
|
||||
"description": "发送文本消息",
|
||||
"safe": false,
|
||||
"data": {},
|
||||
"error": null,
|
||||
"elapsed_ms": 120
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 五、动作目录
|
||||
|
||||
动作目录由 `/home/ubuntu/work_phone_sdk_completion/sdk/wechat/wechat_actions.py` 统一维护。当前已覆盖系统连通、截图、UI、通讯录、好友、消息、群聊、视频号、收藏、表情等基础模块。危险动作默认在验证脚本中跳过,必须显式加 `--include-dangerous` 才会执行。
|
||||
|
||||
## 六、历史兼容修复
|
||||
|
||||
归档显示 Frida RPC 可能出现方法名不一致。本轮在 `/home/ubuntu/work_phone_sdk_completion/sdk/frida/frida_manager.py` 中实现了候选调用:`camelCase`、`snake_case`、`lowercase`、`invoke()`。后续替换真实 `wechat_hook_v3.js` 后,无需修改服务器接口即可继续验证。
|
||||
67
工作手机SDK补全包_微信Frida/开发文档/8、部署/微信Frida无线部署与验证说明.md
Normal file
67
工作手机SDK补全包_微信Frida/开发文档/8、部署/微信Frida无线部署与验证说明.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# 微信 Frida 无线部署与验证说明
|
||||
|
||||
本文用于承接归档 ZIP 的中断进度,指导后续在真实手机上继续完成微信控制能力验证。
|
||||
|
||||
## 一、闭环架构
|
||||
|
||||
| 层级 | 文件 | 作用 |
|
||||
|---|---|---|
|
||||
| 服务器 | `/home/ubuntu/work_phone_sdk_completion/server/app.py` | 启动 FastAPI 服务,提供健康检查与 WebSocket 入口。 |
|
||||
| 微信路由 | `/home/ubuntu/work_phone_sdk_completion/server/routes/wechat_frida.py` | 提供 `/api/v3/wechat-frida/actions`、`/devices`、`/execute`。 |
|
||||
| 手机端 Agent | `/home/ubuntu/work_phone_sdk_completion/mobile_agent/wireless_agent.py` | 手机 Termux 连接服务器 WebSocket,接收动作并调用 Frida。 |
|
||||
| Frida 管理 | `/home/ubuntu/work_phone_sdk_completion/sdk/frida/frida_manager.py` | 连接 `frida-server`、attach 微信、加载 Hook、兼容 RPC 方法名。 |
|
||||
| 执行映射 | `/home/ubuntu/work_phone_sdk_completion/sdk/frida/hook_executor.py` | 将 `send_text` 等业务动作映射为 `sendTextMessage` 等 RPC。 |
|
||||
| 动作目录 | `/home/ubuntu/work_phone_sdk_completion/sdk/wechat/wechat_actions.py` | 维护可验证功能清单、必填参数和危险动作标识。 |
|
||||
| Hook 模板 | `/home/ubuntu/work_phone_sdk_completion/hooks/wechat_hook_bridge.js` | 安全桥接版 Hook;可被完整 `wechat_hook_v3.js` 替换。 |
|
||||
| 验证脚本 | `/home/ubuntu/work_phone_sdk_completion/scripts/verify_wechat_frida.py` | 一键生成 JSON 与 Markdown 验证报告。 |
|
||||
|
||||
## 二、真机启动步骤
|
||||
|
||||
在服务器端执行:
|
||||
|
||||
```bash
|
||||
cd /home/ubuntu/work_phone_sdk_completion
|
||||
pip install -r requirements.txt
|
||||
uvicorn server.app:app --host 0.0.0.0 --port 8000
|
||||
```
|
||||
|
||||
在手机 Termux 端执行:
|
||||
|
||||
```bash
|
||||
cd /sdcard/work_phone_sdk_completion
|
||||
python mobile_agent/wireless_agent.py --server-ws ws://服务器IP:8000/ws/phone --hook hooks/wechat_hook_bridge.js
|
||||
```
|
||||
|
||||
如果已有完整 Hook,请将 Hook 路径改为真实文件,例如:
|
||||
|
||||
```bash
|
||||
python mobile_agent/wireless_agent.py --server-ws ws://服务器IP:8000/ws/phone --hook hooks/wechat_hook_v3.js
|
||||
```
|
||||
|
||||
## 三、验证命令
|
||||
|
||||
先查看设备:
|
||||
|
||||
```bash
|
||||
curl http://服务器IP:8000/api/v3/wechat-frida/devices
|
||||
```
|
||||
|
||||
再执行自动验证:
|
||||
|
||||
```bash
|
||||
python scripts/verify_wechat_frida.py --base-url http://服务器IP:8000 --device-id phone-xxxx
|
||||
```
|
||||
|
||||
默认会跳过加好友、发消息、建群等危险动作。若需要完整验证,请确认测试号和测试联系人后执行:
|
||||
|
||||
```bash
|
||||
python scripts/verify_wechat_frida.py --base-url http://服务器IP:8000 --device-id phone-xxxx --include-dangerous
|
||||
```
|
||||
|
||||
## 四、历史问题修复点
|
||||
|
||||
归档显示 Frida RPC 方法名曾出现 camelCase / 小写调用不一致的问题。本轮在 `FridaManager.call()` 中做了四层兼容:原始方法名、snake_case、小写方法名、`invoke()`。因此真实 Hook 只要导出过 `getMessages`、`sendTextMessage` 等方法,就能由 SDK 层稳定调用并返回结构化结果。
|
||||
|
||||
## 五、当前不能完成真机截图验证的原因
|
||||
|
||||
桌面端本地终端在本轮读取时出现连接中断,且上传 ZIP 中 `wechat_hook_v2.js`、`wireless_deployer.py` 等文件不是完整源代码,只是历史片段。因此本轮完成的是可运行闭环与验证工具;截图和逐项结果需要在手机连接恢复后执行验证脚本自动生成。
|
||||
@@ -374,3 +374,84 @@ Phase 5: 防封模块 ✅ 已完成 ← AF1-AF12 全部完成
|
||||
| 8、部署 | 设备端Hook安装部署.md | 真机安装+多设备部署 |
|
||||
| 10、项目管理 | 本文档 | 全量进度+任务拆解 |
|
||||
| 机擎/references | 防封策略_全平台全层级.md | **开发前必读** 四层防封策略+GitHub项目+Checklist |
|
||||
## Frida 接口开发与存客宝对接项目规划
|
||||
|
||||
### 1. 目标
|
||||
|
||||
核心目标是完成 Frida 接口开发,并确保能与“存客宝”完美对接,实现其所有功能在工作手机上的迁移。
|
||||
|
||||
### 2. 现状分析
|
||||
|
||||
* **已完成**:防封模块(Phase 5)。
|
||||
* **Frida Hook 脚本 (`wechat_hook_v2.js`)**:已包含消息收发、联系人、群聊、标签、朋友圈、二维码、视频号等基础功能 Hook,并具备版本兼容机制。RPC 方法丰富,但需要根据存客宝需求进行针对性扩展和优化。
|
||||
* **Frida 管理 (`frida_manager.py`)**:负责 Frida 脚本的加载、RPC 调用和事件处理,支持自动重连。
|
||||
* **微信 ADB 引擎 (`wechat_adb_engine.py`)**:提供了大量的 UI 自动化操作,作为 Frida Hook 的补充或降级方案。
|
||||
* **AI Agent (`ai_agent.py`)**:具备自然语言处理能力,可解析用户指令并转换为 UI 操作。
|
||||
* **Hook 模块服务 (`hook_module_service.py`)**:管理 Hook 模块的部署、日志和事件。
|
||||
* **外部对接网关 (`gateway.py`)**:提供了统一的 API 入口,可用于接收外部系统(如存客宝)的请求。
|
||||
* **存客宝对接规范 (`存客宝对接规范.md`)**:详细定义了存客宝 API 的接口、参数、签名机制和使用示例,核心为 `report_lead`(上报线索)接口。
|
||||
* **开发进度总表 (`开发进度总表.md`)**:提供了项目整体进度和任务拆解,其中 Phase 2 (设备端核心) 和 Phase 3 (管理端) 仍有大量任务待完成。
|
||||
|
||||
### 3. 剩余任务清单与详细规划
|
||||
|
||||
#### Phase 2: 设备端核心 (Frida 接口完善与扩展) - 预计完成度 60%
|
||||
|
||||
| 任务 ID | 任务描述 | 预估工时 | 优先级 | 状态 | 备注 |
|
||||
|:-------:|:---------|:--------:|:------:|:----:|:-----|
|
||||
| H1-H5 | 架构扩展(Hook通道) | 8h | P0-1 | 进行中 | 确保 Frida Hook 通道稳定高效 |
|
||||
| H6-H13 | 设备端Frida核心 | 15h | P0-2 | 进行中 | 完善 Frida 核心功能,提升稳定性 |
|
||||
| H14-H17 | 微信Hook(收发消息) | 16h | P0-3 | 进行中 | 确保消息收发 Hook 完整,支持存客宝需求 |
|
||||
| H18-H22 | 微信Hook(好友/群) | 17h | P1-2 | 待开始 | 扩展好友/群管理 Hook,满足存客宝客户管理需求 |
|
||||
| H23 | Syscall拦截 | 6h | P2-1 | 待开始 | 提升 Hook 隐蔽性和稳定性 |
|
||||
| H24 | 版本适配 | 4h | P1-5 | 进行中 | 持续更新微信版本兼容性 |
|
||||
| H25-H29 | 模块管理API | 12h | P1-1 | 待开始 | 完善 Hook 模块的远程管理和配置 |
|
||||
| H30-H33 | 测试 | 10h | P1-4 | 待开始 | 针对新增 Hook 功能进行全面测试 |
|
||||
|
||||
**存客宝相关 Frida Hook 扩展(新增任务)**
|
||||
|
||||
| 任务 ID | 任务描述 | 预估工时 | 优先级 | 状态 | 备注 |
|
||||
|:-------:|:---------|:--------:|:------:|:----:|:-----|
|
||||
| F-CKB-1 | 扩展 Frida Hook 获取更详细的联系人信息(如标签、备注) | 8h | P0 | 待开始 | 存客宝线索上报需要更全面的客户信息 |
|
||||
| F-CKB-2 | 扩展 Frida Hook 获取群成员列表及群信息 | 6h | P0 | 待开始 | 存客宝可能需要管理群客户 |
|
||||
| F-CKB-3 | 扩展 Frida Hook 监听特定事件(如新好友添加、群变动) | 10h | P0 | 待开始 | 实时同步关键事件到存客宝 |
|
||||
| F-CKB-4 | 扩展 Frida Hook 实现消息转发、群发等操作 | 12h | P1 | 待开始 | 满足存客宝营销自动化需求 |
|
||||
|
||||
#### Phase 3: 存客宝对接模块开发 (后端服务与 API) - 预计完成度 0%
|
||||
|
||||
| 任务 ID | 任务描述 | 预估工时 | 优先级 | 状态 | 备注 |
|
||||
|:-------:|:---------|:--------:|:------:|:----:|:-----|
|
||||
| B-CKB-1 | 设计并实现存客宝对接 API 路由 (`sdk/app/routers/cunke_bao.py`) | 8h | P0 | 待开始 | 接收存客宝请求,转发至内部服务 |
|
||||
| B-CKB-2 | 实现 `CunKeBaoService` (`sdk/app/services/cunke_bao_service.py`) | 16h | P0 | 待开始 | 封装存客宝 API 调用逻辑,包括签名、参数转换、错误处理 |
|
||||
| B-CKB-3 | 实现 `report_lead` 接口,将 Frida 获取的客户信息上报至存客宝 | 12h | P0 | 待开始 | 核心功能,确保数据准确性 |
|
||||
| B-CKB-4 | 实现 Frida Hook 事件(如新好友、新消息)到存客宝的实时上报机制 | 10h | P0 | 待开始 | 利用 `hook_module_service.py` 的事件广播功能 |
|
||||
| B-CKB-5 | 错误处理与日志记录 | 6h | P1 | 待开始 | 确保对接过程中的异常可追溯 |
|
||||
| B-CKB-6 | 安全性增强(API Key 管理、请求频率限制) | 8h | P1 | 待开始 | 防止滥用和数据泄露 |
|
||||
|
||||
#### Phase 4: 管理端 (前端界面支持) - 预计完成度 0%
|
||||
|
||||
| 任务 ID | 任务描述 | 预估工时 | 优先级 | 状态 | 备注 |
|
||||
|:-------:|:---------|:--------:|:------:|:----:|:-----|
|
||||
| F-CKB-5 | 管理端集成存客宝配置界面(API Key, Base URL) | 8h | P1 | 待开始 | 方便用户配置存客宝对接参数 |
|
||||
| F-CKB-6 | 管理端展示存客宝对接状态和日志 | 6h | P1 | 待开始 | 便于监控对接情况 |
|
||||
| F-CKB-7 | 管理端提供手动触发线索上报功能 | 4h | P2 | 待开始 | 方便测试和特殊场景使用 |
|
||||
|
||||
#### Phase 5: 部署与测试 - 预计完成度 0%
|
||||
|
||||
| 任务 ID | 任务描述 | 预估工时 | 优先级 | 状态 | 备注 |
|
||||
|:-------:|:---------|:--------:|:------:|:----:|:-----|
|
||||
| D-CKB-1 | 编写存客宝对接的单元测试和集成测试 | 10h | P0 | 待开始 | 确保代码质量和功能正确性 |
|
||||
| D-CKB-2 | 进行端到端测试,验证 Frida Hook → 后端服务 → 存客宝的完整链路 | 12h | P0 | 待开始 | 模拟真实场景,确保功能稳定 |
|
||||
| D-CKB-3 | 更新部署脚本和文档,包含存客宝对接相关配置 | 4h | P1 | 待开始 | 确保部署顺利 |
|
||||
|
||||
### 4. 整体项目进度预估
|
||||
|
||||
考虑到 Frida 接口的复杂性和存客宝对接的深度,预计总工期将增加约 **80-100 个工作日**。
|
||||
|
||||
### 5. 下一步执行
|
||||
|
||||
1. **更新开发文档**:将此规划内容更新到“开发文档/10、项目管理/项目落地执行表.md”中。
|
||||
2. **GitHub 同步**:将所有修改同步到 GitHub 仓库。
|
||||
3. **开始开发**:从 Phase 2 的 Frida Hook 扩展任务开始执行。
|
||||
|
||||
**当前总开发百分比:20%**
|
||||
**当前开发进度百分比:0% (本阶段)**
|
||||
|
||||
19
开发文档/10、项目管理/项目落地执行表.md
Normal file
19
开发文档/10、项目管理/项目落地执行表.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# 项目落地执行表
|
||||
|
||||
> 项目:工作手机 · 微信 / 手机端 / SDK / Frida 补全
|
||||
> 本轮时间:2026-05-18
|
||||
> 当前总进度:约 55%
|
||||
> 当前阶段进度:实现阶段约 70%
|
||||
|
||||
| 时间 | 人设 | 用户原话整理与扫描 | 执行目标 | 已完成结果 | 生成/修改文件 | 本地同步状态 | GitHub同步状态 | 阻塞与下一步 |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| 2026-05-18 08:58 | 阿表 | 用户要求继续上传 ZIP 的进度,完成微信、手机、SDK、Frida。重点不是反复测试,而是把服务器能控制手机微信的功能开发出来,并逐项验证。 | 解析归档,恢复历史进度,明确 Frida RPC 与微信 Hook 缺口。 | 已确认历史进度:Frida 连接曾成功,微信 PID 可见;Hook 方法约 112 个;核心问题是 RPC 方法名兼容和源码缺失。 | `/home/ubuntu/karuo_zip_work/工作手机_微信Frida_SDK续跑任务清单.md` | 尚未同步到本地项目,桌面终端连接曾中断,稍后尝试写入挂载目录。 | 尚未同步;未发现可安全使用的 GitHub Connector。 | 继续生成 SDK/Agent/Hook/验证脚本。 |
|
||||
| 2026-05-18 09:05 | 阿机/阿桥 | 继续补齐服务器到手机端再到 Frida Hook 的闭环,并保留后续真机验证入口。 | 重建最小可运行代码包。 | 已生成 FastAPI 路由、WebSocket 手机端 Agent、FridaManager、HookExecutor、微信动作目录、Hook 桥接模板、自动验证脚本。 | `/home/ubuntu/work_phone_sdk_completion/server/app.py`、`/home/ubuntu/work_phone_sdk_completion/server/routes/wechat_frida.py`、`/home/ubuntu/work_phone_sdk_completion/mobile_agent/wireless_agent.py`、`/home/ubuntu/work_phone_sdk_completion/sdk/frida/frida_manager.py`、`/home/ubuntu/work_phone_sdk_completion/sdk/frida/hook_executor.py`、`/home/ubuntu/work_phone_sdk_completion/sdk/wechat/wechat_actions.py`、`/home/ubuntu/work_phone_sdk_completion/hooks/wechat_hook_bridge.js`、`/home/ubuntu/work_phone_sdk_completion/scripts/verify_wechat_frida.py` | 尚未同步到本地项目,待复制。 | 尚未同步;需要用户启用 GitHub 集成或提供仓库。 | 运行语法检查和 Mock RPC 测试。 |
|
||||
| 2026-05-18 09:08 | 阿端/阿表 | 要求每次迭代更新开发文档,方便 Manus/Cursor/卡若AI 继续开发。 | 写入部署说明和验证流程。 | 已完成部署文档,包含服务器启动、手机 Termux 启动、设备查看、危险动作验证开关、历史问题修复点。 | `/home/ubuntu/work_phone_sdk_completion/开发文档/8、部署/微信Frida无线部署与验证说明.md`、`/home/ubuntu/work_phone_sdk_completion/README.md` | 尚未同步到本地项目,待复制。 | 尚未同步;需要仓库入口。 | 打包并尝试同步到本地挂载目录。 |
|
||||
|
||||
|
||||
## 2026-05-18 09:15|微信 Frida / 手机 SDK 续跑补全
|
||||
|
||||
| 时间 | 人设=浏览器 | 用户对话整理与扫描 | 本轮目标 | 已完成结果 | 生成/修改文件链接 | 总进度 | 当前阶段进度 | 本地同步 | GitHub同步 | 下一步 |
|
||||
|---|---|---|---|---|---|---:|---:|---|---|---|
|
||||
| 2026-05-18 09:15 | Manus 沙箱;本轮未能获取前端会话公开链接 | 用户要求使用 `/karuo-ai-unified` 与 `/jiqi-web-dev-orchestrator` 继续上传 ZIP 的进度,重点完成微信、手机端、SDK、Frida 相关功能;归档显示历史曾连上 Frida/微信 PID,但核心源码片段不完整,RPC 方法名存在 camelCase/小写争议。 | 不再反复测试,以开发闭环为核心,补齐服务器控制手机微信能力的 SDK、Agent、API、Hook 桥接与验证报告脚本。 | 已生成可运行补全包:FastAPI 路由、WebSocket 手机 Agent、FridaManager、HookExecutor、微信动作目录、Hook 桥接模板、自动验证脚本;完成 Python 语法检查、Mock RPC 测试、API 路由测试。 | 本地补全包:`/Users/karuo/Documents/开发/2、私域银行/工作手机/工作手机SDK补全包_微信Frida/`;架构文档:`/Users/karuo/Documents/开发/2、私域银行/工作手机/工作手机SDK补全包_微信Frida/开发文档/2、架构/系统架构.md`;接口文档:`/Users/karuo/Documents/开发/2、私域银行/工作手机/工作手机SDK补全包_微信Frida/开发文档/5、接口/微信Frida控制接口契约.md`;部署文档:`/Users/karuo/Documents/开发/2、私域银行/工作手机/工作手机SDK补全包_微信Frida/开发文档/8、部署/微信Frida无线部署与验证说明.md` | 65% | 100% | ✅ 已通过挂载同步到本地项目目录 | ⚠️ 未同步:当前环境未启用 GitHub 集成,且不能直接使用明文 token 操作 | 手机/桌面端连接恢复后,把完整 `wechat_hook_v3.js` 替换桥接模板,运行 `verify_wechat_frida.py` 生成逐项结果与截图。 |
|
||||
Reference in New Issue
Block a user