Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e6d2706e2b | ||
|
|
a31a7d4d03 | ||
|
|
2f77fabd76 | ||
|
|
43abf4fe8c | ||
|
|
c71c22f7d1 | ||
|
|
970c0bad7d | ||
|
|
69983bfbf0 | ||
|
|
e419752a5c | ||
|
|
68598ed26d | ||
|
|
e7d33319c8 | ||
|
|
6a94b31be7 | ||
|
|
c7653cc698 |
@@ -1,145 +1,68 @@
|
|||||||
# First
|
# First
|
||||||
|
|
||||||
> 微信小程序安全调试工具 —— 基于 Frida + CDP 代理,支持 Windows / macOS 双平台,GUI 与 CLI 双模式
|
微信小程序安全调试工具 — Frida + CDP,支持 macOS / Windows
|
||||||
|
|
||||||
---
|
> 自v1.1.0版本后开始闭源,往后版本更新只上传编译好的版本,下载最新版请去release下载。
|
||||||
|
|
||||||
## 截图预览
|
## 免责声明
|
||||||
|
|
||||||
### 主界面
|
本工具仅供安全研究与学习使用,请勿用于未授权的目标,使用者须自行承担相关法律责任。
|
||||||
|
|
||||||

|
## 功能
|
||||||
|
|
||||||
### 路由导航
|
- Frida 注入微信,CDP 代理桥接 Chrome DevTools
|
||||||
|
- 云函数动态捕获(Hook wx.cloud.callFunction,参数修改重放)
|
||||||
|
- wx.* API 捕获(login / request / getUserProfile 等)
|
||||||
|
- 路由枚举与导航,跳转守卫检测
|
||||||
|
- wxapkg 解密解包 + 敏感信息扫描
|
||||||
|
- MCP Server 集成(AI 辅助分析)
|
||||||
|
- 偏移量 / Skills 在线更新
|
||||||
|
|
||||||

|
## 截图
|
||||||
|
### 主页
|
||||||
|

|
||||||
|
### 云函数捕获
|
||||||
|

|
||||||
|
### wx.api捕获
|
||||||
|

|
||||||
|
### 反编译文件编辑搜索
|
||||||
|

|
||||||
|
### Frist-MCP
|
||||||
|

|
||||||
|

|
||||||
|
### 自动更新
|
||||||
|

|
||||||
|
|
||||||
### 云函数分析
|
## 下载
|
||||||
|
|
||||||

