Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 26 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,19 @@

[English](README.md) | [中文](README.zh.md)

Yet another WeChat miniapp debugger on Windows (WMPF).
Yet another WeChat miniapp debugger on Windows / macOS (WMPF).

This debugger (tweak) exploits Remote Debug feature provided by wechatdevtools and patches serval restrictions to force miniapp runtime to support full Chrome Debug Protocol, and thus can be directly applied to standard devtools shipped with chromium-based browsers.


## Support Status


Version histories:
Version histories (macOS):

* 17078 / WeChat 3.8.7 (macOS arm64, credit @dm)

Version histories (Windows):

* 19459 (latest, credit @snowflake-x)
* 19339 (credit @hidacow)
Expand Down Expand Up @@ -53,7 +57,7 @@ Version histories:

To debug web pages of WeChat embedded browser, please refer to [EXTENSION.md](EXTENSION.md). Note that this feature has many limitations currently and is simply a basic workaround.

To check your installed version, navigate to Task Manager -> WeChatAppEx -> Right click -> Open file location -> Check the number between `RadiumWMPF` and `extracted`.
To check your installed version, navigate to Task Manager -> WeChatAppEx -> Right click -> Open file location -> Check the number between `RadiumWMPF` and `extracted`. On macOS, run `grep CFBundleVersion "/Applications/WeChat.app/Contents/MacOS/WeChatAppEx.app/Contents/Info.plist"` and use the last number in the version string.

To adapt to another version, please find the instructions in [ADAPTATION.md](ADAPTATION.md). Alternatively, you can submit an issue for new version adaption and I will try that if I have the binary. Note that only newer version adaption requests will be considered.

Expand All @@ -62,6 +66,25 @@ To upgrade to the latest WMPF (WeChat version > 4.x), download the latest WeChat

To upgrade to the latest WMPF (WeChat version < 4.x), type in `:showcmdwnd` in the search bar (do not hit enter), then the command window should pop up. Type in `/plugin set_grayvalue=202&check_update_force` and hit enter, the latest WMPF plugin should be downloaded, if any updates are available. Restart the WeChat to apply plugin upgrade.


## macOS arm64 Support

macOS arm64 (Apple Silicon) is now supported. The implementation is based on the same WMPF internals as Windows, with platform-specific adaptations:

- Module name: `WeChatAppEx Framework` (instead of `flue.dll` on Windows)
- Register conventions: `x0` / `x1` (instead of `rcx` / `rdx` on Windows x64)
- CDPFilter: patched at `retval+8` (instead of `*args[0]+8` on Windows)
- Config file: `mac.addresses.{version}.json` (e.g. `mac.addresses.17078.json`)

**Check WMPF version on macOS:**

```bash
grep CFBundleVersion "/Applications/WeChat.app/Contents/MacOS/WeChatAppEx.app/Contents/Info.plist"
# Look for the last number in the version string, e.g. 6.17078 → 17078
```

For new version adaptation on macOS, you can use the Frida auto-detection script at `frida/detect_offsets.js` to find hook offsets automatically.

## Prerequisites

* node.js (requires at least LTS v22)
Expand Down
29 changes: 26 additions & 3 deletions README.zh.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
# WMPFDebugger

又一个 Windows 微信小程序调试工具
又一个 Windows / macOS 微信小程序调试工具

这个工具通过 patch 一些 Chrome 调试协议(CDP)的过滤器和其他的条件判断来强制小程序连接到外部调试器(也就是远程调试,LanDebug 模式)。这个调试协议是基于 protobuf 实现的私有协议,通过逆向开发者工具提取相应的协议实现,该工具实现了一个简单的小程序调试协议转换为标准 Chrome 调试协议,从而允许我们使用标准基于 chromium 浏览器的内嵌开发者工具来调试任意小程序


## 支持状态

支持的 WMPF 版本:
支持的 WMPF 版本 (macOS):

* 17078 / 微信 3.8.7 (macOS arm64, credit @dm)

支持的 WMPF 版本 (Windows):

* 19459 (最新, credit @snowflake-x)
* 19339 (credit @hidacow)
Expand Down Expand Up @@ -50,7 +54,7 @@

如何调试微信内置浏览器页面:参见 [EXTENSION.md](EXTENSION.md)。注意,目前该方法仅有基础调试功能

如何检查版本:打开任务管理器,找到 WeChatAppEx 进程,右键,打开文件所在的位置,检查在 `RadiumWMPF` 和 `extracted` 之间的数字
如何检查版本:打开任务管理器,找到 WeChatAppEx 进程,右键,打开文件所在的位置,检查在 `RadiumWMPF` 和 `extracted` 之间的数字。macOS 上执行 `grep CFBundleVersion "/Applications/WeChat.app/Contents/MacOS/WeChatAppEx.app/Contents/Info.plist"`,取版本字符串中最后一个数字

如何适配到其他版本:参见 [ADAPTATION.md](ADAPTATION.md)。另外,你也可以提交版本适配的 Issue,我会尝试适配该版本如果我有相应的版本的 binary。仅更新版本的适配请求会被考虑

Expand All @@ -59,6 +63,25 @@
如何更新到最新的 WMPF 版本(微信版本 < 4.x):搜索框输入 `:showcmdwnd`(不要按回车触发搜索)弹出命令窗口,输入 `/plugin set_grayvalue=202&check_update_force` 并回车等待更新(如果有新版本)。重启微信以生效。


## macOS arm64 支持

现已支持 macOS arm64 (Apple Silicon)。实现基于与 Windows 相同的 WMPF 内部机制,针对平台差异做了适配:

