diff --git a/README.md b/README.md
index 250e053..e5cbdeb 100644
--- a/README.md
+++ b/README.md
@@ -1,43 +1,47 @@
# First
-> 微信小程序调试工具 —— 基于 Frida + CDP 代理,支持 GUI 与 CLI 双模式
+> 微信小程序安全调试工具 —— 基于 Frida + CDP 代理,支持 Windows / macOS 双平台,GUI 与 CLI 双模式
+
---
-本项目仅支持windows,macos用户使用pd虚拟机即可
+
## 截图预览
-### 主界面 / Control Panel
+### 主界面
-
+
### 路由导航
-
+
+
### 云函数分析
-
-### 调试开关(JSRPC 与调用 wx.cloud)
-
+
+
+### 调试开关(JSRPC / wx.cloud 调用)
+
+
### 敏感信息提取
-
+
-
+
-
+
---
## 功能特性
-- 通过 Frida 注入微信客户端,转发小程序调试协议
-- CDP 代理桥接,支持 Chrome DevTools 直接连接调试
-- 路由枚举与一键跳转导航
-- 云函数调用监控与参数分析
-- UserScript 自动注入(支持按 URL 匹配、run-at 时机控制)
-- 内置安全扫描模块,生成扫描报告
+- Frida 动态注入微信客户端,转发小程序调试协议
+- CDP 代理桥接,Chrome DevTools 直连调试
+- 路由枚举、分类与一键跳转
+- 云函数调用监控与参数分析(动态 Hook + 静态扫描)
+- UserScript 自动注入(支持 URL 匹配、run-at 时机控制)
+- wxapkg 解密解包 + 敏感信息扫描(IP / 密钥 / 云存储 / JWT 等)
- 深色 / 浅色主题切换
-- GUI(PySide6)与 CLI 双模式运行
+- GUI(PySide6)与 CLI 双模式
---
@@ -50,6 +54,7 @@
| websockets | >= 12.0 |
| protobuf | >= 4.0.0 |
| PySide6 | >= 6.5.0 |
+| pycryptodome | 最新版 |
安装依赖:
@@ -59,26 +64,45 @@ pip install -r requirements.txt
---
-## 微信版本要求
+## 支持的微信版本
-- 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)
+### 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)
-### GUI 模式(推荐)
+### 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
```
-双击启动.bat
-启动后在主界面点击 **启动调试** 即可开始调试。
-### 小程序页面操作
+### macOS
-点击启动调试前请勿打开小程序, **启动调试**打开后再次启动**小程序**即可。
+**一键启动:** 终端执行 `./启动.sh`,首次运行会自动安装依赖。
+
+**手动启动:**
+
+```bash
+python3 gui.py
+```
+
+启动后在主界面点击 **启动调试**,然后再打开小程序即可(请勿在启动调试前打开小程序)。
### CLI 模式
@@ -92,6 +116,7 @@ python main.py --cdp-port 62000
# 开启详细日志
python main.py --debug-main --debug-frida
```
+
### 连接 Chrome DevTools
启动后,在 Chrome 地址栏输入:
@@ -100,7 +125,25 @@ python main.py --debug-main --debug-frida
devtools://devtools/bundled/inspector.html?ws=127.0.0.1:62000
```
-### 参数说明
+---
+
+## macOS 注意事项
+
+如果 Frida 注入报错,需要解除系统对进程附加的限制,任选其一:
+
+**方案一:关闭 SIP(系统完整性保护)**
+
+> 关闭后 Frida 才能正常注入进程。参考教程:[macOS SIP 开启关闭教程](https://cloud.tencent.com/developer/article/1496058)
+
+**方案二:强制重签名 WeChat**
+
+```bash
+sudo codesign --force --deep --sign - /Applications/WeChat.app
+```
+
+---
+
+## 参数说明
| 参数 | 默认值 | 说明 |
|------|--------|------|
@@ -111,11 +154,11 @@ devtools://devtools/bundled/inspector.html?ws=127.0.0.1:62000
| `--scripts-dir` | `./userscripts` | UserScript 目录路径 |
| `--script` | — | 指定单个 .js 文件注入(可多次使用) |
-### UserScript 注入
+---
-将 `.js` 脚本放入 `userscripts/` 目录,工具启动时自动加载并按规则注入。
+## UserScript 注入
-也可手动指定文件:
+将 `.js` 脚本放入 `userscripts/` 目录,启动时自动加载并注入。也可手动指定:
```bash
python main.py --script ./my_hook.js --script ./another.js
@@ -131,21 +174,27 @@ pyinstaller WMPFDebugger.spec
---
-## 常见问题解决办法
+## 常见问题
-1. Frida 已显示连接,但小程序端显示未连接或无法断点调试
+**Q: Frida 已连接,但小程序端显示未连接或无法断点调试?**
-若操作顺序无误,建议先彻底卸载微信并重启电脑(如有重要聊天记录请提前备份)。
-删除路径C:\Users\用户名\AppData\Roaming\Tencent\xwechat\XPlugin\Plugins\RadiumWMPF下所有以数字命名的文件夹,再次重启电脑后,安装微信 4.1.0.30 版本。安装完成后检查上述路径,确认文件夹编号为 16389。
+确认操作顺序无误后,尝试以下步骤:
+
+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)
+- [进击的HACK / JSRPC 与调用 wx.cloud](https://mp.weixin.qq.com/s/hTlekrCPiMJCvsHYx7CAxw)
+- [linguo2625469/WMPFDebugger-mac](https://github.com/linguo2625469/WMPFDebugger-mac)
+
---
## 致谢
@@ -153,9 +202,14 @@ pyinstaller WMPFDebugger.spec
感谢 **0xsdeo** 师傅的大力支持与思路提供。
---
+
## 交流群
-群满200变成手动挡了。加我拉
-
+
+群满 200 人后需要手动邀请,请加我拉群:
+
+
+
+---
## 免责声明
diff --git a/frida/config/mac/addresses.18152.json b/frida/config/mac/addresses.18152.json
new file mode 100644
index 0000000..09288f1
--- /dev/null
+++ b/frida/config/mac/addresses.18152.json
@@ -0,0 +1,20 @@
+{
+ "Version": 18152,
+ "Arch": {
+ "arm64": {
+ "LoadStartHookOffset": "0x4F43880",
+ "LoadStartHookOffset2": "0x8394F24",
+ "CDPFilterHookOffset": "0x8385F44",
+ "ResourceCachePolicyHookOffset": "0x4FA746C",
+ "StructOffset": 1376
+ },
+ "x64": {
+ "LoadStartHookOffset": "0x5971FF0",
+ "LoadStartHookOffset2": "0x91D58F0",
+ "CDPFilterHookOffset": "0x91C68D0",
+ "ResourceCachePolicyHookOffset": "0x59E0950",
+ "StructOffset": 1376
+ }
+ }
+ }
+
\ No newline at end of file
diff --git a/frida/config/mac/addresses.18788.json b/frida/config/mac/addresses.18788.json
new file mode 100644
index 0000000..ffba541
--- /dev/null
+++ b/frida/config/mac/addresses.18788.json
@@ -0,0 +1,19 @@
+{
+ "Version": 18788,
+ "Arch": {
+ "arm64": {
+ "LoadStartHookOffset": "0x4F6454C",
+ "LoadStartHookOffset2": "0x83BC8C8",
+ "CDPFilterHookOffset": "0x83AE884",
+ "ResourceCachePolicyHookOffset": "0x4FCAD90",
+ "StructOffset": 1368
+ },
+ "x64": {
+ "LoadStartHookOffset": "0x5994BA0",
+ "LoadStartHookOffset2": "0x91FFB90",
+ "CDPFilterHookOffset": "0x91F1C20",
+ "ResourceCachePolicyHookOffset": "0x5A066E0",
+ "StructOffset": 1368
+ }
+ }
+}
diff --git a/frida/config/addresses.11581.json b/frida/config/win/addresses.11581.json
similarity index 100%
rename from frida/config/addresses.11581.json
rename to frida/config/win/addresses.11581.json
diff --git a/frida/config/addresses.11633.json b/frida/config/win/addresses.11633.json
similarity index 100%
rename from frida/config/addresses.11633.json
rename to frida/config/win/addresses.11633.json
diff --git a/frida/config/addresses.13331.json b/frida/config/win/addresses.13331.json
similarity index 100%
rename from frida/config/addresses.13331.json
rename to frida/config/win/addresses.13331.json
diff --git a/frida/config/addresses.13341.json b/frida/config/win/addresses.13341.json
similarity index 100%
rename from frida/config/addresses.13341.json
rename to frida/config/win/addresses.13341.json
diff --git a/frida/config/addresses.13487.json b/frida/config/win/addresses.13487.json
similarity index 100%
rename from frida/config/addresses.13487.json
rename to frida/config/win/addresses.13487.json
diff --git a/frida/config/addresses.13639.json b/frida/config/win/addresses.13639.json
similarity index 100%
rename from frida/config/addresses.13639.json
rename to frida/config/win/addresses.13639.json
diff --git a/frida/config/addresses.13655.json b/frida/config/win/addresses.13655.json
similarity index 100%
rename from frida/config/addresses.13655.json
rename to frida/config/win/addresses.13655.json
diff --git a/frida/config/addresses.13871.json b/frida/config/win/addresses.13871.json
similarity index 100%
rename from frida/config/addresses.13871.json
rename to frida/config/win/addresses.13871.json
diff --git a/frida/config/addresses.13909.json b/frida/config/win/addresses.13909.json
similarity index 100%
rename from frida/config/addresses.13909.json
rename to frida/config/win/addresses.13909.json
diff --git a/frida/config/addresses.14161.json b/frida/config/win/addresses.14161.json
similarity index 100%
rename from frida/config/addresses.14161.json
rename to frida/config/win/addresses.14161.json
diff --git a/frida/config/addresses.14199.json b/frida/config/win/addresses.14199.json
similarity index 100%
rename from frida/config/addresses.14199.json
rename to frida/config/win/addresses.14199.json
diff --git a/frida/config/addresses.14315.json b/frida/config/win/addresses.14315.json
similarity index 100%
rename from frida/config/addresses.14315.json
rename to frida/config/win/addresses.14315.json
diff --git a/frida/config/addresses.16133.json b/frida/config/win/addresses.16133.json
similarity index 100%
rename from frida/config/addresses.16133.json
rename to frida/config/win/addresses.16133.json
diff --git a/frida/config/addresses.16203.json b/frida/config/win/addresses.16203.json
similarity index 100%
rename from frida/config/addresses.16203.json
rename to frida/config/win/addresses.16203.json
diff --git a/frida/config/addresses.16389.json b/frida/config/win/addresses.16389.json
similarity index 100%
rename from frida/config/addresses.16389.json
rename to frida/config/win/addresses.16389.json
diff --git a/frida/config/addresses.16467.json b/frida/config/win/addresses.16467.json
similarity index 100%
rename from frida/config/addresses.16467.json
rename to frida/config/win/addresses.16467.json
diff --git a/frida/config/addresses.16771.json b/frida/config/win/addresses.16771.json
similarity index 100%
rename from frida/config/addresses.16771.json
rename to frida/config/win/addresses.16771.json
diff --git a/frida/config/addresses.16815.json b/frida/config/win/addresses.16815.json
similarity index 100%
rename from frida/config/addresses.16815.json
rename to frida/config/win/addresses.16815.json
diff --git a/frida/config/addresses.16965.json b/frida/config/win/addresses.16965.json
similarity index 100%
rename from frida/config/addresses.16965.json
rename to frida/config/win/addresses.16965.json
diff --git a/frida/config/addresses.17037.json b/frida/config/win/addresses.17037.json
similarity index 100%
rename from frida/config/addresses.17037.json
rename to frida/config/win/addresses.17037.json
diff --git a/frida/config/addresses.17071.json b/frida/config/win/addresses.17071.json
similarity index 100%
rename from frida/config/addresses.17071.json
rename to frida/config/win/addresses.17071.json
diff --git a/frida/config/addresses.17127.json b/frida/config/win/addresses.17127.json
similarity index 100%
rename from frida/config/addresses.17127.json
rename to frida/config/win/addresses.17127.json
diff --git a/frida/config/addresses.18055.json b/frida/config/win/addresses.18055.json
similarity index 100%
rename from frida/config/addresses.18055.json
rename to frida/config/win/addresses.18055.json
diff --git a/frida/config/addresses.18151.json b/frida/config/win/addresses.18151.json
similarity index 100%
rename from frida/config/addresses.18151.json
rename to frida/config/win/addresses.18151.json
diff --git a/frida/config/addresses.18787.json b/frida/config/win/addresses.18787.json
similarity index 100%
rename from frida/config/addresses.18787.json
rename to frida/config/win/addresses.18787.json
diff --git a/frida/config/addresses.18891.json b/frida/config/win/addresses.18891.json
similarity index 100%
rename from frida/config/addresses.18891.json
rename to frida/config/win/addresses.18891.json
diff --git a/frida/config/addresses.18955.json b/frida/config/win/addresses.18955.json
similarity index 100%
rename from frida/config/addresses.18955.json
rename to frida/config/win/addresses.18955.json
diff --git a/frida/config/addresses.19027.json b/frida/config/win/addresses.19027.json
similarity index 100%
rename from frida/config/addresses.19027.json
rename to frida/config/win/addresses.19027.json
diff --git a/frida/config/addresses.19201.json b/frida/config/win/addresses.19201.json
similarity index 100%
rename from frida/config/addresses.19201.json
rename to frida/config/win/addresses.19201.json
diff --git a/frida/hook.js b/frida/hook.js
index c37d6e1..e675de3 100644
--- a/frida/hook.js
+++ b/frida/hook.js
@@ -1,119 +1,232 @@
+const VERBOSE = false;
+const PLATFORM = Process.platform; // 'darwin' | 'windows'
+const ARCH = Process.arch; // 'arm64' | 'x64' | 'x86_64' | ...
+
+// ── Module Resolution ──
+
const getMainModule = (version) => {
+ if (PLATFORM === "darwin") {
+ return Process.findModuleByName("WeChatAppEx Framework");
+ }
+ // Windows: flue.dll for newer versions
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;
+// ── Scene Values ──
+
+const SCENE_WHITELIST = [
+ 1005, 1007, 1008, 1012, 1027, 1035, 1053, 1074,
+ 1145, 1168, 1178, 1256, 1260, 1302, 1308,
+];
+
+// ── CDP Filter Patch (shared logic, platform-specific pointer access) ──
+
+const patchCDPFilter = (base, offset) => {
+ // xref: SendToClientFilter / devtools_message_filter_applet_webview.cc
Interceptor.attach(base.add(offset), {
onEnter(args) {
- send(
- `[patch] CDP filter on enter, original value of input: ${args[0].readPointer()}`,
- );
- this.inputValue = args[0];
+ if (PLATFORM === "windows") {
+ 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;
+ let target;
+ if (PLATFORM === "darwin") {
+ // macOS: retval points directly to the struct
+ if (!retval || retval.isNull()) return;
+ target = retval.add(8);
+ } else {
+ // Windows: dereference args[0] to get the struct pointer
+ const ptr = this.inputValue.readPointer();
+ if (ptr.isNull() || ptr.add(8).isNull()) return;
+ target = ptr.add(8);
}
-
- 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);
+ VERBOSE && console.log(`[patch] CDP filter v8[2]: ${target.readU32()}`);
+ if (target.readU32() === 6) {
+ target.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()}`);
+// ── Resource Cache Policy Patch (darwin only) ──
- // 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);
+const patchResourceCachePolicy = (base, offset) => {
+ // xref: WAPCAdapterAppIndex.js
+ Interceptor.attach(base.add(offset), {
+ onLeave(retval) {
+ VERBOSE && console.log(`[patch] cache policy retval: ${retval.toInt32()} -> 0`);
+ retval.replace(0x0);
+ },
+ });
};
-const patchOnLoadStart = (base, config) => {
- // xref: AppletIndexContainer::OnLoadStart
- Interceptor.attach(base.add(config.LoadStartHookOffset), {
+// ── OnLoadStart: Darwin ──
+
+const darwinPatchLoadStart = (base, offset) => {
+ // xref: AppletIndexContainer::OnLoadStart (first match)
+ // Force debug mode by setting second param to true
+ Interceptor.attach(base.add(offset), {
+ onEnter() {
+ console.log("[hook] AppletIndexContainer::OnLoadStart onEnter");
+ if (ARCH === "arm64") {
+ this.context.x1 = (this.context.x1 & ~0xff) | 0x1;
+ } else {
+ // x64 / x86_64: System V ABI, second param = RSI
+ this.context.rsi = (this.context.rsi & ~0xff) | 0x1;
+ }
+ },
+ onLeave() {},
+ });
+};
+
+const darwinPatchLoadStart2 = (base, offset, structOffset) => {
+ // xref: AppletIndexContainer::OnLoadStart (last function)
+ // Hijack scene value via struct pointer chain
+ Interceptor.attach(base.add(offset), {
onEnter(args) {
- send(
- `[inteceptor] AppletIndexContainer::OnLoadStart onEnter, ` +
- `indexContainer.this: ${this.context.rcx}`,
- );
- // write dl to 0x1
+ try {
+ const v4 = args[0].add(8).readPointer();
+ if (!v4 || v4.isNull()) return;
+ const q1 = v4.add(structOffset).readPointer();
+ if (!q1 || q1.isNull()) return;
+ const q2 = q1.add(16).readPointer();
+ if (!q2 || q2.isNull()) return;
+ const scenePtr = q2.add(488);
+ const scene = scenePtr.readInt();
+ console.log(`[hook] scene: ${scene}`);
+ if (SCENE_WHITELIST.includes(scene)) {
+ console.log("[hook] hook scene -> 1101");
+ scenePtr.writeInt(1101);
+ }
+ } catch (e) {
+ console.error(`[hook] onLoadStart2 error: ${e}`);
+ }
+ },
+ onLeave() {},
+ });
+};
+
+// ── OnLoadStart: Windows ──
+
+const winPatchLoadStart = (base, config) => {
+ // xref: AppletIndexContainer::OnLoadStart
+ // Windows combines debug flag + scene hijack in one hook
+ Interceptor.attach(base.add(config.LoadStartHookOffset), {
+ onEnter() {
+ // Microsoft x64 ABI: second param = RDX, force debug mode
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
+ // Scene hijack via SceneOffsets pointer chain
+ try {
+ const offsets = config.SceneOffsets;
+ const ptr1 = this.context.rcx.add(56).readPointer().add(offsets[0]).readPointer();
+ const scenePtr = ptr1.add(8).readPointer().add(offsets[1]).readPointer().add(16).readPointer().add(488);
+ const scene = scenePtr.readInt();
+ send(`[hook] scene: ${scene}`);
+ if (SCENE_WHITELIST.includes(scene)) {
+ send("[hook] hook scene -> 1101");
+ scenePtr.writeInt(1101);
+ }
+ } catch (e) {
+ send(`[hook] scene hook error: ${e}`);
+ }
},
+ onLeave() {},
});
};
+// ── Config Parsing ──
+
const parseConfig = () => {
const rawConfig = `@@CONFIG@@`;
if (rawConfig.includes("@@")) {
- // test addresses
+ // Fallback test config
+ if (PLATFORM === "darwin") {
+ return {
+ Version: 17078,
+ Arch: {
+ arm64: {
+ LoadStartHookOffset: "0x4F0620C",
+ LoadStartHookOffset2: "0x81CEC08",
+ CDPFilterHookOffset: "0x81BFC04",
+ ResourceCachePolicyHookOffset: "0x4F699E8",
+ StructOffset: 1376,
+ },
+ },
+ };
+ }
return {
Version: 18955,
LoadStartHookOffset: "0x25B52C0",
CDPFilterHookOffset: "0x30248B0",
- SceneOffsets: [1408, 1344],
+ SceneOffsets: [1408, 1344, 488],
};
}
return JSON.parse(rawConfig);
};
+const resolveArchConfig = (config) => {
+ // Windows configs have offsets at top level — return directly
+ if (config && config.LoadStartHookOffset) {
+ return config;
+ }
+ // Mac configs nest under Arch.{arm64|x64}
+ const table = config?.Arch || config?.arch;
+ if (!table) return null;
+
+ const candidates = [ARCH];
+ if (ARCH === "x86_64") candidates.push("x64");
+ if (ARCH === "amd64") candidates.push("x64");
+
+ for (const key of candidates) {
+ const picked = table[key];
+ if (picked && picked.LoadStartHookOffset) {
+ return { ...picked, Version: config.Version, __arch: key };
+ }
+ }
+ return null;
+};
+
+// ── Main ──
+
const main = () => {
- const config = parseConfig();
+ const rawConfig = parseConfig();
+ const config = resolveArchConfig(rawConfig);
+
+ if (!config) {
+ console.error(`[frida] no config for platform=${PLATFORM} arch=${ARCH}`);
+ return;
+ }
+
const mainModule = getMainModule(config.Version);
- patchOnLoadStart(mainModule.base, config);
- patchCDPFilter(mainModule.base, config);
+ if (!mainModule) {
+ const expected = PLATFORM === "darwin" ? "WeChatAppEx Framework" : "flue.dll / WeChatAppEx.exe";
+ console.error(`[frida] module not found: ${expected}`);
+ return;
+ }
+
+ console.log(`[frida] version=${config.Version} platform=${PLATFORM} arch=${config.__arch || ARCH}`);
+ console.log(`[frida] module base: ${mainModule.base}`);
+
+ const base = mainModule.base;
+
+ // CDP filter patch — both platforms
+ patchCDPFilter(base, config.CDPFilterHookOffset);
+
+ if (PLATFORM === "darwin") {
+ darwinPatchLoadStart(base, config.LoadStartHookOffset);
+ darwinPatchLoadStart2(base, config.LoadStartHookOffset2, config.StructOffset);
+ if (config.ResourceCachePolicyHookOffset) {
+ patchResourceCachePolicy(base, config.ResourceCachePolicyHookOffset);
+ }
+ } else {
+ winPatchLoadStart(base, config);
+ }
};
main();
diff --git a/gui.py b/gui.py
index 3f175d3..790781a 100644
--- a/gui.py
+++ b/gui.py
@@ -641,8 +641,9 @@ def _make_entry(placeholder="", width=None):
class App(QMainWindow):
def __init__(self):
super().__init__()
- self.setWindowTitle("First")
- _ico = os.path.join(_BASE_DIR, "icon.ico")
+ self._os_tag = "macOS" if sys.platform == "darwin" else "Windows"
+ self.setWindowTitle(f"First-{self._os_tag}")
+ _ico = os.path.join(_BASE_DIR, "icon.png")
if os.path.exists(_ico):
self.setWindowIcon(QIcon(_ico))
self.resize(960, 620)
@@ -726,14 +727,14 @@ class App(QMainWindow):
logo_row.setContentsMargins(0, 0, 0, 0)
logo_row.setSpacing(6)
logo_row.addStretch()
- _ico_path = os.path.join(_BASE_DIR, "icon.ico")
+ _ico_path = os.path.join(_BASE_DIR, "icon.png")
if os.path.exists(_ico_path):
_ico_lbl = QLabel()
_pix = QPixmap(_ico_path).scaled(20, 20, Qt.KeepAspectRatio, Qt.SmoothTransformation)
_ico_lbl.setPixmap(_pix)
_ico_lbl.setFixedSize(20, 20)
logo_row.addWidget(_ico_lbl)
- self._sb_logo = QLabel("First")
+ self._sb_logo = QLabel(f"First-{self._os_tag}")
self._sb_logo.setObjectName("sb_logo")
logo_row.addWidget(self._sb_logo)
logo_row.addStretch()
@@ -818,6 +819,11 @@ class App(QMainWindow):
QDesktopServices.openUrl(QUrl("https://github.com/Spade-sec/First")),
self._log_add("info", "[gui] 已打开 GitHub 页面"))
sb_lay.addWidget(sb_gh)
+ sb_ver = QLabel("v1.0.7")
+ sb_ver.setObjectName("sb_theme")
+ sb_ver.setAlignment(Qt.AlignCenter)
+ sb_ver.setFont(QFont(_FN, 7))
+ sb_lay.addWidget(sb_ver)
sb_lay.addSpacing(12)
self._update_theme_label()
@@ -1366,15 +1372,8 @@ class App(QMainWindow):
path_row.addWidget(_make_label("Applet目录", bold=True))
self._ext_path_ent = _make_entry("wxapkg 包目录路径...")
# 自动检测默认路径
- default_pkg = ""
- if os.name == "nt":
- user_home = os.path.expanduser("~")
- default_pkg = os.path.join(
- user_home, "AppData", "Roaming", "Tencent",
- "xwechat", "radium", "Applet", "packages"
- )
- if not os.path.isdir(default_pkg):
- default_pkg = ""
+ from src.wxapkg import get_default_packages_dir
+ default_pkg = get_default_packages_dir() or ""
saved_path = self._cfg.get("extract_packages_dir", "")
if saved_path:
self._ext_path_ent.setText(saved_path)
@@ -1635,23 +1634,22 @@ class App(QMainWindow):
def _ext_auto_detect(self):
"""自动检测默认路径"""
- if os.name == "nt":
- user_home = os.path.expanduser("~")
- default_pkg = os.path.join(
- user_home, "AppData", "Roaming", "Tencent",
- "xwechat", "radium", "Applet", "packages"
- )
- if os.path.isdir(default_pkg):
- if self._ext_path_ent.text().strip() == default_pkg:
- # 路径相同但强制刷新
- self._ext_refresh_apps()
- else:
- self._ext_path_ent.setText(default_pkg)
- self._ext_log("已自动检测到 Applet 目录")
+ from src.wxapkg import get_default_packages_dir
+ default_pkg = get_default_packages_dir()
+ if default_pkg:
+ if self._ext_path_ent.text().strip() == default_pkg:
+ self._ext_refresh_apps()
else:
- self._ext_log("未找到默认 Applet 目录,请手动选择")
+ self._ext_path_ent.setText(default_pkg)
+ self._ext_log(f"已自动检测到 Applet 目录: {default_pkg}")
else:
- self._ext_log("非 Windows 系统,请手动选择目录")
+ hint = ("~/Library/Containers/com.tencent.xinWeChat/Data/Documents/"
+ "app_data/radium/users/<微信用户ID>/applet/packages"
+ if sys.platform == "darwin" else
+ "未找到默认 Applet 目录")
+ self._ext_log(f"未自动检测到目录,请手动选择。macOS 参考路径: {hint}"
+ if sys.platform == "darwin" else
+ f"{hint},请手动选择")
def _ext_auto_process_new(self, new_ids):
"""对新增的小程序自动执行反编译/扫描(按开关状态)"""
@@ -3939,7 +3937,7 @@ if __name__ == "__main__":
app = QApplication(sys.argv)
app.setFont(QFont(_FN, 9))
- _ico = os.path.join(_BASE_DIR, "icon.ico")
+ _ico = os.path.join(_BASE_DIR, "icon.png")
if os.path.exists(_ico):
app.setWindowIcon(QIcon(_ico))
window = App()
diff --git a/icon.png b/icon.png
new file mode 100644
index 0000000..6796bb3
Binary files /dev/null and b/icon.png differ
diff --git a/requirements.txt b/requirements.txt
index 39f8a71..cdc96c6 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -2,3 +2,4 @@ frida>=17.0.0
websockets>=12.0
protobuf>=4.0.0
PySide6>=6.5.0
+pycryptodome
\ No newline at end of file
diff --git a/src/__pycache__/__init__.cpython-310.pyc b/src/__pycache__/__init__.cpython-310.pyc
new file mode 100644
index 0000000..10cf212
Binary files /dev/null and b/src/__pycache__/__init__.cpython-310.pyc differ
diff --git a/src/__pycache__/cli.cpython-310.pyc b/src/__pycache__/cli.cpython-310.pyc
new file mode 100644
index 0000000..c52b43e
Binary files /dev/null and b/src/__pycache__/cli.cpython-310.pyc differ
diff --git a/src/__pycache__/cloud_audit.cpython-310.pyc b/src/__pycache__/cloud_audit.cpython-310.pyc
new file mode 100644
index 0000000..cb76612
Binary files /dev/null and b/src/__pycache__/cloud_audit.cpython-310.pyc differ
diff --git a/src/__pycache__/codex.cpython-310.pyc b/src/__pycache__/codex.cpython-310.pyc
new file mode 100644
index 0000000..5ae5b11
Binary files /dev/null and b/src/__pycache__/codex.cpython-310.pyc differ
diff --git a/src/__pycache__/constants.cpython-310.pyc b/src/__pycache__/constants.cpython-310.pyc
new file mode 100644
index 0000000..e7f7cba
Binary files /dev/null and b/src/__pycache__/constants.cpython-310.pyc differ
diff --git a/src/__pycache__/engine.cpython-310.pyc b/src/__pycache__/engine.cpython-310.pyc
new file mode 100644
index 0000000..d9c62f0
Binary files /dev/null and b/src/__pycache__/engine.cpython-310.pyc differ
diff --git a/src/__pycache__/extractor.cpython-310.pyc b/src/__pycache__/extractor.cpython-310.pyc
new file mode 100644
index 0000000..91883b5
Binary files /dev/null and b/src/__pycache__/extractor.cpython-310.pyc differ
diff --git a/src/__pycache__/logger.cpython-310.pyc b/src/__pycache__/logger.cpython-310.pyc
new file mode 100644
index 0000000..1c55523
Binary files /dev/null and b/src/__pycache__/logger.cpython-310.pyc differ
diff --git a/src/__pycache__/navigator.cpython-310.pyc b/src/__pycache__/navigator.cpython-310.pyc
new file mode 100644
index 0000000..56db187
Binary files /dev/null and b/src/__pycache__/navigator.cpython-310.pyc differ
diff --git a/src/__pycache__/userscript.cpython-310.pyc b/src/__pycache__/userscript.cpython-310.pyc
new file mode 100644
index 0000000..e7895e2
Binary files /dev/null and b/src/__pycache__/userscript.cpython-310.pyc differ
diff --git a/src/__pycache__/wxapkg.cpython-310.pyc b/src/__pycache__/wxapkg.cpython-310.pyc
new file mode 100644
index 0000000..0a97dbc
Binary files /dev/null and b/src/__pycache__/wxapkg.cpython-310.pyc differ
diff --git a/src/engine.py b/src/engine.py
index 93dd7b0..d72a45c 100644
--- a/src/engine.py
+++ b/src/engine.py
@@ -4,7 +4,9 @@ DebugEngine - wraps debug_server, proxy_server, frida_server into a reusable cla
import asyncio
import json
import random
+import platform
import re
+import subprocess
import sys
from collections import Counter
from pathlib import Path
@@ -434,66 +436,95 @@ class DebugEngine:
# ── Frida ──
- async def _start_frida(self):
- logger = self.logger
+ # ── Frida: platform helpers ──
+
+ def _get_project_root(self):
+ if getattr(sys, 'frozen', False):
+ return Path(getattr(sys, '_MEIPASS', Path(sys.executable).parent)).resolve()
+ return Path(__file__).parent.parent.resolve()
+
+ def _find_wmpf_pids_win(self):
+ """Windows: find main WeChatAppEx.exe PID via ppid frequency."""
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:
+ wmpf_procs = [p for p in processes if p.name == "WeChatAppEx.exe"]
+ if not wmpf_procs:
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)
+ ppids = [p.parameters.get("ppid", 0) or 0 for p in wmpf_procs]
+ main_pid = Counter(ppids).most_common(1)[0][0]
+ if main_pid == 0:
+ raise RuntimeError("[frida] WeChatAppEx.exe main process not found")
- if not wmpf_ppids:
- raise RuntimeError("[frida] WeChatAppEx.exe process not found")
+ main_proc = next((p for p in processes if p.pid == main_pid), None)
+ if main_proc is None:
+ raise RuntimeError("[frida] could not locate main WMPF process")
- pid_counts = Counter(wmpf_ppids)
- wmpf_pid = pid_counts.most_common(1)[0][0]
+ proc_path = main_proc.parameters.get("path", "")
+ nums = re.findall(r"\d+", proc_path)
+ if not nums:
+ raise RuntimeError("[frida] cannot detect WMPF version from path")
+ version = int(nums[-1])
+ if version == 0:
+ raise RuntimeError("[frida] invalid WMPF version")
- if wmpf_pid == 0:
- raise RuntimeError("[frida] WeChatAppEx.exe process not found")
+ return [main_pid], version
- wmpf_process = None
- for p in processes:
- if p.pid == wmpf_pid:
- wmpf_process = p
- break
+ def _find_wmpf_pids_darwin(self):
+ """macOS: find WeChatAppEx PIDs via pgrep, version via Info.plist."""
+ try:
+ out = subprocess.check_output(
+ ["pgrep", "-f", "/MacOS/WeChatAppEx.app/Contents/MacOS/WeChatAppEx"],
+ text=True,
+ ).strip()
+ pids = [int(p) for p in out.splitlines() if p.strip()]
+ except (subprocess.CalledProcessError, ValueError):
+ pids = []
+ if not pids:
+ raise RuntimeError("[frida] WeChatAppEx process not found on macOS")
- if wmpf_process is None:
- raise RuntimeError("[frida] Could not find main WMPF process")
+ try:
+ ver_out = subprocess.check_output(
+ ["defaults", "read",
+ "/Applications/WeChat.app/Contents/MacOS/WeChatAppEx.app/Contents/Info.plist",
+ "CFBundleVersion"],
+ text=True,
+ ).strip()
+ version = int(ver_out.split(".")[1])
+ except Exception:
+ raise RuntimeError("[frida] cannot detect WeChatAppEx version from Info.plist")
- 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")
+ return pids, version
- session = device.attach(wmpf_pid)
+ async def _start_frida(self):
+ logger = self.logger
+ is_mac = sys.platform == "darwin"
+ platform_dir = "mac" if is_mac else "win"
- 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()
+ # Find PIDs and version
+ if is_mac:
+ pids, wmpf_version = self._find_wmpf_pids_darwin()
else:
- project_root = Path(__file__).parent.parent.resolve()
+ pids, wmpf_version = self._find_wmpf_pids_win()
+ # Load hook script and config
+ project_root = self._get_project_root()
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"
+ config_path = project_root / "frida" / "config" / platform_dir / 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))
+ raise RuntimeError(f"[frida] version config not found: {platform_dir}/{wmpf_version}")
+ script_content = hook_path.read_text(encoding="utf-8")
+ config_content = json.dumps(json.loads(config_path.read_text(encoding="utf-8")))
final_script = script_content.replace("@@CONFIG@@", config_content)
- script = session.create_script(final_script)
+
+ # Inject into all target PIDs
+ device = frida.get_local_device()
+ session = None
+ script = None
def on_message(message, data):
if message.get("type") == "error":
@@ -501,10 +532,22 @@ class DebugEngine:
return
logger.frida_debug("[frida client]", message.get("payload", ""))
- script.on("message", on_message)
- script.load()
+ for pid in pids:
+ try:
+ s = device.attach(pid)
+ sc = s.create_script(final_script)
+ sc.on("message", on_message)
+ sc.load()
+ logger.info(f"[frida] injected pid={pid}, version={wmpf_version}")
+ # Keep first successful session/script as primary
+ if session is None:
+ session, script = s, sc
+ except Exception as e:
+ logger.error(f"[frida] failed to inject pid={pid}: {e}")
- logger.info(f"[frida] script loaded, WMPF version: {wmpf_version}, pid: {wmpf_pid}")
+ if session is None:
+ raise RuntimeError("[frida] failed to inject any WeChatAppEx process")
+
+ logger.info(f"[frida] ready ({platform_dir}), version={wmpf_version}, {len(pids)} process(es)")
logger.info("[frida] you can now open any miniapps")
-
return session, script
diff --git a/src/third_party/__pycache__/__init__.cpython-310.pyc b/src/third_party/__pycache__/__init__.cpython-310.pyc
index 88f07c7..d347d20 100644
Binary files a/src/third_party/__pycache__/__init__.cpython-310.pyc and b/src/third_party/__pycache__/__init__.cpython-310.pyc differ
diff --git a/src/third_party/__pycache__/wmpf_debug_pb2.cpython-310.pyc b/src/third_party/__pycache__/wmpf_debug_pb2.cpython-310.pyc
index 050636c..22c7768 100644
Binary files a/src/third_party/__pycache__/wmpf_debug_pb2.cpython-310.pyc and b/src/third_party/__pycache__/wmpf_debug_pb2.cpython-310.pyc differ
diff --git a/src/wxapkg.py b/src/wxapkg.py
index 2fd936c..be89c51 100644
--- a/src/wxapkg.py
+++ b/src/wxapkg.py
@@ -210,14 +210,33 @@ def find_wxapkg_files(packages_dir: str) -> List[dict]:
def get_default_packages_dir() -> Optional[str]:
- """获取默认的微信小程序包路径(Windows)"""
- if os.name != "nt":
- return None
+ """获取默认的微信小程序包路径(Windows / macOS)"""
+ import sys
user_home = os.path.expanduser("~")
- pkg_dir = os.path.join(
- user_home, "AppData", "Roaming", "Tencent",
- "xwechat", "radium", "Applet", "packages"
- )
- if os.path.isdir(pkg_dir):
- return pkg_dir
+ if sys.platform == "darwin":
+ # macOS: ~/Library/Containers/com.tencent.xinWeChat/Data/Documents/app_data/radium/users/{wxid}/applet/packages
+ users_dir = os.path.join(
+ user_home, "Library", "Containers", "com.tencent.xinWeChat",
+ "Data", "Documents", "app_data", "radium", "users"
+ )
+ if not os.path.isdir(users_dir):
+ return None
+ # 找最近修改的用户目录
+ user_dirs = [
+ os.path.join(users_dir, d)
+ for d in os.listdir(users_dir)
+ if os.path.isdir(os.path.join(users_dir, d)) and not d.startswith(".")
+ ]
+ for ud in sorted(user_dirs, key=os.path.getmtime, reverse=True):
+ pkg_dir = os.path.join(ud, "applet", "packages")
+ if os.path.isdir(pkg_dir):
+ return pkg_dir
+ return None
+ elif os.name == "nt":
+ pkg_dir = os.path.join(
+ user_home, "AppData", "Roaming", "Tencent",
+ "xwechat", "radium", "Applet", "packages"
+ )
+ if os.path.isdir(pkg_dir):
+ return pkg_dir
return None
diff --git a/启动.sh b/启动.sh
new file mode 100755
index 0000000..176828f
--- /dev/null
+++ b/启动.sh
@@ -0,0 +1,51 @@
+#!/bin/bash
+cd "$(dirname "$0")"
+
+# Skip dependency check if already installed before
+if [ -f ".deps_installed" ]; then
+ python3 gui.py &
+ exit 0
+fi
+
+# Check if requirements.txt exists
+if [ ! -f "requirements.txt" ]; then
+ echo
+ echo " [WARNING] requirements.txt not found!"
+ echo
+ read -p " Skip dependency install and launch directly? [Y/N]: " choice
+ echo
+ if [[ "$choice" =~ ^[Yy]$ ]]; then
+ python3 gui.py &
+ exit 0
+ fi
+ echo " Cancelled."
+ exit 1
+fi
+
+# requirements.txt found, ask to install
+echo
+echo " [OK] requirements.txt found."
+echo
+read -p " Install dependencies? [Y/N]: " install
+echo
+
+if [[ "$install" =~ ^[Yy]$ ]]; then
+ echo " Installing dependencies, please wait..."
+ echo
+ pip3 install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
+ if [ $? -ne 0 ]; then
+ echo
+ echo " [ERROR] Installation failed! Check your network or pip config."
+ exit 1
+ fi
+ echo
+ echo " [OK] Done! Will skip this step on next launch."
+ touch .deps_installed
+ echo
+else
+ touch .deps_installed
+fi
+
+# Launch
+python3 gui.py &
+exit 0