Wechat 开启 H5 DevTools
27k字 •
53分钟
环境: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 | |
一、系统 WebView 内核方案
1. 做法总览
强制微信用系统 WebView 渲染 H5 + 开启系统 WebView 调试。 setWebContentsDebuggingEnabled(true) 只对 系统 WebView 那套生效
2. 选择内核
1 | |
2.1 枚举 f1
1 | |
2.2 WebView.H0 — 决策入口
1 | |
代码里直接读出三点:
- 只决策一次:
WebView.m != NONE就直接返回。WebView.m由I0(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.4 z3.f — 打开 xweb_debug
1 | |
串起来:往 xweb_debug 写 HardCodeWebViewmm = WV_KIND_SYS → u0.c("mm") 返回 SYS → H0 采用、返回 SYS → I0/J0 写进 WebView.m → 系统内核渲染、标准调试端口有页面。
3. 落地代码
1 | |
4. 注入哪个进程、具体怎么做
核心前提:hookDevTools 只要保证 WebView.H0 在首次点开 H5 网页之前被 hook 住即可。
注入进程:UI 进程 com.tencent.mm(无后缀)。它是最终渲染 H5、H0 选择内核的进程。
完整流程:
1 | |
二、Pinus 内核自己的私有调试(存档)
「不切系统内核、直接在 Pinus 上调试」的可行性分析
结论:技术可行,但要自己搭中转 + 破触发锁,投入产出比低
基于libxwebcore.so(Chrome116)逆向
6. Pinus 调试是「往外拨」,不是「等你连」
系统 WebView 是开个本地端口等 chrome 来连(server 模型)。Pinus 相反 —— 它是客户端,
主动拨号连微信服务器,把 CDP 流量通过 WebSocket 隧道转出去:
1 | |
设备隧道命令(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_device、rd/device/%s、rd/target/%s?token=%s。
8. 触发被三重锁死(私有化的真正门槛)
想让 Pinus 自己发起,入口是页面 JS window.xweb_remote_debug.RemoteDebugStart(token),但:
- JS 注册域名锁:
RemoteDebugStart只在页面域名 =xweb.weixin.qq.com时才挂到window.xweb_remote_debug(sub_2715ABC)。普通 H5 页面只有RemoteDebugStatus/Stop。 - 二次域名校验:
HandleRemoteDebugStart(0x2716500)里又查一遍,非该域名返回invalid domain。 - 需要 token + renderer 线程:token 来自微信调试门户,
create_device也要联微信服务器。
9. 私有化路线(若非做不可)
- spawn 时(页面加载前)hook
std::string == "xweb.weixin.qq.com"(0x26D8860,窄化到只对
该域名返回真)绕过两处域名锁。 - hook
XWeb_RD_CreateDevice(0x2711A18)、XWebTarget_StartClientProxy(0x270D194),把xweb.weixin.qq.com改写成ws://localhost:<port>。 - 本地中转扮演三个角色:
create_device(返 deviceId)+ 设备隧道(下发XWeb.inspect{targetId, deviceToken},targetId 从XWeb.targets返回里拿)+ 目标隧道
(裸 CDP 双向桥给 chrome 前端)。 - 注入帧要打进真正渲染内容的 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。