Wechat 开启 H5 DevTools

环境:Android 微信 8.0.69
目标:让 chrome://inspect 能看到并调试微信内打开的 H5 网页 (真实 DOM / Console / Network / 断点), 不包括小程序

背景

微信有两套浏览器内核

微信打开网页不是用一个固定的浏览器,而是在 XWeb 框架下可选多个内核。本文只关心两个:

Pinus 内核(默认)

Pinus 是腾讯基于 Chromium 自研裁剪的浏览器内核(源码路径里叫 weblayer,基于 Chrome 116),
libxwebcore.so(135MB)的形式打包在微信里,类名 com.tencent.xweb.pinus.sdk.WebView,
内核类型枚举值 WV_KIND_PINUS

它的调试不走标准通道:Pinus 不开标准的 webview_devtools_remote 调试端口,而是走微信
自己的私有远程调试(往 xweb.weixin.qq.com 拨 WebSocket 隧道),chrome 连不上

系统 WebView 内核

就是 Android 系统自带的 WebView(本机是 libmonochrome_64.so,171MB,Chrome 130),
内核类型 WV_KIND_SYS。它是标准 Chromium,开启调试后会 bind 标准的
webview_devtools_remote_<pid> 端口,chrome://inspect 直接就能连

开启 webview 调试为什么无法连接上

调用 android.webkit.WebView.setWebContentsDebuggingEnabled(true) 想开调试但因为渲染页面的是 Pinus,开调试端口的却是系统 WebView。所以 chrome://inspect 里能看到”远程浏览器”,却没有可点的 page。

1
2
3
4
5
com.tencent.mm (UI 进程)
├── Pinus 内核 (libxwebcore, Chrome116)
│ 渲染 H5 页面 ✓ 不开标准调试端口 ✗ -> 走私有隧道 → xweb.weixin.qq.com
└── 系统 WebView 内核 (libmonochrome, Chrome130)
开调试端口 ✓ 默认不渲染页面 ✗ -> /json 恒为 [],无法 inspect

一、系统 WebView 内核方案

1. 做法总览

强制微信用系统 WebView 渲染 H5 + 开启系统 WebView 调试。 setWebContentsDebuggingEnabled(true) 只对 系统 WebView 那套生效

2. 选择内核

1
2
3
4
5
6
WebView.I0(ctx, kind, module, cb)       initWebviewCore — 真正的初始化入口
├─ WebView.H0(kind, module) getPreferedWebviewType — 解析用哪个内核
│ └─ t0.a.c(module) t0.a = new u0() (u0 的静态单例)
│ └─ u0.c(module) 读 HardCodeWebView<module> → ABTestWebView<module>
│ └─ z3.f() 打开 SharedPreferences "xweb_debug"
└─ WebView.J0(ctx, H0 返回值, …) 成功则 WebView.m = kind, 进程内只决策一次

2.1 枚举 f1

1
2
3
4
5
6
7
8
// com.tencent.xweb.f1  (X5 字段已移除, 仅留在 values 数组)
public enum f1 {
WV_KIND_NONE, // ordinal 0 → 字段 d
WV_KIND_CW, // ordinal 1 → 字段 e
WV_KIND_X5, // ordinal 2 (无字段, 已移除)
WV_KIND_SYS, // ordinal 3 → 字段 f
WV_KIND_PINUS // ordinal 4 → 字段 g
}

2.2 WebView.H0 — 决策入口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// com.tencent.xweb.WebView.H0(f1 kind, String module) 原名 getPreferedWebviewType
public static f1 H0(f1 f10, String s) {
f1 f11 = f1.d; // d = NONE
if (WebView.m != f11) return WebView.m; // ★ 已决策过 → 直接用缓存 (进程内只一次)
if (br5.a.c()) { br5.s0.d(69L, 1); return f1.f; } // x86 → f = SYS

f1 f13 = t0.a.c(s); // 读 HardCodeWebView<module> (见 2.3)
if (f13 == f11) { // 没配 HardCode
String s1 = b.m().g("setwebtype", s); // 退而读服务器 cmd 配置
if (s1 != null && !s1.isEmpty()) f10 = f1.valueOf(s1);
} else {
f10 = f13; // 用 HardCode (优先级最高, 跳过 cmd 配置)
}

f1 f12 = f1.f; // 默认 = SYS
if (f10 != f1.e && f10 != f1.g) { // 非 CW(e) 且 非 PINUS(g)
f12 = f10; // 采用 f10
} else {
WebView.p = true; // 标记 xweb 不可用, 兜底 SYS
}
return f12; // 交给 I0→J0 写进 WebView.m
}

代码里直接读出三点:

  • 只决策一次:WebView.m != NONE 就直接返回。WebView.mI0(initWebviewCore)→ J0 在第一个 WebView 创建时写一次(WebView.m = f10),之后全进程复用 —— 运行时改 WebView.m 或刷页面没用,旧 WebView 早用 Pinus 建好了。
  • HardCode 优先级最高:t0.a.c(module) 返回非 NONE 就直接采用,根本不读服务器 setwebtype
  • CW / PINUS 兜底成 SYS:落 else 分支 WebView.p=true。我们写 WV_KIND_SYS 不触发兜底,稳走 SYS。