- 模块名:`WeChatAppEx Framework`(对应 Windows 的 `flue.dll`)
- 寄存器约定:`x0` / `x1`(对应 Windows x64 的 `rcx` / `rdx`)
- CDPFilter:修补 `retval+8`(对应 Windows 的 `*args[0]+8`)
- 配置文件:`mac.addresses.{version}.json`(如 `mac.addresses.17078.json`)

**如何在 macOS 上检查 WMPF 版本:**

```bash
grep CFBundleVersion "/Applications/WeChat.app/Contents/MacOS/WeChatAppEx.app/Contents/Info.plist"
# 查看版本字符串中的最后一个数字,如 6.17078 → 17078
```

适配新版本:可使用 `frida/detect_offsets.js` 自动探测 hook 偏移量。


## 准备

* node.js (需要至少 LTS v22)
Expand Down
7 changes: 7 additions & 0 deletions frida/config/mac.addresses.17078.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"Version": 17078,
"Platform": "mac",
"LoadStartHookOffset": "0x4F0620C",
"CDPFilterHookOffset": "0x81BFC04",
"SceneOffsets": [1432, 1376, 488]
}
85 changes: 60 additions & 25 deletions frida/hook.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
const getMainModule = (version) => {
const getMainModule = (version, platform) => {
if (platform === "mac") {
return Process.findModuleByName("WeChatAppEx Framework");
}
if (version >= 13331) {
return Process.findModuleByName("flue.dll");
}
Expand All @@ -7,28 +10,45 @@ const getMainModule = (version) => {

const patchCDPFilter = (base, config) => {
// xref: SendToClientFilter OR devtools_message_filter_applet_webview.cc
const isMac = config.Platform === "mac";
const offset = config.CDPFilterHookOffset;
Interceptor.attach(base.add(offset), {
onEnter(args) {
send(
`[patch] CDP filter on enter, original value of input: ${args[0].readPointer()}`,
);
this.inputValue = args[0];
if (!isMac) {
// x64 windows: save args[0] for use in onLeave
send(
`[patch] CDP filter on enter, original value of input: ${args[0].readPointer()}`,
);
this.inputValue = args[0];
}
},
onLeave(retval) {
const inputValue = this.inputValue.readPointer();
if (inputValue.isNull() || inputValue.add(8).isNull()) {
// there's a chance the value could be null
// return here to avoid crash
return;
}

send(
`[patch] CDP filter on leave, patch input, now value: ${inputValue}; ` +
`*(input + 8) = ${inputValue.add(8).readU32()}`,
);
if (inputValue.add(8).readU32() == 6) {
inputValue.add(8).writeU32(0x0);
if (isMac) {
// arm64 mac: caller checks retval+8 == 6, patch it to 0
if (retval.isNull()) return;
try {
const val = retval.add(8).readU32();
send(`[patch] CDP filter on leave (mac), retval+8 = ${val}`);
if (val === 6) {
retval.add(8).writeU32(0x0);
send("[patch] CDP filter patched (mac)");
}
} catch (e) {
send(`[patch] CDP filter error: ${e}`);
}
} else {
// x64 windows: patch *args[0]+8
const inputValue = this.inputValue.readPointer();
if (inputValue.isNull() || inputValue.add(8).isNull()) {
return;
}
send(
`[patch] CDP filter on leave, patch input, now value: ${inputValue}; ` +
`*(input + 8) = ${inputValue.add(8).readU32()}`,
);
if (inputValue.add(8).readU32() == 6) {
inputValue.add(8).writeU32(0x0);
}
}
},
});
Expand Down Expand Up @@ -81,18 +101,29 @@ const hookOnLoadScene = (a1, sceneOffsets) => {

const patchOnLoadStart = (base, config) => {
// xref: AppletIndexContainer::OnLoadStart
const isMac = config.Platform === "mac";
Interceptor.attach(base.add(config.LoadStartHookOffset), {
onEnter(args) {
// arm64 (mac): x0=this, x1=debug_flag
// x64 (windows): rcx=this, rdx=debug_flag
const thisPtr = isMac ? this.context.x0 : this.context.rcx;
send(
`[inteceptor] AppletIndexContainer::OnLoadStart onEnter, ` +
`indexContainer.this: ${this.context.rcx}`,
`indexContainer.this: ${thisPtr}`,
);
// write dl to 0x1
if ((this.context.rdx & 0xff) !== 1) {
this.context.rdx = (this.context.rdx & ~0xff) | 0x1;
if (isMac) {
// arm64: set x1 (debug flag) to 1
if ((this.context.x1.toInt32() & 0xff) !== 1) {
this.context.x1 = ptr(1);
}
} else {
// x64: write dl to 0x1
if ((this.context.rdx & 0xff) !== 1) {
this.context.rdx = (this.context.rdx & ~0xff) | 0x1;
}
}
// handle onLoad scene
hookOnLoadScene(this.context.rcx, config.SceneOffsets);
hookOnLoadScene(thisPtr, config.SceneOffsets);
},
onLeave(retval) {
// do nothing
Expand All @@ -116,9 +147,13 @@ const parseConfig = () => {

const main = () => {
const config = parseConfig();
const mainModule = getMainModule(config.Version);
const mainModule = getMainModule(config.Version, config.Platform);
patchOnLoadStart(mainModule.base, config);
patchCDPFilter(mainModule.base, config);
if (!config.DisableCDPFilter) {
patchCDPFilter(mainModule.base, config);
} else {
send("[info] CDPFilter hook disabled");
}
};

main();
Loading