|
[Releases](https://github.com/Spade-sec/First/releases)
|
||||||
|
|
||||||
### 调试开关(JSRPC / wx.cloud 调用)
|
| 平台 | 文件 |
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
### 敏感信息提取
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
<img src="https://s1.galgame.fun/imgb/u55/20260416_69e0be0c38e39.png" alt="敏感信息提取-2" />
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 功能特性
|
|
||||||
|
|
||||||
- Frida 动态注入微信客户端,转发小程序调试协议
|
|
||||||
- CDP 代理桥接,Chrome DevTools 直连调试
|
|
||||||
- 路由枚举、分类与一键跳转
|
|
||||||
- 云函数调用监控与参数分析(动态 Hook + 静态扫描)
|
|
||||||
- UserScript 自动注入(支持 URL 匹配、run-at 时机控制)
|
|
||||||
- wxapkg 解密解包 + 敏感信息扫描(IP / 密钥 / 云存储 / JWT 等)
|
|
||||||
- 深色 / 浅色主题切换
|
|
||||||
- GUI(PySide6)与 CLI 双模式
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 环境要求
|
|
||||||
|
|
||||||
| 依赖 | 版本 |
|
|
||||||
|------|------|
|
|------|------|
|
||||||
| Python | >= 3.10 |
|
| macOS (Apple Silicon) | `First-ARM.dmg` |
|
||||||
| frida | >= 17.0.0 |
|
| macOS (Intel) | `First-Intel.dmg` |
|
||||||
| websockets | >= 12.0 |
|
| Windows | `First-Windows.zip` |
|
||||||
| protobuf | >= 4.0.0 |
|
|
||||||
| PySide6 | >= 6.5.0 |
|
|
||||||
| pycryptodome | 最新版 |
|
|
||||||
|
|
||||||
安装依赖:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pip install -r requirements.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 支持的微信版本
|
## 支持的微信版本
|
||||||
|
|
||||||
### Windows
|
### Windows
|
||||||
|
|
||||||
- WMPF 版本:11581, 11633, 13331, 13341, 13487, 13639, 13655, 13871, 13909, 14161, 14199, 14315, 16133, 16203, 16389, 16467, 16771, 16815, 16965, 17037, 17071, 17127, 18055, 18151, 18787, 18891, 18955, 19027, 19201
|
- WMPF 版本:11581, 11633, 13331, 13341, 13487, 13639, 13655, 13871, 13909, 14161, 14199, 14315, 16133, 16203, 16389, 16467, 16771, 16815, 16965, 17037, 17071, 17127, 18055, 18151, 18787, 18891, 18955, 19027, 19201(实时更新)
|
||||||
- 推荐微信版本:**4.1.0.30**
|
- 推荐微信版本:**4.1.0.30**
|
||||||
- 下载地址:[weixin/4.1.0.30](https://github.com/vs-olitus/wx-version/releases/tag/4.1.0.30)
|
- 下载地址:[weixin/4.1.0.30](https://github.com/vs-olitus/wx-version/releases/tag/4.1.0.30)
|
||||||
|
|
||||||
### macOS
|
### macOS
|
||||||
|
|
||||||
- WMPF 版本:18152, 18788
|
- WMPF 版本:18152, 18788(实时更新)
|
||||||
- 推荐微信版本:**4.1.7.30**
|
- 推荐微信版本:**4.1.7.30**
|
||||||
- 下载地址:[weixin/4.1.7.30](https://github.com/vs-olitus/wx-version/releases/tag/4.1.7.30)
|
- 下载地址:[weixin/4.1.7.30](https://github.com/vs-olitus/wx-version/releases/tag/4.1.7.30)
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 快速开始
|
## 快速开始
|
||||||
|
|
||||||
### Windows
|
1. 打开 First
|
||||||
|
2. 点击启动 — Frida 自动注入微信
|
||||||
**一键启动:** 双击 `启动.bat`,首次运行会自动安装依赖。
|
3. 在微信中打开小程序
|
||||||
|
4. 使用 DevTools / 云捕获 / 导航等功能
|
||||||
**手动启动:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python gui.py
|
|
||||||
```
|
|
||||||
|
|
||||||
### macOS
|
|
||||||
|
|
||||||
**一键启动:** 终端执行 `./启动.sh`,首次运行会自动安装依赖。
|
|
||||||
|
|
||||||
**手动启动:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python3 gui.py
|
|
||||||
```
|
|
||||||
|
|
||||||
启动后在主界面点击 **启动调试**,然后再打开小程序即可(请勿在启动调试前打开小程序)。
|
|
||||||
|
|
||||||
### CLI 模式
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 默认端口启动
|
|
||||||
python main.py
|
|
||||||
|
|
||||||
# 自定义端口
|
|
||||||
python main.py --cdp-port 62000
|
|
||||||
|
|
||||||
# 开启详细日志
|
|
||||||
python main.py --debug-main --debug-frida
|
|
||||||
```
|
|
||||||
|
|
||||||
### 连接 Chrome DevTools
|
|
||||||
|
|
||||||
启动后,在 Chrome 地址栏输入:
|
|
||||||
|
|
||||||
```
|
|
||||||
devtools://devtools/bundled/inspector.html?ws=127.0.0.1:62000
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 服务号调试操作
|
|
||||||
|
|
||||||
1. 打开一个小程序页面作为占位(保持不关闭)
|
|
||||||
2. 访问需要调试的服务号内容(如 H5 网页)
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
3. 在工具栏「服务」目标列表中找到目标网页,双击后切换至调试视图即可开始调试
|
|
||||||
|
|
||||||

|
|
||||||

|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## macOS 注意事项
|
## macOS 注意事项
|
||||||
|
|
||||||
@@ -155,53 +78,7 @@ devtools://devtools/bundled/inspector.html?ws=127.0.0.1:62000
|
|||||||
sudo codesign --force --deep --sign - /Applications/WeChat.app
|
sudo codesign --force --deep --sign - /Applications/WeChat.app
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
## 参考
|
||||||
|
|
||||||
## 参数说明
|
|
||||||
|
|
||||||
| 参数 | 默认值 | 说明 |
|
|
||||||
|------|--------|------|
|
|
||||||
| `--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
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 常见问题
|
|
||||||
|
|
||||||
**Q: Frida 已连接,但小程序端显示未连接或无法断点调试?**
|
|
||||||
|
|
||||||
确认操作顺序无误后,尝试以下步骤:
|
|
||||||
|
|
||||||
1. 彻底卸载微信并重启电脑(重要聊天记录请提前备份)
|
|
||||||
2. 删除 `C:\Users\用户名\AppData\Roaming\Tencent\xwechat\XPlugin\Plugins\RadiumWMPF` 下所有数字命名的文件夹
|
|
||||||
3. 再次重启后安装微信 4.1.0.30 版本
|
|
||||||
4. 检查上述路径,确认文件夹编号为 `16389`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 参考项目
|
|
||||||
|
|
||||||
- [evi0s/WMPFDebugger](https://github.com/evi0s/WMPFDebugger)
|
- [evi0s/WMPFDebugger](https://github.com/evi0s/WMPFDebugger)
|
||||||
- [0xsdeo/HeartK](https://github.com/0xsdeo/HeartK)
|
- [0xsdeo/HeartK](https://github.com/0xsdeo/HeartK)
|
||||||
@@ -209,22 +86,15 @@ pyinstaller WMPFDebugger.spec
|
|||||||
- [进击的HACK / JSRPC 与调用 wx.cloud](https://mp.weixin.qq.com/s/hTlekrCPiMJCvsHYx7CAxw)
|
- [进击的HACK / JSRPC 与调用 wx.cloud](https://mp.weixin.qq.com/s/hTlekrCPiMJCvsHYx7CAxw)
|
||||||
- [linguo2625469/WMPFDebugger-mac](https://github.com/linguo2625469/WMPFDebugger-mac)
|
- [linguo2625469/WMPFDebugger-mac](https://github.com/linguo2625469/WMPFDebugger-mac)
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 致谢
|
## 致谢
|
||||||
|
|
||||||
感谢 **0xsdeo** 师傅的大力支持与思路提供。
|
感谢 **0xsdeo** 师傅的大力支持与思路提供。
|
||||||
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 交流群
|
## 交流群
|
||||||
|
|
||||||
群满 200 人后需要手动邀请,请加我拉群:
|
群满 200 人后需要手动邀请,请加我拉群:
|
||||||
|
|
||||||

|
<img src="https://s1.galgame.fun/imgb/u55/20260601_6a1d196618084.png" alt="微信二维码" width="300" />
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 免责声明
|
|
||||||
|
|
||||||
本工具仅供安全研究与学习使用,请勿用于未授权的目标,使用者须自行承担相关法律责任。
|
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
"Version": 11581,
|
"Version": 11581,
|
||||||
"LoadStartHookOffset": "0x28E9190",
|
"LoadStartHookOffset": "0x28E9190",
|
||||||
"CDPFilterHookOffset": "0x38C4350",
|
"CDPFilterHookOffset": "0x38C4350",
|
||||||
"SceneOffsets": [1208, 1160, 488]
|
"SceneOffsets": [56, 1208, 8, 1160, 16, 488]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
"Version": 11633,
|
"Version": 11633,
|
||||||
"LoadStartHookOffset": "0x28F22A0",
|
"LoadStartHookOffset": "0x28F22A0",
|
||||||
"CDPFilterHookOffset": "0x38D41E0",
|
"CDPFilterHookOffset": "0x38D41E0",
|
||||||
"SceneOffsets": [1208, 1160, 488]
|
"SceneOffsets": [56, 1208, 8, 1160, 16, 488]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
"Version": 13331,
|
"Version": 13331,
|
||||||
"LoadStartHookOffset": "0x0FFC200",
|
"LoadStartHookOffset": "0x0FFC200",
|
||||||
"CDPFilterHookOffset": "0x2420100",
|
"CDPFilterHookOffset": "0x2420100",
|
||||||
"SceneOffsets": [1272, 1224, 488]
|
"SceneOffsets": [56, 1272, 8, 1224, 16, 488]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
"Version": 13341,
|
"Version": 13341,
|
||||||
"LoadStartHookOffset": "0x10009E0",
|
"LoadStartHookOffset": "0x10009E0",
|
||||||
"CDPFilterHookOffset": "0x242E8E0",
|
"CDPFilterHookOffset": "0x242E8E0",
|
||||||
"SceneOffsets": [1272, 1224, 488]
|
"SceneOffsets": [56, 1272, 8, 1224, 16, 488]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
"Version": 13487,
|
"Version": 13487,
|
||||||
"LoadStartHookOffset": "0x0FFB600",
|
"LoadStartHookOffset": "0x0FFB600",
|
||||||
"CDPFilterHookOffset": "0x241FEB0",
|
"CDPFilterHookOffset": "0x241FEB0",
|
||||||
"SceneOffsets": [1272, 1224, 488]
|
"SceneOffsets": [56, 1272, 8, 1224, 16, 488]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
"Version": 13639,
|
"Version": 13639,
|
||||||
"LoadStartHookOffset": "0x1000990",
|
"LoadStartHookOffset": "0x1000990",
|
||||||
"CDPFilterHookOffset": "0x2424DE0",
|
"CDPFilterHookOffset": "0x2424DE0",
|
||||||
"SceneOffsets": [1272, 1224, 488]
|
"SceneOffsets": [56, 1272, 8, 1224, 16, 488]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
"Version": 13655,
|
"Version": 13655,
|
||||||
"LoadStartHookOffset": "0x100F4B0",
|
"LoadStartHookOffset": "0x100F4B0",
|
||||||
"CDPFilterHookOffset": "0x244A9E0",
|
"CDPFilterHookOffset": "0x244A9E0",
|
||||||
"SceneOffsets": [1280, 1232, 488]
|
"SceneOffsets": [56, 1280, 8, 1232, 16, 488]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
"Version": 13871,
|
"Version": 13871,
|
||||||
"LoadStartHookOffset": "0x101F160",
|
"LoadStartHookOffset": "0x101F160",
|
||||||
"CDPFilterHookOffset": "0x246FC40",
|
"CDPFilterHookOffset": "0x246FC40",
|
||||||
"SceneOffsets": [1360, 1312, 488]
|
"SceneOffsets": [56, 1360, 8, 1312, 16, 488]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
"Version": 13909,
|
"Version": 13909,
|
||||||
"LoadStartHookOffset": "0x101F0E0",
|
"LoadStartHookOffset": "0x101F0E0",
|
||||||
"CDPFilterHookOffset": "0x246FDC0",
|
"CDPFilterHookOffset": "0x246FDC0",
|
||||||
"SceneOffsets": [1360, 1312, 488]
|
"SceneOffsets": [56, 1360, 8, 1312, 16, 488]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
"Version": 14161,
|
"Version": 14161,
|
||||||
"LoadStartHookOffset": "0x10246C0",
|
"LoadStartHookOffset": "0x10246C0",
|
||||||
"CDPFilterHookOffset": "0x24839B0",
|
"CDPFilterHookOffset": "0x24839B0",
|
||||||
"SceneOffsets": [1360, 1312, 488]
|
"SceneOffsets": [56, 1360, 8, 1312, 16, 488]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
"Version": 14199,
|
"Version": 14199,
|
||||||
"LoadStartHookOffset": "0x10246F0",
|
"LoadStartHookOffset": "0x10246F0",
|
||||||
"CDPFilterHookOffset": "0x24839E0",
|
"CDPFilterHookOffset": "0x24839E0",
|
||||||
"SceneOffsets": [1360, 1312, 488]
|
"SceneOffsets": [56, 1360, 8, 1312, 16, 488]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
"Version": 14315,
|
"Version": 14315,
|
||||||
"LoadStartHookOffset": "0x10004C0",
|
"LoadStartHookOffset": "0x10004C0",
|
||||||
"CDPFilterHookOffset": "0x2424B50",
|
"CDPFilterHookOffset": "0x2424B50",
|
||||||
"SceneOffsets": [1272, 1224, 488]
|
"SceneOffsets": [56, 1272, 8, 1224, 16, 488]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
"Version": 16133,
|
"Version": 16133,
|
||||||
"LoadStartHookOffset": "0x470FAD0",
|
"LoadStartHookOffset": "0x470FAD0",
|
||||||
"CDPFilterHookOffset": "0x90FC7E0",
|
"CDPFilterHookOffset": "0x90FC7E0",
|
||||||
"SceneOffsets": [1360, 1312, 488]
|
"SceneOffsets": [56, 1360, 8, 1312, 16, 488]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
"Version": 16203,
|
"Version": 16203,
|
||||||
"LoadStartHookOffset": "0x4710890",
|
"LoadStartHookOffset": "0x4710890",
|
||||||
"CDPFilterHookOffset": "0x90FD640",
|
"CDPFilterHookOffset": "0x90FD640",
|
||||||
"SceneOffsets": [1360, 1312, 488]
|
"SceneOffsets": [56, 1360, 8, 1312, 16, 488]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
"Version": 16389,
|
"Version": 16389,
|
||||||
"LoadStartHookOffset": "0x24E4830",
|
"LoadStartHookOffset": "0x24E4830",
|
||||||
"CDPFilterHookOffset": "0x2E2A880",
|
"CDPFilterHookOffset": "0x2E2A880",
|
||||||
"SceneOffsets": [1360, 1312, 488]
|
"SceneOffsets": [56, 1360, 8, 1312, 16, 488]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
"Version": 16467,
|
"Version": 16467,
|
||||||
"LoadStartHookOffset": "0x24E4FD0",
|
"LoadStartHookOffset": "0x24E4FD0",
|
||||||
"CDPFilterHookOffset": "0x2E2CC90",
|
"CDPFilterHookOffset": "0x2E2CC90",
|
||||||
"SceneOffsets": [1360, 1312, 488]
|
"SceneOffsets": [56, 1360, 8, 1312, 16, 488]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
"Version": 16771,
|
"Version": 16771,
|
||||||
"LoadStartHookOffset": "0x24E9130",
|
"LoadStartHookOffset": "0x24E9130",
|
||||||
"CDPFilterHookOffset": "0x2E3C470",
|
"CDPFilterHookOffset": "0x2E3C470",
|
||||||
"SceneOffsets": [1360, 1312, 488]
|
"SceneOffsets": [56, 1360, 8, 1312, 16, 488]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
"Version": 16815,
|
"Version": 16815,
|
||||||
"LoadStartHookOffset": "0x2509690",
|
"LoadStartHookOffset": "0x2509690",
|
||||||
"CDPFilterHookOffset": "0x2E768D0",
|
"CDPFilterHookOffset": "0x2E768D0",
|
||||||
"SceneOffsets": [1416, 1360, 488]
|
"SceneOffsets": [56, 1416, 8, 1360, 16, 488]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
"Version": 16965,
|
"Version": 16965,
|
||||||
"LoadStartHookOffset": "0x2570220",
|
"LoadStartHookOffset": "0x2570220",
|
||||||
"CDPFilterHookOffset": "0x2F844A0",
|
"CDPFilterHookOffset": "0x2F844A0",
|
||||||
"SceneOffsets": [1416, 1360, 488]
|
"SceneOffsets": [56, 1416, 8, 1360, 16, 488]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
"Version": 17037,
|
"Version": 17037,
|
||||||
"LoadStartHookOffset": "0x257D0A0",
|
"LoadStartHookOffset": "0x257D0A0",
|
||||||
"CDPFilterHookOffset": "0x2FB2310",
|
"CDPFilterHookOffset": "0x2FB2310",
|
||||||
"SceneOffsets": [1408, 1352, 488]
|
"SceneOffsets": [56, 1408, 8, 1352, 16, 488]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
"Version": 17071,
|
"Version": 17071,
|
||||||
"LoadStartHookOffset": "0x258F370",
|
"LoadStartHookOffset": "0x258F370",
|
||||||
"CDPFilterHookOffset": "0x2FD3080",
|
"CDPFilterHookOffset": "0x2FD3080",
|
||||||
"SceneOffsets": [1408, 1352, 488]
|
"SceneOffsets": [56, 1408, 8, 1352, 16, 488]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
"Version": 17127,
|
"Version": 17127,
|
||||||
"LoadStartHookOffset": "0x2590910",
|
"LoadStartHookOffset": "0x2590910",
|
||||||
"CDPFilterHookOffset": "0x2FD4040",
|
"CDPFilterHookOffset": "0x2FD4040",
|
||||||
"SceneOffsets": [1408, 1352, 488]
|
"SceneOffsets": [56, 1408, 8, 1352, 16, 488]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
"Version": 18055,
|
"Version": 18055,
|
||||||
"LoadStartHookOffset": "0x25A1040",
|
"LoadStartHookOffset": "0x25A1040",
|
||||||
"CDPFilterHookOffset": "0x30031E0",
|
"CDPFilterHookOffset": "0x30031E0",
|
||||||
"SceneOffsets": [1416, 1352, 488]
|
"SceneOffsets": [56, 1416, 8, 1352, 16, 488]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
"Version": 18151,
|
"Version": 18151,
|
||||||
"LoadStartHookOffset": "0x25A2E20",
|
"LoadStartHookOffset": "0x25A2E20",
|
||||||
"CDPFilterHookOffset": "0x3013D20",
|
"CDPFilterHookOffset": "0x3013D20",
|
||||||
"SceneOffsets": [1416, 1352, 488]
|
"SceneOffsets": [56, 1416, 8, 1352, 16, 488]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
"Version": 18787,
|
"Version": 18787,
|
||||||
"LoadStartHookOffset": "0x25B2870",
|
"LoadStartHookOffset": "0x25B2870",
|
||||||
"CDPFilterHookOffset": "0x3028AD0",
|
"CDPFilterHookOffset": "0x3028AD0",
|
||||||
"SceneOffsets": [1408, 1344, 488]
|
"SceneOffsets": [56, 1408, 8, 1344, 16, 488]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
"Version": 18891,
|
"Version": 18891,
|
||||||
"LoadStartHookOffset": "0x25B50C0",
|
"LoadStartHookOffset": "0x25B50C0",
|
||||||
"CDPFilterHookOffset": "0x30245E0",
|
"CDPFilterHookOffset": "0x30245E0",
|
||||||
"SceneOffsets": [1408, 1344, 488]
|
"SceneOffsets": [56, 1408, 8, 1344, 16, 488]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
"Version": 18955,
|
"Version": 18955,
|
||||||
"LoadStartHookOffset": "0x25B52C0",
|
"LoadStartHookOffset": "0x25B52C0",
|
||||||
"CDPFilterHookOffset": "0x30248B0",
|
"CDPFilterHookOffset": "0x30248B0",
|
||||||
"SceneOffsets": [1408, 1344, 488]
|
"SceneOffsets": [56, 1408, 8, 1344, 16, 488]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
"Version": 19027,
|
"Version": 19027,
|
||||||
"LoadStartHookOffset": "0x25B52D0",
|
"LoadStartHookOffset": "0x25B52D0",
|
||||||
"CDPFilterHookOffset": "0x3024AD0",
|
"CDPFilterHookOffset": "0x3024AD0",
|
||||||
"SceneOffsets": [1408, 1344, 488]
|
"SceneOffsets": [56, 1408, 8, 1344, 16, 488]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
"Version": 19201,
|
"Version": 19201,
|
||||||
"LoadStartHookOffset": "0x25B5DD0",
|
"LoadStartHookOffset": "0x25B5DD0",
|
||||||
"CDPFilterHookOffset": "0x301B3C0",
|
"CDPFilterHookOffset": "0x301B3C0",
|
||||||
"SceneOffsets": [1376, 1312, 456]
|
"SceneOffsets": [56, 1376, 8, 1312, 16, 456]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"Version": 19339,
|
||||||
|
"LoadStartHookOffset": "0x25B5DD0",
|
||||||
|
"CDPFilterHookOffset": "0x301BA00",
|
||||||
|
"SceneOffsets": [56, 1376, 8, 1312, 16, 456]
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"Version": 19459,
|
||||||
|
"LoadStartHookOffset": "0x25BB580",
|
||||||
|
"CDPFilterHookOffset": "0x3022F20",
|
||||||
|
"SceneOffsets": [56, 1376, 8, 1312, 16, 456]
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"Version": 19481,
|
||||||
|
"LoadStartHookOffset": "0x25BBA80",
|
||||||
|
"CDPFilterHookOffset": "0x3023420",
|
||||||
|
"SceneOffsets": [56, 1376, 8, 1312, 16, 456]
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"Version": 19749,
|
||||||
|
"LoadStartHookOffset": "0x25E77C0",
|
||||||
|
"CDPFilterHookOffset": "0x30ABC60",
|
||||||
|
"SceneOffsets": [64, 1408, 8, 1344, 16, 456]
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"Version": 19769,
|
||||||
|
"LoadStartHookOffset": "0x25E7990",
|
||||||
|
"CDPFilterHookOffset": "0x30AC0A0",
|
||||||
|
"SceneOffsets": [64, 1408, 8, 1344, 16, 456]
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"Version": 19823,
|
||||||
|
"LoadStartHookOffset": "0x25ED1C0",
|
||||||
|
"CDPFilterHookOffset": "0x30B3320",
|
||||||
|
"SceneOffsets": [64, 1408, 8, 1344, 16, 456]
|
||||||
|
}
|
||||||
+14
-3
@@ -124,8 +124,19 @@ const winPatchLoadStart = (base, config) => {
|
|||||||
// Scene hijack via SceneOffsets pointer chain
|
// Scene hijack via SceneOffsets pointer chain
|
||||||
try {
|
try {
|
||||||
const offsets = config.SceneOffsets;
|
const offsets = config.SceneOffsets;
|
||||||
const ptr1 = this.context.rcx.add(56).readPointer().add(offsets[0]).readPointer();
|
const miniappConfigPtr = this.context.rcx
|
||||||
const scenePtr = ptr1.add(8).readPointer().add(offsets[1]).readPointer().add(16).readPointer().add(488);
|
.add(offsets[0])
|
||||||
|
.readPointer()
|
||||||
|
.add(offsets[1])
|
||||||
|
.readPointer();
|
||||||
|
const scenePtr = miniappConfigPtr
|
||||||
|
.add(offsets[2])
|
||||||
|
.readPointer()
|
||||||
|
.add(offsets[3])
|
||||||
|
.readPointer()
|
||||||
|
.add(offsets[4])
|
||||||
|
.readPointer()
|
||||||
|
.add(offsets[5]);
|
||||||
const scene = scenePtr.readInt();
|
const scene = scenePtr.readInt();
|
||||||
send(`[hook] scene: ${scene}`);
|
send(`[hook] scene: ${scene}`);
|
||||||
if (SCENE_WHITELIST.includes(scene)) {
|
if (SCENE_WHITELIST.includes(scene)) {
|
||||||
@@ -164,7 +175,7 @@ const parseConfig = () => {
|
|||||||
Version: 18955,
|
Version: 18955,
|
||||||
LoadStartHookOffset: "0x25B52C0",
|
LoadStartHookOffset: "0x25B52C0",
|
||||||
CDPFilterHookOffset: "0x30248B0",
|
CDPFilterHookOffset: "0x30248B0",
|
||||||
SceneOffsets: [1408, 1344, 488],
|
SceneOffsets: [56, 1408, 8, 1344, 16, 488],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return JSON.parse(rawConfig);
|
return JSON.parse(rawConfig);
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ from src.logger import Logger
|
|||||||
from src.engine import DebugEngine
|
from src.engine import DebugEngine
|
||||||
from src.navigator import MiniProgramNavigator
|
from src.navigator import MiniProgramNavigator
|
||||||
from src.cloud_audit import CloudAuditor
|
from src.cloud_audit import CloudAuditor
|
||||||
|
|
||||||
# ══════════════════════════════════════════
|
# ══════════════════════════════════════════
|
||||||
# 配色
|
# 配色
|
||||||
# ══════════════════════════════════════════
|
# ══════════════════════════════════════════
|
||||||
@@ -64,6 +63,7 @@ _MENU = [
|
|||||||
("hook", "◈", "Hook"),
|
("hook", "◈", "Hook"),
|
||||||
("targets", "◎", "服务目标"),
|
("targets", "◎", "服务目标"),
|
||||||
("cloud", "☁", "云扫描"),
|
("cloud", "☁", "云扫描"),
|
||||||
|
("wxapi", "⬢", "WxAPI捕获"),
|
||||||
("extract", "◆", "敏感信息提取"),
|
("extract", "◆", "敏感信息提取"),
|
||||||
("vconsole", "◇", "调试开关"),
|
("vconsole", "◇", "调试开关"),
|
||||||
("logs", "≡", "运行日志"),
|
("logs", "≡", "运行日志"),
|
||||||
@@ -706,6 +706,12 @@ class App(QMainWindow):
|
|||||||
self._cloud_call_history = {}
|
self._cloud_call_history = {}
|
||||||
self._cloud_all_items = []
|
self._cloud_all_items = []
|
||||||
self._cloud_row_results = {}
|
self._cloud_row_results = {}
|
||||||
|
self._cloud_live_items = [] # 动态捕获完整 call 对象列表
|
||||||
|
self._wxapi_auditor = None
|
||||||
|
self._wxapi_items = []
|
||||||
|
self._wxapi_active = False
|
||||||
|
self._wxapi_poll_timer = None
|
||||||
|
self._wxapi_selected_idx = -1
|
||||||
self._cancel_ev = None
|
self._cancel_ev = None
|
||||||
self._route_poll_id = None
|
self._route_poll_id = None
|
||||||
self._all_routes = []
|
self._all_routes = []
|
||||||
@@ -724,6 +730,7 @@ class App(QMainWindow):
|
|||||||
self._sts_q = queue.Queue()
|
self._sts_q = queue.Queue()
|
||||||
self._rte_q = queue.Queue()
|
self._rte_q = queue.Queue()
|
||||||
self._cld_q = queue.Queue()
|
self._cld_q = queue.Queue()
|
||||||
|
self._wxapi_q = queue.Queue()
|
||||||
self._tgt_q = queue.Queue()
|
self._tgt_q = queue.Queue()
|
||||||
self._nav_route_idx = -1
|
self._nav_route_idx = -1
|
||||||
|
|
||||||
@@ -869,7 +876,7 @@ class App(QMainWindow):
|
|||||||
QDesktopServices.openUrl(QUrl("https://github.com/Spade-sec/First")),
|
QDesktopServices.openUrl(QUrl("https://github.com/Spade-sec/First")),
|
||||||
self._log_add("info", "[gui] 已打开 GitHub 页面"))
|
self._log_add("info", "[gui] 已打开 GitHub 页面"))
|
||||||
sb_lay.addWidget(sb_gh)
|
sb_lay.addWidget(sb_gh)
|
||||||
sb_ver = QLabel("v1.0.8")
|
sb_ver = QLabel("v1.0.9")
|
||||||
sb_ver.setObjectName("sb_theme")
|
sb_ver.setObjectName("sb_theme")
|
||||||
sb_ver.setAlignment(Qt.AlignCenter)
|
sb_ver.setAlignment(Qt.AlignCenter)
|
||||||
sb_ver.setFont(_qfn(-2))
|
sb_ver.setFont(_qfn(-2))
|
||||||
@@ -914,6 +921,7 @@ class App(QMainWindow):
|
|||||||
self._build_hook()
|
self._build_hook()
|
||||||
self._build_targets()
|
self._build_targets()
|
||||||
self._build_cloud()
|
self._build_cloud()
|
||||||
|
self._build_wxapi()
|
||||||
self._build_extract()
|
self._build_extract()
|
||||||
self._build_vconsole()
|
self._build_vconsole()
|
||||||
self._build_logs()
|
self._build_logs()
|
||||||
@@ -1471,12 +1479,22 @@ class App(QMainWindow):
|
|||||||
def _build_cloud(self):
|
def _build_cloud(self):
|
||||||
page = QWidget()
|
page = QWidget()
|
||||||
lay = QVBoxLayout(page)
|
lay = QVBoxLayout(page)
|
||||||
lay.setContentsMargins(24, 12, 24, 16)
|
lay.setContentsMargins(0, 0, 0, 0)
|
||||||
lay.setSpacing(10)
|
lay.setSpacing(0)
|
||||||
|
|
||||||
|
# 内嵌 QStackedWidget: 主视图(0) + 动态捕获视图(1)
|
||||||
|
self._cloud_stack = QStackedWidget()
|
||||||
|
lay.addWidget(self._cloud_stack)
|
||||||
|
|
||||||
|
# ── 主视图 (index 0) ──
|
||||||
|
main_page = QWidget()
|
||||||
|
main_lay = QVBoxLayout(main_page)
|
||||||
|
main_lay.setContentsMargins(24, 12, 24, 16)
|
||||||
|
main_lay.setSpacing(10)
|
||||||
|
|
||||||
ctrl = QHBoxLayout()
|
ctrl = QHBoxLayout()
|
||||||
self._btn_cloud_toggle = _make_btn("停止捕获", self._cloud_do_toggle)
|
self._btn_cloud_live = _make_btn("动态捕获", self._cloud_show_live)
|
||||||
ctrl.addWidget(self._btn_cloud_toggle)
|
ctrl.addWidget(self._btn_cloud_live)
|
||||||
self._btn_cloud_static = _make_btn("静态扫描", self._cloud_do_static_scan)
|
self._btn_cloud_static = _make_btn("静态扫描", self._cloud_do_static_scan)
|
||||||
ctrl.addWidget(self._btn_cloud_static)
|
ctrl.addWidget(self._btn_cloud_static)
|
||||||
self._btn_cloud_clear = _make_btn("清空记录", self._cloud_do_clear)
|
self._btn_cloud_clear = _make_btn("清空记录", self._cloud_do_clear)
|
||||||
@@ -1486,7 +1504,7 @@ class App(QMainWindow):
|
|||||||
ctrl.addStretch()
|
ctrl.addStretch()
|
||||||
self._btn_cloud_export = _make_btn("导出报告", self._cloud_do_export)
|
self._btn_cloud_export = _make_btn("导出报告", self._cloud_do_export)
|
||||||
ctrl.addWidget(self._btn_cloud_export)
|
ctrl.addWidget(self._btn_cloud_export)
|
||||||
lay.addLayout(ctrl)
|
main_lay.addLayout(ctrl)
|
||||||
|
|
||||||
tc = _make_card()
|
tc = _make_card()
|
||||||
tc_lay = QVBoxLayout(tc)
|
tc_lay = QVBoxLayout(tc)
|
||||||
@@ -1529,7 +1547,7 @@ class App(QMainWindow):
|
|||||||
self._cloud_tree.setContextMenuPolicy(Qt.CustomContextMenu)
|
self._cloud_tree.setContextMenuPolicy(Qt.CustomContextMenu)
|
||||||
self._cloud_tree.customContextMenuRequested.connect(self._cloud_tree_context_menu)
|
self._cloud_tree.customContextMenuRequested.connect(self._cloud_tree_context_menu)
|
||||||
tc_lay.addWidget(self._cloud_tree)
|
tc_lay.addWidget(self._cloud_tree)
|
||||||
lay.addWidget(tc, 1)
|
main_lay.addWidget(tc, 1)
|
||||||
|
|
||||||
call_row = QHBoxLayout()
|
call_row = QHBoxLayout()
|
||||||
call_row.addWidget(QLabel("手动调用"))
|
call_row.addWidget(QLabel("手动调用"))
|
||||||
@@ -1541,23 +1559,223 @@ class App(QMainWindow):
|
|||||||
call_row.addWidget(self._cloud_data_ent, 1)
|
call_row.addWidget(self._cloud_data_ent, 1)
|
||||||
self._btn_cloud_call = _make_btn("调用", self._cloud_do_call)
|
self._btn_cloud_call = _make_btn("调用", self._cloud_do_call)
|
||||||
call_row.addWidget(self._btn_cloud_call)
|
call_row.addWidget(self._btn_cloud_call)
|
||||||
lay.addLayout(call_row)
|
main_lay.addLayout(call_row)
|
||||||
|
|
||||||
self._cloud_result = QTextEdit()
|
self._cloud_result = QTextEdit()
|
||||||
self._cloud_result.setReadOnly(True)
|
self._cloud_result.setReadOnly(True)
|
||||||
self._cloud_result.setFixedHeight(120)
|
self._cloud_result.setFixedHeight(120)
|
||||||
self._cloud_result.setFont(_qfm())
|
self._cloud_result.setFont(_qfm())
|
||||||
lay.addWidget(self._cloud_result)
|
main_lay.addWidget(self._cloud_result)
|
||||||
|
|
||||||
bot = QHBoxLayout()
|
bot = QHBoxLayout()
|
||||||
self._cloud_status_lbl = QLabel("捕获: 0 条")
|
self._cloud_status_lbl = QLabel("捕获: 0 条")
|
||||||
bot.addWidget(self._cloud_status_lbl)
|
bot.addWidget(self._cloud_status_lbl)
|
||||||
bot.addStretch()
|
bot.addStretch()
|
||||||
lay.addLayout(bot)
|
main_lay.addLayout(bot)
|
||||||
|
|
||||||
|
self._cloud_stack.addWidget(main_page) # index 0: 主视图
|
||||||
|
|
||||||
|
# ── 动态捕获视图 (index 1) ──
|
||||||
|
self._build_cloud_live_page()
|
||||||
|
|
||||||
self._stack.addWidget(page)
|
self._stack.addWidget(page)
|
||||||
self._page_map["cloud"] = self._stack.count() - 1
|
self._page_map["cloud"] = self._stack.count() - 1
|
||||||
|
|
||||||
|
def _build_cloud_live_page(self):
|
||||||
|
"""构建动态捕获子页面 (cloud_stack index 1)"""
|
||||||
|
live_page = QWidget()
|
||||||
|
live_lay = QVBoxLayout(live_page)
|
||||||
|
live_lay.setContentsMargins(24, 12, 24, 16)
|
||||||
|
live_lay.setSpacing(10)
|
||||||
|
|
||||||
|
# 顶部控制栏
|
||||||
|
top_bar = QHBoxLayout()
|
||||||
|
self._btn_cloud_live_back = _make_btn("← 返回", self._cloud_live_back)
|
||||||
|
top_bar.addWidget(self._btn_cloud_live_back)
|
||||||
|
top_bar.addWidget(_make_label("动态捕获", bold=True))
|
||||||
|
self._cloud_live_status_lbl = QLabel("捕获中...")
|
||||||
|
top_bar.addWidget(self._cloud_live_status_lbl)
|
||||||
|
top_bar.addStretch()
|
||||||
|
self._cloud_live_search_ent = _make_entry(width=160)
|
||||||
|
self._cloud_live_search_ent.setPlaceholderText("搜索...")
|
||||||
|
self._cloud_live_search_ent.textChanged.connect(self._cloud_live_filter)
|
||||||
|
top_bar.addWidget(self._cloud_live_search_ent)
|
||||||
|
self._btn_cloud_live_clear = _make_btn("清空", self._cloud_live_clear)
|
||||||
|
top_bar.addWidget(self._btn_cloud_live_clear)
|
||||||
|
live_lay.addLayout(top_bar)
|
||||||
|
|
||||||
|
# 左右分栏
|
||||||
|
splitter = QHBoxLayout()
|
||||||
|
|
||||||
|
# 左侧: 捕获列表
|
||||||
|
left_card = _make_card()
|
||||||
|
left_lay = QVBoxLayout(left_card)
|
||||||
|
left_lay.setContentsMargins(8, 8, 8, 8)
|
||||||
|
left_lay.setSpacing(4)
|
||||||
|
self._cloud_live_tree = QTreeWidget()
|
||||||
|
self._cloud_live_tree.setFont(_qfn())
|
||||||
|
self._cloud_live_tree.header().setFont(_qfn())
|
||||||
|
self._cloud_live_tree.setRootIsDecorated(False)
|
||||||
|
self._cloud_live_tree.setIndentation(0)
|
||||||
|
self._cloud_live_tree.setHeaderLabels(["类型", "名称", "状态", "时间"])
|
||||||
|
lh = self._cloud_live_tree.header()
|
||||||
|
lh.setDefaultAlignment(Qt.AlignLeft | Qt.AlignVCenter)
|
||||||
|
lh.setStretchLastSection(False)
|
||||||
|
lh.setSectionResizeMode(0, QHeaderView.Interactive)
|
||||||
|
lh.setSectionResizeMode(1, QHeaderView.Stretch)
|
||||||
|
lh.setSectionResizeMode(2, QHeaderView.Interactive)
|
||||||
|
lh.setSectionResizeMode(3, QHeaderView.Interactive)
|
||||||
|
self._cloud_live_tree.setColumnWidth(0, 70)
|
||||||
|
self._cloud_live_tree.setColumnWidth(2, 50)
|
||||||
|
self._cloud_live_tree.setColumnWidth(3, 70)
|
||||||
|
self._cloud_live_tree.setSelectionMode(QAbstractItemView.SingleSelection)
|
||||||
|
self._cloud_live_tree.itemClicked.connect(self._cloud_live_select)
|
||||||
|
left_lay.addWidget(self._cloud_live_tree)
|
||||||
|
splitter.addWidget(left_card, 2)
|
||||||
|
|
||||||
|
# 右侧: 详情面板
|
||||||
|
right_card = _make_card()
|
||||||
|
right_lay = QVBoxLayout(right_card)
|
||||||
|
right_lay.setContentsMargins(12, 8, 12, 8)
|
||||||
|
right_lay.setSpacing(6)
|
||||||
|
|
||||||
|
right_lay.addWidget(_make_label("请求参数", bold=True))
|
||||||
|
self._cloud_live_req = QTextEdit()
|
||||||
|
self._cloud_live_req.setReadOnly(True)
|
||||||
|
self._cloud_live_req.setFont(_qfm())
|
||||||
|
self._cloud_live_req.setPlaceholderText("选择左侧记录查看请求参数")
|
||||||
|
right_lay.addWidget(self._cloud_live_req, 1)
|
||||||
|
|
||||||
|
right_lay.addWidget(_make_label("返回结果", bold=True))
|
||||||
|
self._cloud_live_resp = QTextEdit()
|
||||||
|
self._cloud_live_resp.setReadOnly(True)
|
||||||
|
self._cloud_live_resp.setFont(_qfm())
|
||||||
|
self._cloud_live_resp.setPlaceholderText("选择左侧记录查看返回结果")
|
||||||
|
right_lay.addWidget(self._cloud_live_resp, 1)
|
||||||
|
|
||||||
|
btn_row = QHBoxLayout()
|
||||||
|
btn_row.addStretch()
|
||||||
|
self._btn_cloud_live_replay = _make_btn("一键重放", self._cloud_live_replay)
|
||||||
|
self._btn_cloud_live_replay.setEnabled(False)
|
||||||
|
btn_row.addWidget(self._btn_cloud_live_replay)
|
||||||
|
right_lay.addLayout(btn_row)
|
||||||
|
|
||||||
|
splitter.addWidget(right_card, 3)
|
||||||
|
live_lay.addLayout(splitter, 1)
|
||||||
|
|
||||||
|
self._cloud_stack.addWidget(live_page) # index 1: 动态捕获视图
|
||||||
|
self._cloud_live_selected_idx = -1
|
||||||
|
|
||||||
|
# ── WxAPI 捕获 ──
|
||||||
|
|
||||||
|
def _build_wxapi(self):
|
||||||
|
page = QWidget()
|
||||||
|
lay = QVBoxLayout(page)
|
||||||
|
lay.setContentsMargins(24, 12, 24, 16)
|
||||||
|
lay.setSpacing(10)
|
||||||
|
|
||||||
|
# 顶部控制栏
|
||||||
|
top_bar = QHBoxLayout()
|
||||||
|
top_bar.addWidget(_make_label("WxAPI 捕获", bold=True))
|
||||||
|
self._btn_wxapi_toggle = _make_btn("开启捕获", self._wxapi_toggle)
|
||||||
|
top_bar.addWidget(self._btn_wxapi_toggle)
|
||||||
|
self._wxapi_status_lbl = QLabel("已关闭")
|
||||||
|
top_bar.addWidget(self._wxapi_status_lbl)
|
||||||
|
top_bar.addStretch()
|
||||||
|
# 原生桥接开关(默认关闭)
|
||||||
|
self._wxapi_bridge_cb = QCheckBox("原生桥接")
|
||||||
|
self._wxapi_bridge_cb.setChecked(False)
|
||||||
|
self._wxapi_bridge_cb.setToolTip("捕获 WeixinJSBridge 底层调用(会产生大量噪音)")
|
||||||
|
top_bar.addWidget(self._wxapi_bridge_cb)
|
||||||
|
# wx.request 开关(默认关闭)
|
||||||
|
self._wxapi_request_cb = QCheckBox("wx.request")
|
||||||
|
self._wxapi_request_cb.setChecked(False)
|
||||||
|
self._wxapi_request_cb.setToolTip("捕获 wx.request 网络请求(数量较多)")
|
||||||
|
top_bar.addWidget(self._wxapi_request_cb)
|
||||||
|
# wx.storage 开关(默认关闭)
|
||||||
|
self._wxapi_storage_cb = QCheckBox("缓存")
|
||||||
|
self._wxapi_storage_cb.setChecked(False)
|
||||||
|
self._wxapi_storage_cb.setToolTip("捕获 wx.setStorage/getStorage 等缓存操作")
|
||||||
|
top_bar.addWidget(self._wxapi_storage_cb)
|
||||||
|
self._wxapi_search_ent = _make_entry(width=160)
|
||||||
|
self._wxapi_search_ent.setPlaceholderText("搜索...")
|
||||||
|
self._wxapi_search_ent.textChanged.connect(self._wxapi_filter)
|
||||||
|
top_bar.addWidget(self._wxapi_search_ent)
|
||||||
|
self._btn_wxapi_clear = _make_btn("清空", self._wxapi_clear)
|
||||||
|
top_bar.addWidget(self._btn_wxapi_clear)
|
||||||
|
lay.addLayout(top_bar)
|
||||||
|
|
||||||
|
# 左右分栏
|
||||||
|
splitter = QHBoxLayout()
|
||||||
|
|
||||||
|
# 左侧: 捕获列表
|
||||||
|
left_card = _make_card()
|
||||||
|
left_lay = QVBoxLayout(left_card)
|
||||||
|
left_lay.setContentsMargins(8, 8, 8, 8)
|
||||||
|
left_lay.setSpacing(4)
|
||||||
|
self._wxapi_tree = QTreeWidget()
|
||||||
|
self._wxapi_tree.setFont(_qfn())
|
||||||
|
self._wxapi_tree.header().setFont(_qfn())
|
||||||
|
self._wxapi_tree.setRootIsDecorated(False)
|
||||||
|
self._wxapi_tree.setIndentation(0)
|
||||||
|
self._wxapi_tree.setHeaderLabels(["类型", "名称", "状态", "时间"])
|
||||||
|
wh = self._wxapi_tree.header()
|
||||||
|
wh.setDefaultAlignment(Qt.AlignLeft | Qt.AlignVCenter)
|
||||||
|
wh.setStretchLastSection(False)
|
||||||
|
wh.setSectionResizeMode(0, QHeaderView.Interactive)
|
||||||
|
wh.setSectionResizeMode(1, QHeaderView.Stretch)
|
||||||
|
wh.setSectionResizeMode(2, QHeaderView.Interactive)
|
||||||
|
wh.setSectionResizeMode(3, QHeaderView.Interactive)
|
||||||
|
self._wxapi_tree.setColumnWidth(0, 90)
|
||||||
|
self._wxapi_tree.setColumnWidth(2, 60)
|
||||||
|
self._wxapi_tree.setColumnWidth(3, 80)
|
||||||
|
self._wxapi_tree.setSelectionMode(QAbstractItemView.SingleSelection)
|
||||||
|
self._wxapi_tree.itemClicked.connect(self._wxapi_select)
|
||||||
|
left_lay.addWidget(self._wxapi_tree)
|
||||||
|
splitter.addWidget(left_card, 2)
|
||||||
|
|
||||||
|
# 右侧: 详情面板
|
||||||
|
right_card = _make_card()
|
||||||
|
right_lay = QVBoxLayout(right_card)
|
||||||
|
right_lay.setContentsMargins(12, 8, 12, 8)
|
||||||
|
right_lay.setSpacing(6)
|
||||||
|
|
||||||
|
# 类型 + 名称标题
|
||||||
|
self._wxapi_detail_title = _make_label("", bold=True)
|
||||||
|
self._wxapi_detail_title.setWordWrap(True)
|
||||||
|
right_lay.addWidget(self._wxapi_detail_title)
|
||||||
|
|
||||||
|
right_lay.addWidget(_make_label("请求参数", bold=False))
|
||||||
|
self._wxapi_req = QTextEdit()
|
||||||
|
self._wxapi_req.setReadOnly(True)
|
||||||
|
self._wxapi_req.setFont(_qfm())
|
||||||
|
self._wxapi_req.setPlaceholderText("选择左侧记录查看请求参数")
|
||||||
|
self._wxapi_req.setTextInteractionFlags(
|
||||||
|
Qt.TextSelectableByMouse | Qt.TextSelectableByKeyboard)
|
||||||
|
right_lay.addWidget(self._wxapi_req, 1)
|
||||||
|
|
||||||
|
right_lay.addWidget(_make_label("返回结果", bold=False))
|
||||||
|
self._wxapi_resp = QTextEdit()
|
||||||
|
self._wxapi_resp.setReadOnly(True)
|
||||||
|
self._wxapi_resp.setFont(_qfm())
|
||||||
|
self._wxapi_resp.setPlaceholderText("选择左侧记录查看返回结果")
|
||||||
|
self._wxapi_resp.setTextInteractionFlags(
|
||||||
|
Qt.TextSelectableByMouse | Qt.TextSelectableByKeyboard)
|
||||||
|
right_lay.addWidget(self._wxapi_resp, 1)
|
||||||
|
|
||||||
|
btn_row = QHBoxLayout()
|
||||||
|
btn_row.addStretch()
|
||||||
|
self._btn_wxapi_replay = _make_btn("一键重放", self._wxapi_replay)
|
||||||
|
self._btn_wxapi_replay.setEnabled(False)
|
||||||
|
btn_row.addWidget(self._btn_wxapi_replay)
|
||||||
|
right_lay.addLayout(btn_row)
|
||||||
|
|
||||||
|
splitter.addWidget(right_card, 3)
|
||||||
|
lay.addLayout(splitter, 1)
|
||||||
|
|
||||||
|
self._stack.addWidget(page)
|
||||||
|
self._page_map["wxapi"] = self._stack.count() - 1
|
||||||
|
|
||||||
# ── 敏感信息提取 ──
|
# ── 敏感信息提取 ──
|
||||||
|
|
||||||
def _build_extract(self):
|
def _build_extract(self):
|
||||||
@@ -3219,6 +3437,11 @@ class App(QMainWindow):
|
|||||||
if self._cloud_scan_poll_timer:
|
if self._cloud_scan_poll_timer:
|
||||||
self._cloud_scan_poll_timer.stop()
|
self._cloud_scan_poll_timer.stop()
|
||||||
self._cloud_scan_poll_timer = None
|
self._cloud_scan_poll_timer = None
|
||||||
|
if self._wxapi_active:
|
||||||
|
self._wxapi_active = False
|
||||||
|
if self._wxapi_poll_timer:
|
||||||
|
self._wxapi_poll_timer.stop()
|
||||||
|
self._wxapi_poll_timer = None
|
||||||
if self._cancel_ev:
|
if self._cancel_ev:
|
||||||
self._cancel_ev.set()
|
self._cancel_ev.set()
|
||||||
if self._engine and self._loop and self._loop.is_running():
|
if self._engine and self._loop and self._loop.is_running():
|
||||||
@@ -3326,6 +3549,7 @@ class App(QMainWindow):
|
|||||||
if not self._cloud_scan_active and self._auditor:
|
if not self._cloud_scan_active and self._auditor:
|
||||||
self._cloud_start_scan()
|
self._cloud_start_scan()
|
||||||
self._log_add("info", "[云扫描] 小程序连接后自动恢复捕获")
|
self._log_add("info", "[云扫描] 小程序连接后自动恢复捕获")
|
||||||
|
# 自动启动 WxAPI 捕获(不自动启动,用户手动开启)
|
||||||
|
|
||||||
def _delayed_fetch_app_info(self, gen):
|
def _delayed_fetch_app_info(self, gen):
|
||||||
"""延迟调用,只有最后一次触发的 gen 匹配才执行。"""
|
"""延迟调用,只有最后一次触发的 gen 匹配才执行。"""
|
||||||
@@ -3715,19 +3939,120 @@ class App(QMainWindow):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def _cloud_do_toggle(self):
|
def _cloud_do_toggle(self):
|
||||||
|
"""兼容:现在改为切换到动态捕获页面"""
|
||||||
|
self._cloud_show_live()
|
||||||
|
|
||||||
|
def _cloud_show_live(self):
|
||||||
|
"""切换到动态捕获子页面"""
|
||||||
|
self._cloud_stack.setCurrentIndex(1)
|
||||||
|
# 确保捕获已启动
|
||||||
|
if not self._cloud_scan_active:
|
||||||
|
self._cloud_start_scan()
|
||||||
|
self._cloud_live_update_status()
|
||||||
|
|
||||||
|
def _cloud_live_back(self):
|
||||||
|
"""返回主视图"""
|
||||||
|
self._cloud_stack.setCurrentIndex(0)
|
||||||
|
|
||||||
|
def _cloud_live_select(self, item):
|
||||||
|
"""点击动态捕获列表项,显示详情"""
|
||||||
|
idx = self._cloud_live_tree.indexOfTopLevelItem(item)
|
||||||
|
if idx < 0 or idx >= len(self._cloud_live_items):
|
||||||
|
return
|
||||||
|
self._cloud_live_selected_idx = idx
|
||||||
|
call = self._cloud_live_items[idx]
|
||||||
|
# 显示请求参数
|
||||||
|
req_data = call.get("data", {})
|
||||||
|
self._cloud_live_req.setPlainText(
|
||||||
|
json.dumps(req_data, ensure_ascii=False, indent=2, default=str))
|
||||||
|
# 显示返回结果
|
||||||
|
c = _TH[self._tn]
|
||||||
|
status = call.get("status", "")
|
||||||
|
if call.get("result") is not None:
|
||||||
|
resp_text = json.dumps(call["result"], ensure_ascii=False, indent=2, default=str)
|
||||||
|
self._cloud_live_resp.setHtml(
|
||||||
|
f'<span style="color:{c["success"]}">{resp_text}</span>')
|
||||||
|
elif call.get("error") is not None:
|
||||||
|
err_text = json.dumps(call["error"], ensure_ascii=False, indent=2, default=str)
|
||||||
|
self._cloud_live_resp.setHtml(
|
||||||
|
f'<span style="color:{c["error"]}">{err_text}</span>')
|
||||||
|
else:
|
||||||
|
self._cloud_live_resp.setPlainText(f"状态: {status} (无返回数据)")
|
||||||
|
self._btn_cloud_live_replay.setEnabled(True)
|
||||||
|
|
||||||
|
def _cloud_live_replay(self):
|
||||||
|
"""一键重放选中的云函数调用"""
|
||||||
|
idx = self._cloud_live_selected_idx
|
||||||
|
if idx < 0 or idx >= len(self._cloud_live_items):
|
||||||
|
return
|
||||||
if not self._cloud_ensure_auditor():
|
if not self._cloud_ensure_auditor():
|
||||||
return
|
return
|
||||||
|
call = self._cloud_live_items[idx]
|
||||||
|
name = call.get("name", "")
|
||||||
|
data = call.get("data", {})
|
||||||
|
if not name:
|
||||||
|
return
|
||||||
|
self._btn_cloud_live_replay.setEnabled(False)
|
||||||
|
self._cloud_live_resp.setPlainText(f"正在重放 {name} ...")
|
||||||
|
asyncio.run_coroutine_threadsafe(
|
||||||
|
self._acloud_live_replay(name, data), self._loop)
|
||||||
|
|
||||||
|
async def _acloud_live_replay(self, name, data):
|
||||||
|
try:
|
||||||
|
res = await self._auditor.call_function(name, data)
|
||||||
|
self._cld_q.put(("live_replay_result", name, res))
|
||||||
|
except Exception as e:
|
||||||
|
self._cld_q.put(("live_replay_result", name,
|
||||||
|
{"ok": False, "status": "fail", "error": str(e)}))
|
||||||
|
|
||||||
|
def _cloud_live_clear(self):
|
||||||
|
"""清空动态捕获列表"""
|
||||||
|
self._cloud_live_items.clear()
|
||||||
|
self._cloud_live_tree.clear()
|
||||||
|
self._cloud_live_req.clear()
|
||||||
|
self._cloud_live_resp.clear()
|
||||||
|
self._btn_cloud_live_replay.setEnabled(False)
|
||||||
|
self._cloud_live_selected_idx = -1
|
||||||
|
self._cloud_live_update_status()
|
||||||
|
|
||||||
|
def _cloud_live_filter(self):
|
||||||
|
"""动态捕获列表搜索过滤"""
|
||||||
|
kw = self._cloud_live_search_ent.text().strip().lower()
|
||||||
|
self._cloud_live_tree.clear()
|
||||||
|
_type_cn = {"function": "云函数", "storage": "存储", "container": "容器",
|
||||||
|
"wx.auth": "认证", "wx.request": "请求", "wx.pay": "支付",
|
||||||
|
"wx.storage": "缓存", "wx.share": "分享", "wx.location": "位置",
|
||||||
|
"wx.device": "设备", "wx.nav": "导航", "wx.media": "媒体",
|
||||||
|
"wx.clipboard": "剪贴板", "wx.bridge": "原生桥接"}
|
||||||
|
for call in self._cloud_live_items:
|
||||||
|
ctype = call.get("type", "function")
|
||||||
|
type_label = _type_cn.get(ctype, ctype)
|
||||||
|
if ctype.startswith("db"):
|
||||||
|
type_label = "数据库"
|
||||||
|
name = call.get("name", "")
|
||||||
|
status = call.get("status", "")
|
||||||
|
ts = call.get("timestamp", "")
|
||||||
|
if kw and kw not in type_label.lower() and kw not in name.lower():
|
||||||
|
continue
|
||||||
|
tree_item = QTreeWidgetItem([type_label, name, status, str(ts)])
|
||||||
|
self._cloud_live_tree.addTopLevelItem(tree_item)
|
||||||
|
|
||||||
|
def _cloud_live_update_status(self):
|
||||||
|
count = len(self._cloud_live_items)
|
||||||
if self._cloud_scan_active:
|
if self._cloud_scan_active:
|
||||||
self._cloud_stop_scan()
|
self._cloud_live_status_lbl.setText(f"捕获中... {count} 条")
|
||||||
|
c = _TH[self._tn]
|
||||||
|
self._cloud_live_status_lbl.setStyleSheet(f"color: {c['success']};")
|
||||||
else:
|
else:
|
||||||
self._cloud_start_scan()
|
self._cloud_live_status_lbl.setText(f"已停止 ({count} 条)")
|
||||||
|
c = _TH[self._tn]
|
||||||
|
self._cloud_live_status_lbl.setStyleSheet(f"color: {c['text3']};")
|
||||||
|
|
||||||
def _cloud_start_scan(self):
|
def _cloud_start_scan(self):
|
||||||
if not self._cloud_ensure_auditor():
|
if not self._cloud_ensure_auditor():
|
||||||
return
|
return
|
||||||
self._cloud_scan_active = True
|
self._cloud_scan_active = True
|
||||||
c = _TH[self._tn]
|
c = _TH[self._tn]
|
||||||
self._btn_cloud_toggle.setText("停止捕获")
|
|
||||||
self._cloud_scan_lbl.setText("捕获中...")
|
self._cloud_scan_lbl.setText("捕获中...")
|
||||||
self._cloud_scan_lbl.setStyleSheet(f"color: {c['success']};")
|
self._cloud_scan_lbl.setStyleSheet(f"color: {c['success']};")
|
||||||
self._log_add("info", "[云扫描] 全局捕获已启动")
|
self._log_add("info", "[云扫描] 全局捕获已启动")
|
||||||
@@ -3743,7 +4068,6 @@ class App(QMainWindow):
|
|||||||
def _cloud_stop_scan(self):
|
def _cloud_stop_scan(self):
|
||||||
self._cloud_scan_active = False
|
self._cloud_scan_active = False
|
||||||
c = _TH[self._tn]
|
c = _TH[self._tn]
|
||||||
self._btn_cloud_toggle.setText("开启捕获")
|
|
||||||
self._cloud_scan_lbl.setText("已停止")
|
self._cloud_scan_lbl.setText("已停止")
|
||||||
self._cloud_scan_lbl.setStyleSheet(f"color: {c['text3']};")
|
self._cloud_scan_lbl.setStyleSheet(f"color: {c['text3']};")
|
||||||
if self._cloud_scan_poll_timer:
|
if self._cloud_scan_poll_timer:
|
||||||
@@ -3834,6 +4158,209 @@ class App(QMainWindow):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
self._log_add("error", f"[云扫描] 导出失败: {e}")
|
self._log_add("error", f"[云扫描] 导出失败: {e}")
|
||||||
|
|
||||||
|
# ──────────────────────────────────
|
||||||
|
# WxAPI 捕获逻辑
|
||||||
|
# ──────────────────────────────────
|
||||||
|
|
||||||
|
def _wxapi_ensure(self):
|
||||||
|
if not self._engine or not self._loop or not self._loop.is_running():
|
||||||
|
self._log_add("error", "[WxAPI] 请先启动调试")
|
||||||
|
return False
|
||||||
|
if not self._wxapi_auditor:
|
||||||
|
self._wxapi_auditor = WxApiAuditor(self._engine)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _wxapi_toggle(self):
|
||||||
|
if self._wxapi_active:
|
||||||
|
self._wxapi_stop()
|
||||||
|
self._btn_wxapi_toggle.setText("开启捕获")
|
||||||
|
c = _TH[self._tn]
|
||||||
|
self._wxapi_status_lbl.setText("已关闭")
|
||||||
|
self._wxapi_status_lbl.setStyleSheet(f"color: {c['text3']};")
|
||||||
|
else:
|
||||||
|
self._wxapi_start()
|
||||||
|
self._btn_wxapi_toggle.setText("停止捕获")
|
||||||
|
|
||||||
|
def _wxapi_start(self):
|
||||||
|
if not self._wxapi_ensure():
|
||||||
|
return
|
||||||
|
if self._wxapi_active:
|
||||||
|
return
|
||||||
|
self._wxapi_active = True
|
||||||
|
c = _TH[self._tn]
|
||||||
|
self._wxapi_status_lbl.setText("捕获中...")
|
||||||
|
self._wxapi_status_lbl.setStyleSheet(f"color: {c['success']};")
|
||||||
|
self._log_add("info", "[WxAPI] 捕获已启动")
|
||||||
|
asyncio.run_coroutine_threadsafe(self._awxapi_start(), self._loop)
|
||||||
|
self._wxapi_poll()
|
||||||
|
|
||||||
|
async def _awxapi_start(self):
|
||||||
|
try:
|
||||||
|
await self._wxapi_auditor.start()
|
||||||
|
except Exception as e:
|
||||||
|
self._log_q.put(("error", f"[WxAPI] Hook 启动异常: {e}"))
|
||||||
|
|
||||||
|
def _wxapi_stop(self):
|
||||||
|
self._wxapi_active = False
|
||||||
|
if self._wxapi_poll_timer:
|
||||||
|
self._wxapi_poll_timer.stop()
|
||||||
|
self._wxapi_poll_timer = None
|
||||||
|
if self._wxapi_auditor and self._loop and self._loop.is_running():
|
||||||
|
asyncio.run_coroutine_threadsafe(self._wxapi_auditor.stop(), self._loop)
|
||||||
|
|
||||||
|
def _wxapi_poll(self):
|
||||||
|
if not self._wxapi_active or not self._wxapi_auditor:
|
||||||
|
return
|
||||||
|
if self._loop and self._loop.is_running():
|
||||||
|
asyncio.run_coroutine_threadsafe(self._awxapi_poll(), self._loop)
|
||||||
|
self._wxapi_poll_timer = QTimer()
|
||||||
|
self._wxapi_poll_timer.setSingleShot(True)
|
||||||
|
self._wxapi_poll_timer.timeout.connect(self._wxapi_poll)
|
||||||
|
self._wxapi_poll_timer.start(2000)
|
||||||
|
|
||||||
|
async def _awxapi_poll(self):
|
||||||
|
try:
|
||||||
|
new_calls = await self._wxapi_auditor.poll()
|
||||||
|
if new_calls:
|
||||||
|
self._wxapi_q.put(("new_calls", new_calls))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _wxapi_select(self, item):
|
||||||
|
idx = self._wxapi_tree.indexOfTopLevelItem(item)
|
||||||
|
if idx < 0 or idx >= len(self._wxapi_items):
|
||||||
|
return
|
||||||
|
self._wxapi_selected_idx = idx
|
||||||
|
call = self._wxapi_items[idx]
|
||||||
|
# 显示类型 + 名称标题
|
||||||
|
ctype = call.get("type", "wx.api")
|
||||||
|
name = call.get("name", "")
|
||||||
|
self._wxapi_detail_title.setText(f"[{ctype}] {name}")
|
||||||
|
# 请求参数 - 使用 setPlainText 确保可复制
|
||||||
|
req_data = call.get("data", {})
|
||||||
|
self._wxapi_req.setPlainText(
|
||||||
|
json.dumps(req_data, ensure_ascii=False, indent=2, default=str))
|
||||||
|
# 返回结果
|
||||||
|
if call.get("result") is not None:
|
||||||
|
resp_text = json.dumps(call["result"], ensure_ascii=False, indent=2, default=str)
|
||||||
|
self._wxapi_resp.setPlainText(resp_text)
|
||||||
|
elif call.get("error") is not None:
|
||||||
|
err_text = json.dumps(call["error"], ensure_ascii=False, indent=2, default=str)
|
||||||
|
self._wxapi_resp.setPlainText(err_text)
|
||||||
|
else:
|
||||||
|
self._wxapi_resp.setPlainText(f"状态: {call.get('status', '')} (无返回数据)")
|
||||||
|
self._btn_wxapi_replay.setEnabled(True)
|
||||||
|
|
||||||
|
def _wxapi_replay(self):
|
||||||
|
idx = self._wxapi_selected_idx
|
||||||
|
if idx < 0 or idx >= len(self._wxapi_items):
|
||||||
|
return
|
||||||
|
if not self._wxapi_ensure():
|
||||||
|
return
|
||||||
|
call = self._wxapi_items[idx]
|
||||||
|
# 从 name 中提取原始 API 名称
|
||||||
|
api_name = call.get("name", "")
|
||||||
|
# wx.request 的 name 格式是 "GET https://..." 需要用 "request"
|
||||||
|
if call.get("type") == "wx.request":
|
||||||
|
api_name = "request"
|
||||||
|
data = call.get("data", {})
|
||||||
|
self._btn_wxapi_replay.setEnabled(False)
|
||||||
|
self._wxapi_resp.setPlainText(f"正在重放 {api_name} ...")
|
||||||
|
asyncio.run_coroutine_threadsafe(
|
||||||
|
self._awxapi_replay(api_name, data), self._loop)
|
||||||
|
|
||||||
|
async def _awxapi_replay(self, api_name, data):
|
||||||
|
try:
|
||||||
|
res = await self._wxapi_auditor.replay(api_name, data)
|
||||||
|
self._wxapi_q.put(("replay_result", api_name, res))
|
||||||
|
except Exception as e:
|
||||||
|
self._wxapi_q.put(("replay_result", api_name,
|
||||||
|
{"ok": False, "status": "fail", "error": str(e)}))
|
||||||
|
|
||||||
|
def _wxapi_clear(self):
|
||||||
|
self._wxapi_items.clear()
|
||||||
|
self._wxapi_tree.clear()
|
||||||
|
self._wxapi_req.clear()
|
||||||
|
self._wxapi_resp.clear()
|
||||||
|
self._btn_wxapi_replay.setEnabled(False)
|
||||||
|
self._wxapi_selected_idx = -1
|
||||||
|
self._wxapi_status_lbl.setText(f"捕获中... 0 条" if self._wxapi_active else "已停止")
|
||||||
|
if self._wxapi_auditor and self._loop and self._loop.is_running():
|
||||||
|
asyncio.run_coroutine_threadsafe(self._wxapi_auditor.clear(), self._loop)
|
||||||
|
|
||||||
|
def _wxapi_filter(self):
|
||||||
|
kw = self._wxapi_search_ent.text().strip().lower()
|
||||||
|
self._wxapi_tree.clear()
|
||||||
|
_type_cn = {"wx.auth": "认证", "wx.request": "请求", "wx.pay": "支付",
|
||||||
|
"wx.storage": "缓存", "wx.share": "分享", "wx.location": "位置",
|
||||||
|
"wx.device": "设备", "wx.nav": "导航", "wx.media": "媒体",
|
||||||
|
"wx.clipboard": "剪贴板"}
|
||||||
|
for call in self._wxapi_items:
|
||||||
|
ctype = call.get("type", "wx.api")
|
||||||
|
type_label = _type_cn.get(ctype, ctype)
|
||||||
|
name = call.get("name", "")
|
||||||
|
status = call.get("status", "")
|
||||||
|
ts = call.get("timestamp", "")
|
||||||
|
if kw and kw not in type_label.lower() and kw not in name.lower():
|
||||||
|
continue
|
||||||
|
tree_item = QTreeWidgetItem([type_label, name, status, str(ts)])
|
||||||
|
self._wxapi_tree.addTopLevelItem(tree_item)
|
||||||
|
|
||||||
|
def _handle_wxapi(self, item):
|
||||||
|
kind = item[0]
|
||||||
|
c = _TH[self._tn]
|
||||||
|
_type_cn = {"wx.auth": "认证", "wx.request": "请求", "wx.pay": "支付",
|
||||||
|
"wx.storage": "缓存", "wx.share": "分享", "wx.location": "位置",
|
||||||
|
"wx.device": "设备", "wx.nav": "导航", "wx.media": "媒体",
|
||||||
|
"wx.clipboard": "剪贴板", "wx.bridge": "原生桥接"}
|
||||||
|
if kind == "new_calls":
|
||||||
|
calls = item[1]
|
||||||
|
if calls:
|
||||||
|
kw = self._wxapi_search_ent.text().strip().lower()
|
||||||
|
show_bridge = self._wxapi_bridge_cb.isChecked()
|
||||||
|
show_request = self._wxapi_request_cb.isChecked()
|
||||||
|
show_storage = self._wxapi_storage_cb.isChecked()
|
||||||
|
for call in calls:
|
||||||
|
ctype = call.get("type", "wx.api")
|
||||||
|
# 原生桥接开关关闭时跳过 bridge 类型
|
||||||
|
if ctype == "wx.bridge" and not show_bridge:
|
||||||
|
continue
|
||||||
|
# wx.request 开关关闭时跳过
|
||||||
|
if ctype == "wx.request" and not show_request:
|
||||||
|
continue
|
||||||
|
# wx.storage 开关关闭时跳过
|
||||||
|
if ctype == "wx.storage" and not show_storage:
|
||||||
|
continue
|
||||||
|
self._wxapi_items.append(call)
|
||||||
|
type_label = _type_cn.get(ctype, ctype)
|
||||||
|
name = call.get("name", "")
|
||||||
|
status = call.get("status", "")
|
||||||
|
ts = call.get("timestamp", "")
|
||||||
|
if kw and kw not in type_label.lower() and kw not in name.lower():
|
||||||
|
continue
|
||||||
|
tree_item = QTreeWidgetItem([type_label, name, status, str(ts)])
|
||||||
|
self._wxapi_tree.addTopLevelItem(tree_item)
|
||||||
|
self._wxapi_tree.scrollToBottom()
|
||||||
|
count = len(self._wxapi_items)
|
||||||
|
self._wxapi_status_lbl.setText(f"捕获中... {count} 条")
|
||||||
|
self._wxapi_status_lbl.setStyleSheet(f"color: {c['success']};")
|
||||||
|
elif kind == "replay_result":
|
||||||
|
_, name, res = item
|
||||||
|
self._btn_wxapi_replay.setEnabled(True)
|
||||||
|
status = res.get("status", "unknown")
|
||||||
|
if status == "success":
|
||||||
|
detail = json.dumps(res.get("result", {}), ensure_ascii=False, indent=2, default=str)
|
||||||
|
self._wxapi_resp.setHtml(
|
||||||
|
f'<span style="color:{c["success"]}">重放成功:\n{detail}</span>')
|
||||||
|
elif status == "fail":
|
||||||
|
err = res.get("error", "") or res.get("reason", "未知错误")
|
||||||
|
self._wxapi_resp.setHtml(
|
||||||
|
f'<span style="color:{c["error"]}">重放失败: {err}</span>')
|
||||||
|
else:
|
||||||
|
detail = json.dumps(res, ensure_ascii=False, indent=2, default=str)
|
||||||
|
self._wxapi_resp.setHtml(
|
||||||
|
f'<span style="color:{c["warning"]}">重放结果:\n{detail}</span>')
|
||||||
|
|
||||||
# ──────────────────────────────────
|
# ──────────────────────────────────
|
||||||
# 轮询
|
# 轮询
|
||||||
# ──────────────────────────────────
|
# ──────────────────────────────────
|
||||||
@@ -3882,6 +4409,12 @@ class App(QMainWindow):
|
|||||||
except queue.Empty:
|
except queue.Empty:
|
||||||
break
|
break
|
||||||
self._handle_ext(item)
|
self._handle_ext(item)
|
||||||
|
for _ in range(50):
|
||||||
|
try:
|
||||||
|
item = self._wxapi_q.get_nowait()
|
||||||
|
except queue.Empty:
|
||||||
|
break
|
||||||
|
self._handle_wxapi(item)
|
||||||
|
|
||||||
def _apply_sts(self, sts):
|
def _apply_sts(self, sts):
|
||||||
c = _TH[self._tn]
|
c = _TH[self._tn]
|
||||||
@@ -4009,11 +4542,16 @@ class App(QMainWindow):
|
|||||||
def _handle_cld(self, item):
|
def _handle_cld(self, item):
|
||||||
kind = item[0]
|
kind = item[0]
|
||||||
c = _TH[self._tn]
|
c = _TH[self._tn]
|
||||||
_type_cn = {"function": "云函数", "storage": "存储", "container": "容器"}
|
_type_cn = {"function": "云函数", "storage": "存储", "container": "容器",
|
||||||
|
"wx.auth": "认证", "wx.request": "请求", "wx.pay": "支付",
|
||||||
|
"wx.storage": "缓存", "wx.share": "分享", "wx.location": "位置",
|
||||||
|
"wx.device": "设备", "wx.nav": "导航", "wx.media": "媒体",
|
||||||
|
"wx.clipboard": "剪贴板", "wx.bridge": "原生桥接"}
|
||||||
if kind == "new_calls":
|
if kind == "new_calls":
|
||||||
calls = item[1]
|
calls = item[1]
|
||||||
if calls:
|
if calls:
|
||||||
kw = self._cloud_search_ent.text().strip().lower()
|
kw = self._cloud_search_ent.text().strip().lower()
|
||||||
|
live_kw = self._cloud_live_search_ent.text().strip().lower()
|
||||||
for call in calls:
|
for call in calls:
|
||||||
data_str = json.dumps(call.get("data", {}), ensure_ascii=False)
|
data_str = json.dumps(call.get("data", {}), ensure_ascii=False)
|
||||||
if len(data_str) > 80:
|
if len(data_str) > 80:
|
||||||
@@ -4027,8 +4565,7 @@ class App(QMainWindow):
|
|||||||
call.get("name", ""), data_str,
|
call.get("name", ""), data_str,
|
||||||
status, call.get("timestamp", ""))
|
status, call.get("timestamp", ""))
|
||||||
self._cloud_all_items.append(vals)
|
self._cloud_all_items.append(vals)
|
||||||
if kw and not any(kw in str(v).lower() for v in vals):
|
if not (kw and not any(kw in str(v).lower() for v in vals)):
|
||||||
continue
|
|
||||||
tree_item = QTreeWidgetItem([str(v) for v in vals])
|
tree_item = QTreeWidgetItem([str(v) for v in vals])
|
||||||
self._cloud_tree.addTopLevelItem(tree_item)
|
self._cloud_tree.addTopLevelItem(tree_item)
|
||||||
result_data = call.get("result") or call.get("error")
|
result_data = call.get("result") or call.get("error")
|
||||||
@@ -4039,8 +4576,18 @@ class App(QMainWindow):
|
|||||||
"error": call.get("error"),
|
"error": call.get("error"),
|
||||||
"data": call.get("data"),
|
"data": call.get("data"),
|
||||||
}
|
}
|
||||||
|
# 同时推送到动态捕获列表
|
||||||
|
self._cloud_live_items.append(call)
|
||||||
|
name = call.get("name", "")
|
||||||
|
ts = call.get("timestamp", "")
|
||||||
|
if not (live_kw and live_kw not in type_label.lower()
|
||||||
|
and live_kw not in name.lower()):
|
||||||
|
live_item = QTreeWidgetItem([type_label, name, status, str(ts)])
|
||||||
|
self._cloud_live_tree.addTopLevelItem(live_item)
|
||||||
self._cloud_tree.scrollToBottom()
|
self._cloud_tree.scrollToBottom()
|
||||||
|
self._cloud_live_tree.scrollToBottom()
|
||||||
self._cloud_update_status()
|
self._cloud_update_status()
|
||||||
|
self._cloud_live_update_status()
|
||||||
self._cloud_scan_lbl.setText(f"捕获中... {len(self._cloud_all_items)} 条")
|
self._cloud_scan_lbl.setText(f"捕获中... {len(self._cloud_all_items)} 条")
|
||||||
self._cloud_scan_lbl.setStyleSheet(f"color: {c['success']};")
|
self._cloud_scan_lbl.setStyleSheet(f"color: {c['success']};")
|
||||||
elif kind == "static_results":
|
elif kind == "static_results":
|
||||||
@@ -4082,6 +4629,22 @@ class App(QMainWindow):
|
|||||||
detail = json.dumps(res, ensure_ascii=False, default=str)
|
detail = json.dumps(res, ensure_ascii=False, default=str)
|
||||||
self._cloud_result.setHtml(
|
self._cloud_result.setHtml(
|
||||||
f'<span style="color:{c["warning"]}">{name} -> {detail}</span>')
|
f'<span style="color:{c["warning"]}">{name} -> {detail}</span>')
|
||||||
|
elif kind == "live_replay_result":
|
||||||
|
_, name, res = item
|
||||||
|
self._btn_cloud_live_replay.setEnabled(True)
|
||||||
|
status = res.get("status", "unknown")
|
||||||
|
if status == "success":
|
||||||
|
detail = json.dumps(res.get("result", {}), ensure_ascii=False, indent=2, default=str)
|
||||||
|
self._cloud_live_resp.setHtml(
|
||||||
|
f'<span style="color:{c["success"]}">重放成功:\n{detail}</span>')
|
||||||
|
elif status == "fail":
|
||||||
|
err = res.get("error", "") or res.get("reason", "未知错误")
|
||||||
|
self._cloud_live_resp.setHtml(
|
||||||
|
f'<span style="color:{c["error"]}">重放失败: {err}</span>')
|
||||||
|
else:
|
||||||
|
detail = json.dumps(res, ensure_ascii=False, indent=2, default=str)
|
||||||
|
self._cloud_live_resp.setHtml(
|
||||||
|
f'<span style="color:{c["warning"]}">重放结果:\n{detail}</span>')
|
||||||
|
|
||||||
def _handle_ext(self, item):
|
def _handle_ext(self, item):
|
||||||
kind = item.get("type", "")
|
kind = item.get("type", "")
|
||||||
|
|||||||
+230
@@ -0,0 +1,230 @@
|
|||||||
|
# First
|
||||||
|
|
||||||
|
> 微信小程序安全调试工具 —— 基于 Frida + CDP 代理,支持 Windows / macOS 双平台,GUI 与 CLI 双模式
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 截图预览
|
||||||
|
|
||||||
|
### 主界面
|
||||||
|
|
||||||
|

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

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

|
||||||
|
|
||||||
|
### 调试开关(JSRPC / wx.cloud 调用)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### 敏感信息提取
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
<img src="https://s1.galgame.fun/imgb/u55/20260416_69e0be0c38e39.png" alt="敏感信息提取-2" />
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 功能特性
|
||||||
|
|
||||||
|
- Frida 动态注入微信客户端,转发小程序调试协议
|
||||||
|
- CDP 代理桥接,Chrome DevTools 直连调试
|
||||||
|
- 路由枚举、分类与一键跳转
|
||||||
|
- 云函数调用监控与参数分析(动态 Hook + 静态扫描)
|
||||||
|
- UserScript 自动注入(支持 URL 匹配、run-at 时机控制)
|
||||||
|
- wxapkg 解密解包 + 敏感信息扫描(IP / 密钥 / 云存储 / JWT 等)
|
||||||
|
- 深色 / 浅色主题切换
|
||||||
|
- GUI(PySide6)与 CLI 双模式
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 环境要求
|
||||||
|
|
||||||
|
| 依赖 | 版本 |
|
||||||
|
|------|------|
|
||||||
|
| Python | >= 3.10 |
|
||||||
|
| frida | >= 17.0.0 |
|
||||||
|
| websockets | >= 12.0 |
|
||||||
|
| protobuf | >= 4.0.0 |
|
||||||
|
| PySide6 | >= 6.5.0 |
|
||||||
|
| pycryptodome | 最新版 |
|
||||||
|
|
||||||
|
安装依赖:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 支持的微信版本
|
||||||
|
|
||||||
|
### Windows
|
||||||
|
|
||||||
|
- WMPF 版本:11581, 11633, 13331, 13341, 13487, 13639, 13655, 13871, 13909, 14161, 14199, 14315, 16133, 16203, 16389, 16467, 16771, 16815, 16965, 17037, 17071, 17127, 18055, 18151, 18787, 18891, 18955, 19027, 19201
|
||||||
|
- 推荐微信版本:**4.1.0.30**
|
||||||
|
- 下载地址:[weixin/4.1.0.30](https://github.com/vs-olitus/wx-version/releases/tag/4.1.0.30)
|
||||||
|
|
||||||
|
### macOS
|
||||||
|
|
||||||
|
- WMPF 版本:18152, 18788
|
||||||
|
- 推荐微信版本:**4.1.7.30**
|
||||||
|
- 下载地址:[weixin/4.1.7.30](https://github.com/vs-olitus/wx-version/releases/tag/4.1.7.30)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
### Windows
|
||||||
|
|
||||||
|
**一键启动:** 双击 `启动.bat`,首次运行会自动安装依赖。
|
||||||
|
|
||||||
|
**手动启动:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python gui.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### macOS
|
||||||
|
|
||||||
|
**一键启动:** 终端执行 `./启动.sh`,首次运行会自动安装依赖。
|
||||||
|
|
||||||
|
**手动启动:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 gui.py
|
||||||
|
```
|
||||||
|
|
||||||
|
启动后在主界面点击 **启动调试**,然后再打开小程序即可(请勿在启动调试前打开小程序)。
|
||||||
|
|
||||||
|
### CLI 模式
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 默认端口启动
|
||||||
|
python main.py
|
||||||
|
|
||||||
|
# 自定义端口
|
||||||
|
python main.py --cdp-port 62000
|
||||||
|
|
||||||
|
# 开启详细日志
|
||||||
|
python main.py --debug-main --debug-frida
|
||||||
|
```
|
||||||
|
|
||||||
|
### 连接 Chrome DevTools
|
||||||
|
|
||||||
|
启动后,在 Chrome 地址栏输入:
|
||||||
|
|
||||||
|
```
|
||||||
|
devtools://devtools/bundled/inspector.html?ws=127.0.0.1:62000
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 服务号调试操作
|
||||||
|
|
||||||
|
1. 打开一个小程序页面作为占位(保持不关闭)
|
||||||
|
2. 访问需要调试的服务号内容(如 H5 网页)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
3. 在工具栏「服务」目标列表中找到目标网页,双击后切换至调试视图即可开始调试
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## macOS 注意事项
|
||||||
|
|
||||||
|
如果 Frida 注入报错,需要解除系统对进程附加的限制,任选其一:
|
||||||
|
|
||||||
|
**方案一:关闭 SIP(系统完整性保护)**
|
||||||
|
|
||||||
|
> 关闭后 Frida 才能正常注入进程。参考教程:[macOS SIP 开启关闭教程](https://cloud.tencent.com/developer/article/1496058)
|
||||||
|
|
||||||
|
**方案二:强制重签名 WeChat**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo codesign --force --deep --sign - /Applications/WeChat.app
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 参数说明
|
||||||
|
|
||||||
|
| 参数 | 默认值 | 说明 |
|
||||||
|
|------|--------|------|
|
||||||
|
| `--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
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
**Q: Frida 已连接,但小程序端显示未连接或无法断点调试?**
|
||||||
|
|
||||||
|
确认操作顺序无误后,尝试以下步骤:
|
||||||
|
|
||||||
|
1. 彻底卸载微信并重启电脑(重要聊天记录请提前备份)
|
||||||
|
2. 删除 `C:\Users\用户名\AppData\Roaming\Tencent\xwechat\XPlugin\Plugins\RadiumWMPF` 下所有数字命名的文件夹
|
||||||
|
3. 再次重启后安装微信 4.1.0.30 版本
|
||||||
|
4. 检查上述路径,确认文件夹编号为 `16389`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 参考项目
|
||||||
|
|
||||||
|
- [evi0s/WMPFDebugger](https://github.com/evi0s/WMPFDebugger)
|
||||||
|
- [0xsdeo/HeartK](https://github.com/0xsdeo/HeartK)
|
||||||
|
- [残笑/FindSomething](https://github.com/momosecurity/FindSomething)
|
||||||
|
- [进击的HACK / JSRPC 与调用 wx.cloud](https://mp.weixin.qq.com/s/hTlekrCPiMJCvsHYx7CAxw)
|
||||||
|
- [linguo2625469/WMPFDebugger-mac](https://github.com/linguo2625469/WMPFDebugger-mac)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 致谢
|
||||||
|
|
||||||
|
感谢 **0xsdeo** 师傅的大力支持与思路提供。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 交流群
|
||||||
|
|
||||||
|
群满 200 人后需要手动邀请,请加我拉群:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 免责声明
|
||||||
|
|
||||||
|
本工具仅供安全研究与学习使用,请勿用于未授权的目标,使用者须自行承担相关法律责任。
|
||||||
Reference in New Issue
Block a user