2.3 u0.c — 读 HardCodeWebView

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// com.tencent.xweb.u0.c(String module)
public f1 c(String s) {
f1 f10 = f1.d; // NONE
if (s == null || s.isEmpty() || this.a == null) return f10;
SharedPreferences sp = z3.f(); // 打开 "xweb_debug" (见 2.4)
if (sp == null) return f10;
String s1 = sp.getString("HardCodeWebView" + s, ""); // ★ 先 HardCode
if (s1 == null || s1.isEmpty() || s1.equals("WV_KIND_NONE")) {
s1 = sp.getString("ABTestWebView" + s, ""); // 再 ABTest 兜底
}
if (s1 == null || s1.isEmpty()) return f10;
try { this.b = f1.valueOf(s1); } catch (Throwable t) { this.b = f10; }
return this.b;
}

2.4 z3.f — 打开 xweb_debug

1
2
3
4
5
6
7
8
// br5.z3
public static SharedPreferences f() { return z3.h("xweb_debug"); }

public static SharedPreferences h(String s) { // z3.h = 经典 SP (非 MMKV)
Context context0 = z3.b;
if (context0 == null) return null;
return context0.getSharedPreferences(s, 4); // MODE_MULTI_PROCESS
}

串起来:往 xweb_debugHardCodeWebViewmm = WV_KIND_SYSu0.c("mm") 返回 SYS → H0 采用、返回 SYS → I0/J0 写进 WebView.m → 系统内核渲染、标准调试端口有页面。

3. 落地代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function hookDevTools() {
// 选择内核 H0 恒返回 SYS
const W = Java.use('com.tencent.xweb.WebView')
const F: any = Java.use('com.tencent.xweb.f1')
const SYS = F.valueOf('WV_KIND_SYS')
W.H0.implementation = function (kind: any, m: any) {
W.m.value = SYS
log('call com.tencent.xweb.WebView.H0', kind, m, '->', SYS)
return SYS
}
// 启用调试
Java.perform(() => {
Java.scheduleOnMainThread(() => {
Java.use('android.webkit.WebView').setWebContentsDebuggingEnabled(true);
utils.log('call WebView.setWebContentsDebuggingEnabled(true)')
})
})
}

4. 注入哪个进程、具体怎么做

核心前提:hookDevTools 只要保证 WebView.H0首次点开 H5 网页之前被 hook 住即可。

注入进程:UI 进程 com.tencent.mm(无后缀)。它是最终渲染 H5、H0 选择内核的进程。

完整流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 1. 完全关闭微信
adb shell am force-stop com.tencent.mm

# 2. 手动打开微信 App 先别点任何网页

# 3. frida attach UI 进程, 加载脚本

# 4. 在微信里点开目标 H5 网页
# → 首次点击触发 H0 → 被 hook 强制成系统 WebView 内核渲染
# → setWebContentsDebuggingEnabled(true) 已开, 页面注册进标准调试端口

# 5. 电脑 chrome 打开 chrome://inspect
# → Devices 里直接出现该页面 → 点 inspect (真 DOM/Console/Network/断点)

二、Pinus 内核自己的私有调试(存档)

「不切系统内核、直接在 Pinus 上调试」的可行性分析
结论:技术可行,但要自己搭中转 + 破触发锁,投入产出比低
基于 libxwebcore.so(Chrome116)逆向

6. Pinus 调试是「往外拨」,不是「等你连」

系统 WebView 是开个本地端口等 chrome 来连(server 模型)。Pinus 相反 —— 它是客户端,
主动拨号连微信服务器,把 CDP 流量通过 WebSocket 隧道转出去:

1
2
3
4
5
6
7
8
9
10
11
12
13
页面 JS 调 window.xweb_remote_debug.RemoteDebugStart(token)
│ (该 JS 对象由 render frame observer 注入)

① 注册设备 POST https://xweb.weixin.qq.com/api/rd/create_device

② 设备隧道 连 wss://xweb.weixin.qq.com/rd/device/<deviceId>?role=user
▼ (等服务器下发 JSON 命令)
③ 服务器下发 {"method":"XWeb.inspect","params":{"targetId":..,"deviceToken":..}}

④ Pinus 用 targetId 找到真实 DevToolsAgentHost (和系统内核同一套 host 查找)

⑤ 目标隧道 连 wss://xweb.weixin.qq.com/rd/target/<targetId>?role=user&token=<token>
这条隧道里跑的就是【真实原生 CDP】

设备隧道命令(XWebDevTools_OnTunnelData):XWeb.targets / XWeb.version /
XWeb.inspect / XWeb.close / XWeb.NetLog*CDP 会话本身是完整原生的,只是被套进
隧道发去了微信服务器。

