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 +### 主界面 -![zhuyem](https://s1.galgame.fun/imgb/u0/20260408_69d655f49ca7e.png) +![主界面](https://s1.galgame.fun/imgb/u0/20260408_69d655f49ca7e.png) ### 路由导航 -![luyou](https://s1.galgame.fun/imgb/u0/20260408_69d6560ca1a39.png) + +![路由导航](https://s1.galgame.fun/imgb/u0/20260408_69d6560ca1a39.png) ### 云函数分析 -![QQ20260402-192623](https://s1.galgame.fun/imgb/u0/20260402_69ce527ca5480.png) -### 调试开关(JSRPC 与调用 wx.cloud) -![调试](https://s1.galgame.fun/imgb/u0/20260408_69d6564195009.png) +![云函数分析](https://s1.galgame.fun/imgb/u0/20260402_69ce527ca5480.png) + +### 调试开关(JSRPC / wx.cloud 调用) + +![调试开关](https://s1.galgame.fun/imgb/u0/20260408_69d6564195009.png) ### 敏感信息提取 -![PixPin_2026-04-16_18-45-08](https://s1.galgame.fun/imgb/u55/20260416_69e0be0e1c1d7.png) +![敏感信息提取-1](https://s1.galgame.fun/imgb/u55/20260416_69e0be0e1c1d7.png) -PixPin_2026-04-16_18-45-59 +敏感信息提取-2 -![PixPin_2026-04-16_18-45-52](https://s1.galgame.fun/imgb/u55/20260416_69e0be0c70d46.png) +![敏感信息提取-3](https://s1.galgame.fun/imgb/u55/20260416_69e0be0c70d46.png) --- ## 功能特性 -- 通过 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变成手动挡了。加我拉 -![1db5b1b2699ae709a191207cf97dcf4b](https://s1.galgame.fun/imgb/u55/20260413_69dcaf1310fc4.jpg) + +群满 200 人后需要手动邀请,请加我拉群: + +![微信二维码](https://s1.galgame.fun/imgb/u55/20260413_69dcaf1310fc4.jpg) + +--- ## 免责声明 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