First commit
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
# First
|
||||
|
||||
> 微信小程序调试工具 —— 基于 Frida + CDP 代理,支持 GUI 与 CLI 双模式
|
||||
|
||||
---
|
||||
|
||||
## 截图预览
|
||||
|
||||
### 主界面 / Control Panel
|
||||

|
||||
|
||||
### 路由导航
|
||||

|
||||
|
||||
### 云函数分析
|
||||

|
||||
|
||||
### 安全扫描
|
||||
待优化
|
||||
|
||||
---
|
||||
|
||||
## 功能特性
|
||||
|
||||
- 通过 Frida 注入微信客户端,转发小程序调试协议
|
||||
- CDP 代理桥接,支持 Chrome DevTools 直接连接调试
|
||||
- 路由枚举与一键跳转导航
|
||||
- 云函数调用监控与参数分析
|
||||
- UserScript 自动注入(支持按 URL 匹配、run-at 时机控制)
|
||||
- 内置安全扫描模块,生成扫描报告
|
||||
- 深色 / 浅色主题切换
|
||||
- GUI(PySide6)与 CLI 双模式运行
|
||||
|
||||
---
|
||||
|
||||
## 环境要求
|
||||
|
||||
| 依赖 | 版本 |
|
||||
|------|------|
|
||||
| Python | >= 3.9 |
|
||||
| frida | >= 17.0.0 |
|
||||
| websockets | >= 12.0 |
|
||||
| protobuf | >= 4.0.0 |
|
||||
| PySide6 | >= 6.5.0 |
|
||||
|
||||
安装依赖:
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 使用方法
|
||||
|
||||
### GUI 模式(推荐)
|
||||
|
||||
```bash
|
||||
python gui.py
|
||||
```
|
||||
|
||||
启动后在主界面点击 **Start** 即可开始调试。
|
||||
|
||||
### CLI 模式
|
||||
|
||||
```bash
|
||||
# 默认端口启动
|
||||
python main.py
|
||||
|
||||
# 自定义端口
|
||||
python main.py --debug-port 9421 --cdp-port 62000
|
||||
|
||||
# 开启详细日志
|
||||
python main.py --debug-main --debug-frida
|
||||
```
|
||||
|
||||
### 连接 Chrome DevTools
|
||||
|
||||
启动后,在 Chrome 地址栏输入:
|
||||
|
||||
```
|
||||
devtools://devtools/bundled/inspector.html?ws=127.0.0.1:62000
|
||||
```
|
||||
|
||||
### 参数说明
|
||||
|
||||
| 参数 | 默认值 | 说明 |
|
||||
|------|--------|------|
|
||||
| `--debug-port` | `9421` | 远程调试服务端口 |
|
||||
| `--cdp-port` | `62000` | CDP 代理监听端口 |
|
||||
| `--debug-main` | 关闭 | 输出主进程调试日志 |
|
||||
| `--debug-frida` | 关闭 | 输出 Frida 客户端日志 |
|
||||
| `--scripts-dir` | `./userscripts` | UserScript 目录路径 |
|
||||
| `--script` | — | 指定单个 .js 文件注入(可多次使用) |
|
||||
|
||||
### UserScript 注入
|
||||
|
||||
将 `.js` 脚本放入 `userscripts/` 目录,工具启动时自动加载并按规则注入。
|
||||
|
||||
也可手动指定文件:
|
||||
|
||||
```bash
|
||||
python main.py --script ./my_hook.js --script ./another.js
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 打包为可执行文件
|
||||
|
||||
```bash
|
||||
pyinstaller WMPFDebugger.spec
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 参考项目
|
||||
|
||||
本项目参考并学习了以下优秀工具的实现思路:
|
||||
|
||||
- [evi0s/WMPFDebugger](https://github.com/evi0s/WMPFDebugger)
|
||||
|
||||
---
|
||||
|
||||
## 致谢
|
||||
|
||||
感谢 **0xsdeo** 师傅的大力支持与思路提供。
|
||||
|
||||
---
|
||||
|
||||
## 免责声明
|
||||
|
||||
本工具仅供安全研究与学习使用,请勿用于未授权的目标,使用者须自行承担相关法律责任。
|
||||
@@ -0,0 +1,52 @@
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
|
||||
|
||||
a = Analysis(
|
||||
['gui.py'],
|
||||
pathex=[],
|
||||
binaries=[],
|
||||
datas=[
|
||||
('src/nav_inject.js', 'src'),
|
||||
('src/cloud_audit_inject.js', 'src'),
|
||||
('frida/hook.js', 'frida'),
|
||||
('frida/config', 'frida/config'),
|
||||
('icon.ico', '.'),
|
||||
],
|
||||
hiddenimports=[
|
||||
'frida',
|
||||
'websockets',
|
||||
'google.protobuf',
|
||||
'PySide6.QtCore',
|
||||
'PySide6.QtGui',
|
||||
'PySide6.QtWidgets',
|
||||
],
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
runtime_hooks=[],
|
||||
excludes=[],
|
||||
noarchive=False,
|
||||
optimize=0,
|
||||
)
|
||||
pyz = PYZ(a.pure)
|
||||
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.datas,
|
||||
[],
|
||||
name='First',
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
runtime_tmpdir=None,
|
||||
console=False,
|
||||
disable_windowed_traceback=False,
|
||||
argv_emulation=False,
|
||||
target_arch=None,
|
||||
codesign_identity=None,
|
||||
entitlements_file=None,
|
||||
icon=['icon.ico'],
|
||||
)
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"Version": 11581,
|
||||
"LoadStartHookOffset": "0x28E9190",
|
||||
"CDPFilterHookOffset": "0x38C4350",
|
||||
"SceneOffsets": [1208, 1160]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"Version": 11633,
|
||||
"LoadStartHookOffset": "0x28F22A0",
|
||||
"CDPFilterHookOffset": "0x38D41E0",
|
||||
"SceneOffsets": [1208, 1160]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"Version": 13331,
|
||||
"LoadStartHookOffset": "0x0FFC200",
|
||||
"CDPFilterHookOffset": "0x2420100",
|
||||
"SceneOffsets": [1272, 1224]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"Version": 13341,
|
||||
"LoadStartHookOffset": "0x10009E0",
|
||||
"CDPFilterHookOffset": "0x242E8E0",
|
||||
"SceneOffsets": [1272, 1224]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"Version": 13487,
|
||||
"LoadStartHookOffset": "0x0FFB600",
|
||||
"CDPFilterHookOffset": "0x241FEB0",
|
||||
"SceneOffsets": [1272, 1224]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"Version": 13639,
|
||||
"LoadStartHookOffset": "0x1000990",
|
||||
"CDPFilterHookOffset": "0x2424DE0",
|
||||
"SceneOffsets": [1272, 1224]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"Version": 13655,
|
||||
"LoadStartHookOffset": "0x100F4B0",
|
||||
"CDPFilterHookOffset": "0x244A9E0",
|
||||
"SceneOffsets": [1280, 1232]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"Version": 13871,
|
||||
"LoadStartHookOffset": "0x101F160",
|
||||
"CDPFilterHookOffset": "0x246FC40",
|
||||
"SceneOffsets": [1360, 1312]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"Version": 13909,
|
||||
"LoadStartHookOffset": "0x101F0E0",
|
||||
"CDPFilterHookOffset": "0x246FDC0",
|
||||
"SceneOffsets": [1360, 1312]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"Version": 14161,
|
||||
"LoadStartHookOffset": "0x10246C0",
|
||||
"CDPFilterHookOffset": "0x24839B0",
|
||||
"SceneOffsets": [1360, 1312]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"Version": 14199,
|
||||
"LoadStartHookOffset": "0x10246F0",
|
||||
"CDPFilterHookOffset": "0x24839E0",
|
||||
"SceneOffsets": [1360, 1312]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"Version": 14315,
|
||||
"LoadStartHookOffset": "0x10004C0",
|
||||
"CDPFilterHookOffset": "0x2424B50",
|
||||
"SceneOffsets": [1272, 1224]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"Version": 16133,
|
||||
"LoadStartHookOffset": "0x470FAD0",
|
||||
"CDPFilterHookOffset": "0x90FC7E0",
|
||||
"SceneOffsets": [1360, 1312]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"Version": 16203,
|
||||
"LoadStartHookOffset": "0x4710890",
|
||||
"CDPFilterHookOffset": "0x90FD640",
|
||||
"SceneOffsets": [1360, 1312]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"Version": 16389,
|
||||
"LoadStartHookOffset": "0x24E4830",
|
||||
"CDPFilterHookOffset": "0x2E2A880",
|
||||
"SceneOffsets": [1360, 1312]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"Version": 16467,
|
||||
"LoadStartHookOffset": "0x24E4FD0",
|
||||
"CDPFilterHookOffset": "0x2E2CC90",
|
||||
"SceneOffsets": [1360, 1312]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"Version": 16771,
|
||||
"LoadStartHookOffset": "0x24E9130",
|
||||
"CDPFilterHookOffset": "0x2E3C470",
|
||||
"SceneOffsets": [1360, 1312]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"Version": 16815,
|
||||
"LoadStartHookOffset": "0x2509690",
|
||||
"CDPFilterHookOffset": "0x2E768D0",
|
||||
"SceneOffsets": [1416, 1360]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"Version": 16965,
|
||||
"LoadStartHookOffset": "0x2570220",
|
||||
"CDPFilterHookOffset": "0x2F844A0",
|
||||
"SceneOffsets": [1416, 1360]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"Version": 17037,
|
||||
"LoadStartHookOffset": "0x257D0A0",
|
||||
"CDPFilterHookOffset": "0x2FB2310",
|
||||
"SceneOffsets": [1408, 1352]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"Version": 17071,
|
||||
"LoadStartHookOffset": "0x258F370",
|
||||
"CDPFilterHookOffset": "0x2FD3080",
|
||||
"SceneOffsets": [1408, 1352]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"Version": 17127,
|
||||
"LoadStartHookOffset": "0x2590910",
|
||||
"CDPFilterHookOffset": "0x2FD4040",
|
||||
"SceneOffsets": [1408, 1352]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"Version": 18055,
|
||||
"LoadStartHookOffset": "0x25A1040",
|
||||
"CDPFilterHookOffset": "0x30031E0",
|
||||
"SceneOffsets": [1416, 1352]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"Version": 18151,
|
||||
"LoadStartHookOffset": "0x25A2E20",
|
||||
"CDPFilterHookOffset": "0x3013D20",
|
||||
"SceneOffsets": [1416, 1352]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"Version": 18787,
|
||||
"LoadStartHookOffset": "0x25B2870",
|
||||
"CDPFilterHookOffset": "0x3028AD0",
|
||||
"SceneOffsets": [1408, 1344]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"Version": 18891,
|
||||
"LoadStartHookOffset": "0x25B50C0",
|
||||
"CDPFilterHookOffset": "0x30245E0",
|
||||
"SceneOffsets": [1408, 1344]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"Version": 18955,
|
||||
"LoadStartHookOffset": "0x25B52C0",
|
||||
"CDPFilterHookOffset": "0x30248B0",
|
||||
"SceneOffsets": [1408, 1344]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"Version": 19027,
|
||||
"LoadStartHookOffset": "0x25B52D0",
|
||||
"CDPFilterHookOffset": "0x3024AD0",
|
||||
"SceneOffsets": [1408, 1344]
|
||||
}
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
const getMainModule = (version) => {
|
||||
if (version >= 13331) {
|
||||
return Process.findModuleByName("flue.dll");
|
||||
}
|
||||
return Process.findModuleByName("WeChatAppEx.exe");
|
||||
};
|
||||
|
||||
const patchCDPFilter = (base, config) => {
|
||||
// xref: SendToClientFilter OR devtools_message_filter_applet_webview.cc
|
||||
const offset = config.CDPFilterHookOffset;
|
||||
Interceptor.attach(base.add(offset), {
|
||||
onEnter(args) {
|
||||
send(
|
||||
`[patch] CDP filter on enter, original value of input: ${args[0].readPointer()}`,
|
||||
);
|
||||
this.inputValue = args[0];
|
||||
},
|
||||
onLeave(retval) {
|
||||
const inputValue = this.inputValue.readPointer();
|
||||
if (inputValue.isNull() || inputValue.add(8).isNull()) {
|
||||
// there's a chance the value could be null
|
||||
// return here to avoid crash
|
||||
return;
|
||||
}
|
||||
|
||||
send(
|
||||
`[patch] CDP filter on leave, patch input, now value: ${inputValue}; ` +
|
||||
`*(input + 8) = ${inputValue.add(8).readU32()}`,
|
||||
);
|
||||
if (inputValue.add(8).readU32() == 6) {
|
||||
inputValue.add(8).writeU32(0x0);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const hookOnLoadScene = (a1, sceneOffsets) => {
|
||||
const miniappConfigPtr = a1
|
||||
.add(56)
|
||||
.readPointer()
|
||||
.add(sceneOffsets[0])
|
||||
.readPointer();
|
||||
const miniappScenePtr = miniappConfigPtr
|
||||
.add(8)
|
||||
.readPointer()
|
||||
.add(sceneOffsets[1])
|
||||
.readPointer()
|
||||
.add(16)
|
||||
.readPointer()
|
||||
.add(488);
|
||||
send(`[hook] scene: ${miniappScenePtr.readInt()}`);
|
||||
|
||||
// 1000: from issue #83 <-- will crash the process
|
||||
// 1007: from issue #80
|
||||
// 1008: from issue #53
|
||||
// 1027: from issue #78
|
||||
// 1035: from issue #78
|
||||
// 1053: from issue #25
|
||||
// 1074: from issue #32
|
||||
// 1145: from search
|
||||
// 1178: from phone (issue #117)
|
||||
// 1256: from recent
|
||||
// 1260: from frequently used
|
||||
// 1302: from services
|
||||
// 1308: minigame?
|
||||
const sceneNumberArray = [
|
||||
1005, 1007, 1008, 1027, 1035, 1053, 1074, 1145, 1178, 1256, 1260, 1302,
|
||||
1308,
|
||||
];
|
||||
if (!sceneNumberArray.includes(miniappScenePtr.readInt())) {
|
||||
return;
|
||||
}
|
||||
send("[hook] hook scene condition -> 1101");
|
||||
miniappScenePtr.writeInt(1101);
|
||||
|
||||
// TODO: customize debugging endpoint
|
||||
// const websocketServerStringPtr = passArgs.add(8).readPointer().add(520);
|
||||
// VERBOSE && console.log("[hook] hook websocket server, original: ", websocketServerStringPtr.readUtf8String());
|
||||
// websocketServerStringPtr.writeUtf8String("ws://127.0.0.1:8189/");
|
||||
};
|
||||
|
||||
const patchOnLoadStart = (base, config) => {
|
||||
// xref: AppletIndexContainer::OnLoadStart
|
||||
Interceptor.attach(base.add(config.LoadStartHookOffset), {
|
||||
onEnter(args) {
|
||||
send(
|
||||
`[inteceptor] AppletIndexContainer::OnLoadStart onEnter, ` +
|
||||
`indexContainer.this: ${this.context.rcx}`,
|
||||
);
|
||||
// write dl to 0x1
|
||||
if ((this.context.rdx & 0xff) !== 1) {
|
||||
this.context.rdx = (this.context.rdx & ~0xff) | 0x1;
|
||||
}
|
||||
// handle onLoad scene
|
||||
hookOnLoadScene(this.context.rcx, config.SceneOffsets);
|
||||
},
|
||||
onLeave(retval) {
|
||||
// do nothing
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const parseConfig = () => {
|
||||
const rawConfig = `@@CONFIG@@`;
|
||||
if (rawConfig.includes("@@")) {
|
||||
// test addresses
|
||||
return {
|
||||
Version: 18955,
|
||||
LoadStartHookOffset: "0x25B52C0",
|
||||
CDPFilterHookOffset: "0x30248B0",
|
||||
SceneOffsets: [1408, 1344],
|
||||
};
|
||||
}
|
||||
return JSON.parse(rawConfig);
|
||||
};
|
||||
|
||||
const main = () => {
|
||||
const config = parseConfig();
|
||||
const mainModule = getMainModule(config.Version);
|
||||
patchOnLoadStart(mainModule.base, config);
|
||||
patchCDPFilter(mainModule.base, config);
|
||||
};
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"theme": "dark",
|
||||
"debug_port": "9421",
|
||||
"cdp_port": "62000",
|
||||
"debug_main": false,
|
||||
"debug_frida": false,
|
||||
"selected_preload_scripts": [],
|
||||
"custom_headers": []
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
console.log("[Hook 测试] Hello from hook_scripts! 注入成功 ✓");
|
||||
@@ -0,0 +1,49 @@
|
||||
"""
|
||||
First
|
||||
Author: vs-olitus | https://github.com/vs-olitus/First
|
||||
"""
|
||||
import asyncio
|
||||
import sys
|
||||
|
||||
from src.cli import parse_cli_options
|
||||
from src.logger import create_logger
|
||||
from src.engine import DebugEngine
|
||||
from src.userscript import load_userscripts, load_userscripts_by_files
|
||||
|
||||
|
||||
async def main():
|
||||
options = parse_cli_options()
|
||||
logger = create_logger(options)
|
||||
|
||||
# Load userscripts
|
||||
if options.script_files:
|
||||
userscripts = load_userscripts_by_files(options.script_files)
|
||||
else:
|
||||
userscripts = load_userscripts(options.scripts_dir)
|
||||
if userscripts:
|
||||
logger.info(f"[userscript] loaded {len(userscripts)} userscript(s) from {options.scripts_dir}:")
|
||||
for s in userscripts:
|
||||
logger.info(f"[userscript] - {s.name} (run-at: {s.run_at}, match: {s.match})")
|
||||
else:
|
||||
logger.info(f"[userscript] no userscripts found in {options.scripts_dir}")
|
||||
|
||||
engine = DebugEngine(options, logger, userscripts)
|
||||
try:
|
||||
await engine.start()
|
||||
except Exception:
|
||||
sys.exit(1)
|
||||
|
||||
# Keep running until interrupted
|
||||
try:
|
||||
await asyncio.Future()
|
||||
except (KeyboardInterrupt, asyncio.CancelledError):
|
||||
logger.info("[server] shutting down...")
|
||||
finally:
|
||||
await engine.stop()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
asyncio.run(main())
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
@@ -0,0 +1,299 @@
|
||||
syntax = "proto2";
|
||||
|
||||
package mmbizwxadevremote;
|
||||
|
||||
message WARemoteDebug_DataFormat {
|
||||
optional uint32 cmd = 1;
|
||||
optional string uuid = 2;
|
||||
optional bytes data = 3;
|
||||
}
|
||||
|
||||
message WARemoteDebug_BaseReq {
|
||||
optional uint32 clientVersion = 1;
|
||||
}
|
||||
|
||||
message WARemoteDebug_BaseResp {
|
||||
optional int32 errcode = 1;
|
||||
optional string errmsg = 2;
|
||||
}
|
||||
|
||||
message WARemoteDebug_CommReq {
|
||||
optional WARemoteDebug_BaseReq baseRequest = 1;
|
||||
}
|
||||
|
||||
message WARemoteDebug_CommResp {
|
||||
optional WARemoteDebug_BaseResp baseResponse = 1;
|
||||
}
|
||||
|
||||
message WARemoteDebug_DebugMessage {
|
||||
optional uint32 seq = 1;
|
||||
optional uint32 after = 2;
|
||||
optional string category = 3;
|
||||
optional bytes data = 4;
|
||||
optional uint32 compressAlgo = 5;
|
||||
optional uint32 originalSize = 6;
|
||||
}
|
||||
|
||||
message WARemoteDebug_SendDebugMessageReq {
|
||||
optional WARemoteDebug_BaseReq baseRequest = 1;
|
||||
repeated WARemoteDebug_DebugMessage debugMessageList = 2;
|
||||
optional uint32 recvAck = 3;
|
||||
}
|
||||
|
||||
message WARemoteDebug_SendDebugMessageResp {
|
||||
optional WARemoteDebug_BaseResp baseResponse = 1;
|
||||
optional uint32 sendAck = 2;
|
||||
}
|
||||
|
||||
message WARemoteDebug_NewSendDebugMessageReq {
|
||||
optional WARemoteDebug_BaseReq baseRequest = 1;
|
||||
repeated WARemoteDebug_DebugMessage debugMessageList = 2;
|
||||
optional uint32 recvAck = 3;
|
||||
}
|
||||
|
||||
message WARemoteDebug_NewSendDebugMessageResp {
|
||||
optional WARemoteDebug_BaseResp baseResponse = 1;
|
||||
optional uint32 minAck = 2;
|
||||
optional uint32 maxAck = 3;
|
||||
}
|
||||
|
||||
message WARemoteDebug_MessageNotify {
|
||||
repeated WARemoteDebug_DebugMessage debugMessageList = 1;
|
||||
}
|
||||
|
||||
message WARemoteDebug_MessageNotifyResp {
|
||||
optional uint32 recvAck = 1;
|
||||
}
|
||||
|
||||
message WARemoteDebug_EventNotify {
|
||||
}
|
||||
|
||||
message WARemoteDebug_EventNotifyResp {
|
||||
}
|
||||
|
||||
message WARemoteDebug_WxHeartBeatReq {
|
||||
optional WARemoteDebug_BaseReq baseRequest = 1;
|
||||
optional uint32 recvAck = 2;
|
||||
}
|
||||
|
||||
message WARemoteDebug_WxHeartBeatResp {
|
||||
optional WARemoteDebug_BaseResp baseResponse = 1;
|
||||
}
|
||||
|
||||
message WARemoteDebug_DevHeartBeatReq {
|
||||
optional WARemoteDebug_BaseReq baseRequest = 1;
|
||||
optional uint32 recvAck = 2;
|
||||
}
|
||||
|
||||
message WARemoteDebug_DevHeartBeatResp {
|
||||
optional WARemoteDebug_BaseResp baseResponse = 1;
|
||||
}
|
||||
|
||||
message WARemoteDebug_RoomInfo {
|
||||
optional bool joinRoom = 1;
|
||||
optional string roomId = 2;
|
||||
optional string originalMd5 = 3;
|
||||
optional uint32 roomStatus = 4;
|
||||
optional uint32 wxConnStatus = 5;
|
||||
optional uint32 devConnStatus = 6;
|
||||
}
|
||||
|
||||
message WARemoteDebug_WxLoginReq {
|
||||
optional WARemoteDebug_BaseReq baseRequest = 1;
|
||||
optional string loginTicket = 2;
|
||||
}
|
||||
|
||||
message WARemoteDebug_WxLoginResp {
|
||||
optional WARemoteDebug_BaseResp baseResponse = 1;
|
||||
optional WARemoteDebug_RoomInfo roomInfo = 2;
|
||||
}
|
||||
|
||||
message WARemoteDebug_DevLoginReq {
|
||||
optional WARemoteDebug_BaseReq baseRequest = 1;
|
||||
optional string newticket = 2;
|
||||
optional uint32 autodev = 3;
|
||||
}
|
||||
|
||||
message WARemoteDebug_DevLoginResp {
|
||||
optional WARemoteDebug_BaseResp baseResponse = 1;
|
||||
optional WARemoteDebug_RoomInfo roomInfo = 2;
|
||||
}
|
||||
|
||||
message WARemoteDebug_WxJoinRoomReq {
|
||||
optional WARemoteDebug_BaseReq baseRequest = 1;
|
||||
optional string username = 2;
|
||||
optional string roomId = 3;
|
||||
optional string wxpkgInfo = 4;
|
||||
}
|
||||
|
||||
message WARemoteDebug_WxJoinRoomResp {
|
||||
optional WARemoteDebug_BaseResp baseResponse = 1;
|
||||
}
|
||||
|
||||
message WARemoteDebug_DevJoinRoomReq {
|
||||
optional WARemoteDebug_BaseReq baseRequest = 1;
|
||||
optional string appid = 2;
|
||||
optional string roomId = 3;
|
||||
optional string wxpkgInfo = 4;
|
||||
}
|
||||
|
||||
message WARemoteDebug_DevJoinRoomResp {
|
||||
optional WARemoteDebug_BaseResp baseResponse = 1;
|
||||
}
|
||||
|
||||
message WARemoteDebug_WxQuitRoomReq {
|
||||
optional WARemoteDebug_BaseReq baseRequest = 1;
|
||||
}
|
||||
|
||||
message WARemoteDebug_WxQuitRoomResp {
|
||||
optional WARemoteDebug_BaseResp baseResponse = 1;
|
||||
}
|
||||
|
||||
message WARemoteDebug_DevQuitRoomReq {
|
||||
optional WARemoteDebug_BaseReq baseRequest = 1;
|
||||
}
|
||||
|
||||
message WARemoteDebug_DevQuitRoomResp {
|
||||
optional WARemoteDebug_BaseResp baseResponse = 1;
|
||||
}
|
||||
|
||||
message WARemoteDebug_WxSyncMessageReq {
|
||||
optional WARemoteDebug_BaseReq baseRequest = 1;
|
||||
optional uint32 minSeq = 2;
|
||||
optional uint32 maxSeq = 3;
|
||||
}
|
||||
|
||||
message WARemoteDebug_WxSyncMessageResp {
|
||||
optional WARemoteDebug_BaseResp baseResponse = 1;
|
||||
repeated WARemoteDebug_DebugMessage debugMessageList = 2;
|
||||
optional uint32 sendAck = 3;
|
||||
}
|
||||
|
||||
message WARemoteDebug_DevSyncMessageReq {
|
||||
optional WARemoteDebug_BaseReq baseRequest = 1;
|
||||
optional uint32 minSeq = 2;
|
||||
optional uint32 maxSeq = 3;
|
||||
}
|
||||
|
||||
message WARemoteDebug_DevSyncMessageResp {
|
||||
optional WARemoteDebug_BaseResp baseResponse = 1;
|
||||
repeated WARemoteDebug_DebugMessage debugMessageList = 2;
|
||||
optional uint32 sendAck = 3;
|
||||
}
|
||||
|
||||
message WARemoteDebug_MethodWithArgs {
|
||||
optional string methodName = 1;
|
||||
repeated string methodArgList = 2;
|
||||
}
|
||||
|
||||
message WARemoteDebug_RegisterInterface {
|
||||
optional string objName = 1;
|
||||
repeated WARemoteDebug_MethodWithArgs objMethodList = 2;
|
||||
}
|
||||
|
||||
message WARemoteDebug_DeviceInfo {
|
||||
optional string deviceName = 1;
|
||||
optional string deviceModel = 2;
|
||||
optional string systemVersion = 3;
|
||||
optional string wechatVersion = 4;
|
||||
optional uint32 publibVersion = 5;
|
||||
optional float screenWidth = 6;
|
||||
optional float pixelRatio = 7;
|
||||
optional string userAgent = 8;
|
||||
}
|
||||
|
||||
message WARemoteDebug_SetupContext {
|
||||
optional WARemoteDebug_RegisterInterface registerInterface = 1;
|
||||
optional WARemoteDebug_DeviceInfo deviceInfo = 2;
|
||||
optional string configureJs = 3;
|
||||
optional string publicJsMd5 = 4;
|
||||
optional string threeJsMd5 = 5;
|
||||
optional uint32 supportCompressAlgo = 6;
|
||||
}
|
||||
|
||||
message WARemoteDebug_CallInterface {
|
||||
optional string objName = 1;
|
||||
optional string methodName = 2;
|
||||
repeated string methodArgList = 3;
|
||||
optional uint32 callId = 4;
|
||||
}
|
||||
|
||||
message WARemoteDebug_CallInterfaceResult {
|
||||
optional string ret = 1;
|
||||
optional uint32 callId = 2;
|
||||
optional string debugInfo = 3;
|
||||
}
|
||||
|
||||
message WARemoteDebug_EvaluateJavascript {
|
||||
optional string script = 1;
|
||||
optional uint32 evaluateId = 2;
|
||||
optional string debugInfo = 3;
|
||||
}
|
||||
|
||||
message WARemoteDebug_EvaluateJavascriptResult {
|
||||
optional string ret = 1;
|
||||
optional uint32 evaluateId = 2;
|
||||
}
|
||||
|
||||
message WARemoteDebug_Breakpoint {
|
||||
optional bool isHit = 1;
|
||||
}
|
||||
|
||||
message WARemoteDebug_Ping {
|
||||
optional uint64 pingId = 1;
|
||||
optional string payload = 2;
|
||||
}
|
||||
|
||||
message WARemoteDebug_Pong {
|
||||
optional uint64 pingId = 1;
|
||||
optional uint32 networkType = 2;
|
||||
optional string payload = 3;
|
||||
}
|
||||
|
||||
message WARemoteDebug_DomOp {
|
||||
optional string params = 1;
|
||||
optional uint32 webviewId = 2;
|
||||
}
|
||||
|
||||
message WARemoteDebug_DomEvent {
|
||||
optional string params = 1;
|
||||
optional uint32 webviewId = 2;
|
||||
}
|
||||
|
||||
message WARemoteDebug_NetworkDebugAPI {
|
||||
optional string apiName = 1;
|
||||
optional string taskId = 2;
|
||||
optional string requestHeaders = 3;
|
||||
optional uint64 timestamp = 4;
|
||||
}
|
||||
|
||||
message WARemoteDebug_ChromeDevtools {
|
||||
optional uint64 opId = 1;
|
||||
optional string payload = 2;
|
||||
optional string jscontextId = 3;
|
||||
}
|
||||
|
||||
message WARemoteDebug_ChromeDevtoolsResult {
|
||||
optional uint64 opId = 1;
|
||||
optional string payload = 2;
|
||||
optional string jscontextId = 3;
|
||||
}
|
||||
|
||||
message WARemoteDebug_AddJsContext {
|
||||
optional string jscontextId = 1;
|
||||
optional string jscontextName = 2;
|
||||
}
|
||||
|
||||
message WARemoteDebug_RemoveJsContext {
|
||||
optional string jscontextId = 1;
|
||||
}
|
||||
|
||||
message WARemoteDebug_ConnectJsContext {
|
||||
optional string jscontextId = 1;
|
||||
}
|
||||
|
||||
message WARemoteDebug_CustomMessage {
|
||||
optional string method = 1;
|
||||
optional string payload = 2;
|
||||
optional bytes raw = 3;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
frida>=17.0.0
|
||||
websockets>=12.0
|
||||
protobuf>=4.0.0
|
||||
PySide6>=6.5.0
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+81
@@ -0,0 +1,81 @@
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
|
||||
DEBUG_PORT = 9421
|
||||
CDP_PORT = 62000
|
||||
|
||||
|
||||
class CliOptions:
|
||||
def __init__(self, debug_port: int, cdp_port: int, debug_main: bool, debug_frida: bool,
|
||||
scripts_dir: str = "", script_files: list = None):
|
||||
self.debug_port = debug_port
|
||||
self.cdp_port = cdp_port
|
||||
self.debug_main = debug_main
|
||||
self.debug_frida = debug_frida
|
||||
self.scripts_dir = scripts_dir
|
||||
self.script_files = script_files or []
|
||||
|
||||
|
||||
def parse_port(name: str, value, default_value: int) -> int:
|
||||
if value is None:
|
||||
return default_value
|
||||
try:
|
||||
port = int(value)
|
||||
except (ValueError, TypeError):
|
||||
raise ValueError(f"[main] invalid {name}: {value}")
|
||||
if port < 1 or port > 65535:
|
||||
raise ValueError(f"[main] invalid {name}: {value}")
|
||||
return port
|
||||
|
||||
|
||||
def parse_cli_options() -> CliOptions:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="First - WeChat Miniapp Debugger",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog=f"""Examples:
|
||||
python main.py
|
||||
python main.py --debug-port 9421 --cdp-port 62000
|
||||
python main.py --debug-main --debug-frida
|
||||
|
||||
Default ports:
|
||||
Debug server: {DEBUG_PORT}
|
||||
CDP proxy: {CDP_PORT}
|
||||
|
||||
Usage:
|
||||
Connect Chrome DevTools to: devtools://devtools/bundled/inspector.html?ws=127.0.0.1:<cdp-port>"""
|
||||
)
|
||||
parser.add_argument("--debug-port", type=str, default=None,
|
||||
help=f"Remote debug server port (default: {DEBUG_PORT})")
|
||||
parser.add_argument("--cdp-port", type=str, default=None,
|
||||
help=f"CDP proxy server port (default: {CDP_PORT})")
|
||||
parser.add_argument("--debug-main", action="store_true", default=False,
|
||||
help="Output main process debug messages")
|
||||
parser.add_argument("--debug-frida", action="store_true", default=False,
|
||||
help="Output Frida client messages")
|
||||
parser.add_argument("--scripts-dir", type=str, default=None,
|
||||
help="UserScripts directory path (default: ./userscripts)")
|
||||
parser.add_argument("--script", type=str, action="append", default=None,
|
||||
help="Specific .js file(s) to inject (can be used multiple times). "
|
||||
"When set, --scripts-dir is ignored.")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.scripts_dir:
|
||||
scripts_dir = os.path.abspath(args.scripts_dir)
|
||||
else:
|
||||
scripts_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "userscripts")
|
||||
|
||||
script_files = []
|
||||
if args.script:
|
||||
for f in args.script:
|
||||
script_files.append(os.path.abspath(f))
|
||||
|
||||
return CliOptions(
|
||||
debug_port=parse_port("--debug-port", args.debug_port, DEBUG_PORT),
|
||||
cdp_port=parse_port("--cdp-port", args.cdp_port, CDP_PORT),
|
||||
debug_main=args.debug_main,
|
||||
debug_frida=args.debug_frida,
|
||||
scripts_dir=scripts_dir,
|
||||
script_files=script_files,
|
||||
)
|
||||
@@ -0,0 +1,245 @@
|
||||
"""
|
||||
CloudAuditor — 全局云函数捕获(只显示实际触发过的)+ 手动调用
|
||||
"""
|
||||
import asyncio
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
_CLOUD_JS = (Path(__file__).parent / "cloud_audit_inject.js").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
class CloudAuditor:
|
||||
"""全局云函数 Hook 捕获"""
|
||||
|
||||
def __init__(self, engine):
|
||||
self.engine = engine
|
||||
self._injected = False
|
||||
self._enabled = False
|
||||
self._seen_count = 0 # 上次拉取到的 hookedCalls 长度
|
||||
|
||||
async def inject(self):
|
||||
await self.engine.evaluate_js(_CLOUD_JS, timeout=10.0)
|
||||
self._injected = True
|
||||
|
||||
async def start(self):
|
||||
"""启动全局 Hook:注入 JS,安装 hook,启动自动扫描新 frame"""
|
||||
if self._enabled:
|
||||
return {"ok": True, "already": True}
|
||||
await self.inject()
|
||||
result = await self.engine.evaluate_js(
|
||||
"JSON.stringify(window.cloudAudit.installHook())", timeout=5.0)
|
||||
value = self._extract_value(result)
|
||||
if value:
|
||||
try:
|
||||
info = json.loads(value)
|
||||
if info.get("ok"):
|
||||
self._enabled = True
|
||||
return info
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
pass
|
||||
return {"ok": False}
|
||||
|
||||
async def stop(self):
|
||||
"""停止 Hook"""
|
||||
if not self._enabled:
|
||||
return
|
||||
self._enabled = False
|
||||
try:
|
||||
await self.inject()
|
||||
await self.engine.evaluate_js(
|
||||
"window.cloudAudit.uninstallHook()", timeout=5.0)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
async def poll(self):
|
||||
"""拉取新捕获的云函数调用,只返回增量"""
|
||||
if not self._enabled:
|
||||
return []
|
||||
try:
|
||||
await self.inject()
|
||||
result = await self.engine.evaluate_js(
|
||||
"JSON.stringify(window.cloudAudit.getHookedCalls())", timeout=5.0)
|
||||
value = self._extract_value(result)
|
||||
if not value:
|
||||
return []
|
||||
calls = json.loads(value)
|
||||
if len(calls) <= self._seen_count:
|
||||
return []
|
||||
new_calls = calls[self._seen_count:]
|
||||
self._seen_count = len(calls)
|
||||
return new_calls
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
async def clear(self):
|
||||
"""清空捕获记录"""
|
||||
self._seen_count = 0
|
||||
try:
|
||||
await self.inject()
|
||||
await self.engine.evaluate_js(
|
||||
"window.cloudAudit.clearHookedCalls()", timeout=3.0)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
async def static_scan(self, on_progress=None):
|
||||
"""静态扫描:通过 CDP Debugger 获取 JS 源码,提取云函数引用"""
|
||||
import re
|
||||
_RE_NAME = re.compile(r'name\s*:\s*["\']([^"\']+)["\']')
|
||||
_RE_DATA = re.compile(r'data\s*:\s*\{([^}]{1,500})\}')
|
||||
_RE_FIELD = re.compile(r'(\w+)\s*:')
|
||||
_RE_COLL = re.compile(r'\.collection\s*\(\s*["\']([^"\']+)["\']\s*\)')
|
||||
_SKIP = {"name", "success", "fail", "complete", "config", "env", "data"}
|
||||
_FILE_M = {"uploadFile", "downloadFile", "deleteFile", "getTempFileURL"}
|
||||
_DB_OPS = {"add", "get", "update", "remove", "count", "aggregate", "doc", "where"}
|
||||
|
||||
# 获取当前 appId
|
||||
await self.inject()
|
||||
try:
|
||||
r = await self.engine.evaluate_js(
|
||||
"JSON.stringify(window.cloudAudit.detectEnv())", timeout=5.0)
|
||||
v = self._extract_value(r)
|
||||
if v:
|
||||
info = json.loads(v)
|
||||
if info.get("ok"):
|
||||
self._current_appid = info.get("appId", "")
|
||||
except Exception:
|
||||
pass
|
||||
appid = getattr(self, '_current_appid', '')
|
||||
|
||||
# 收集脚本
|
||||
script_ids = []
|
||||
def _on_parsed(data):
|
||||
p = data.get("params", {})
|
||||
sid = p.get("scriptId")
|
||||
if sid:
|
||||
script_ids.append((sid, p.get("url", "")))
|
||||
|
||||
self.engine.on_cdp_event("Debugger.scriptParsed", _on_parsed)
|
||||
try:
|
||||
try:
|
||||
await self.engine.send_cdp_command("Debugger.disable", timeout=3.0)
|
||||
except Exception:
|
||||
pass
|
||||
await asyncio.sleep(0.2)
|
||||
await self.engine.send_cdp_command("Debugger.enable", timeout=5.0)
|
||||
await asyncio.sleep(1.5)
|
||||
prev = 0
|
||||
for _ in range(5):
|
||||
if len(script_ids) == prev and prev > 0:
|
||||
break
|
||||
prev = len(script_ids)
|
||||
await asyncio.sleep(0.4)
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
self.engine.off_cdp_event("Debugger.scriptParsed", _on_parsed)
|
||||
|
||||
if on_progress:
|
||||
on_progress(f"发现 {len(script_ids)} 个脚本,开始扫描...")
|
||||
|
||||
found = {}
|
||||
for i, (sid, url) in enumerate(script_ids):
|
||||
if on_progress and i % 10 == 0:
|
||||
on_progress(f"扫描中... ({i}/{len(script_ids)}) 已发现 {len(found)} 个")
|
||||
try:
|
||||
resp = await self.engine.send_cdp_command(
|
||||
"Debugger.getScriptSource", {"scriptId": sid}, timeout=8.0)
|
||||
source = resp.get("result", {}).get("scriptSource", "")
|
||||
if not source:
|
||||
continue
|
||||
# callFunction
|
||||
pos = 0
|
||||
while True:
|
||||
pos = source.find("callFunction", pos)
|
||||
if pos == -1:
|
||||
break
|
||||
w = source[pos:pos+1000]
|
||||
nm = _RE_NAME.search(w)
|
||||
if nm:
|
||||
name = nm.group(1)
|
||||
key = f"fn:{name}"
|
||||
if key not in found:
|
||||
found[key] = {"type": "function", "name": name, "params": [], "count": 0}
|
||||
found[key]["count"] += 1
|
||||
dm = _RE_DATA.search(w)
|
||||
if dm:
|
||||
for fm in _RE_FIELD.finditer(dm.group(1)):
|
||||
f = fm.group(1)
|
||||
if f not in _SKIP and f not in found[key]["params"]:
|
||||
found[key]["params"].append(f)
|
||||
pos += 12
|
||||
# collection
|
||||
for m in _RE_COLL.finditer(source):
|
||||
coll = m.group(1)
|
||||
key = f"db:{coll}"
|
||||
if key not in found:
|
||||
found[key] = {"type": "database", "name": coll, "params": [], "count": 0}
|
||||
found[key]["count"] += 1
|
||||
after = source[m.end():m.end()+300]
|
||||
for op in _DB_OPS:
|
||||
if f".{op}(" in after and op not in found[key]["params"]:
|
||||
found[key]["params"].append(op)
|
||||
# storage
|
||||
for fm in _FILE_M:
|
||||
if fm in source:
|
||||
key = f"storage:{fm}"
|
||||
if key not in found:
|
||||
found[key] = {"type": "storage", "name": fm, "params": [], "count": 0}
|
||||
found[key]["count"] += 1
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
try:
|
||||
await self.engine.send_cdp_command("Debugger.disable", timeout=3.0)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
results = []
|
||||
for key, info in found.items():
|
||||
results.append({
|
||||
"name": info["name"],
|
||||
"type": info["type"],
|
||||
"appId": appid,
|
||||
"params": info["params"],
|
||||
"count": info["count"],
|
||||
})
|
||||
if on_progress:
|
||||
on_progress(f"扫描完成,发现 {len(results)} 个云函数引用")
|
||||
return results
|
||||
|
||||
async def call_function(self, name, data=None):
|
||||
await self.inject()
|
||||
safe_name = name.replace("'", "\\'")
|
||||
safe_data = json.dumps(data or {}, ensure_ascii=False)
|
||||
await self.engine.evaluate_js(
|
||||
"window._cloudAuditLastResult=null", timeout=3.0)
|
||||
js = (f"window.cloudAudit.callFunction('{safe_name}', {safe_data})"
|
||||
f".then(function(r){{window._cloudAuditLastResult=JSON.stringify(r)}})"
|
||||
f"['catch'](function(e){{window._cloudAuditLastResult="
|
||||
f"JSON.stringify({{ok:false,status:'fail',error:e.message||String(e)}})}})")
|
||||
await self.engine.evaluate_js(js, timeout=15.0)
|
||||
for _ in range(24):
|
||||
await asyncio.sleep(0.5)
|
||||
result = await self.engine.evaluate_js(
|
||||
"window._cloudAuditLastResult", timeout=5.0)
|
||||
value = self._extract_value(result)
|
||||
if value:
|
||||
try:
|
||||
return json.loads(value)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
return {"ok": False, "status": "fail", "reason": "parse error"}
|
||||
return {"ok": False, "status": "fail", "reason": "调用超时,无响应"}
|
||||
|
||||
def export_report(self, all_items, call_history):
|
||||
return {
|
||||
"captured_calls": all_items,
|
||||
"call_history": {k: v for k, v in call_history.items() if v},
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _extract_value(result):
|
||||
if not result:
|
||||
return None
|
||||
r = result.get("result", {})
|
||||
inner = r.get("result", {})
|
||||
return inner.get("value")
|
||||
@@ -0,0 +1,525 @@
|
||||
(function() {
|
||||
if (window._cloudAuditInjected) return;
|
||||
window._cloudAuditInjected = true;
|
||||
|
||||
var cloudAudit = {
|
||||
hookedCalls: [],
|
||||
_hookedClouds: [], // [{cloud, appId, origMethods}]
|
||||
_autoHookTimer: null,
|
||||
autoHookEnabled: false,
|
||||
|
||||
// ── 扫描所有 frame,收集所有 wx.cloud 实例 ──
|
||||
_findAllFrames: function() {
|
||||
var frames = [];
|
||||
var seen = [];
|
||||
function tryAdd(w) {
|
||||
try {
|
||||
if (!w || !w.wx || !w.__wxConfig) return;
|
||||
for (var i = 0; i < seen.length; i++) { if (seen[i] === w) return; }
|
||||
seen.push(w);
|
||||
frames.push(w);
|
||||
} catch(e) {}
|
||||
}
|
||||
tryAdd(window);
|
||||
var sources = [window];
|
||||
try { if (window.parent && window.parent !== window) sources.push(window.parent); } catch(e) {}
|
||||
for (var s = 0; s < sources.length; s++) {
|
||||
try {
|
||||
var src = sources[s];
|
||||
if (src.frames) {
|
||||
for (var i = 0; i < src.frames.length; i++) {
|
||||
try { tryAdd(src.frames[i]); } catch(e) {}
|
||||
}
|
||||
}
|
||||
} catch(e) {}
|
||||
}
|
||||
return frames;
|
||||
},
|
||||
|
||||
_getAppIdFromFrame: function(frame) {
|
||||
try {
|
||||
var cfg = frame.__wxConfig || {};
|
||||
if (cfg.accountInfo && cfg.accountInfo.appId) return cfg.accountInfo.appId;
|
||||
if (cfg.appId) return cfg.appId;
|
||||
} catch(e) {}
|
||||
return '';
|
||||
},
|
||||
|
||||
_getEnvFromFrame: function(frame) {
|
||||
try {
|
||||
var cfg = frame.__wxConfig || {};
|
||||
if (cfg.envList) return cfg.envList;
|
||||
if (cfg.cloud && cfg.cloud.env) return cfg.cloud.env;
|
||||
if (frame.wx.cloud && frame.wx.cloud._config) return frame.wx.cloud._config.env;
|
||||
} catch(e) {}
|
||||
return null;
|
||||
},
|
||||
|
||||
// ── 环境探测 (返回所有发现的小程序) ──
|
||||
detectEnv: function() {
|
||||
var frames = this._findAllFrames();
|
||||
if (frames.length === 0) {
|
||||
return { ok: false, reason: 'no miniprogram frames found' };
|
||||
}
|
||||
var apps = [];
|
||||
for (var i = 0; i < frames.length; i++) {
|
||||
var f = frames[i];
|
||||
try {
|
||||
if (!f.wx || !f.wx.cloud) continue;
|
||||
apps.push({
|
||||
appId: this._getAppIdFromFrame(f),
|
||||
env: this._getEnvFromFrame(f),
|
||||
inited: !!(f.wx.cloud._config || f.wx.cloud.inited)
|
||||
});
|
||||
} catch(e) {}
|
||||
}
|
||||
return {
|
||||
ok: apps.length > 0,
|
||||
reason: apps.length === 0 ? 'wx.cloud not available' : undefined,
|
||||
hasCloud: apps.length > 0,
|
||||
apps: apps,
|
||||
// 兼容旧接口
|
||||
appId: apps.length > 0 ? apps[0].appId : '',
|
||||
env: apps.length > 0 ? apps[0].env : null,
|
||||
inited: apps.length > 0 ? apps[0].inited : false
|
||||
};
|
||||
},
|
||||
|
||||
// ── 通用工具 ──
|
||||
_safeClone: function(obj) {
|
||||
try { return JSON.parse(JSON.stringify(obj)); } catch(e) { return String(obj); }
|
||||
},
|
||||
|
||||
_record: function(type, name, appId, data, result, status, error) {
|
||||
var r = {
|
||||
type: type, name: name, appId: appId,
|
||||
data: data, timestamp: new Date().toLocaleTimeString(),
|
||||
ts: Date.now(), status: status || 'pending'
|
||||
};
|
||||
if (result !== undefined) r.result = result;
|
||||
if (error !== undefined) r.error = error;
|
||||
this.hookedCalls.push(r);
|
||||
},
|
||||
|
||||
// ── 通用 hook 包装器 ──
|
||||
_wrapMethod: function(cloud, method, type, nameExtract, appId, origStore) {
|
||||
var orig = cloud[method];
|
||||
if (!orig || typeof orig !== 'function') return false;
|
||||
origStore[method] = orig.bind(cloud);
|
||||
var self = this;
|
||||
cloud[method] = function(options) {
|
||||
options = options || {};
|
||||
var callName = nameExtract ? nameExtract(options) : method;
|
||||
var callData = self._safeClone(options);
|
||||
delete callData.success; delete callData.fail; delete callData.complete;
|
||||
delete callData.filePath; delete callData.tempFilePath;
|
||||
|
||||
var recorded = false;
|
||||
var hasCb = !!(options.success || options.fail);
|
||||
var origSuccess = options.success;
|
||||
var origFail = options.fail;
|
||||
|
||||
options.success = function(res) {
|
||||
if (!recorded) { recorded = true; self._record(type, callName, appId, callData, self._safeClone(res), 'success'); }
|
||||
if (origSuccess) origSuccess(res);
|
||||
};
|
||||
options.fail = function(err) {
|
||||
if (!recorded) { recorded = true; self._record(type, callName, appId, callData, null, 'fail', err ? (err.errMsg || JSON.stringify(err)) : 'unknown'); }
|
||||
if (origFail) origFail(err);
|
||||
};
|
||||
|
||||
var ret = origStore[method](options);
|
||||
if (!hasCb && ret && typeof ret.then === 'function') {
|
||||
ret.then(function(res) {
|
||||
if (!recorded) { recorded = true; self._record(type, callName, appId, callData, self._safeClone(res), 'success'); }
|
||||
})['catch'](function(err) {
|
||||
if (!recorded) { recorded = true; self._record(type, callName, appId, callData, null, 'fail', err ? (err.errMsg || JSON.stringify(err)) : 'unknown'); }
|
||||
});
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
return true;
|
||||
},
|
||||
|
||||
// ── Hook 数据库 ──
|
||||
_hookDatabase: function(cloud, appId, origStore) {
|
||||
var origDb = cloud.database;
|
||||
if (!origDb) return;
|
||||
origStore['database'] = origDb.bind(cloud);
|
||||
var self = this;
|
||||
|
||||
cloud.database = function(opts) {
|
||||
var db = origStore['database'](opts);
|
||||
if (!db) return db;
|
||||
self._proxyDbCollection(db, appId);
|
||||
return db;
|
||||
};
|
||||
},
|
||||
|
||||
_proxyDbCollection: function(db, appId) {
|
||||
var origCol = db.collection;
|
||||
if (!origCol) return;
|
||||
var self = this;
|
||||
|
||||
db.collection = function(collName) {
|
||||
var col = origCol.call(db, collName);
|
||||
if (!col) return col;
|
||||
|
||||
// Hook 终端操作
|
||||
['add', 'get', 'update', 'remove', 'count'].forEach(function(m) {
|
||||
if (col[m]) self._wrapTerminal(col, m, 'db.' + m, collName, appId);
|
||||
});
|
||||
|
||||
// Hook doc()
|
||||
if (col.doc) {
|
||||
var origDoc = col.doc.bind(col);
|
||||
col.doc = function(docId) {
|
||||
var ref = origDoc(docId);
|
||||
if (!ref) return ref;
|
||||
['get', 'update', 'set', 'remove'].forEach(function(m) {
|
||||
if (ref[m]) self._wrapTerminal(ref, m, 'db.doc.' + m, collName + '/' + docId, appId);
|
||||
});
|
||||
return ref;
|
||||
};
|
||||
}
|
||||
|
||||
// Hook where()
|
||||
if (col.where) {
|
||||
var origWhere = col.where.bind(col);
|
||||
col.where = function(cond) {
|
||||
var q = origWhere(cond);
|
||||
if (!q) return q;
|
||||
['get', 'update', 'remove', 'count'].forEach(function(m) {
|
||||
if (q[m]) self._wrapTerminal(q, m, 'db.where.' + m, collName, appId, { where: self._safeClone(cond) });
|
||||
});
|
||||
return q;
|
||||
};
|
||||
}
|
||||
|
||||
// Hook aggregate()
|
||||
if (col.aggregate) {
|
||||
var origAgg = col.aggregate.bind(col);
|
||||
col.aggregate = function() {
|
||||
var agg = origAgg();
|
||||
if (!agg || !agg.end) return agg;
|
||||
self._wrapTerminal(agg, 'end', 'db.aggregate', collName, appId);
|
||||
return agg;
|
||||
};
|
||||
}
|
||||
|
||||
return col;
|
||||
};
|
||||
},
|
||||
|
||||
// 包装一个终端方法(支持 callback + promise)
|
||||
_wrapTerminal: function(obj, method, type, name, appId, extraData) {
|
||||
var orig = obj[method].bind(obj);
|
||||
var self = this;
|
||||
obj[method] = function(opts) {
|
||||
opts = opts || {};
|
||||
var callData = extraData ? self._safeClone(extraData) : {};
|
||||
if (opts.data) callData.data = self._safeClone(opts.data);
|
||||
|
||||
var recorded = false;
|
||||
var hasCb = !!(opts.success || opts.fail);
|
||||
var origSuccess = opts.success;
|
||||
var origFail = opts.fail;
|
||||
|
||||
opts.success = function(res) {
|
||||
if (!recorded) { recorded = true; self._record(type, name, appId, callData, self._safeClone(res), 'success'); }
|
||||
if (origSuccess) origSuccess(res);
|
||||
};
|
||||
opts.fail = function(err) {
|
||||
if (!recorded) { recorded = true; self._record(type, name, appId, callData, null, 'fail', err ? (err.errMsg || JSON.stringify(err)) : 'unknown'); }
|
||||
if (origFail) origFail(err);
|
||||
};
|
||||
|
||||
var ret = orig(opts);
|
||||
if (!hasCb && ret && typeof ret.then === 'function') {
|
||||
ret.then(function(res) {
|
||||
if (!recorded) { recorded = true; self._record(type, name, appId, callData, self._safeClone(res), 'success'); }
|
||||
})['catch'](function(err) {
|
||||
if (!recorded) { recorded = true; self._record(type, name, appId, callData, null, 'fail', err ? (err.errMsg || JSON.stringify(err)) : 'unknown'); }
|
||||
});
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
},
|
||||
|
||||
// ── 已知方法的类型和名称提取器 ──
|
||||
_knownMethods: {
|
||||
'callFunction': { type: 'function', ne: function(o) { return o.name || 'unknown'; } },
|
||||
'uploadFile': { type: 'storage', ne: function(o) { return 'uploadFile: ' + (o.cloudPath || ''); } },
|
||||
'downloadFile': { type: 'storage', ne: function(o) { return 'downloadFile: ' + (o.fileID || ''); } },
|
||||
'deleteFile': { type: 'storage', ne: function(o) { return 'deleteFile(' + (o.fileList||[]).length + ')'; } },
|
||||
'getTempFileURL': { type: 'storage', ne: function(o) { return 'getTempFileURL(' + (o.fileList||[]).length + ')'; } },
|
||||
'callContainer': { type: 'container', ne: function(o) { return o.path || 'callContainer'; } },
|
||||
'connectContainer': { type: 'container', ne: function(o) { return 'connectContainer: ' + (o.service || ''); } }
|
||||
},
|
||||
_skipProps: { 'init':1, 'database':1, 'CloudID':1, 'constructor':1, 'prototype':1, '__proto__':1 },
|
||||
|
||||
// ── 对一个 cloud 实例安装全部 hook ──
|
||||
_hookOneCloud: function(cloud, appId) {
|
||||
var origStore = {};
|
||||
var hookedList = [];
|
||||
|
||||
// 动态枚举所有方法
|
||||
var keys = [];
|
||||
try { keys = Object.keys(cloud); } catch(e) {}
|
||||
try {
|
||||
var proto = Object.getPrototypeOf(cloud);
|
||||
if (proto) {
|
||||
var pk = Object.getOwnPropertyNames(proto);
|
||||
for (var i = 0; i < pk.length; i++) { if (keys.indexOf(pk[i]) === -1) keys.push(pk[i]); }
|
||||
}
|
||||
} catch(e) {}
|
||||
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
var k = keys[i];
|
||||
if (this._skipProps[k] || k.charAt(0) === '_') continue;
|
||||
try { if (typeof cloud[k] !== 'function') continue; } catch(e) { continue; }
|
||||
|
||||
var known = this._knownMethods[k];
|
||||
var type = known ? known.type : 'cloud';
|
||||
var ne = known ? known.ne : (function(name) { return function() { return name; }; })(k);
|
||||
|
||||
if (this._wrapMethod(cloud, k, type, ne, appId, origStore)) {
|
||||
hookedList.push(k);
|
||||
}
|
||||
}
|
||||
|
||||
// 数据库特殊处理
|
||||
this._hookDatabase(cloud, appId, origStore);
|
||||
|
||||
this._hookedClouds.push({ cloud: cloud, appId: appId, origMethods: origStore });
|
||||
return hookedList;
|
||||
},
|
||||
|
||||
// ── 自动扫描所有 frame 并 hook ──
|
||||
autoHookScan: function() {
|
||||
var frames = this._findAllFrames();
|
||||
var newApps = [];
|
||||
for (var i = 0; i < frames.length; i++) {
|
||||
var f = frames[i];
|
||||
try {
|
||||
if (!f.wx || !f.wx.cloud) continue;
|
||||
var cloud = f.wx.cloud;
|
||||
// 检查是否已经 hook 过这个 cloud 实例
|
||||
var alreadyHooked = false;
|
||||
for (var j = 0; j < this._hookedClouds.length; j++) {
|
||||
if (this._hookedClouds[j].cloud === cloud) { alreadyHooked = true; break; }
|
||||
}
|
||||
if (alreadyHooked) continue;
|
||||
|
||||
var appId = this._getAppIdFromFrame(f);
|
||||
var methods = this._hookOneCloud(cloud, appId);
|
||||
newApps.push({ appId: appId, methods: methods });
|
||||
} catch(e) {}
|
||||
}
|
||||
return newApps;
|
||||
},
|
||||
|
||||
// ── 公开 API ──
|
||||
installHook: function() {
|
||||
var newApps = this.autoHookScan();
|
||||
if (this._hookedClouds.length === 0) {
|
||||
return { ok: false, reason: 'wx.cloud not available in any frame' };
|
||||
}
|
||||
// 启动自动扫描定时器(每 3 秒扫描新 frame)
|
||||
this._startAutoScan();
|
||||
return {
|
||||
ok: true,
|
||||
totalHooked: this._hookedClouds.length,
|
||||
newApps: newApps,
|
||||
hookedMethods: newApps.length > 0 ? newApps[0].methods : []
|
||||
};
|
||||
},
|
||||
|
||||
_startAutoScan: function() {
|
||||
if (this._autoHookTimer) return;
|
||||
this.autoHookEnabled = true;
|
||||
var self = this;
|
||||
this._autoHookTimer = setInterval(function() {
|
||||
if (!self.autoHookEnabled) return;
|
||||
self.autoHookScan();
|
||||
}, 3000);
|
||||
},
|
||||
|
||||
stopAutoHook: function() {
|
||||
this.autoHookEnabled = false;
|
||||
if (this._autoHookTimer) {
|
||||
clearInterval(this._autoHookTimer);
|
||||
this._autoHookTimer = null;
|
||||
}
|
||||
},
|
||||
|
||||
uninstallHook: function() {
|
||||
this.stopAutoHook();
|
||||
for (var i = 0; i < this._hookedClouds.length; i++) {
|
||||
var entry = this._hookedClouds[i];
|
||||
try {
|
||||
for (var m in entry.origMethods) {
|
||||
try { entry.cloud[m] = entry.origMethods[m]; } catch(e) {}
|
||||
}
|
||||
} catch(e) {}
|
||||
}
|
||||
this._hookedClouds = [];
|
||||
},
|
||||
|
||||
getHookedCalls: function() { return this.hookedCalls.slice(); },
|
||||
clearHookedCalls: function() { this.hookedCalls = []; },
|
||||
|
||||
getHookedApps: function() {
|
||||
var apps = [];
|
||||
for (var i = 0; i < this._hookedClouds.length; i++) {
|
||||
apps.push(this._hookedClouds[i].appId);
|
||||
}
|
||||
return apps;
|
||||
},
|
||||
|
||||
getDiscoveredFunctions: function() {
|
||||
var map = {};
|
||||
for (var i = 0; i < this.hookedCalls.length; i++) {
|
||||
var c = this.hookedCalls[i];
|
||||
var key = (c.type || 'function') + ':' + c.appId + ':' + c.name;
|
||||
if (!map[key]) {
|
||||
map[key] = { name: c.name, type: c.type || 'function', appId: c.appId || '', params: [], count: 0 };
|
||||
}
|
||||
map[key].count++;
|
||||
if (c.data && typeof c.data === 'object') {
|
||||
var keys = Object.keys(c.data);
|
||||
for (var k = 0; k < keys.length; k++) {
|
||||
if (map[key].params.indexOf(keys[k]) === -1) map[key].params.push(keys[k]);
|
||||
}
|
||||
}
|
||||
}
|
||||
var result = [];
|
||||
for (var k in map) { result.push(map[k]); }
|
||||
return result;
|
||||
},
|
||||
|
||||
// ── 补充扫描 ──
|
||||
scanCloudFunctions: function() {
|
||||
var frames = this._findAllFrames();
|
||||
var found = {};
|
||||
for (var fi = 0; fi < frames.length; fi++) {
|
||||
var f = frames[fi];
|
||||
var appCodes = [];
|
||||
try { if (f.__wxAppCode__) appCodes.push(f.__wxAppCode__); } catch(e) {}
|
||||
for (var a = 0; a < appCodes.length; a++) {
|
||||
var code = appCodes[a];
|
||||
for (var key in code) {
|
||||
try {
|
||||
var val = code[key];
|
||||
var src = typeof val === 'string' ? val :
|
||||
(typeof val === 'function' ? val.toString() : null);
|
||||
if (src && src.length > 20) {
|
||||
if (src.indexOf('callFunction') !== -1) this._extractCalls(src, found);
|
||||
if (src.indexOf('.collection(') !== -1) this._extractDbOps(src, found);
|
||||
this._extractFileOps(src, found);
|
||||
}
|
||||
} catch(e) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
var result = [];
|
||||
for (var name in found) {
|
||||
result.push({ name: name, type: found[name].type, params: found[name].params, count: found[name].count });
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
_extractCalls: function(src, found) {
|
||||
var re = /callFunction\s*\(\s*\{[^}]{0,500}?name\s*:\s*["']([^"']+)["']/g;
|
||||
var m;
|
||||
while ((m = re.exec(src)) !== null) {
|
||||
var key = 'fn:' + m[1];
|
||||
if (!found[key]) found[key] = { type: 'function', params: [], count: 0 };
|
||||
found[key].count++;
|
||||
var after = src.substring(m.index, Math.min(m.index + 600, src.length));
|
||||
var dm = after.match(/data\s*:\s*\{([^}]{1,400})\}/);
|
||||
if (dm) {
|
||||
var fields = dm[1].match(/(\w+)\s*:/g);
|
||||
if (fields) {
|
||||
for (var i = 0; i < fields.length; i++) {
|
||||
var fn = fields[i].replace(/\s*:$/, '');
|
||||
if (fn !== 'name' && fn !== 'success' && fn !== 'fail' && fn !== 'complete'
|
||||
&& found[key].params.indexOf(fn) === -1) found[key].params.push(fn);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_extractDbOps: function(src, found) {
|
||||
var re = /\.collection\s*\(\s*["']([^"']+)["']\s*\)/g;
|
||||
var m;
|
||||
while ((m = re.exec(src)) !== null) {
|
||||
var key = 'db:' + m[1];
|
||||
if (!found[key]) found[key] = { type: 'database', params: [], count: 0 };
|
||||
found[key].count++;
|
||||
var after = src.substring(m.index, Math.min(m.index + 300, src.length));
|
||||
['add','get','update','remove','count','aggregate','doc','where'].forEach(function(op) {
|
||||
if (after.indexOf('.' + op + '(') !== -1 && found[key].params.indexOf(op) === -1)
|
||||
found[key].params.push(op);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
_extractFileOps: function(src, found) {
|
||||
['uploadFile','downloadFile','deleteFile','getTempFileURL'].forEach(function(m) {
|
||||
if (src.indexOf(m) !== -1) {
|
||||
var key = 'storage:' + m;
|
||||
if (!found[key]) found[key] = { type: 'storage', params: [], count: 0 };
|
||||
found[key].count++;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// ── 手动调用云函数 ──
|
||||
callFunction: function(name, data) {
|
||||
// 从已 hook 的 cloud 中找一个可用的 callFunction
|
||||
var caller = null;
|
||||
for (var i = 0; i < this._hookedClouds.length; i++) {
|
||||
var entry = this._hookedClouds[i];
|
||||
if (entry.origMethods['callFunction']) {
|
||||
caller = entry.origMethods['callFunction'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!caller) {
|
||||
// 没 hook 过则尝试直接找
|
||||
var frames = this._findAllFrames();
|
||||
for (var i = 0; i < frames.length; i++) {
|
||||
try {
|
||||
if (frames[i].wx && frames[i].wx.cloud && frames[i].wx.cloud.callFunction) {
|
||||
caller = frames[i].wx.cloud.callFunction.bind(frames[i].wx.cloud);
|
||||
break;
|
||||
}
|
||||
} catch(e) {}
|
||||
}
|
||||
}
|
||||
if (!caller) return Promise.resolve({ ok: false, reason: 'wx.cloud not available' });
|
||||
|
||||
return new Promise(function(resolve) {
|
||||
var t = setTimeout(function() {
|
||||
resolve({ ok: true, status: 'timeout', error: '调用超时(10s)' });
|
||||
}, 10000);
|
||||
caller({
|
||||
name: name, data: data || {},
|
||||
success: function(res) {
|
||||
clearTimeout(t);
|
||||
try { resolve({ ok: true, status: 'success', result: JSON.parse(JSON.stringify(res && res.result ? res.result : res)) }); }
|
||||
catch(e) { resolve({ ok: true, status: 'success', result: String(res) }); }
|
||||
},
|
||||
fail: function(err) {
|
||||
clearTimeout(t);
|
||||
resolve({ ok: true, status: 'fail', error: err ? (err.errMsg || JSON.stringify(err)) : 'unknown' });
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
window.cloudAudit = cloudAudit;
|
||||
})();
|
||||
+304
@@ -0,0 +1,304 @@
|
||||
"""
|
||||
Port of RemoteDebugCodex.js - Protocol encode/decode for WeChat Remote Debug.
|
||||
Handles protobuf wrapping/unwrapping of debug messages.
|
||||
"""
|
||||
import zlib
|
||||
from .constants import DebugMessageCategory, CompressAlgo
|
||||
from .third_party import wmpf_debug_pb2 as proto
|
||||
|
||||
|
||||
def wrap_debug_message_data(data: dict, category: str, compress_algo: int = 0):
|
||||
"""
|
||||
Encode data into a protobuf debug message payload based on category.
|
||||
Returns dict with 'buffer' (bytes) and 'originalSize' (int).
|
||||
"""
|
||||
buf = None
|
||||
|
||||
if category == DebugMessageCategory.CallInterface:
|
||||
msg = proto.WARemoteDebug_CallInterface()
|
||||
msg.objName = data.get("name", "")
|
||||
msg.methodName = data.get("method", "")
|
||||
for arg in data.get("args", []):
|
||||
msg.methodArgList.append(str(arg))
|
||||
if "call_id" in data and data["call_id"] is not None:
|
||||
msg.callId = data["call_id"]
|
||||
buf = msg.SerializeToString()
|
||||
|
||||
elif category == DebugMessageCategory.EvaluateJavascriptResult:
|
||||
msg = proto.WARemoteDebug_EvaluateJavascriptResult()
|
||||
if data.get("ret") is not None:
|
||||
msg.ret = data["ret"]
|
||||
if data.get("evaluate_id") is not None:
|
||||
msg.evaluateId = data["evaluate_id"]
|
||||
buf = msg.SerializeToString()
|
||||
|
||||
elif category == DebugMessageCategory.Ping:
|
||||
msg = proto.WARemoteDebug_Ping()
|
||||
if data.get("ping_id") is not None:
|
||||
msg.pingId = data["ping_id"]
|
||||
if data.get("payload") is not None:
|
||||
msg.payload = data["payload"]
|
||||
buf = msg.SerializeToString()
|
||||
|
||||
elif category == DebugMessageCategory.Breakpoint:
|
||||
msg = proto.WARemoteDebug_Breakpoint()
|
||||
msg.isHit = bool(data.get("is_hit", False))
|
||||
buf = msg.SerializeToString()
|
||||
|
||||
elif category == DebugMessageCategory.DomOp:
|
||||
msg = proto.WARemoteDebug_DomOp()
|
||||
if data.get("params") is not None:
|
||||
msg.params = data["params"]
|
||||
if data.get("webview_id") is not None:
|
||||
msg.webviewId = data["webview_id"]
|
||||
buf = msg.SerializeToString()
|
||||
|
||||
elif category == DebugMessageCategory.DomEvent:
|
||||
msg = proto.WARemoteDebug_DomEvent()
|
||||
if data.get("params") is not None:
|
||||
msg.params = data["params"]
|
||||
if data.get("webview_id") is not None:
|
||||
msg.webviewId = data["webview_id"]
|
||||
buf = msg.SerializeToString()
|
||||
|
||||
elif category == DebugMessageCategory.ChromeDevtools:
|
||||
msg = proto.WARemoteDebug_ChromeDevtools()
|
||||
if data.get("op_id") is not None:
|
||||
msg.opId = data["op_id"]
|
||||
if data.get("payload") is not None:
|
||||
msg.payload = data["payload"]
|
||||
if data.get("jscontext_id") is not None:
|
||||
msg.jscontextId = data["jscontext_id"]
|
||||
buf = msg.SerializeToString()
|
||||
|
||||
elif category == DebugMessageCategory.ConnectJsContext:
|
||||
msg = proto.WARemoteDebug_ConnectJsContext()
|
||||
if data.get("jscontext_id") is not None:
|
||||
msg.jscontextId = data["jscontext_id"]
|
||||
buf = msg.SerializeToString()
|
||||
|
||||
elif category == DebugMessageCategory.CustomMessage:
|
||||
msg = proto.WARemoteDebug_CustomMessage()
|
||||
if data.get("method") is not None:
|
||||
msg.method = data["method"]
|
||||
if data.get("payload") is not None:
|
||||
msg.payload = data["payload"]
|
||||
if data.get("raw") is not None:
|
||||
msg.raw = data["raw"]
|
||||
buf = msg.SerializeToString()
|
||||
|
||||
else:
|
||||
raise ValueError(f"invalid debug message category: {category}")
|
||||
|
||||
original_size = 0
|
||||
if buf is not None and compress_algo and (compress_algo & CompressAlgo.Zlib) != 0:
|
||||
original_size = len(buf)
|
||||
buf = zlib.compress(buf)
|
||||
|
||||
return {
|
||||
"buffer": buf,
|
||||
"originalSize": original_size,
|
||||
}
|
||||
|
||||
|
||||
def unwrap_debug_message_data(msg) -> dict:
|
||||
"""
|
||||
Decode a WARemoteDebug_DebugMessage protobuf object into a plain dict.
|
||||
msg can be a protobuf message object or a dict-like with fields:
|
||||
seq, after, category, data, compressAlgo, originalSize
|
||||
"""
|
||||
raw_data = msg.data if hasattr(msg, "data") else msg.get("data", b"")
|
||||
category = msg.category if hasattr(msg, "category") else msg.get("category", "")
|
||||
compress_algo = msg.compressAlgo if hasattr(msg, "compressAlgo") else msg.get("compressAlgo", 0)
|
||||
seq = msg.seq if hasattr(msg, "seq") else msg.get("seq", 0)
|
||||
after = msg.after if hasattr(msg, "after") else msg.get("after", 0)
|
||||
original_size_field = msg.originalSize if hasattr(msg, "originalSize") else msg.get("originalSize", 0)
|
||||
|
||||
if isinstance(raw_data, str):
|
||||
raw_data = raw_data.encode("latin-1")
|
||||
|
||||
buf = raw_data
|
||||
compressed_size = 0
|
||||
if buf and compress_algo and (compress_algo & CompressAlgo.Zlib) != 0:
|
||||
compressed_size = len(buf)
|
||||
buf = zlib.decompress(buf)
|
||||
|
||||
decoded = None
|
||||
|
||||
if buf:
|
||||
if category == DebugMessageCategory.Breakpoint:
|
||||
m = proto.WARemoteDebug_Breakpoint()
|
||||
m.ParseFromString(buf)
|
||||
decoded = {"is_hit": 1 if m.isHit else 0}
|
||||
|
||||
elif category == DebugMessageCategory.CallInterface:
|
||||
m = proto.WARemoteDebug_CallInterface()
|
||||
m.ParseFromString(buf)
|
||||
decoded = {
|
||||
"name": m.objName,
|
||||
"method": m.methodName,
|
||||
"args": list(m.methodArgList),
|
||||
"call_id": m.callId,
|
||||
}
|
||||
|
||||
elif category == DebugMessageCategory.CallInterfaceResult:
|
||||
m = proto.WARemoteDebug_CallInterfaceResult()
|
||||
m.ParseFromString(buf)
|
||||
decoded = {
|
||||
"ret": m.ret,
|
||||
"call_id": m.callId,
|
||||
"debug_info": m.debugInfo,
|
||||
}
|
||||
|
||||
elif category == DebugMessageCategory.EvaluateJavascript:
|
||||
m = proto.WARemoteDebug_EvaluateJavascript()
|
||||
m.ParseFromString(buf)
|
||||
decoded = {
|
||||
"script": m.script,
|
||||
"evaluate_id": m.evaluateId,
|
||||
"debug_info": m.debugInfo,
|
||||
}
|
||||
|
||||
elif category == DebugMessageCategory.EvaluateJavascriptResult:
|
||||
m = proto.WARemoteDebug_EvaluateJavascriptResult()
|
||||
m.ParseFromString(buf)
|
||||
decoded = {
|
||||
"ret": m.ret,
|
||||
"evaluate_id": m.evaluateId,
|
||||
}
|
||||
|
||||
elif category == DebugMessageCategory.Ping:
|
||||
m = proto.WARemoteDebug_Ping()
|
||||
m.ParseFromString(buf)
|
||||
decoded = {
|
||||
"ping_id": m.pingId,
|
||||
"payload": m.payload,
|
||||
}
|
||||
|
||||
elif category == DebugMessageCategory.Pong:
|
||||
m = proto.WARemoteDebug_Pong()
|
||||
m.ParseFromString(buf)
|
||||
decoded = {
|
||||
"ping_id": m.pingId,
|
||||
"network_type": m.networkType,
|
||||
"payload": m.payload,
|
||||
}
|
||||
|
||||
elif category == DebugMessageCategory.SetupContext:
|
||||
m = proto.WARemoteDebug_SetupContext()
|
||||
m.ParseFromString(buf)
|
||||
ri = m.registerInterface if m.HasField("registerInterface") else None
|
||||
di = m.deviceInfo if m.HasField("deviceInfo") else None
|
||||
decoded = {
|
||||
"register_interface": {
|
||||
"obj_name": ri.objName if ri else "",
|
||||
"obj_methods": [
|
||||
{
|
||||
"method_name": meth.methodName,
|
||||
"method_args": list(meth.methodArgList),
|
||||
}
|
||||
for meth in (ri.objMethodList if ri else [])
|
||||
],
|
||||
} if ri else {},
|
||||
"configure_js": m.configureJs,
|
||||
"public_js_md5": m.publicJsMd5,
|
||||
"three_js_md5": m.threeJsMd5,
|
||||
"device_info": {
|
||||
"device_name": di.deviceName if di else "",
|
||||
"device_model": di.deviceModel if di else "",
|
||||
"os": di.systemVersion if di else "",
|
||||
"wechat_version": di.wechatVersion if di else "",
|
||||
"pixel_ratio": di.pixelRatio if di else 0,
|
||||
"screen_width": di.screenWidth if di else 0,
|
||||
"publib": di.publibVersion if di else 0,
|
||||
"user_agent": di.userAgent if di else "",
|
||||
} if di else {},
|
||||
"support_compress_algo": m.supportCompressAlgo,
|
||||
}
|
||||
|
||||
elif category == DebugMessageCategory.DomOp:
|
||||
m = proto.WARemoteDebug_DomOp()
|
||||
m.ParseFromString(buf)
|
||||
decoded = {
|
||||
"params": m.params,
|
||||
"webview_id": m.webviewId,
|
||||
}
|
||||
|
||||
elif category == DebugMessageCategory.DomEvent:
|
||||
m = proto.WARemoteDebug_DomEvent()
|
||||
m.ParseFromString(buf)
|
||||
decoded = {
|
||||
"params": m.params,
|
||||
"webview_id": m.webviewId,
|
||||
}
|
||||
|
||||
elif category == DebugMessageCategory.NetworkDebugAPI:
|
||||
m = proto.WARemoteDebug_NetworkDebugAPI()
|
||||
m.ParseFromString(buf)
|
||||
decoded = {
|
||||
"api_name": m.apiName,
|
||||
"task_id": m.taskId,
|
||||
"request_headers": m.requestHeaders,
|
||||
"timestamp": m.timestamp,
|
||||
}
|
||||
|
||||
elif category == DebugMessageCategory.ChromeDevtools:
|
||||
m = proto.WARemoteDebug_ChromeDevtools()
|
||||
m.ParseFromString(buf)
|
||||
decoded = {
|
||||
"op_id": m.opId,
|
||||
"payload": m.payload,
|
||||
"jscontext_id": m.jscontextId,
|
||||
}
|
||||
|
||||
elif category == DebugMessageCategory.ChromeDevtoolsResult:
|
||||
m = proto.WARemoteDebug_ChromeDevtoolsResult()
|
||||
m.ParseFromString(buf)
|
||||
decoded = {
|
||||
"op_id": m.opId,
|
||||
"payload": m.payload,
|
||||
"jscontext_id": m.jscontextId,
|
||||
}
|
||||
|
||||
elif category == DebugMessageCategory.AddJsContext:
|
||||
m = proto.WARemoteDebug_AddJsContext()
|
||||
m.ParseFromString(buf)
|
||||
decoded = {
|
||||
"jscontext_id": m.jscontextId,
|
||||
"jscontext_name": m.jscontextName,
|
||||
}
|
||||
|
||||
elif category == DebugMessageCategory.RemoveJsContext:
|
||||
m = proto.WARemoteDebug_RemoveJsContext()
|
||||
m.ParseFromString(buf)
|
||||
decoded = {
|
||||
"jscontext_id": m.jscontextId,
|
||||
}
|
||||
|
||||
elif category == DebugMessageCategory.ConnectJsContext:
|
||||
m = proto.WARemoteDebug_ConnectJsContext()
|
||||
m.ParseFromString(buf)
|
||||
decoded = {
|
||||
"jscontext_id": m.jscontextId,
|
||||
}
|
||||
|
||||
elif category == DebugMessageCategory.CustomMessage:
|
||||
m = proto.WARemoteDebug_CustomMessage()
|
||||
m.ParseFromString(buf)
|
||||
decoded = {
|
||||
"method": m.method,
|
||||
"payload": m.payload,
|
||||
"raw": m.raw,
|
||||
}
|
||||
|
||||
else:
|
||||
decoded = None
|
||||
|
||||
return {
|
||||
"seq": seq or 0,
|
||||
"delay": after or 0,
|
||||
"category": category or DebugMessageCategory.Ping,
|
||||
"data": decoded or {},
|
||||
"compress_algo": compress_algo or 0,
|
||||
"original_size": original_size_field or compressed_size,
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
"""
|
||||
Port of RemoteDebugConstants.js - Protocol constants for WeChat Remote Debug.
|
||||
"""
|
||||
|
||||
|
||||
class ResponseType:
|
||||
Heartbeat = 2001
|
||||
Login = 2002
|
||||
EventNotifyBegin = 3001
|
||||
EventNotifyEnd = 3002
|
||||
EventNotifyBlock = 3003
|
||||
JoinRoom = 2003
|
||||
SendDebugMessage = 2000
|
||||
SendDebugMessageParallelly = 2006
|
||||
QuitRoom = 2004
|
||||
MessageNotify = 1000
|
||||
MessageNotifyParallelly = 1006
|
||||
SyncMessage = 2005
|
||||
Unknown = -1
|
||||
|
||||
|
||||
class CompressAlgo:
|
||||
Non = 0
|
||||
Zlib = 1
|
||||
|
||||
|
||||
class RequestType:
|
||||
Heartbeat = 2001
|
||||
Login = 2002
|
||||
EventNotifyBegin = 3001
|
||||
EventNotifyEnd = 3002
|
||||
EventNotifyBlock = 3003
|
||||
JoinRoom = 2003
|
||||
SendDebugMessage = 2000
|
||||
SendDebugMessageParallelly = 2006
|
||||
QuitRoom = 2004
|
||||
MessageNotify = 1000
|
||||
MessageNotifyParallelly = 1006
|
||||
SyncMessage = 2005
|
||||
Unknown = -1
|
||||
|
||||
|
||||
class ClientRequestType:
|
||||
Heartbeat = 1001
|
||||
Login = 1002
|
||||
EventNotifyBegin = 3001
|
||||
EventNotifyEnd = 3002
|
||||
EventNotifyBlock = 3003
|
||||
JoinRoom = 1003
|
||||
SendDebugMessage = 1000
|
||||
SendDebugMessageParallelly = 1006
|
||||
QuitRoom = 1004
|
||||
MessageNotify = 2000
|
||||
MessageNotifyParallelly = 2006
|
||||
SyncMessage = 1005
|
||||
Unknown = -1
|
||||
|
||||
|
||||
class ClientResponseType:
|
||||
Heartbeat = 1001
|
||||
Login = 1002
|
||||
EventNotifyBegin = 3001
|
||||
EventNotifyEnd = 3002
|
||||
EventNotifyBlock = 3003
|
||||
JoinRoom = 1003
|
||||
SendDebugMessage = 1000
|
||||
SendDebugMessageParallelly = 1006
|
||||
QuitRoom = 1004
|
||||
MessageNotify = 2000
|
||||
MessageNotifyParallelly = 2006
|
||||
SyncMessage = 1005
|
||||
Unknown = -1
|
||||
|
||||
|
||||
class RequestCmd:
|
||||
Heartbeat = 2001
|
||||
Login = 2002
|
||||
EventNotifyBegin = 3001
|
||||
EventNotifyEnd = 3002
|
||||
EventNotifyBlock = 3003
|
||||
JoinRoom = 2003
|
||||
SendDebugMessage = 2000
|
||||
SendDebugMessageParallelly = 2006
|
||||
QuitRoom = 2004
|
||||
MessageNotify = 1000
|
||||
MessageNotifyParallelly = 1006
|
||||
SyncMessage = 2005
|
||||
Unknown = -1
|
||||
|
||||
|
||||
class ClientRequestCmd:
|
||||
Heartbeat = 1001
|
||||
Login = 1002
|
||||
EventNotifyBegin = 3001
|
||||
EventNotifyEnd = 3002
|
||||
EventNotifyBlock = 3003
|
||||
JoinRoom = 1003
|
||||
SendDebugMessage = 1000
|
||||
SendDebugMessageParallelly = 1006
|
||||
QuitRoom = 1004
|
||||
MessageNotify = 2000
|
||||
MessageNotifyParallelly = 2006
|
||||
SyncMessage = 1005
|
||||
Unknown = -1
|
||||
|
||||
|
||||
class DebugMessageCategory:
|
||||
SetupContext = "setupContext"
|
||||
CallInterface = "callInterface"
|
||||
EvaluateJavascript = "evaluateJavascript"
|
||||
CallInterfaceResult = "callInterfaceResult"
|
||||
EvaluateJavascriptResult = "evaluateJavascriptResult"
|
||||
Breakpoint = "breakpoint"
|
||||
Ping = "ping"
|
||||
Pong = "pong"
|
||||
DomOp = "domOp"
|
||||
DomEvent = "domEvent"
|
||||
NetworkDebugAPI = "networkDebugAPI"
|
||||
ChromeDevtools = "chromeDevtools"
|
||||
ChromeDevtoolsResult = "chromeDevtoolsResult"
|
||||
AddJsContext = "addJsContext"
|
||||
RemoveJsContext = "removeJsContext"
|
||||
ConnectJsContext = "connectJsContext"
|
||||
EngineEvent = "engineEvent"
|
||||
EngineOp = "engineOp"
|
||||
CustomMessage = "customMessage"
|
||||
|
||||
|
||||
class KnownErrorCode:
|
||||
OK = 0
|
||||
ERR_SYS = -1
|
||||
NOT_EXIST = 1
|
||||
INVALID_ARGS = -2
|
||||
SYSTEM_BUSY = -3
|
||||
INVALID_LOGIN_TICKET = -50001
|
||||
HAS_NO_PERMISSION = -50002
|
||||
ROOM_IN_DEBUGGING = -50003
|
||||
NO_EXIST_ROOM = -50004
|
||||
MD5_NOT_MATCH = -50005
|
||||
USER_IN_DEBUGGING = -50006
|
||||
SEQ_ERROR = -50010
|
||||
SEND_MSG_BUSY = -50011
|
||||
SEND_MSG_SEQ_RANGE_ERROR = -50012
|
||||
+505
@@ -0,0 +1,505 @@
|
||||
"""
|
||||
DebugEngine - wraps debug_server, proxy_server, frida_server into a reusable class.
|
||||
"""
|
||||
import asyncio
|
||||
import json
|
||||
import random
|
||||
import re
|
||||
import sys
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
|
||||
import frida
|
||||
import websockets
|
||||
import websockets.server
|
||||
import websockets.exceptions
|
||||
|
||||
from .codex import wrap_debug_message_data, unwrap_debug_message_data
|
||||
from .third_party import wmpf_debug_pb2 as proto
|
||||
from .userscript import (
|
||||
build_cdp_enable_page_command,
|
||||
build_cdp_add_script_command,
|
||||
build_injection_wrapper,
|
||||
)
|
||||
|
||||
|
||||
class DebugMessageBus:
|
||||
"""Central event bus bridging debug server and CDP proxy server."""
|
||||
|
||||
def __init__(self):
|
||||
self._cdp_callbacks = []
|
||||
self._proxy_callbacks = []
|
||||
|
||||
def on_cdp_message(self, callback):
|
||||
self._cdp_callbacks.append(callback)
|
||||
|
||||
def on_proxy_message(self, callback):
|
||||
self._proxy_callbacks.append(callback)
|
||||
|
||||
def emit_cdp_message(self, message: str):
|
||||
for cb in self._cdp_callbacks:
|
||||
cb(message)
|
||||
|
||||
def emit_proxy_message(self, message: str):
|
||||
for cb in self._proxy_callbacks:
|
||||
cb(message)
|
||||
|
||||
|
||||
def buffer_to_hex_string(data: bytes) -> str:
|
||||
return data.hex()
|
||||
|
||||
|
||||
def _build_protobuf_cdp_message(cdp_json: str, seq: int) -> bytes:
|
||||
"""Wrap a CDP JSON command into protobuf binary for sending to the miniapp runtime."""
|
||||
raw_payload = {
|
||||
"jscontext_id": "",
|
||||
"op_id": round(100 * random.random()),
|
||||
"payload": cdp_json,
|
||||
}
|
||||
wrapped = wrap_debug_message_data(raw_payload, "chromeDevtools", 0)
|
||||
out_msg = proto.WARemoteDebug_DebugMessage()
|
||||
out_msg.seq = seq
|
||||
out_msg.category = "chromeDevtools"
|
||||
out_msg.data = wrapped["buffer"]
|
||||
out_msg.compressAlgo = 0
|
||||
out_msg.originalSize = wrapped["originalSize"]
|
||||
return out_msg.SerializeToString()
|
||||
|
||||
|
||||
class DebugEngine:
|
||||
"""Wraps all three subsystems: debug server, proxy server, frida injection."""
|
||||
|
||||
def __init__(self, options, logger, userscripts=None):
|
||||
self.options = options
|
||||
self.logger = logger
|
||||
self.bus = DebugMessageBus()
|
||||
self.userscripts = userscripts or []
|
||||
self.debug_srv = None
|
||||
self.proxy_srv = None
|
||||
self.frida_session = None
|
||||
self.frida_script = None
|
||||
self.miniapp_clients = set()
|
||||
self.devtools_clients = set()
|
||||
self.message_counter = 0
|
||||
self._pending_responses = {}
|
||||
self._cmd_counter = 80000
|
||||
self._status_callbacks = []
|
||||
self._event_listeners = {} # method -> [callback, ...]
|
||||
self.status = {"frida": False, "miniapp": False, "devtools": False}
|
||||
|
||||
def on_status_change(self, callback):
|
||||
self._status_callbacks.append(callback)
|
||||
|
||||
def _notify_status(self, key, value):
|
||||
self.status[key] = value
|
||||
for cb in self._status_callbacks:
|
||||
try:
|
||||
cb(dict(self.status))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _next_cmd_id(self):
|
||||
self._cmd_counter += 1
|
||||
return self._cmd_counter
|
||||
|
||||
async def start(self):
|
||||
"""Start all three subsystems."""
|
||||
self.debug_srv = await self._start_debug_server()
|
||||
self.proxy_srv = await self._start_proxy_server()
|
||||
try:
|
||||
self.frida_session, self.frida_script = await self._start_frida()
|
||||
self._notify_status("frida", True)
|
||||
except Exception as e:
|
||||
self.logger.error(str(e))
|
||||
raise
|
||||
|
||||
async def stop(self):
|
||||
"""Graceful shutdown."""
|
||||
if self.debug_srv:
|
||||
self.debug_srv.close()
|
||||
await self.debug_srv.wait_closed()
|
||||
self.debug_srv = None
|
||||
if self.proxy_srv:
|
||||
self.proxy_srv.close()
|
||||
await self.proxy_srv.wait_closed()
|
||||
self.proxy_srv = None
|
||||
if self.frida_script:
|
||||
try:
|
||||
self.frida_script.unload()
|
||||
except Exception:
|
||||
pass
|
||||
self.frida_script = None
|
||||
if self.frida_session:
|
||||
try:
|
||||
self.frida_session.detach()
|
||||
except Exception:
|
||||
pass
|
||||
self.frida_session = None
|
||||
self._notify_status("frida", False)
|
||||
self._notify_status("miniapp", False)
|
||||
self._notify_status("devtools", False)
|
||||
self.miniapp_clients.clear()
|
||||
self.devtools_clients.clear()
|
||||
self.message_counter = 0
|
||||
self._pending_responses.clear()
|
||||
self.logger.info("[server] engine stopped")
|
||||
|
||||
async def evaluate_js(self, expression, timeout=5.0):
|
||||
"""Send Runtime.evaluate via CDP to the miniapp, return result dict."""
|
||||
if not self.miniapp_clients:
|
||||
raise RuntimeError("No miniapp client connected")
|
||||
cmd_id = self._next_cmd_id()
|
||||
cdp_cmd = json.dumps({
|
||||
"id": cmd_id,
|
||||
"method": "Runtime.evaluate",
|
||||
"params": {"expression": expression, "returnByValue": True}
|
||||
})
|
||||
loop = asyncio.get_event_loop()
|
||||
future = loop.create_future()
|
||||
self._pending_responses[cmd_id] = future
|
||||
self.bus.emit_proxy_message(cdp_cmd)
|
||||
try:
|
||||
return await asyncio.wait_for(future, timeout=timeout)
|
||||
except asyncio.TimeoutError:
|
||||
self._pending_responses.pop(cmd_id, None)
|
||||
raise
|
||||
|
||||
async def send_cdp_command(self, method, params=None, timeout=5.0):
|
||||
"""Send an arbitrary CDP command and return the response."""
|
||||
if not self.miniapp_clients:
|
||||
raise RuntimeError("No miniapp client connected")
|
||||
cmd_id = self._next_cmd_id()
|
||||
cdp_cmd = json.dumps({"id": cmd_id, "method": method, "params": params or {}})
|
||||
loop = asyncio.get_event_loop()
|
||||
future = loop.create_future()
|
||||
self._pending_responses[cmd_id] = future
|
||||
self.bus.emit_proxy_message(cdp_cmd)
|
||||
try:
|
||||
return await asyncio.wait_for(future, timeout=timeout)
|
||||
except asyncio.TimeoutError:
|
||||
self._pending_responses.pop(cmd_id, None)
|
||||
raise
|
||||
|
||||
async def set_extra_headers(self, headers: dict):
|
||||
"""Enable Network domain and set extra HTTP headers via CDP."""
|
||||
await self.send_cdp_command("Network.enable")
|
||||
await self.send_cdp_command("Network.setExtraHTTPHeaders",
|
||||
{"headers": headers})
|
||||
|
||||
def on_cdp_event(self, method, callback):
|
||||
"""Subscribe to a CDP event by method name (e.g. 'Debugger.scriptParsed')."""
|
||||
self._event_listeners.setdefault(method, []).append(callback)
|
||||
|
||||
def off_cdp_event(self, method, callback):
|
||||
"""Unsubscribe from a CDP event."""
|
||||
cbs = self._event_listeners.get(method, [])
|
||||
if callback in cbs:
|
||||
cbs.remove(callback)
|
||||
|
||||
def _handle_cdp_response(self, message_str):
|
||||
"""Check if a CDP response matches a pending evaluate_js call, or dispatch events."""
|
||||
try:
|
||||
data = json.loads(message_str)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
return
|
||||
msg_id = data.get("id")
|
||||
if msg_id is not None and msg_id in self._pending_responses:
|
||||
future = self._pending_responses.pop(msg_id)
|
||||
if not future.done():
|
||||
future.set_result(data)
|
||||
# Dispatch CDP events (messages with "method" but no "id")
|
||||
method = data.get("method")
|
||||
if method and method in self._event_listeners:
|
||||
for cb in self._event_listeners[method]:
|
||||
try:
|
||||
cb(data)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# ── Debug Server ──
|
||||
|
||||
async def _start_debug_server(self):
|
||||
engine = self
|
||||
logger = self.logger
|
||||
bus = self.bus
|
||||
userscripts = self.userscripts
|
||||
|
||||
scripts_injected = False
|
||||
|
||||
def on_proxy_message(message: str):
|
||||
engine.message_counter += 1
|
||||
raw_payload = {
|
||||
"jscontext_id": "",
|
||||
"op_id": round(100 * random.random()),
|
||||
"payload": str(message),
|
||||
}
|
||||
logger.main_debug(raw_payload)
|
||||
wrapped = wrap_debug_message_data(raw_payload, "chromeDevtools", 0)
|
||||
out_msg = proto.WARemoteDebug_DebugMessage()
|
||||
out_msg.seq = engine.message_counter
|
||||
out_msg.category = "chromeDevtools"
|
||||
out_msg.data = wrapped["buffer"]
|
||||
out_msg.compressAlgo = 0
|
||||
out_msg.originalSize = wrapped["originalSize"]
|
||||
encoded = out_msg.SerializeToString()
|
||||
clients_snapshot = list(engine.miniapp_clients)
|
||||
for ws in clients_snapshot:
|
||||
try:
|
||||
task = asyncio.ensure_future(ws.send(encoded))
|
||||
task.add_done_callback(lambda t: t.exception() if t.done() and not t.cancelled() and t.exception() else None)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
bus.on_proxy_message(on_proxy_message)
|
||||
|
||||
async def handler(websocket):
|
||||
nonlocal scripts_injected
|
||||
engine.miniapp_clients.add(websocket)
|
||||
engine._notify_status("miniapp", True)
|
||||
logger.info("[miniapp] miniapp client connected")
|
||||
|
||||
if userscripts and not scripts_injected:
|
||||
scripts_injected = True
|
||||
logger.info("[userscript] registering scripts immediately on connection...")
|
||||
try:
|
||||
seq = engine.message_counter
|
||||
cmd_id = 90000
|
||||
seq += 1
|
||||
await websocket.send(_build_protobuf_cdp_message(
|
||||
build_cdp_enable_page_command(cmd_id), seq
|
||||
))
|
||||
cmd_id += 1
|
||||
seq += 1
|
||||
await websocket.send(_build_protobuf_cdp_message(
|
||||
json.dumps({"id": cmd_id, "method": "Debugger.enable", "params": {}}), seq
|
||||
))
|
||||
cmd_id += 1
|
||||
logger.info("[anti-debug] Debugger.enable sent")
|
||||
seq += 1
|
||||
await websocket.send(_build_protobuf_cdp_message(
|
||||
json.dumps({"id": cmd_id, "method": "Debugger.setSkipAllPauses", "params": {"skip": True}}), seq
|
||||
))
|
||||
cmd_id += 1
|
||||
logger.info("[anti-debug] Debugger.setSkipAllPauses(true) sent")
|
||||
for script in userscripts:
|
||||
seq += 1
|
||||
await websocket.send(_build_protobuf_cdp_message(
|
||||
build_cdp_add_script_command(script, cmd_id), seq
|
||||
))
|
||||
cmd_id += 1
|
||||
logger.info(f"[userscript] registered (persistent): {script.name}")
|
||||
engine.message_counter = seq
|
||||
logger.info("[userscript] registration done, scripts will run on page load")
|
||||
except Exception as e:
|
||||
logger.error(f"[userscript] registration error: {e}")
|
||||
|
||||
immediate_done = False
|
||||
try:
|
||||
async for message in websocket:
|
||||
if isinstance(message, str):
|
||||
message = message.encode("utf-8")
|
||||
msg_category = ""
|
||||
try:
|
||||
decoded_msg = proto.WARemoteDebug_DebugMessage()
|
||||
decoded_msg.ParseFromString(message)
|
||||
msg_category = decoded_msg.category
|
||||
except Exception:
|
||||
pass
|
||||
self._process_miniapp_message(message)
|
||||
if msg_category == "setupContext" and userscripts and not immediate_done:
|
||||
immediate_done = True
|
||||
await asyncio.sleep(0.5)
|
||||
try:
|
||||
seq = engine.message_counter
|
||||
cmd_id = 91000
|
||||
seq += 1
|
||||
await websocket.send(_build_protobuf_cdp_message(
|
||||
json.dumps({"id": cmd_id, "method": "Debugger.enable", "params": {}}), seq
|
||||
))
|
||||
cmd_id += 1
|
||||
seq += 1
|
||||
await websocket.send(_build_protobuf_cdp_message(
|
||||
json.dumps({"id": cmd_id, "method": "Debugger.setSkipAllPauses", "params": {"skip": True}}), seq
|
||||
))
|
||||
cmd_id += 1
|
||||
logger.info("[anti-debug] Debugger.setSkipAllPauses re-sent after setupContext")
|
||||
for script in userscripts:
|
||||
wrapped = build_injection_wrapper(script)
|
||||
seq += 1
|
||||
cdp_cmd = json.dumps({
|
||||
"id": cmd_id,
|
||||
"method": "Runtime.evaluate",
|
||||
"params": {
|
||||
"expression": wrapped,
|
||||
"includeCommandLineAPI": True,
|
||||
"silent": False,
|
||||
},
|
||||
})
|
||||
await websocket.send(_build_protobuf_cdp_message(cdp_cmd, seq))
|
||||
cmd_id += 1
|
||||
logger.info(f"[userscript] immediate inject (Runtime.evaluate): {script.name}")
|
||||
engine.message_counter = seq
|
||||
except Exception as e:
|
||||
logger.error(f"[userscript] immediate inject error: {e}")
|
||||
except websockets.exceptions.ConnectionClosed:
|
||||
pass
|
||||
except Exception as e:
|
||||
logger.error(f"[miniapp] miniapp client err: {e}")
|
||||
finally:
|
||||
engine.miniapp_clients.discard(websocket)
|
||||
if not engine.miniapp_clients:
|
||||
engine._notify_status("miniapp", False)
|
||||
logger.info("[miniapp] miniapp client disconnected")
|
||||
|
||||
server = await websockets.server.serve(
|
||||
handler, "0.0.0.0", self.options.debug_port,
|
||||
max_size=None,
|
||||
)
|
||||
logger.info(f"[server] debug server running on ws://localhost:{self.options.debug_port}")
|
||||
logger.info("[server] debug server waiting for miniapp to connect...")
|
||||
return server
|
||||
|
||||
def _process_miniapp_message(self, message: bytes):
|
||||
self.logger.main_debug(
|
||||
f"[miniapp] client received raw message (hex): {buffer_to_hex_string(message)}"
|
||||
)
|
||||
unwrapped_data = None
|
||||
try:
|
||||
decoded = proto.WARemoteDebug_DebugMessage()
|
||||
decoded.ParseFromString(message)
|
||||
unwrapped_data = unwrap_debug_message_data(decoded)
|
||||
self.logger.main_debug("[miniapp] [DEBUG] decoded data:")
|
||||
self.logger.main_debug(unwrapped_data)
|
||||
except Exception as e:
|
||||
self.logger.error(f"[miniapp] miniapp client err: {e}")
|
||||
|
||||
if unwrapped_data is None:
|
||||
return
|
||||
|
||||
if unwrapped_data.get("category") == "chromeDevtoolsResult":
|
||||
payload = unwrapped_data["data"].get("payload", "")
|
||||
self.bus.emit_cdp_message(payload)
|
||||
# Also check for pending evaluate_js responses
|
||||
self._handle_cdp_response(payload)
|
||||
|
||||
# ── Proxy Server ──
|
||||
|
||||
async def _start_proxy_server(self):
|
||||
engine = self
|
||||
logger = self.logger
|
||||
bus = self.bus
|
||||
|
||||
def on_cdp_message(message: str):
|
||||
clients_snapshot = list(engine.devtools_clients)
|
||||
for ws in clients_snapshot:
|
||||
try:
|
||||
task = asyncio.ensure_future(ws.send(message))
|
||||
task.add_done_callback(lambda t: t.exception() if t.done() and not t.cancelled() and t.exception() else None)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
bus.on_cdp_message(on_cdp_message)
|
||||
|
||||
async def handler(websocket):
|
||||
engine.devtools_clients.add(websocket)
|
||||
engine._notify_status("devtools", True)
|
||||
logger.info("[cdp] CDP client connected")
|
||||
try:
|
||||
async for message in websocket:
|
||||
if isinstance(message, bytes):
|
||||
message = message.decode("utf-8")
|
||||
bus.emit_proxy_message(message)
|
||||
except websockets.exceptions.ConnectionClosed:
|
||||
pass
|
||||
except Exception as e:
|
||||
logger.error(f"[cdp] CDP client err: {e}")
|
||||
finally:
|
||||
engine.devtools_clients.discard(websocket)
|
||||
if not engine.devtools_clients:
|
||||
engine._notify_status("devtools", False)
|
||||
logger.info("[cdp] CDP client disconnected")
|
||||
|
||||
server = await websockets.server.serve(
|
||||
handler, "0.0.0.0", self.options.cdp_port,
|
||||
max_size=None,
|
||||
)
|
||||
logger.info(f"[server] proxy server running on ws://localhost:{self.options.cdp_port}")
|
||||
logger.info(f"[server] link: devtools://devtools/bundled/inspector.html?ws=127.0.0.1:{self.options.cdp_port}")
|
||||
return server
|
||||
|
||||
# ── Frida ──
|
||||
|
||||
async def _start_frida(self):
|
||||
logger = self.logger
|
||||
device = frida.get_local_device()
|
||||
processes = device.enumerate_processes(scope="metadata")
|
||||
wmpf_processes = [p for p in processes if p.name == "WeChatAppEx.exe"]
|
||||
if not wmpf_processes:
|
||||
raise RuntimeError("[frida] WeChatAppEx.exe process not found")
|
||||
|
||||
wmpf_ppids = []
|
||||
for p in wmpf_processes:
|
||||
ppid = p.parameters.get("ppid", 0)
|
||||
wmpf_ppids.append(ppid if ppid else 0)
|
||||
|
||||
if not wmpf_ppids:
|
||||
raise RuntimeError("[frida] WeChatAppEx.exe process not found")
|
||||
|
||||
pid_counts = Counter(wmpf_ppids)
|
||||
wmpf_pid = pid_counts.most_common(1)[0][0]
|
||||
|
||||
if wmpf_pid == 0:
|
||||
raise RuntimeError("[frida] WeChatAppEx.exe process not found")
|
||||
|
||||
wmpf_process = None
|
||||
for p in processes:
|
||||
if p.pid == wmpf_pid:
|
||||
wmpf_process = p
|
||||
break
|
||||
|
||||
if wmpf_process is None:
|
||||
raise RuntimeError("[frida] Could not find main WMPF process")
|
||||
|
||||
wmpf_process_path = wmpf_process.parameters.get("path", "")
|
||||
version_matches = re.findall(r"\d+", wmpf_process_path)
|
||||
if not version_matches:
|
||||
raise RuntimeError("[frida] error in find wmpf version")
|
||||
wmpf_version = int(version_matches[-1])
|
||||
if wmpf_version == 0:
|
||||
raise RuntimeError("[frida] error in find wmpf version")
|
||||
|
||||
session = device.attach(wmpf_pid)
|
||||
|
||||
if getattr(sys, 'frozen', False):
|
||||
# PyInstaller onefile: resources in _MEIPASS; onedir: next to exe
|
||||
project_root = Path(getattr(sys, '_MEIPASS', Path(sys.executable).parent)).resolve()
|
||||
else:
|
||||
project_root = Path(__file__).parent.parent.resolve()
|
||||
|
||||
hook_path = project_root / "frida" / "hook.js"
|
||||
if not hook_path.exists():
|
||||
raise RuntimeError("[frida] hook script not found")
|
||||
script_content = hook_path.read_text(encoding="utf-8")
|
||||
|
||||
config_path = project_root / "frida" / "config" / f"addresses.{wmpf_version}.json"
|
||||
if not config_path.exists():
|
||||
raise RuntimeError(f"[frida] version config not found: {wmpf_version}")
|
||||
config_content = config_path.read_text(encoding="utf-8")
|
||||
config_content = json.dumps(json.loads(config_content))
|
||||
|
||||
final_script = script_content.replace("@@CONFIG@@", config_content)
|
||||
script = session.create_script(final_script)
|
||||
|
||||
def on_message(message, data):
|
||||
if message.get("type") == "error":
|
||||
logger.error("[frida client]", message)
|
||||
return
|
||||
logger.frida_debug("[frida client]", message.get("payload", ""))
|
||||
|
||||
script.on("message", on_message)
|
||||
script.load()
|
||||
|
||||
logger.info(f"[frida] script loaded, WMPF version: {wmpf_version}, pid: {wmpf_pid}")
|
||||
logger.info("[frida] you can now open any miniapps")
|
||||
|
||||
return session, script
|
||||
+1119
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,40 @@
|
||||
import sys
|
||||
from .cli import CliOptions
|
||||
|
||||
|
||||
class Logger:
|
||||
def __init__(self, options: CliOptions):
|
||||
self._debug_main = options.debug_main
|
||||
self._debug_frida = options.debug_frida
|
||||
self._output_callback = None
|
||||
|
||||
def set_output_callback(self, callback):
|
||||
"""Set callback(level, text) for GUI log capture."""
|
||||
self._output_callback = callback
|
||||
|
||||
def _emit(self, level, *messages):
|
||||
text = " ".join(str(m) for m in messages)
|
||||
print(text, flush=True, file=sys.stderr if level == "error" else sys.stdout)
|
||||
if self._output_callback:
|
||||
try:
|
||||
self._output_callback(level, text)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def info(self, *messages):
|
||||
self._emit("info", *messages)
|
||||
|
||||
def error(self, *messages):
|
||||
self._emit("error", *messages)
|
||||
|
||||
def main_debug(self, *messages):
|
||||
if self._debug_main:
|
||||
self._emit("debug", *messages)
|
||||
|
||||
def frida_debug(self, *messages):
|
||||
if self._debug_frida:
|
||||
self._emit("frida", *messages)
|
||||
|
||||
|
||||
def create_logger(options: CliOptions) -> Logger:
|
||||
return Logger(options)
|
||||
@@ -0,0 +1,352 @@
|
||||
(function() {
|
||||
// 允许重新注入以支持切换小程序
|
||||
if (window._navInjected && window.nav && window.nav.wxFrame) {
|
||||
// 检测 wxFrame 是否仍有效
|
||||
try {
|
||||
var testConfig = window.nav.wxFrame.__wxConfig;
|
||||
if (testConfig && testConfig.pages) return; // 仍有效,跳过
|
||||
} catch(e) {}
|
||||
}
|
||||
window._navInjected = true;
|
||||
|
||||
class UniversalMiniProgramNavigator {
|
||||
constructor() {
|
||||
this.wxFrame = null;
|
||||
this.config = null;
|
||||
this.allPages = [];
|
||||
this.tabBarPages = [];
|
||||
this.categorizedPages = {};
|
||||
this.menuItems = [];
|
||||
this._customHeaders = {};
|
||||
this._redirectGuard = false;
|
||||
this._blockedRedirects = [];
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
if (!this.detectMiniProgramEnvironment()) {
|
||||
console.error('未检测到小程序环境');
|
||||
return;
|
||||
}
|
||||
this.loadConfiguration();
|
||||
this.categorizePages();
|
||||
}
|
||||
|
||||
detectMiniProgramEnvironment() {
|
||||
if (typeof wx !== 'undefined' && typeof getCurrentPages !== 'undefined') {
|
||||
this.wxFrame = window;
|
||||
return true;
|
||||
}
|
||||
if (typeof window !== 'undefined' && window.frames) {
|
||||
for (let i = 0; i < window.frames.length; i++) {
|
||||
try {
|
||||
const frame = window.frames[i];
|
||||
if (frame.wx && frame.__wxConfig) {
|
||||
this.wxFrame = frame;
|
||||
return true;
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
try {
|
||||
if (window.parent && window.parent.frames) {
|
||||
for (let i = 0; i < window.parent.frames.length; i++) {
|
||||
try {
|
||||
const frame = window.parent.frames[i];
|
||||
if (frame.wx && frame.__wxConfig) {
|
||||
this.wxFrame = frame;
|
||||
return true;
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
return false;
|
||||
}
|
||||
|
||||
loadConfiguration() {
|
||||
this.config = this.wxFrame.__wxConfig;
|
||||
this.allPages = [].concat(this.config.pages || []);
|
||||
|
||||
// 补充分包页面
|
||||
var subPkgs = this.config.subPackages || this.config.subpackages || [];
|
||||
var allPages = this.allPages;
|
||||
subPkgs.forEach(function(pkg) {
|
||||
(pkg.pages || []).forEach(function(page) {
|
||||
var fullPath = pkg.root + '/' + page;
|
||||
if (allPages.indexOf(fullPath) === -1) {
|
||||
allPages.push(fullPath);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 去重
|
||||
var seen = {};
|
||||
this.allPages = this.allPages.filter(function(p) {
|
||||
if (seen[p]) return false;
|
||||
seen[p] = true;
|
||||
return true;
|
||||
});
|
||||
|
||||
if (this.config.tabBar && this.config.tabBar.list) {
|
||||
this.tabBarPages = this.config.tabBar.list.map(function(tab) {
|
||||
return tab.pagePath.replace('.html', '');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
categorizePages() {
|
||||
this.categorizedPages = {
|
||||
tabbar: [], auth: [], home: [], list: [], detail: [],
|
||||
form: [], user: [], order: [], payment: [], setting: [], other: []
|
||||
};
|
||||
var self = this;
|
||||
this.tabBarPages.forEach(function(page) {
|
||||
self.categorizedPages.tabbar.push({
|
||||
url: page, name: page, method: 'switchTab', type: 'tabbar'
|
||||
});
|
||||
});
|
||||
var categoryKeywords = {
|
||||
auth: ['login', 'register', 'auth', 'sign', 'entry', 'bridge'],
|
||||
home: ['home', 'index', 'main', 'dashboard'],
|
||||
list: ['list', 'List'],
|
||||
detail: ['detail', 'Detail', 'info', 'Info'],
|
||||
form: ['form', 'Form', 'add', 'Add', 'edit', 'Edit', 'confirm'],
|
||||
user: ['user', 'User', 'profile', 'Profile', 'person'],
|
||||
order: ['order', 'Order', 'transaction', 'record'],
|
||||
payment: ['pay', 'Pay', 'payment', 'recharge'],
|
||||
setting: ['setting', 'Setting', 'config', 'Config']
|
||||
};
|
||||
this.allPages.forEach(function(page) {
|
||||
if (self.tabBarPages.includes(page)) return;
|
||||
var pageInfo = { url: page, name: page, method: 'navigateTo', type: 'normal' };
|
||||
var categorized = false;
|
||||
for (var category in categoryKeywords) {
|
||||
var keywords = categoryKeywords[category];
|
||||
if (keywords.some(function(kw) { return page.toLowerCase().includes(kw.toLowerCase()); })) {
|
||||
self.categorizedPages[category].push(pageInfo);
|
||||
categorized = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!categorized) self.categorizedPages.other.push(pageInfo);
|
||||
});
|
||||
// Build flat menuItems
|
||||
var items = [];
|
||||
Object.keys(this.categorizedPages).forEach(function(cat) {
|
||||
self.categorizedPages[cat].forEach(function(p) { items.push(p); });
|
||||
});
|
||||
this.menuItems = items;
|
||||
}
|
||||
|
||||
// 获取原始导航方法(绕过防跳转 hook)
|
||||
_getNav(method) {
|
||||
if (this._redirectGuard) {
|
||||
if (method === 'redirectTo' && this._origRedirectTo) return this._origRedirectTo;
|
||||
if (method === 'reLaunch' && this._origReLaunch) return this._origReLaunch;
|
||||
if (method === 'navigateTo' && this._origNavigateTo) return this._origNavigateTo;
|
||||
}
|
||||
return this.wxFrame.wx[method];
|
||||
}
|
||||
|
||||
goTo(url) {
|
||||
var isTabBar = this.tabBarPages.some(function(page) {
|
||||
return page === url || page === url.replace('/', '') || ('/' + page) === url;
|
||||
});
|
||||
var options = {
|
||||
url: url.startsWith('/') ? url : '/' + url,
|
||||
success: function() {},
|
||||
fail: function() {}
|
||||
};
|
||||
if (isTabBar) {
|
||||
this.wxFrame.wx.switchTab(options);
|
||||
} else {
|
||||
this._getNav('navigateTo').call(this.wxFrame.wx, options);
|
||||
}
|
||||
}
|
||||
|
||||
_safeNavigate(pageUrl) {
|
||||
var self = this;
|
||||
return new Promise(function(resolve) {
|
||||
var url = pageUrl.startsWith('/') ? pageUrl : '/' + pageUrl;
|
||||
self._getNav('reLaunch').call(self.wxFrame.wx, {
|
||||
url: url,
|
||||
success: function() { resolve(true); },
|
||||
fail: function() {
|
||||
self.wxFrame.wx.switchTab({
|
||||
url: url,
|
||||
success: function() { resolve(true); },
|
||||
fail: function() {
|
||||
self._getNav('redirectTo').call(self.wxFrame.wx, {
|
||||
url: url,
|
||||
success: function() { resolve(true); },
|
||||
fail: function() { resolve(false); }
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
back(delta) {
|
||||
delta = delta || 1;
|
||||
this.wxFrame.wx.navigateBack({
|
||||
delta: delta,
|
||||
success: function() {},
|
||||
fail: function() {}
|
||||
});
|
||||
}
|
||||
|
||||
current() {
|
||||
try {
|
||||
if (this.wxFrame.getCurrentPages) {
|
||||
var pages = this.wxFrame.getCurrentPages();
|
||||
if (pages.length > 0) {
|
||||
var cur = pages[pages.length - 1];
|
||||
return cur.route || cur.__route__ || '';
|
||||
}
|
||||
}
|
||||
return '';
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
_installHook() {
|
||||
if (this._hooked) return;
|
||||
this._hooked = true;
|
||||
this._capturedAPIs = [];
|
||||
this._allCapturedAPIs = {};
|
||||
this._globalAPIs = [];
|
||||
this._globalAPISet = new Set();
|
||||
var wx = this.wxFrame.wx;
|
||||
var self = this;
|
||||
var methods = ['request', 'uploadFile', 'downloadFile'];
|
||||
this._originalMethods = {};
|
||||
methods.forEach(function(method) {
|
||||
if (wx[method]) {
|
||||
self._originalMethods[method] = wx[method];
|
||||
wx[method] = function(options) {
|
||||
var reqMethod = (options.method || (method === 'request' ? 'GET' : method)).toUpperCase();
|
||||
var url = options.url || '';
|
||||
var apiInfo = { url: url, method: reqMethod, timestamp: new Date().toLocaleTimeString(), type: method };
|
||||
self._capturedAPIs.push(apiInfo);
|
||||
var dedupeKey = reqMethod + '|' + self._extractPath(url);
|
||||
if (!self._globalAPISet.has(dedupeKey)) {
|
||||
self._globalAPISet.add(dedupeKey);
|
||||
self._globalAPIs.push(apiInfo);
|
||||
}
|
||||
// merge custom headers
|
||||
if (self._customHeaders && Object.keys(self._customHeaders).length > 0) {
|
||||
options.header = options.header || {};
|
||||
for (var hk in self._customHeaders) {
|
||||
if (self._customHeaders.hasOwnProperty(hk)) {
|
||||
options.header[hk] = self._customHeaders[hk];
|
||||
}
|
||||
}
|
||||
}
|
||||
return self._originalMethods[method].call(wx, options);
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_uninstallHook() {
|
||||
if (!this._hooked) return;
|
||||
var wx = this.wxFrame.wx;
|
||||
var self = this;
|
||||
Object.keys(this._originalMethods).forEach(function(method) {
|
||||
wx[method] = self._originalMethods[method];
|
||||
});
|
||||
this._hooked = false;
|
||||
}
|
||||
|
||||
_extractPath(url) {
|
||||
if (!this._pathCache) this._pathCache = {};
|
||||
if (this._pathCache[url]) return this._pathCache[url];
|
||||
var path;
|
||||
try { path = new URL(url).pathname; } catch(e) { path = url.split('?')[0]; }
|
||||
this._pathCache[url] = path;
|
||||
return path;
|
||||
}
|
||||
|
||||
_sleep(ms) {
|
||||
return new Promise(function(resolve) { setTimeout(resolve, ms); });
|
||||
}
|
||||
|
||||
stopAutoVisit() {
|
||||
this._isAutoVisiting = false;
|
||||
}
|
||||
|
||||
getAPIs() {
|
||||
return this._globalAPIs || [];
|
||||
}
|
||||
|
||||
getResults() {
|
||||
return this._autoVisitResults || {};
|
||||
}
|
||||
|
||||
enableRedirectGuard() {
|
||||
if (this._redirectGuard) return {ok:true, already:true};
|
||||
this._redirectGuard = true;
|
||||
this._blockedRedirects = [];
|
||||
var wx = this.wxFrame.wx;
|
||||
var self = this;
|
||||
this._origRedirectTo = wx.redirectTo;
|
||||
this._origReLaunch = wx.reLaunch;
|
||||
this._origNavigateTo = wx.navigateTo;
|
||||
// hook redirectTo — 拦截强制跳转,调用 success 防止页面卡死
|
||||
wx.redirectTo = function(options) {
|
||||
var url = (options && options.url) || '';
|
||||
self._blockedRedirects.push({type:'redirectTo', url:url, time:new Date().toLocaleTimeString()});
|
||||
console.warn('[防跳转] 已拦截 redirectTo:', url);
|
||||
if (options && options.success) options.success({errMsg:'redirectTo:ok'});
|
||||
if (options && options.complete) options.complete({errMsg:'redirectTo:ok'});
|
||||
};
|
||||
// hook reLaunch — 拦截强制重启
|
||||
wx.reLaunch = function(options) {
|
||||
var url = (options && options.url) || '';
|
||||
self._blockedRedirects.push({type:'reLaunch', url:url, time:new Date().toLocaleTimeString()});
|
||||
console.warn('[防跳转] 已拦截 reLaunch:', url);
|
||||
if (options && options.success) options.success({errMsg:'reLaunch:ok'});
|
||||
if (options && options.complete) options.complete({errMsg:'reLaunch:ok'});
|
||||
};
|
||||
// hook navigateTo — 拦截跳转新页面
|
||||
wx.navigateTo = function(options) {
|
||||
var url = (options && options.url) || '';
|
||||
self._blockedRedirects.push({type:'navigateTo', url:url, time:new Date().toLocaleTimeString()});
|
||||
console.warn('[防跳转] 已拦截 navigateTo:', url);
|
||||
if (options && options.success) options.success({errMsg:'navigateTo:ok'});
|
||||
if (options && options.complete) options.complete({errMsg:'navigateTo:ok'});
|
||||
};
|
||||
return {ok:true};
|
||||
}
|
||||
|
||||
disableRedirectGuard() {
|
||||
if (!this._redirectGuard) return;
|
||||
this._redirectGuard = false;
|
||||
var wx = this.wxFrame.wx;
|
||||
if (this._origRedirectTo) wx.redirectTo = this._origRedirectTo;
|
||||
if (this._origReLaunch) wx.reLaunch = this._origReLaunch;
|
||||
if (this._origNavigateTo) wx.navigateTo = this._origNavigateTo;
|
||||
this._origRedirectTo = null;
|
||||
this._origReLaunch = null;
|
||||
this._origNavigateTo = null;
|
||||
}
|
||||
|
||||
getBlockedRedirects() {
|
||||
return this._blockedRedirects || [];
|
||||
}
|
||||
|
||||
isRedirectGuardOn() {
|
||||
return !!this._redirectGuard;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
window.nav = new UniversalMiniProgramNavigator();
|
||||
} catch (e) {
|
||||
console.error('Navigator init failed:', e.message);
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,149 @@
|
||||
"""
|
||||
MiniProgramNavigator - inject JS navigator and control mini program pages.
|
||||
"""
|
||||
import asyncio
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
_NAV_JS = (Path(__file__).parent / "nav_inject.js").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
class MiniProgramNavigator:
|
||||
"""Inject universal navigator JS, then use window.nav for all operations."""
|
||||
|
||||
def __init__(self, engine):
|
||||
self.engine = engine
|
||||
self.pages = []
|
||||
self.tab_bar_pages = []
|
||||
self.app_info = {}
|
||||
self._injected = False
|
||||
|
||||
async def _ensure(self, force=False):
|
||||
if force or not self._injected:
|
||||
await self.engine.evaluate_js(_NAV_JS, timeout=10.0)
|
||||
self._injected = True
|
||||
|
||||
async def fetch_config(self):
|
||||
"""Inject navigator and read pages/tabBar/appid from window.nav."""
|
||||
# 强制重新注入,以支持切换小程序后获取新配置
|
||||
await self._ensure(force=True)
|
||||
result = await self.engine.evaluate_js(
|
||||
"JSON.stringify({pages:window.nav?window.nav.allPages:[],"
|
||||
"tabBar:window.nav?window.nav.tabBarPages:[],"
|
||||
"appid:window.nav&&window.nav.config?(window.nav.config.appid||''):'',"
|
||||
"entry:window.nav&&window.nav.config?(window.nav.config.entryPagePath||''):'',"
|
||||
"name:(function(){try{var a=window.__wxConfig||window.wx&&window.wx.__wxConfig||{};"
|
||||
"var b=a.accountInfo&&a.accountInfo.appAccount;"
|
||||
"return b&&b.nickname||a.appname||''}catch(e){return ''}})()"
|
||||
"})",
|
||||
timeout=5.0,
|
||||
)
|
||||
value = self._extract_value(result)
|
||||
if not value:
|
||||
return
|
||||
try:
|
||||
config = json.loads(value)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
return
|
||||
self.pages = config.get("pages", [])
|
||||
self.tab_bar_pages = config.get("tabBar", [])
|
||||
self.app_info = {
|
||||
"appid": config.get("appid", ""),
|
||||
"entry": config.get("entry", ""),
|
||||
"name": config.get("name", ""),
|
||||
}
|
||||
|
||||
async def navigate_to(self, route):
|
||||
"""Smart navigate via window.nav.goTo (switchTab for tabbar, navigateTo for others)."""
|
||||
await self._ensure()
|
||||
safe = route.replace("'", "\\'")
|
||||
await self.engine.evaluate_js(f"window.nav.goTo('{safe}')", timeout=5.0)
|
||||
|
||||
async def redirect_to(self, route):
|
||||
"""wx.redirectTo via the detected wxFrame."""
|
||||
await self._ensure()
|
||||
safe = route.replace("'", "\\'")
|
||||
await self.engine.evaluate_js(
|
||||
f"window.nav.wxFrame.wx.redirectTo({{url:'/{safe}'}})", timeout=5.0
|
||||
)
|
||||
|
||||
async def relaunch_to(self, route):
|
||||
"""Safe navigate: reLaunch → switchTab → redirectTo fallback chain."""
|
||||
await self._ensure()
|
||||
safe = route.replace("'", "\\'")
|
||||
await self.engine.evaluate_js(
|
||||
f"window.nav._safeNavigate('{safe}')", timeout=5.0
|
||||
)
|
||||
|
||||
async def navigate_back(self, delta=1):
|
||||
"""wx.navigateBack via window.nav.back."""
|
||||
await self._ensure()
|
||||
await self.engine.evaluate_js(f"window.nav.back({delta})", timeout=5.0)
|
||||
|
||||
async def get_current_route(self):
|
||||
"""Get current page route via the detected wxFrame."""
|
||||
await self._ensure()
|
||||
result = await self.engine.evaluate_js(
|
||||
"(function(){try{if(window.nav&&window.nav.wxFrame){"
|
||||
"var p=window.nav.wxFrame.getCurrentPages();"
|
||||
"return p.length?p[p.length-1].route||p[p.length-1].__route__||'':''"
|
||||
"}return ''}catch(e){return ''}})()",
|
||||
timeout=3.0,
|
||||
)
|
||||
return self._extract_value(result) or ""
|
||||
|
||||
async def auto_visit(self, pages, delay=2.0, on_progress=None, cancel_event=None):
|
||||
"""Visit pages sequentially using safe navigation."""
|
||||
total = len(pages)
|
||||
for i, route in enumerate(pages):
|
||||
if cancel_event and cancel_event.is_set():
|
||||
break
|
||||
if on_progress:
|
||||
on_progress(i, total, route)
|
||||
try:
|
||||
await self.relaunch_to(route)
|
||||
except Exception:
|
||||
pass
|
||||
await asyncio.sleep(delay)
|
||||
if on_progress:
|
||||
on_progress(total, total, "done")
|
||||
|
||||
async def enable_redirect_guard(self):
|
||||
"""开启防强制跳转"""
|
||||
await self._ensure()
|
||||
result = await self.engine.evaluate_js(
|
||||
"JSON.stringify(window.nav.enableRedirectGuard())", timeout=5.0)
|
||||
value = self._extract_value(result)
|
||||
if value:
|
||||
try:
|
||||
return json.loads(value)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
pass
|
||||
return {"ok": False}
|
||||
|
||||
async def disable_redirect_guard(self):
|
||||
"""关闭防强制跳转"""
|
||||
await self._ensure()
|
||||
await self.engine.evaluate_js(
|
||||
"window.nav.disableRedirectGuard()", timeout=5.0)
|
||||
|
||||
async def get_blocked_redirects(self):
|
||||
"""获取被拦截的跳转记录"""
|
||||
await self._ensure()
|
||||
result = await self.engine.evaluate_js(
|
||||
"JSON.stringify(window.nav.getBlockedRedirects())", timeout=5.0)
|
||||
value = self._extract_value(result)
|
||||
if value:
|
||||
try:
|
||||
return json.loads(value)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
pass
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def _extract_value(result):
|
||||
if not result:
|
||||
return None
|
||||
r = result.get("result", {})
|
||||
inner = r.get("result", {})
|
||||
return inner.get("value")
|
||||
Vendored
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Vendored
+144
File diff suppressed because one or more lines are too long
@@ -0,0 +1,111 @@
|
||||
"""
|
||||
UserScript loader - parses Tampermonkey-style metadata and builds CDP injection commands.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
|
||||
@dataclass
|
||||
class UserScript:
|
||||
name: str
|
||||
description: str
|
||||
match: str
|
||||
run_at: str
|
||||
inject_into: str
|
||||
source: str
|
||||
file_path: str
|
||||
|
||||
def __repr__(self):
|
||||
return f"UserScript(name={self.name!r}, run_at={self.run_at!r}, file={os.path.basename(self.file_path)!r})"
|
||||
|
||||
|
||||
def parse_metadata(source: str) -> dict:
|
||||
metadata = {}
|
||||
in_block = False
|
||||
for line in source.splitlines():
|
||||
stripped = line.strip()
|
||||
if stripped == "// ==UserScript==":
|
||||
in_block = True
|
||||
continue
|
||||
if stripped == "// ==/UserScript==":
|
||||
break
|
||||
if in_block and stripped.startswith("// @"):
|
||||
m = re.match(r"^//\s*@(\S+)\s*(.*?)\s*$", stripped)
|
||||
if m:
|
||||
metadata[m.group(1)] = m.group(2)
|
||||
return metadata
|
||||
|
||||
|
||||
def load_userscripts(scripts_dir: str) -> List[UserScript]:
|
||||
scripts = []
|
||||
scripts_path = Path(scripts_dir)
|
||||
if not scripts_path.exists():
|
||||
return scripts
|
||||
for js_file in sorted(scripts_path.glob("*.js")):
|
||||
script = _load_single_file(js_file)
|
||||
if script:
|
||||
scripts.append(script)
|
||||
return scripts
|
||||
|
||||
|
||||
def load_userscripts_by_files(file_paths: List[str]) -> List[UserScript]:
|
||||
scripts = []
|
||||
for fp in file_paths:
|
||||
p = Path(fp)
|
||||
if not p.exists():
|
||||
continue
|
||||
script = _load_single_file(p)
|
||||
if script:
|
||||
scripts.append(script)
|
||||
return scripts
|
||||
|
||||
|
||||
def _load_single_file(js_file: Path) -> UserScript | None:
|
||||
try:
|
||||
source = js_file.read_text(encoding="utf-8")
|
||||
except Exception:
|
||||
return None
|
||||
meta = parse_metadata(source)
|
||||
return UserScript(
|
||||
name=meta.get("name", js_file.stem),
|
||||
description=meta.get("description", ""),
|
||||
match=meta.get("match", "*"),
|
||||
run_at=meta.get("run-at", "document-start"),
|
||||
inject_into=meta.get("inject-into", "page"),
|
||||
source=source,
|
||||
file_path=str(js_file),
|
||||
)
|
||||
|
||||
|
||||
def _indent(text: str, spaces: int) -> str:
|
||||
prefix = " " * spaces
|
||||
return "\n".join(prefix + line for line in text.splitlines())
|
||||
|
||||
|
||||
def build_injection_wrapper(script: UserScript) -> str:
|
||||
safe_name = script.name.replace("\\", "\\\\").replace("'", "\\'")
|
||||
return f"""(function() {{
|
||||
'use strict';
|
||||
try {{
|
||||
{_indent(script.source, 4)}
|
||||
}} catch(__e__) {{
|
||||
console.error('[UserScript] Error in "{safe_name}":', __e__);
|
||||
}}
|
||||
}})();"""
|
||||
|
||||
|
||||
def build_cdp_add_script_command(script: UserScript, cmd_id: int) -> str:
|
||||
wrapped = build_injection_wrapper(script)
|
||||
return json.dumps({
|
||||
"id": cmd_id,
|
||||
"method": "Page.addScriptToEvaluateOnNewDocument",
|
||||
"params": {"source": wrapped},
|
||||
})
|
||||
|
||||
|
||||
def build_cdp_enable_page_command(cmd_id: int) -> str:
|
||||
return json.dumps({"id": cmd_id, "method": "Page.enable", "params": {}})
|
||||
@@ -0,0 +1,87 @@
|
||||
// ==UserScript==
|
||||
// @name Bypass_Debugger
|
||||
// @namespace https://github.com/0xsdeo/Bypass_Debugger
|
||||
// @version 2024-12-06
|
||||
// @description Bypass new Function --> debugger && constructor --> debugger && eval --> debugger
|
||||
// @author 0xsdeo
|
||||
// @match http://*/*
|
||||
// @icon data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
|
||||
// @grant none
|
||||
// @run-at document-start
|
||||
// ==/UserScript==
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
globalThis.__INJECTED__ = true;
|
||||
|
||||
// eval may not exist in WeChat miniapp context
|
||||
if (typeof eval !== 'undefined') {
|
||||
try {
|
||||
let temp_eval = eval;
|
||||
window.eval = function () {
|
||||
if (typeof arguments[0] == "string") {
|
||||
arguments[0] = arguments[0].replaceAll(/debugger/g, '');
|
||||
}
|
||||
return temp_eval(...arguments);
|
||||
}
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
try {
|
||||
let OrigFunction = Function;
|
||||
|
||||
let NewFunction = function () {
|
||||
for (let i = 0; i < arguments.length; i++) {
|
||||
if (typeof arguments[i] == "string") {
|
||||
arguments[i] = arguments[i].replaceAll(/debugger/g, '');
|
||||
}
|
||||
}
|
||||
return OrigFunction(...arguments);
|
||||
}
|
||||
|
||||
// Try to set prototype, skip if read-only (WeChat freezes Function)
|
||||
try { NewFunction.prototype = OrigFunction.prototype; } catch(e) {}
|
||||
try {
|
||||
NewFunction.prototype.constructor = function () {
|
||||
for (let i = 0; i < arguments.length; i++) {
|
||||
if (typeof arguments[i] == "string") {
|
||||
arguments[i] = arguments[i].replaceAll(/debugger/g, '');
|
||||
}
|
||||
}
|
||||
return OrigFunction(...arguments);
|
||||
}
|
||||
NewFunction.prototype.constructor.prototype = NewFunction.prototype;
|
||||
} catch(e) {}
|
||||
|
||||
// Replace global Function
|
||||
try { window.Function = NewFunction; } catch(e) {}
|
||||
try { globalThis.Function = NewFunction; } catch(e) {}
|
||||
try { Object.freeze(NewFunction); } catch(e) {}
|
||||
} catch(e) {}
|
||||
|
||||
// Also patch setTimeout/setInterval which can take string arguments with debugger
|
||||
try {
|
||||
let origSetTimeout = setTimeout;
|
||||
window.setTimeout = function(fn, delay) {
|
||||
if (typeof fn === 'string') {
|
||||
fn = fn.replaceAll(/debugger/g, '');
|
||||
}
|
||||
return origSetTimeout.apply(this, arguments);
|
||||
};
|
||||
} catch(e) {}
|
||||
|
||||
try {
|
||||
let origSetInterval = setInterval;
|
||||
window.setInterval = function(fn, delay) {
|
||||
if (typeof fn === 'string') {
|
||||
fn = fn.replaceAll(/debugger/g, '');
|
||||
}
|
||||
return origSetInterval.apply(this, arguments);
|
||||
};
|
||||
} catch(e) {}
|
||||
|
||||
console.log('[Bypass_Debugger] hooks installed');
|
||||
window.__BYPASS_DEBUGGER__ = true;
|
||||
Object.freeze(Function);
|
||||
})();
|
||||
Reference in New Issue
Block a user