7. 帧格式与地址(静态读出)

帧格式:XWebWebSocketAdapter::Write(0x270A3D4)拿一段裸字节直接塞进 WebSocket,
无自定义封包,就是「JSON 文本 over WebSocket」:

  • 设备隧道:收 {"method":"XWeb.inspect","params":{...}},发 {"event":"...","data":{...}}
  • 目标隧道:收发就是裸 CDP JSON({"id":1,"method":"Runtime.evaluate",...} 及其 response),
    进出点 sub_270CF24(server→CDP)/ sub_3627EEC(CDP→server)

三个地址全写死 host = xweb.weixin.qq.com,只有 id/token 是变量:
api/rd/create_devicerd/device/%srd/target/%s?token=%s

8. 触发被三重锁死(私有化的真正门槛)

想让 Pinus 自己发起,入口是页面 JS window.xweb_remote_debug.RemoteDebugStart(token),但:

  1. JS 注册域名锁:RemoteDebugStart 只在页面域名 = xweb.weixin.qq.com 时才挂到
    window.xweb_remote_debug(sub_2715ABC)。普通 H5 页面只有 RemoteDebugStatus/Stop
  2. 二次域名校验:HandleRemoteDebugStart(0x2716500)里又查一遍,非该域名返回 invalid domain
  3. 需要 token + renderer 线程:token 来自微信调试门户,create_device 也要联微信服务器。

9. 私有化路线(若非做不可)

  1. spawn 时(页面加载前)hook std::string == "xweb.weixin.qq.com"(0x26D8860,窄化到只对
    该域名返回真)绕过两处域名锁。
  2. hook XWeb_RD_CreateDevice(0x2711A18)、XWebTarget_StartClientProxy(0x270D194),把
    xweb.weixin.qq.com 改写成 ws://localhost:<port>
  3. 本地中转扮演三个角色:create_device(返 deviceId)+ 设备隧道(下发
    XWeb.inspect{targetId, deviceToken},targetId 从 XWeb.targets 返回里拿)+ 目标隧道
    (裸 CDP 双向桥给 chrome 前端)。
  4. 注入帧要打进真正渲染内容的 render frame,不是 appbrand 的 about:blank 容器帧。

实测尝试运行时抓帧,卡在:触发锁 + 多进程漂移(H5 落 :appbrand0)+ 注入到空容器帧。
比板块一重得多,除非必须用 Pinus 内核渲染又要调试,否则不建议


附:关键符号

符号 作用
com.tencent.xweb.f1 内核类型枚举 (WV_KIND_*)
com.tencent.xweb.WebView.H0(f1, String) 内核决策入口 (getPreferedWebviewType)
com.tencent.xweb.WebView.m 缓存已决策内核的静态字段
com.tencent.xweb.u0.c(String) 从 xweb_debug SP 读 HardCodeWebView
br5.z3.f() 打开 SharedPreferences “xweb_debug”
com.tencent.xweb.pinus.sdk.WebView Pinus 内核 WebView

Native 模块:

模块 说明
libxwebcore.so Pinus 内核 (Chrome 116),渲染页面
libmonochrome_64.so 系统 WebView (Chrome 130),标准调试端口
socket webview_devtools_remote_<pid> 系统 WebView 的调试端口

libxwebcore.so Pinus 私有调试函数(libxwebcore.so.i64):

RVA 名字 作用
0x2715658 XWebFrameHelper_CreateXWebExtend 注入 window.xweb_remote_debug
0x2715ABC (AddRemoteDebugMethods) 注册 JS 方法;RemoteDebugStart 有域名锁
0x2716500 (HandleRemoteDebugStart) Start 处理,二次域名校验 + 取 token
0x2711A18 XWeb_RD_CreateDevice 注册设备,硬编码 create_device URL
0x2711F64 (device tunnel WS) 开设备隧道 rd/device
0x270EB0C XWebDevTools_OnTunnelData 设备隧道命令分发
0x270D194 XWebTarget_StartClientProxy 开目标隧道,硬编码 rd/target URL
0x270CF24 (target onMessage) 目标隧道 server→CDP 入口
0x3627EEC (session dispatch) CDP→server 出口
0x270A3D4 XWebWebSocketAdapter::Write 裸字节写 WS(无自定义封包)
0x26D8860 std::string::operator==(char*) 域名锁字符串比较(绕过点)
0x3615C5C XWeb_GetTabHostById 按 targetId 查真实 DevToolsAgentHost

提取 libxwebcore.so:解 split_delivery.config.arm64_v8a.apk → 取 libxwebfullpack.so
(是个 zip)→ 再解出 libxwebcore.so
提取 libmonochrome_64.so:从 com.google.android.trichromelibrary_*base.apk
lib/arm64-v8a/libmonochrome_64.so

作者:YuHuanTin
链接:https://yuhuantin.icu/2026/07/03/Wechat-开启-H5-DevTools/
来源:YuHuanTin's Blog
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 许可协议。著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。