First commit

This commit is contained in:
牟不懂哎
2026-04-02 21:38:29 +08:00
commit 22326cf1b2
85 changed files with 24385 additions and 0 deletions
+132
View File
@@ -0,0 +1,132 @@
# First
> 微信小程序调试工具 —— 基于 Frida + CDP 代理,支持 GUI 与 CLI 双模式
---
## 截图预览
### 主界面 / Control Panel
![QQ20260402-192426](https://s1.galgame.fun/imgb/u0/20260402_69ce51f6c6f60.png)
### 路由导航
![微信图片_2026-04-02_192531_552](https://s1.galgame.fun/imgb/u0/20260402_69ce523e8b90d.png)
### 云函数分析
![QQ20260402-192623](https://s1.galgame.fun/imgb/u0/20260402_69ce527ca5480.png)
### 安全扫描
待优化
---
## 功能特性
- 通过 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** 师傅的大力支持与思路提供。
---
## 免责声明
本工具仅供安全研究与学习使用,请勿用于未授权的目标,使用者须自行承担相关法律责任。
+52
View File
@@ -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'],
)
+6
View File
@@ -0,0 +1,6 @@
{
"Version": 11581,
"LoadStartHookOffset": "0x28E9190",
"CDPFilterHookOffset": "0x38C4350",
"SceneOffsets": [1208, 1160]
}
+6
View File
@@ -0,0 +1,6 @@
{
"Version": 11633,
"LoadStartHookOffset": "0x28F22A0",
"CDPFilterHookOffset": "0x38D41E0",
"SceneOffsets": [1208, 1160]
}
+6
View File
@@ -0,0 +1,6 @@
{
"Version": 13331,
"LoadStartHookOffset": "0x0FFC200",
"CDPFilterHookOffset": "0x2420100",
"SceneOffsets": [1272, 1224]
}
+6
View File
@@ -0,0 +1,6 @@
{
"Version": 13341,
"LoadStartHookOffset": "0x10009E0",
"CDPFilterHookOffset": "0x242E8E0",
"SceneOffsets": [1272, 1224]
}
+6
View File
@@ -0,0 +1,6 @@
{
"Version": 13487,
"LoadStartHookOffset": "0x0FFB600",
"CDPFilterHookOffset": "0x241FEB0",
"SceneOffsets": [1272, 1224]
}
+6
View File
@@ -0,0 +1,6 @@
{
"Version": 13639,
"LoadStartHookOffset": "0x1000990",
"CDPFilterHookOffset": "0x2424DE0",
"SceneOffsets": [1272, 1224]
}
+6
View File
@@ -0,0 +1,6 @@
{
"Version": 13655,
"LoadStartHookOffset": "0x100F4B0",
"CDPFilterHookOffset": "0x244A9E0",
"SceneOffsets": [1280, 1232]
}
+6
View File
@@ -0,0 +1,6 @@
{
"Version": 13871,
"LoadStartHookOffset": "0x101F160",
"CDPFilterHookOffset": "0x246FC40",
"SceneOffsets": [1360, 1312]
}
+6
View File
@@ -0,0 +1,6 @@
{
"Version": 13909,
"LoadStartHookOffset": "0x101F0E0",
"CDPFilterHookOffset": "0x246FDC0",
"SceneOffsets": [1360, 1312]
}
+6
View File
@@ -0,0 +1,6 @@
{
"Version": 14161,
"LoadStartHookOffset": "0x10246C0",
"CDPFilterHookOffset": "0x24839B0",
"SceneOffsets": [1360, 1312]
}
+6
View File
@@ -0,0 +1,6 @@
{
"Version": 14199,
"LoadStartHookOffset": "0x10246F0",
"CDPFilterHookOffset": "0x24839E0",
"SceneOffsets": [1360, 1312]
}
+6
View File
@@ -0,0 +1,6 @@
{
"Version": 14315,
"LoadStartHookOffset": "0x10004C0",
"CDPFilterHookOffset": "0x2424B50",
"SceneOffsets": [1272, 1224]
}
+6
View File
@@ -0,0 +1,6 @@
{
"Version": 16133,
"LoadStartHookOffset": "0x470FAD0",
"CDPFilterHookOffset": "0x90FC7E0",
"SceneOffsets": [1360, 1312]
}
+6
View File
@@ -0,0 +1,6 @@
{
"Version": 16203,
"LoadStartHookOffset": "0x4710890",
"CDPFilterHookOffset": "0x90FD640",
"SceneOffsets": [1360, 1312]
}
+6
View File
@@ -0,0 +1,6 @@
{
"Version": 16389,
"LoadStartHookOffset": "0x24E4830",
"CDPFilterHookOffset": "0x2E2A880",
"SceneOffsets": [1360, 1312]
}
+6
View File
@@ -0,0 +1,6 @@
{
"Version": 16467,
"LoadStartHookOffset": "0x24E4FD0",
"CDPFilterHookOffset": "0x2E2CC90",
"SceneOffsets": [1360, 1312]
}
+6
View File
@@ -0,0 +1,6 @@
{
"Version": 16771,
"LoadStartHookOffset": "0x24E9130",
"CDPFilterHookOffset": "0x2E3C470",
"SceneOffsets": [1360, 1312]
}
+6
View File
@@ -0,0 +1,6 @@
{
"Version": 16815,
"LoadStartHookOffset": "0x2509690",
"CDPFilterHookOffset": "0x2E768D0",
"SceneOffsets": [1416, 1360]
}
+6
View File
@@ -0,0 +1,6 @@
{
"Version": 16965,
"LoadStartHookOffset": "0x2570220",
"CDPFilterHookOffset": "0x2F844A0",
"SceneOffsets": [1416, 1360]
}
+6
View File
@@ -0,0 +1,6 @@
{
"Version": 17037,
"LoadStartHookOffset": "0x257D0A0",
"CDPFilterHookOffset": "0x2FB2310",
"SceneOffsets": [1408, 1352]
}
+6
View File
@@ -0,0 +1,6 @@
{
"Version": 17071,
"LoadStartHookOffset": "0x258F370",
"CDPFilterHookOffset": "0x2FD3080",
"SceneOffsets": [1408, 1352]
}
+6
View File
@@ -0,0 +1,6 @@
{
"Version": 17127,
"LoadStartHookOffset": "0x2590910",
"CDPFilterHookOffset": "0x2FD4040",
"SceneOffsets": [1408, 1352]
}
+6
View File
@@ -0,0 +1,6 @@
{
"Version": 18055,
"LoadStartHookOffset": "0x25A1040",
"CDPFilterHookOffset": "0x30031E0",
"SceneOffsets": [1416, 1352]
}
+6
View File
@@ -0,0 +1,6 @@
{
"Version": 18151,
"LoadStartHookOffset": "0x25A2E20",
"CDPFilterHookOffset": "0x3013D20",
"SceneOffsets": [1416, 1352]
}
+6
View File
@@ -0,0 +1,6 @@
{
"Version": 18787,
"LoadStartHookOffset": "0x25B2870",
"CDPFilterHookOffset": "0x3028AD0",
"SceneOffsets": [1408, 1344]
}
+6
View File
@@ -0,0 +1,6 @@
{
"Version": 18891,
"LoadStartHookOffset": "0x25B50C0",
"CDPFilterHookOffset": "0x30245E0",
"SceneOffsets": [1408, 1344]
}
+6
View File
@@ -0,0 +1,6 @@
{
"Version": 18955,
"LoadStartHookOffset": "0x25B52C0",
"CDPFilterHookOffset": "0x30248B0",
"SceneOffsets": [1408, 1344]
}
+6
View File
@@ -0,0 +1,6 @@
{
"Version": 19027,
"LoadStartHookOffset": "0x25B52D0",
"CDPFilterHookOffset": "0x3024AD0",
"SceneOffsets": [1408, 1344]
}
+124
View File
@@ -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();
+2722
View File
File diff suppressed because it is too large Load Diff
+9
View File
@@ -0,0 +1,9 @@
{
"theme": "dark",
"debug_port": "9421",
"cdp_port": "62000",
"debug_main": false,
"debug_frida": false,
"selected_preload_scripts": [],
"custom_headers": []
}
+1
View File
@@ -0,0 +1 @@
console.log("[Hook 测试] Hello from hook_scripts! 注入成功 ✓");
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 711 B

+49
View File
@@ -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
+299
View File
@@ -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;
}
+4
View File
@@ -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
View File
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
View File
@@ -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,
)
+245
View File
@@ -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")
+525
View File
@@ -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
View File
@@ -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,
}
+143
View File
@@ -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
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+40
View File
@@ -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)
+352
View File
@@ -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);
}
})();
+149
View File
@@ -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")
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
+111
View File
@@ -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": {}})
+87
View File
@@ -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);
})();