v1.0.7
This commit is contained in:
@@ -1,43 +1,47 @@
|
|||||||
# First
|
# First
|
||||||
|
|
||||||
> 微信小程序调试工具 —— 基于 Frida + CDP 代理,支持 GUI 与 CLI 双模式
|
> 微信小程序安全调试工具 —— 基于 Frida + CDP 代理,支持 Windows / macOS 双平台,GUI 与 CLI 双模式
|
||||||
|
|
||||||
---
|
---
|
||||||
本项目仅支持windows,macos用户使用pd虚拟机即可
|
|
||||||
## 截图预览
|
## 截图预览
|
||||||
|
|
||||||
### 主界面 / Control Panel
|
### 主界面
|
||||||
|
|
||||||

|

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

|
|
||||||
|

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

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

|
||||||

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

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

|

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

|

|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 功能特性
|
## 功能特性
|
||||||
|
|
||||||
- 通过 Frida 注入微信客户端,转发小程序调试协议
|
- Frida 动态注入微信客户端,转发小程序调试协议
|
||||||
- CDP 代理桥接,支持 Chrome DevTools 直接连接调试
|
- CDP 代理桥接,Chrome DevTools 直连调试
|
||||||
- 路由枚举与一键跳转导航
|
- 路由枚举、分类与一键跳转
|
||||||
- 云函数调用监控与参数分析
|
- 云函数调用监控与参数分析(动态 Hook + 静态扫描)
|
||||||
- UserScript 自动注入(支持按 URL 匹配、run-at 时机控制)
|
- UserScript 自动注入(支持 URL 匹配、run-at 时机控制)
|
||||||
- 内置安全扫描模块,生成扫描报告
|
- wxapkg 解密解包 + 敏感信息扫描(IP / 密钥 / 云存储 / JWT 等)
|
||||||
- 深色 / 浅色主题切换
|
- 深色 / 浅色主题切换
|
||||||
- GUI(PySide6)与 CLI 双模式运行
|
- GUI(PySide6)与 CLI 双模式
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -50,6 +54,7 @@
|
|||||||
| websockets | >= 12.0 |
|
| websockets | >= 12.0 |
|
||||||
| protobuf | >= 4.0.0 |
|
| protobuf | >= 4.0.0 |
|
||||||
| PySide6 | >= 6.5.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,
|
### Windows
|
||||||
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)
|
|
||||||
|
|
||||||
## 使用方法
|
- 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
|
```bash
|
||||||
python gui.py
|
python gui.py
|
||||||
```
|
```
|
||||||
双击启动.bat
|
|
||||||
启动后在主界面点击 **启动调试** 即可开始调试。
|
|
||||||
|
|
||||||
### 小程序页面操作
|
### macOS
|
||||||
|
|
||||||
点击启动调试前请勿打开小程序, **启动调试**打开后再次启动**小程序**即可。
|
**一键启动:** 终端执行 `./启动.sh`,首次运行会自动安装依赖。
|
||||||
|
|
||||||
|
**手动启动:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 gui.py
|
||||||
|
```
|
||||||
|
|
||||||
|
启动后在主界面点击 **启动调试**,然后再打开小程序即可(请勿在启动调试前打开小程序)。
|
||||||
|
|
||||||
### CLI 模式
|
### CLI 模式
|
||||||
|
|
||||||
@@ -92,6 +116,7 @@ python main.py --cdp-port 62000
|
|||||||
# 开启详细日志
|
# 开启详细日志
|
||||||
python main.py --debug-main --debug-frida
|
python main.py --debug-main --debug-frida
|
||||||
```
|
```
|
||||||
|
|
||||||
### 连接 Chrome DevTools
|
### 连接 Chrome DevTools
|
||||||
|
|
||||||
启动后,在 Chrome 地址栏输入:
|
启动后,在 Chrome 地址栏输入:
|
||||||
@@ -100,7 +125,25 @@ python main.py --debug-main --debug-frida
|
|||||||
devtools://devtools/bundled/inspector.html?ws=127.0.0.1:62000
|
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 目录路径 |
|
| `--scripts-dir` | `./userscripts` | UserScript 目录路径 |
|
||||||
| `--script` | — | 指定单个 .js 文件注入(可多次使用) |
|
| `--script` | — | 指定单个 .js 文件注入(可多次使用) |
|
||||||
|
|
||||||
### UserScript 注入
|
---
|
||||||
|
|
||||||
将 `.js` 脚本放入 `userscripts/` 目录,工具启动时自动加载并按规则注入。
|
## UserScript 注入
|
||||||
|
|
||||||
也可手动指定文件:
|
将 `.js` 脚本放入 `userscripts/` 目录,启动时自动加载并注入。也可手动指定:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python main.py --script ./my_hook.js --script ./another.js
|
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)
|
- [evi0s/WMPFDebugger](https://github.com/evi0s/WMPFDebugger)
|
||||||
- [0xsdeo/HeartK](https://github.com/0xsdeo/HeartK)
|
- [0xsdeo/HeartK](https://github.com/0xsdeo/HeartK)
|
||||||
- [残笑/FindSomething](https://github.com/momosecurity/FindSomething)
|
- [残笑/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** 师傅的大力支持与思路提供。
|
感谢 **0xsdeo** 师傅的大力支持与思路提供。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 交流群
|
## 交流群
|
||||||
群满200变成手动挡了。加我拉
|
|
||||||

|
群满 200 人后需要手动邀请,请加我拉群:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 免责声明
|
## 免责声明
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+187
-74
@@ -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) => {
|
const getMainModule = (version) => {
|
||||||
|
if (PLATFORM === "darwin") {
|
||||||
|
return Process.findModuleByName("WeChatAppEx Framework");
|
||||||
|
}
|
||||||
|
// Windows: flue.dll for newer versions
|
||||||
if (version >= 13331) {
|
if (version >= 13331) {
|
||||||
return Process.findModuleByName("flue.dll");
|
return Process.findModuleByName("flue.dll");
|
||||||
}
|
}
|
||||||
return Process.findModuleByName("WeChatAppEx.exe");
|
return Process.findModuleByName("WeChatAppEx.exe");
|
||||||
};
|
};
|
||||||
|
|
||||||
const patchCDPFilter = (base, config) => {
|
// ── Scene Values ──
|
||||||
// xref: SendToClientFilter OR devtools_message_filter_applet_webview.cc
|
|
||||||
const offset = config.CDPFilterHookOffset;
|
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), {
|
Interceptor.attach(base.add(offset), {
|
||||||
onEnter(args) {
|
onEnter(args) {
|
||||||
send(
|
if (PLATFORM === "windows") {
|
||||||
`[patch] CDP filter on enter, original value of input: ${args[0].readPointer()}`,
|
this.inputValue = args[0];
|
||||||
);
|
}
|
||||||
this.inputValue = args[0];
|
|
||||||
},
|
},
|
||||||
onLeave(retval) {
|
onLeave(retval) {
|
||||||
const inputValue = this.inputValue.readPointer();
|
let target;
|
||||||
if (inputValue.isNull() || inputValue.add(8).isNull()) {
|
if (PLATFORM === "darwin") {
|
||||||
// there's a chance the value could be null
|
// macOS: retval points directly to the struct
|
||||||
// return here to avoid crash
|
if (!retval || retval.isNull()) return;
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
VERBOSE && console.log(`[patch] CDP filter v8[2]: ${target.readU32()}`);
|
||||||
send(
|
if (target.readU32() === 6) {
|
||||||
`[patch] CDP filter on leave, patch input, now value: ${inputValue}; ` +
|
target.writeU32(0x0);
|
||||||
`*(input + 8) = ${inputValue.add(8).readU32()}`,
|
|
||||||
);
|
|
||||||
if (inputValue.add(8).readU32() == 6) {
|
|
||||||
inputValue.add(8).writeU32(0x0);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const hookOnLoadScene = (a1, sceneOffsets) => {
|
// ── Resource Cache Policy Patch (darwin only) ──
|
||||||
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
|
const patchResourceCachePolicy = (base, offset) => {
|
||||||
// 1007: from issue #80
|
// xref: WAPCAdapterAppIndex.js
|
||||||
// 1008: from issue #53
|
Interceptor.attach(base.add(offset), {
|
||||||
// 1027: from issue #78
|
onLeave(retval) {
|
||||||
// 1035: from issue #78
|
VERBOSE && console.log(`[patch] cache policy retval: ${retval.toInt32()} -> 0`);
|
||||||
// 1053: from issue #25
|
retval.replace(0x0);
|
||||||
// 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 patchOnLoadStart = (base, config) => {
|
// ── OnLoadStart: Darwin ──
|
||||||
// xref: AppletIndexContainer::OnLoadStart
|
|
||||||
Interceptor.attach(base.add(config.LoadStartHookOffset), {
|
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) {
|
onEnter(args) {
|
||||||
send(
|
try {
|
||||||
`[inteceptor] AppletIndexContainer::OnLoadStart onEnter, ` +
|
const v4 = args[0].add(8).readPointer();
|
||||||
`indexContainer.this: ${this.context.rcx}`,
|
if (!v4 || v4.isNull()) return;
|
||||||
);
|
const q1 = v4.add(structOffset).readPointer();
|
||||||
// write dl to 0x1
|
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) {
|
if ((this.context.rdx & 0xff) !== 1) {
|
||||||
this.context.rdx = (this.context.rdx & ~0xff) | 0x1;
|
this.context.rdx = (this.context.rdx & ~0xff) | 0x1;
|
||||||
}
|
}
|
||||||
// handle onLoad scene
|
// Scene hijack via SceneOffsets pointer chain
|
||||||
hookOnLoadScene(this.context.rcx, config.SceneOffsets);
|
try {
|
||||||
},
|
const offsets = config.SceneOffsets;
|
||||||
onLeave(retval) {
|
const ptr1 = this.context.rcx.add(56).readPointer().add(offsets[0]).readPointer();
|
||||||
// do nothing
|
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 parseConfig = () => {
|
||||||
const rawConfig = `@@CONFIG@@`;
|
const rawConfig = `@@CONFIG@@`;
|
||||||
if (rawConfig.includes("@@")) {
|
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 {
|
return {
|
||||||
Version: 18955,
|
Version: 18955,
|
||||||
LoadStartHookOffset: "0x25B52C0",
|
LoadStartHookOffset: "0x25B52C0",
|
||||||
CDPFilterHookOffset: "0x30248B0",
|
CDPFilterHookOffset: "0x30248B0",
|
||||||
SceneOffsets: [1408, 1344],
|
SceneOffsets: [1408, 1344, 488],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return JSON.parse(rawConfig);
|
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 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);
|
const mainModule = getMainModule(config.Version);
|
||||||
patchOnLoadStart(mainModule.base, config);
|
if (!mainModule) {
|
||||||
patchCDPFilter(mainModule.base, config);
|
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();
|
main();
|
||||||
|
|||||||
@@ -641,8 +641,9 @@ def _make_entry(placeholder="", width=None):
|
|||||||
class App(QMainWindow):
|
class App(QMainWindow):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.setWindowTitle("First")
|
self._os_tag = "macOS" if sys.platform == "darwin" else "Windows"
|
||||||
_ico = os.path.join(_BASE_DIR, "icon.ico")
|
self.setWindowTitle(f"First-{self._os_tag}")
|
||||||
|
_ico = os.path.join(_BASE_DIR, "icon.png")
|
||||||
if os.path.exists(_ico):
|
if os.path.exists(_ico):
|
||||||
self.setWindowIcon(QIcon(_ico))
|
self.setWindowIcon(QIcon(_ico))
|
||||||
self.resize(960, 620)
|
self.resize(960, 620)
|
||||||
@@ -726,14 +727,14 @@ class App(QMainWindow):
|
|||||||
logo_row.setContentsMargins(0, 0, 0, 0)
|
logo_row.setContentsMargins(0, 0, 0, 0)
|
||||||
logo_row.setSpacing(6)
|
logo_row.setSpacing(6)
|
||||||
logo_row.addStretch()
|
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):
|
if os.path.exists(_ico_path):
|
||||||
_ico_lbl = QLabel()
|
_ico_lbl = QLabel()
|
||||||
_pix = QPixmap(_ico_path).scaled(20, 20, Qt.KeepAspectRatio, Qt.SmoothTransformation)
|
_pix = QPixmap(_ico_path).scaled(20, 20, Qt.KeepAspectRatio, Qt.SmoothTransformation)
|
||||||
_ico_lbl.setPixmap(_pix)
|
_ico_lbl.setPixmap(_pix)
|
||||||
_ico_lbl.setFixedSize(20, 20)
|
_ico_lbl.setFixedSize(20, 20)
|
||||||
logo_row.addWidget(_ico_lbl)
|
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")
|
self._sb_logo.setObjectName("sb_logo")
|
||||||
logo_row.addWidget(self._sb_logo)
|
logo_row.addWidget(self._sb_logo)
|
||||||
logo_row.addStretch()
|
logo_row.addStretch()
|
||||||
@@ -818,6 +819,11 @@ class App(QMainWindow):
|
|||||||
QDesktopServices.openUrl(QUrl("https://github.com/Spade-sec/First")),
|
QDesktopServices.openUrl(QUrl("https://github.com/Spade-sec/First")),
|
||||||
self._log_add("info", "[gui] 已打开 GitHub 页面"))
|
self._log_add("info", "[gui] 已打开 GitHub 页面"))
|
||||||
sb_lay.addWidget(sb_gh)
|
sb_lay.addWidget(sb_gh)
|
||||||
|
sb_ver = QLabel("v1.0.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)
|
sb_lay.addSpacing(12)
|
||||||
self._update_theme_label()
|
self._update_theme_label()
|
||||||
|
|
||||||
@@ -1366,15 +1372,8 @@ class App(QMainWindow):
|
|||||||
path_row.addWidget(_make_label("Applet目录", bold=True))
|
path_row.addWidget(_make_label("Applet目录", bold=True))
|
||||||
self._ext_path_ent = _make_entry("wxapkg 包目录路径...")
|
self._ext_path_ent = _make_entry("wxapkg 包目录路径...")
|
||||||
# 自动检测默认路径
|
# 自动检测默认路径
|
||||||
default_pkg = ""
|
from src.wxapkg import get_default_packages_dir
|
||||||
if os.name == "nt":
|
default_pkg = get_default_packages_dir() or ""
|
||||||
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 = ""
|
|
||||||
saved_path = self._cfg.get("extract_packages_dir", "")
|
saved_path = self._cfg.get("extract_packages_dir", "")
|
||||||
if saved_path:
|
if saved_path:
|
||||||
self._ext_path_ent.setText(saved_path)
|
self._ext_path_ent.setText(saved_path)
|
||||||
@@ -1635,23 +1634,22 @@ class App(QMainWindow):
|
|||||||
|
|
||||||
def _ext_auto_detect(self):
|
def _ext_auto_detect(self):
|
||||||
"""自动检测默认路径"""
|
"""自动检测默认路径"""
|
||||||
if os.name == "nt":
|
from src.wxapkg import get_default_packages_dir
|
||||||
user_home = os.path.expanduser("~")
|
default_pkg = get_default_packages_dir()
|
||||||
default_pkg = os.path.join(
|
if default_pkg:
|
||||||
user_home, "AppData", "Roaming", "Tencent",
|
if self._ext_path_ent.text().strip() == default_pkg:
|
||||||
"xwechat", "radium", "Applet", "packages"
|
self._ext_refresh_apps()
|
||||||
)
|
|
||||||
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 目录")
|
|
||||||
else:
|
else:
|
||||||
self._ext_log("未找到默认 Applet 目录,请手动选择")
|
self._ext_path_ent.setText(default_pkg)
|
||||||
|
self._ext_log(f"已自动检测到 Applet 目录: {default_pkg}")
|
||||||
else:
|
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):
|
def _ext_auto_process_new(self, new_ids):
|
||||||
"""对新增的小程序自动执行反编译/扫描(按开关状态)"""
|
"""对新增的小程序自动执行反编译/扫描(按开关状态)"""
|
||||||
@@ -3939,7 +3937,7 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
app = QApplication(sys.argv)
|
app = QApplication(sys.argv)
|
||||||
app.setFont(QFont(_FN, 9))
|
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):
|
if os.path.exists(_ico):
|
||||||
app.setWindowIcon(QIcon(_ico))
|
app.setWindowIcon(QIcon(_ico))
|
||||||
window = App()
|
window = App()
|
||||||
|
|||||||
@@ -2,3 +2,4 @@ frida>=17.0.0
|
|||||||
websockets>=12.0
|
websockets>=12.0
|
||||||
protobuf>=4.0.0
|
protobuf>=4.0.0
|
||||||
PySide6>=6.5.0
|
PySide6>=6.5.0
|
||||||
|
pycryptodome
|
||||||
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.
+86
-43
@@ -4,7 +4,9 @@ DebugEngine - wraps debug_server, proxy_server, frida_server into a reusable cla
|
|||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import random
|
import random
|
||||||
|
import platform
|
||||||
import re
|
import re
|
||||||
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -434,66 +436,95 @@ class DebugEngine:
|
|||||||
|
|
||||||
# ── Frida ──
|
# ── Frida ──
|
||||||
|
|
||||||
async def _start_frida(self):
|
# ── Frida: platform helpers ──
|
||||||
logger = self.logger
|
|
||||||
|
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()
|
device = frida.get_local_device()
|
||||||
processes = device.enumerate_processes(scope="metadata")
|
processes = device.enumerate_processes(scope="metadata")
|
||||||
wmpf_processes = [p for p in processes if p.name == "WeChatAppEx.exe"]
|
wmpf_procs = [p for p in processes if p.name == "WeChatAppEx.exe"]
|
||||||
if not wmpf_processes:
|
if not wmpf_procs:
|
||||||
raise RuntimeError("[frida] WeChatAppEx.exe process not found")
|
raise RuntimeError("[frida] WeChatAppEx.exe process not found")
|
||||||
|
|
||||||
wmpf_ppids = []
|
ppids = [p.parameters.get("ppid", 0) or 0 for p in wmpf_procs]
|
||||||
for p in wmpf_processes:
|
main_pid = Counter(ppids).most_common(1)[0][0]
|
||||||
ppid = p.parameters.get("ppid", 0)
|
if main_pid == 0:
|
||||||
wmpf_ppids.append(ppid if ppid else 0)
|
raise RuntimeError("[frida] WeChatAppEx.exe main process not found")
|
||||||
|
|
||||||
if not wmpf_ppids:
|
main_proc = next((p for p in processes if p.pid == main_pid), None)
|
||||||
raise RuntimeError("[frida] WeChatAppEx.exe process not found")
|
if main_proc is None:
|
||||||
|
raise RuntimeError("[frida] could not locate main WMPF process")
|
||||||
|
|
||||||
pid_counts = Counter(wmpf_ppids)
|
proc_path = main_proc.parameters.get("path", "")
|
||||||
wmpf_pid = pid_counts.most_common(1)[0][0]
|
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:
|
return [main_pid], version
|
||||||
raise RuntimeError("[frida] WeChatAppEx.exe process not found")
|
|
||||||
|
|
||||||
wmpf_process = None
|
def _find_wmpf_pids_darwin(self):
|
||||||
for p in processes:
|
"""macOS: find WeChatAppEx PIDs via pgrep, version via Info.plist."""
|
||||||
if p.pid == wmpf_pid:
|
try:
|
||||||
wmpf_process = p
|
out = subprocess.check_output(
|
||||||
break
|
["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:
|
try:
|
||||||
raise RuntimeError("[frida] Could not find main WMPF process")
|
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", "")
|
return pids, version
|
||||||
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)
|
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):
|
# Find PIDs and version
|
||||||
# PyInstaller onefile: resources in _MEIPASS; onedir: next to exe
|
if is_mac:
|
||||||
project_root = Path(getattr(sys, '_MEIPASS', Path(sys.executable).parent)).resolve()
|
pids, wmpf_version = self._find_wmpf_pids_darwin()
|
||||||
else:
|
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"
|
hook_path = project_root / "frida" / "hook.js"
|
||||||
if not hook_path.exists():
|
if not hook_path.exists():
|
||||||
raise RuntimeError("[frida] hook script not found")
|
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():
|
if not config_path.exists():
|
||||||
raise RuntimeError(f"[frida] version config not found: {wmpf_version}")
|
raise RuntimeError(f"[frida] version config not found: {platform_dir}/{wmpf_version}")
|
||||||
config_content = config_path.read_text(encoding="utf-8")
|
|
||||||
config_content = json.dumps(json.loads(config_content))
|
|
||||||
|
|
||||||
|
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)
|
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):
|
def on_message(message, data):
|
||||||
if message.get("type") == "error":
|
if message.get("type") == "error":
|
||||||
@@ -501,10 +532,22 @@ class DebugEngine:
|
|||||||
return
|
return
|
||||||
logger.frida_debug("[frida client]", message.get("payload", ""))
|
logger.frida_debug("[frida client]", message.get("payload", ""))
|
||||||
|
|
||||||
script.on("message", on_message)
|
for pid in pids:
|
||||||
script.load()
|
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")
|
logger.info("[frida] you can now open any miniapps")
|
||||||
|
|
||||||
return session, script
|
return session, script
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
+28
-9
@@ -210,14 +210,33 @@ def find_wxapkg_files(packages_dir: str) -> List[dict]:
|
|||||||
|
|
||||||
|
|
||||||
def get_default_packages_dir() -> Optional[str]:
|
def get_default_packages_dir() -> Optional[str]:
|
||||||
"""获取默认的微信小程序包路径(Windows)"""
|
"""获取默认的微信小程序包路径(Windows / macOS)"""
|
||||||
if os.name != "nt":
|
import sys
|
||||||
return None
|
|
||||||
user_home = os.path.expanduser("~")
|
user_home = os.path.expanduser("~")
|
||||||
pkg_dir = os.path.join(
|
if sys.platform == "darwin":
|
||||||
user_home, "AppData", "Roaming", "Tencent",
|
# macOS: ~/Library/Containers/com.tencent.xinWeChat/Data/Documents/app_data/radium/users/{wxid}/applet/packages
|
||||||
"xwechat", "radium", "Applet", "packages"
|
users_dir = os.path.join(
|
||||||
)
|
user_home, "Library", "Containers", "com.tencent.xinWeChat",
|
||||||
if os.path.isdir(pkg_dir):
|
"Data", "Documents", "app_data", "radium", "users"
|
||||||
return pkg_dir
|
)
|
||||||
|
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
|
return None
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in New Issue
Block a user