Commit b36fc0454aca51a046a38211a68f71778186a721
1 parent
645c4d30
research(wechat): 贴图号发布改用 LSPosed 排查提交调用,两个候选均证伪
改用文字/图片/语音发送同一套 LSPosed 机制(Zygote fork 时挂钩,非 Frida spawn/attach),彻底避开了此前两次搞坏微信主进程的问题——全程真机验证零 故障。 追踪链路 js1.m.c()(brandservice/webview transfer 汇聚点) → wire cgi bizh5webtransfer,真机确认全程只有只读态 mp.weixin.qq.com 调用,没有 提交语义。进一步反编译 LuggageUploadMediaFileManager 追出图片走独立 CDN 服务 m11.l0.d1(),同样零触发。两个最有希望的候选均被真机数据证伪,真正 的提交调用点仍未定位,可能是完全独立的第三条路径。 三个新增 hook(hookBrandservicePublishTransfer/hookAnyCgiCapture/ hookBrandserviceCdnUpload)均已装进 WeChatHook.java,base+Tinker CL 全覆盖,可复用于下一轮排查。
Showing
2 changed files
with
268 additions
and
0 deletions
wechat_send_internal/BRANDSERVICE_PUBLISH_RESEARCH.md
| @@ -239,6 +239,66 @@ API 用)。 | @@ -239,6 +239,66 @@ API 用)。 | ||
| 239 | 239 | ||
| 240 | --- | 240 | --- |
| 241 | 241 | ||
| 242 | +## 四、路线修正:改用 LSPosed(跟文字/图片/语音发送同一套机制),彻底解决主进程注入问题 | ||
| 243 | + | ||
| 244 | +用户指出关键盲点:文字/图片/语音发送逆向全程用的是 **LSPosed**(`xhs_lsposed_a16_fix/wechathook_src/WeChatHook.java`,Zygote fork 时随进程诞生自动挂钩子),从没出过问题;这次改用 Frida spawn/attach 才两次真实搞坏微信。根因很清楚:**LSPosed 挂钩发生在系统自己的进程创建路径里(Zygote fork),不是外部 ptrace 注入一个已经在跑/刚起来的进程**,没有"抢在类加载膨胀前注入"这种时序竞争,自然不会撞上 Frida 那两次事故的问题。 | ||
| 245 | + | ||
| 246 | +### 4.1 改造 WeChatHook.java,加 `hookBrandservicePublishTransfer` | ||
| 247 | + | ||
| 248 | +在 `handleLoadPackage`(base classloader)和 App.onCreate 的 Tinker 重挂钩块(`sTinkerCL`)**两处都注册**——微信这版本 `js1.m` 类实际加载在 Tinker 补丁里,只挂 base CL 那份**装得上但永远不触发**(跟以前语音/图片踩过的"base CL vs Tinker CL"坑一模一样,见 [[wechat-voice-send-internals]])。两处都挂上后,`js1.m.c(TransferRequestInfo, callback)` 在 base + Tinker CL、主进程 + push + appbrand0/1 全部确认 hook 成功(`hooked js1.m.c x1`),**微信全程运行正常,没有任何"无法连接服务器"/"点不动"问题**——证实了用户的判断:LSPosed 这条路径本身没问题,问题出在 Frida spawn/attach 这个注入方式上。 | ||
| 249 | + | ||
| 250 | +字段映射(现场验证): | ||
| 251 | +``` | ||
| 252 | +TransferRequestInfo { d=type(int), e=h5Url, f=scope, h=y24(Lr45/y24;), n=needVerifySignature, i=is_security_check } | ||
| 253 | +y24 { d=reqUrl, e=reqBody(JSON串), f=cgiCmdId, h=method, g=LinkedList<cw3> headers } | ||
| 254 | +cw3 { d=key, e=value } | ||
| 255 | +``` | ||
| 256 | + | ||
| 257 | +### 4.2 真机捕获:js1.m.c() 全程只看到 webview 支持性调用,没看到"提交发布" | ||
| 258 | + | ||
| 259 | +真人走一遍"我的作品"发布流程(POI 选点→编辑→发布→查看结果),`js1.m.c()` 命中 ~58 次,`reqUrl` 全部是明文可读的 `mp.weixin.qq.com/mp/*`:`photo_account_poi_feed`(选点)、`creationcenter`(创作中心)、`appmsg_comment`/`appmsgthank`/`getappmsgext`/`getappmsgad`/`getbizbanner`/`searchwordbaike`/`wapcommreport`(这些全是**发布完之后自动打开文章预览页产生的阅读态/统计上报调用**,含 `publish_time=` 字段证实文章确已发布成功)。**全程没有一次 `reqBody != null` 或看起来像"提交内容"的调用**——`scope=mmbizmp_web` 型调用(reqBody 恒为 null)看起来是把 `reqUrl` 当成普通网页 GET 打,跟真正提交表单/上传图片的调用形态不同。 | ||
| 260 | + | ||
| 261 | +### 4.3 排查了两个疑似候选,都证伪 | ||
| 262 | + | ||
| 263 | +加了 `hookAnyCgiCapture`(复用 `hookVoiceCgiCapture` 的通用 cgi 捕获逻辑,去掉 `uploadvoice` 过滤,改成不设限一律记录 `com.tencent.mm.modelbase.l.a()` 的每次调用)验证还有没有其它内部二进制 cgi 调用可能是真正的提交: | ||
| 264 | + | ||
| 265 | +- **`/cgi-bin/mmbiz-bin/wxartrappsvr/route`(150 次,同窗口最高频)**:反编译排除——追完整条 `js1.m.c()→js1.h→js1.o(获取 h5auth token)→js1.g.a(回调)→ks1.g.b(真正 dispatch)→ks1.h.c()(动态路由型 requester,构造实际请求用的是 `Lr45/ou3`/`Lr45/su3`,cgi url 来自服务器下发的路由表 `Ljs1/s;->e` 缓存,不是硬编码的 wxartrappsvr)`——js1.m.c() 这条链路真正落地的 wire cgi 是 **`/cgi-bin/micromsg-bin/transfer/bizh5webtransfer`**(`r45.ou3`/`r45.su3`,58 次,跟 js1.m.c() 命中次数基本对上,`ou3.d` 字段直接内嵌整个 `y24` 子消息,我们已经从 js1.m.c() 直接拿到了同样的数据,只是没用扁平字段扫描器去 wxartrappsvr/route 的 `r45.zt5`/`r45.au5` 硬翻,翻了也是空——那是完全不相关的另一个机制,大概率是小程序通用心跳/保活(附近有 `js-operatewxdata-keepalive`),跟贴图号发布无关,是这次会话的干扰项,不是漏网的真身)。 | ||
| 266 | +- **`/cgi-bin/micromsg-bin/newsendmsg`(1 次)**:时间戳对不上(14:18:59,早于 14:59 左右的真实发布窗口),是无关的背景消息活动,不是这次发布触发的。 | ||
| 267 | +- **`getphotoworks` / `get_me_tab_pho_items`(各 1-2 次,用 `w71.o1`/`w71.p1` 通用信封,跟 `mp/profile`/`mp/acct` 共用同一对协议类)**:命名和触发时机(打开"我的作品"入口附近)都像是刷新"作品列表",不是新建。 | ||
| 268 | +- 全程扫了所有 `reqUrl` 里的 `action=` 参数值,只有 `get_*`/`batch_report`/`getcomment`/`send_status` 这类读/上报语义,没有任何 `publish`/`post`/`create`/`submit` 字样。 | ||
| 269 | + | ||
| 270 | +### 4.4 现状:提交发布的具体调用仍未定位,但基础设施已经完全打通 | ||
| 271 | + | ||
| 272 | +**没找到"是哪次调用真正创建了这条贴图号动态"**,但已经排除了目前想到的所有候选,且证实: | ||
| 273 | +1. LSPosed 路线安全可靠(这次全程真机验证零故障),Frida spawn/attach 主进程那条路线(§3.6/3.7)**不再需要**——有更安全的替代方案。 | ||
| 274 | +2. `js1.m.c()` + `hookAnyCgiCapture`(通用 cgi 捕获)两个钩子都已在 `WeChatHook.java` 里长期驻留、可复用,改改过滤条件就能对着任意后续假设重新抓。 | ||
| 275 | +3. 真正的提交调用要么是:①落在我们观察窗口之外(用户操作比预想快,提交发生在按下"发布"瞬间,捕获时机没对齐——需要更精细的时间戳比对,或者干脆常驻抓一整晚不设窗口);②走了一条完全不同、不经过 `TransferRequestInfo`/`modelbase.l` 的原生路径(类似图片消息发送 `b80.u.oj` 那种非 h5transfer 的二进制上传通道,更可能,因为提交要带图片二进制数据,用一个设计给"文本 JSON reqBody"的桥接机制传大图不合常理)。 | ||
| 276 | + | ||
| 277 | +**下一步(未做,需要用户决策)**:如果要继续,建议方向是①把 `hookAnyCgiCapture` 常驻挂着,不设时间窗口,用户任意时间点发布一次,回头翻完整 log 找提交调用的准确时间戳;②或者反过来想——参考 `wechat-image-send-internals.md` 记录的图片消息发送链路(`b80.u.oj`/`o70.k`/`o70.h`,走的是二进制 CDN 上传+插库,不是 h5transfer),贴图号发布很可能也是类似的"图片走独立 CDN/上传通道,只有文字元数据走 h5transfer"的混合模式,值得直接搜 `plugin/brandservice` 包里有没有专属的图片上传 NetScene/CDN 提交方法,而不是继续在 `js1.m`/`ks1.*` 这条纯文本 transfer 链路里找。 | ||
| 278 | + | ||
| 279 | +### 4.5 第二个候选也证伪:图片 CDN 服务 `m11.l0.d1()` 没有触发 | ||
| 280 | + | ||
| 281 | +顺着 `LuggageUploadMediaFileManager$UploadMediaFileTask.z()`(webview 的图片上传 IPC 任务壳)往下追,找到一个跟消息发图(`v70.w.oj`/`b80.u.oj`)完全不同的**独立 CDN 提交服务**:`Lm11/l0;->d1(Lk70/h0;ZJ)Lm11/b0;`(`m11.l0` 单例经 `Lm11/b1;->Di()` 拿到)。加了 `hookBrandserviceCdnUpload`(before+after 都 dump,`k70.h0`=提交参数 ~17 字段,`m11.b0`=CDN 结果 ~40 字段)在 base+Tinker CL、全进程装上,真机确认 hook 安装无误、微信全程正常。 | ||
| 282 | + | ||
| 283 | +真人再走一遍完整发布流程(POI→编辑→发布→查看结果):**`m11.l0.d1()` 全程零次触发**,`js1.m.c()`/`bizh5webtransfer` 命中数(63 次)和 URL 分布跟上一轮(4.2/4.3)几乎一模一样,仍然全是 `mp.weixin.qq.com` 只读态调用,没有任何 `action=publish/post/create/submit` 语义。**两个最有希望的候选(文本 transfer 桥 + 图片 CDN 服务)都被真机数据证伪**。 | ||
| 284 | + | ||
| 285 | +### 4.6 现状总结(截至本轮) | ||
| 286 | + | ||
| 287 | +排除法进展: | ||
| 288 | +- ❌ `js1.m.c()`(brandservice/webview JS-API transfer 汇聚点)——只见只读态调用 | ||
| 289 | +- ❌ `Ljs1/m;` 对应的 wire cgi `bizh5webtransfer`——同上,没有提交语义 | ||
| 290 | +- ❌ `wxartrappsvr/route`(表面最高频)——反编译证实是小程序心跳/保活,无关 | ||
| 291 | +- ❌ `newsendmsg`——时间戳对不上,无关背景消息 | ||
| 292 | +- ❌ `m11.l0.d1()`(webview 图片上传 CDN 服务)——零触发 | ||
| 293 | + | ||
| 294 | +真机验证过程本身完全安全(LSPosed 全程零故障,跟用户此前文字/图片/语音发送逆向经验一致),但**核心问题仍未解决**:提交发布的真实调用点还没找到。可能性: | ||
| 295 | +1. 提交发生在某个我们还没识别的第三条路径(Flutter 侧直接走 platform channel 调 native,完全绕开 `TransferRequestInfo`/`modelbase.l`/`m11.l0` 这三层已知桥接)。 | ||
| 296 | +2. 或者提交时机跟我们以为的不一致(比如"发布"按钮按下后有明显延迟才真正提交,我们的操作窗口理解有误差)。 | ||
| 297 | + | ||
| 298 | +本轮结束时决定:鉴于已经连续多轮真机验证都是负结果,且今天会话已经很长(6+ 轮真人发布配合 + 2 次 Frida 主进程事故 + 多轮 LSPosed 验证),暂停在此,留给用户决定是否继续投入,还是改走"半自动"方案(人工点发布,工具只管生成图片+文案)。 | ||
| 299 | + | ||
| 300 | +--- | ||
| 301 | + | ||
| 242 | ## 四、现场状态(写这份记录时) | 302 | ## 四、现场状态(写这份记录时) |
| 243 | 303 | ||
| 244 | - 设备上所有 mitm/iptables/CA 改动均已清理(见上文事故记录),微信网络恢复正常。 | 304 | - 设备上所有 mitm/iptables/CA 改动均已清理(见上文事故记录),微信网络恢复正常。 |
xhs_lsposed_a16_fix/wechathook_src/WeChatHook.java
| @@ -183,6 +183,23 @@ public class WeChatHook implements IXposedHookLoadPackage { | @@ -183,6 +183,23 @@ public class WeChatHook implements IXposedHookLoadPackage { | ||
| 183 | // Capture the real uploadvoice cgi request (this device uses inline-cgi, not CDN). | 183 | // Capture the real uploadvoice cgi request (this device uses inline-cgi, not CDN). |
| 184 | hookVoiceCgiCapture(cl, lpparam.processName); | 184 | hookVoiceCgiCapture(cl, lpparam.processName); |
| 185 | hookVoiceCgiResult(cl, lpparam.processName); | 185 | hookVoiceCgiResult(cl, lpparam.processName); |
| 186 | + | ||
| 187 | + // 贴图号发布逆向(wechat_send_internal/BRANDSERVICE_PUBLISH_RESEARCH.md §3.5): | ||
| 188 | + // both the mini-program (k2/y7) path and the WebView "luggage" JS-API path | ||
| 189 | + // funnel into this one sink before dispatch (local coroutine or IPC to | ||
| 190 | + // PROCESS_MAIN). Read-only: dumps TransferRequestInfo/y24 fields, does not | ||
| 191 | + // touch args or return value. LSPosed-installed (Zygote-time), NOT a live | ||
| 192 | + // Frida spawn/attach — that approach broke WeChat's main-process cold start | ||
| 193 | + // twice in one session (see doc §3.6/3.7); this hook exists precisely to | ||
| 194 | + // avoid repeating that mistake. | ||
| 195 | + hookBrandservicePublishTransfer(cl, lpparam.processName); | ||
| 196 | + // Broad catch-all: js1.m.c() only ever saw plain webview traffic for this flow | ||
| 197 | + // (§3.9), so also watch the generic internal-cgi builder unfiltered in case the | ||
| 198 | + // real publish submission is a native NetScene call instead. | ||
| 199 | + hookAnyCgiCapture(cl, lpparam.processName); | ||
| 200 | + // Image submission for brandservice: js1.m.c()/bizh5webtransfer never showed a | ||
| 201 | + // submit call, traced instead to a separate CDN service (§4 of doc). | ||
| 202 | + hookBrandserviceCdnUpload(cl, lpparam.processName); | ||
| 186 | } | 203 | } |
| 187 | 204 | ||
| 188 | // ── WCDB hook ────────────────────────────────────────────────────────────── | 205 | // ── WCDB hook ────────────────────────────────────────────────────────────── |
| @@ -1525,6 +1542,79 @@ public class WeChatHook implements IXposedHookLoadPackage { | @@ -1525,6 +1542,79 @@ public class WeChatHook implements IXposedHookLoadPackage { | ||
| 1525 | } catch (Throwable t) { XposedBridge.log("[WeChatHook] hookVoiceCgiResult failed in " + procName + ": " + t); } | 1542 | } catch (Throwable t) { XposedBridge.log("[WeChatHook] hookVoiceCgiResult failed in " + procName + ": " + t); } |
| 1526 | } | 1543 | } |
| 1527 | 1544 | ||
| 1545 | + // ── 贴图号发布 sink: js1.m.c(TransferRequestInfo, zq1.g0) ──────────────────── | ||
| 1546 | + //反编译确认(见 BRANDSERVICE_PUBLISH_RESEARCH.md §3.5): brandservice/webview | ||
| 1547 | + // 的 luggage JS-API 层(com.tencent.mm.plugin.webview.luggage.jsapi.c.d())和 | ||
| 1548 | + // 小程序侧的 k2/y7 都把 reqUrl/reqBody/cgiCmdId 填进 TransferRequestInfo 后调 | ||
| 1549 | + // 这一个方法。字段映射: TransferRequestInfo{d=type, e=h5Url, f=scope, h=y24, | ||
| 1550 | + // n=needVerifySignature, i=is_security_check}; y24{d=reqUrl, e=reqBody(JSON串), | ||
| 1551 | + // f=cgiCmdId, h=method, g=LinkedList<cw3> headers(cw3{d=key,e=value})}。 | ||
| 1552 | + private static void dumpFieldsByName(Object obj, String[] names, StringBuilder sb, String indent) { | ||
| 1553 | + if (obj == null) { sb.append(indent).append("(null)\n"); return; } | ||
| 1554 | + for (String name : names) { | ||
| 1555 | + try { | ||
| 1556 | + java.lang.reflect.Field f = obj.getClass().getDeclaredField(name); | ||
| 1557 | + f.setAccessible(true); | ||
| 1558 | + Object v = f.get(obj); | ||
| 1559 | + String vs = v == null ? "null" : v.toString(); | ||
| 1560 | + if (vs.length() > 4000) vs = vs.substring(0, 4000) + "…(truncated)"; | ||
| 1561 | + sb.append(indent).append(name).append(" = ").append(vs).append("\n"); | ||
| 1562 | + } catch (Throwable t) { | ||
| 1563 | + sb.append(indent).append(name).append(" = <err: ").append(t).append(">\n"); | ||
| 1564 | + } | ||
| 1565 | + } | ||
| 1566 | + } | ||
| 1567 | + | ||
| 1568 | + private void hookBrandservicePublishTransfer(ClassLoader cl, String procName) { | ||
| 1569 | + try { | ||
| 1570 | + Class<?> sinkCls = XposedHelpers.findClass("js1.m", cl); | ||
| 1571 | + final Class<?> reqInfoCls = XposedHelpers.findClass( | ||
| 1572 | + "com.tencent.mm.plugin.brandservice.api.TransferRequestInfo", cl); | ||
| 1573 | + int hooked = 0; | ||
| 1574 | + for (java.lang.reflect.Method m : sinkCls.getDeclaredMethods()) { | ||
| 1575 | + Class<?>[] pt = m.getParameterTypes(); | ||
| 1576 | + if (pt.length != 2 || !reqInfoCls.isAssignableFrom(pt[0])) continue; | ||
| 1577 | + m.setAccessible(true); | ||
| 1578 | + XposedBridge.hookMethod(m, new XC_MethodHook() { | ||
| 1579 | + @Override protected void beforeHookedMethod(MethodHookParam p) { | ||
| 1580 | + try { | ||
| 1581 | + Object reqInfo = p.args[0]; | ||
| 1582 | + StringBuilder sb = new StringBuilder( | ||
| 1583 | + "[WeChatHook] BRANDSVC-SINK js1.m.c() called in " + procName + "\n"); | ||
| 1584 | + if (reqInfo == null) { sb.append(" reqInfo = null\n"); XposedBridge.log(sb.toString()); return; } | ||
| 1585 | + dumpFieldsByName(reqInfo, new String[]{"d", "e", "f", "n", "i"}, sb, " "); | ||
| 1586 | + Object h; | ||
| 1587 | + try { | ||
| 1588 | + java.lang.reflect.Field hf = reqInfo.getClass().getDeclaredField("h"); | ||
| 1589 | + hf.setAccessible(true); | ||
| 1590 | + h = hf.get(reqInfo); | ||
| 1591 | + } catch (Throwable t) { h = null; } | ||
| 1592 | + if (h == null) { | ||
| 1593 | + sb.append(" h (y24, reqUrl/reqBody/cgiCmdId container) = null\n"); | ||
| 1594 | + } else { | ||
| 1595 | + sb.append(" h (y24):\n"); | ||
| 1596 | + dumpFieldsByName(h, new String[]{"d", "e", "f", "h"}, sb, " "); | ||
| 1597 | + try { | ||
| 1598 | + java.lang.reflect.Field gf = h.getClass().getDeclaredField("g"); | ||
| 1599 | + gf.setAccessible(true); | ||
| 1600 | + Object headers = gf.get(h); | ||
| 1601 | + sb.append(" g (headers) = ").append(headers == null ? "null" : headers.toString()).append("\n"); | ||
| 1602 | + } catch (Throwable t) { sb.append(" g (headers) = <err: ").append(t).append(">\n"); } | ||
| 1603 | + } | ||
| 1604 | + XposedBridge.log(sb.toString()); | ||
| 1605 | + } catch (Throwable t) { | ||
| 1606 | + XposedBridge.log("[WeChatHook] BRANDSVC-SINK dump err: " + t); | ||
| 1607 | + } | ||
| 1608 | + } | ||
| 1609 | + }); | ||
| 1610 | + hooked++; | ||
| 1611 | + } | ||
| 1612 | + XposedBridge.log("[WeChatHook] hookBrandservicePublishTransfer: hooked js1.m.c x" + hooked + " in " + procName); | ||
| 1613 | + } catch (Throwable t) { | ||
| 1614 | + XposedBridge.log("[WeChatHook] hookBrandservicePublishTransfer failed in " + procName + ": " + t); | ||
| 1615 | + } | ||
| 1616 | + } | ||
| 1617 | + | ||
| 1528 | private void hookVoiceCgiCapture(ClassLoader cl, String procName) { | 1618 | private void hookVoiceCgiCapture(ClassLoader cl, String procName) { |
| 1529 | try { | 1619 | try { |
| 1530 | Class<?> lCls = XposedHelpers.findClass("com.tencent.mm.modelbase.l", cl); | 1620 | Class<?> lCls = XposedHelpers.findClass("com.tencent.mm.modelbase.l", cl); |
| @@ -1573,6 +1663,118 @@ public class WeChatHook implements IXposedHookLoadPackage { | @@ -1573,6 +1663,118 @@ public class WeChatHook implements IXposedHookLoadPackage { | ||
| 1573 | } catch (Throwable t) { XposedBridge.log("[WeChatHook] hookVoiceCgiCapture failed in " + procName + ": " + t); } | 1663 | } catch (Throwable t) { XposedBridge.log("[WeChatHook] hookVoiceCgiCapture failed in " + procName + ": " + t); } |
| 1574 | } | 1664 | } |
| 1575 | 1665 | ||
| 1666 | + // 贴图号发布探测: js1.m.c() 抓到的全是 mp.weixin.qq.com 网页态调用(真实测过,见 | ||
| 1667 | + // BRANDSERVICE_PUBLISH_RESEARCH.md §3.9),没有一次带 protobuf reqBody/cgiCmdId 的—— | ||
| 1668 | + // 说明真正的"提交发布"根本不走 brandservice/webview 那条 JS-API 桥,大概率是原生侧 | ||
| 1669 | + // (Flutter platform channel 之后的 Kotlin/Java 代码)直接建的内部 cgi 调用,类似文字/ | ||
| 1670 | + // 语音发送那条 NetSceneQueue 路子。与其继续在混淆的 Flutter 桥接代码里找具体是哪个类, | ||
| 1671 | + // 直接照抄 hookVoiceCgiCapture 但去掉 "uploadvoice" 过滤——modelbase.l.a() 是所有内部 | ||
| 1672 | + // 二进制 cgi 调用的公共构造入口,不管发起方是谁,只要真是这条路子就跑不掉这个方法。 | ||
| 1673 | + private void hookAnyCgiCapture(ClassLoader cl, String procName) { | ||
| 1674 | + try { | ||
| 1675 | + Class<?> lCls = XposedHelpers.findClass("com.tencent.mm.modelbase.l", cl); | ||
| 1676 | + final Class<?> pbCls = XposedHelpers.findClass("com.tencent.mm.protobuf.f", cl); | ||
| 1677 | + XposedBridge.hookMethod(lCls.getDeclaredMethod("a"), new XC_MethodHook() { | ||
| 1678 | + @Override protected void beforeHookedMethod(MethodHookParam p) { | ||
| 1679 | + try { | ||
| 1680 | + Object self = p.thisObject; | ||
| 1681 | + String url = null; | ||
| 1682 | + java.util.List<Object> pbs = new java.util.ArrayList<>(); | ||
| 1683 | + for (Class<?> c = self.getClass(); c != null && !c.equals(Object.class); c = c.getSuperclass()) { | ||
| 1684 | + for (java.lang.reflect.Field f : c.getDeclaredFields()) { | ||
| 1685 | + if (java.lang.reflect.Modifier.isStatic(f.getModifiers())) continue; | ||
| 1686 | + f.setAccessible(true); | ||
| 1687 | + Object v; try { v = f.get(self); } catch (Throwable t) { continue; } | ||
| 1688 | + if (v instanceof String && ((String) v).contains("cgi-bin")) url = (String) v; | ||
| 1689 | + else if (v != null && pbCls.isInstance(v)) pbs.add(v); | ||
| 1690 | + } | ||
| 1691 | + } | ||
| 1692 | + if (url == null) return; // no gate otherwise — log every internal cgi call | ||
| 1693 | + StringBuilder sb = new StringBuilder("[WeChatHook] ANYCGI url=" + url + " pbCount=" + pbs.size()); | ||
| 1694 | + int idx = 0; | ||
| 1695 | + for (Object req : pbs) { | ||
| 1696 | + sb.append("\n -- pb#").append(idx++).append(" ").append(req.getClass().getName()).append(" --"); | ||
| 1697 | + for (int i = 0; i <= 25; i++) { | ||
| 1698 | + Object val = null; String kind = ""; | ||
| 1699 | + try { Object c = XposedHelpers.callMethod(req, "getCustom", i); | ||
| 1700 | + if (c != null) { val = "custom:" + c.getClass().getName(); kind = "C"; } } catch (Throwable t) {} | ||
| 1701 | + if (val == null) { try { Object s = XposedHelpers.callMethod(req, "getString", i); if (s != null) { val = s; kind = "S"; } } catch (Throwable t) {} } | ||
| 1702 | + if (val == null) { try { Object iv = XposedHelpers.callMethod(req, "getInteger", i); if (iv != null && ((Integer) iv) != 0) { val = iv; kind = "I"; } } catch (Throwable t) {} } | ||
| 1703 | + if (val == null) { try { Object lv = XposedHelpers.callMethod(req, "getLong", i); if (lv != null && ((Long) lv) != 0L) { val = lv; kind = "L"; } } catch (Throwable t) {} } | ||
| 1704 | + if (val != null) { | ||
| 1705 | + String vs = String.valueOf(val); | ||
| 1706 | + if (vs.length() > 200) vs = vs.substring(0, 200) + "…"; | ||
| 1707 | + sb.append("\n [").append(i).append("](").append(kind).append(")=").append(vs); | ||
| 1708 | + } | ||
| 1709 | + } | ||
| 1710 | + } | ||
| 1711 | + XposedBridge.log(sb.toString()); | ||
| 1712 | + } catch (Throwable t) { XposedBridge.log("[WeChatHook] ANYCGI err: " + t); } | ||
| 1713 | + } | ||
| 1714 | + }); | ||
| 1715 | + XposedBridge.log("[WeChatHook] hookAnyCgiCapture: hooked modelbase.l.a() (unfiltered) in " + procName); | ||
| 1716 | + } catch (Throwable t) { XposedBridge.log("[WeChatHook] hookAnyCgiCapture failed in " + procName + ": " + t); } | ||
| 1717 | + } | ||
| 1718 | + | ||
| 1719 | + // 贴图号发布图片提交(BRANDSERVICE_PUBLISH_RESEARCH.md §4):js1.m.c()/bizh5webtransfer | ||
| 1720 | + // 那条纯文本 transfer 链路里从没见过提交调用——反编译顺着 | ||
| 1721 | + // LuggageUploadMediaFileManager$UploadMediaFileTask.z() 往下追,图片走的是完全独立的 | ||
| 1722 | + // CDN 提交服务:Lm11/l0;->d1(Lk70/h0;ZJ)Lm11/b0;(m11.l0 单例经 Lm11/b1;->Di() 拿到, | ||
| 1723 | + // 跟消息发图 v70.w.oj/b80.u.oj 是两套不同的服务)。k70.h0 = 提交参数(本地文件路径等), | ||
| 1724 | + // m11.b0 = CDN 提交结果(约 40 个混淆字段,字段语义未知,先全量 dump 出来看)。 | ||
| 1725 | + private static void dumpAllFields(Object obj, StringBuilder sb, String indent) { | ||
| 1726 | + if (obj == null) { sb.append(indent).append("(null)\n"); return; } | ||
| 1727 | + for (java.lang.reflect.Field f : obj.getClass().getDeclaredFields()) { | ||
| 1728 | + if (java.lang.reflect.Modifier.isStatic(f.getModifiers())) continue; | ||
| 1729 | + try { | ||
| 1730 | + f.setAccessible(true); | ||
| 1731 | + Object v = f.get(obj); | ||
| 1732 | + String vs = v == null ? "null" : v.toString(); | ||
| 1733 | + if (vs.length() > 300) vs = vs.substring(0, 300) + "…"; | ||
| 1734 | + sb.append(indent).append(f.getName()).append(" (").append(f.getType().getSimpleName()) | ||
| 1735 | + .append(") = ").append(vs).append("\n"); | ||
| 1736 | + } catch (Throwable t) { | ||
| 1737 | + sb.append(indent).append(f.getName()).append(" = <err: ").append(t).append(">\n"); | ||
| 1738 | + } | ||
| 1739 | + } | ||
| 1740 | + } | ||
| 1741 | + | ||
| 1742 | + private void hookBrandserviceCdnUpload(ClassLoader cl, String procName) { | ||
| 1743 | + try { | ||
| 1744 | + Class<?> l0Cls = XposedHelpers.findClass("m11.l0", cl); | ||
| 1745 | + final Class<?> h0Cls = XposedHelpers.findClass("k70.h0", cl); | ||
| 1746 | + int hooked = 0; | ||
| 1747 | + for (java.lang.reflect.Method m : l0Cls.getDeclaredMethods()) { | ||
| 1748 | + if (!"d1".equals(m.getName())) continue; | ||
| 1749 | + Class<?>[] pt = m.getParameterTypes(); | ||
| 1750 | + if (pt.length == 0 || !h0Cls.isAssignableFrom(pt[0])) continue; | ||
| 1751 | + m.setAccessible(true); | ||
| 1752 | + XposedBridge.hookMethod(m, new XC_MethodHook() { | ||
| 1753 | + @Override protected void beforeHookedMethod(MethodHookParam p) { | ||
| 1754 | + try { | ||
| 1755 | + StringBuilder sb = new StringBuilder( | ||
| 1756 | + "[WeChatHook] BRANDSVC-CDN m11.l0.d1() called in " + procName + "\n k70.h0 params:\n"); | ||
| 1757 | + dumpAllFields(p.args[0], sb, " "); | ||
| 1758 | + XposedBridge.log(sb.toString()); | ||
| 1759 | + } catch (Throwable t) { XposedBridge.log("[WeChatHook] BRANDSVC-CDN before err: " + t); } | ||
| 1760 | + } | ||
| 1761 | + @Override protected void afterHookedMethod(MethodHookParam p) { | ||
| 1762 | + try { | ||
| 1763 | + StringBuilder sb = new StringBuilder( | ||
| 1764 | + "[WeChatHook] BRANDSVC-CDN m11.l0.d1() returned in " + procName + "\n m11.b0 result:\n"); | ||
| 1765 | + dumpAllFields(p.getResult(), sb, " "); | ||
| 1766 | + XposedBridge.log(sb.toString()); | ||
| 1767 | + } catch (Throwable t) { XposedBridge.log("[WeChatHook] BRANDSVC-CDN after err: " + t); } | ||
| 1768 | + } | ||
| 1769 | + }); | ||
| 1770 | + hooked++; | ||
| 1771 | + } | ||
| 1772 | + XposedBridge.log("[WeChatHook] hookBrandserviceCdnUpload: hooked m11.l0.d1 x" + hooked + " in " + procName); | ||
| 1773 | + } catch (Throwable t) { | ||
| 1774 | + XposedBridge.log("[WeChatHook] hookBrandserviceCdnUpload failed in " + procName + ": " + t); | ||
| 1775 | + } | ||
| 1776 | + } | ||
| 1777 | + | ||
| 1576 | private void hookVoiceUpload(ClassLoader cl, String procName) { | 1778 | private void hookVoiceUpload(ClassLoader cl, String procName) { |
| 1577 | try { | 1779 | try { |
| 1578 | Class<?> g1 = XposedHelpers.findClass("w21.g1", cl); | 1780 | Class<?> g1 = XposedHelpers.findClass("w21.g1", cl); |
| @@ -3050,6 +3252,12 @@ public class WeChatHook implements IXposedHookLoadPackage { | @@ -3050,6 +3252,12 @@ public class WeChatHook implements IXposedHookLoadPackage { | ||
| 3050 | // Capture the real uploadvoice cgi request in Tinker CL. | 3252 | // Capture the real uploadvoice cgi request in Tinker CL. |
| 3051 | hookVoiceCgiCapture(sTinkerCL, procName + "/tinker"); | 3253 | hookVoiceCgiCapture(sTinkerCL, procName + "/tinker"); |
| 3052 | hookVoiceCgiResult(sTinkerCL, procName + "/tinker"); | 3254 | hookVoiceCgiResult(sTinkerCL, procName + "/tinker"); |
| 3255 | + // js1.m may live in a Tinker DEX too (base-CL hook installed | ||
| 3256 | + // but confirmed it never fires — same "base CL vs Tinker CL" | ||
| 3257 | + // gotcha as f9/w11.s1/jg0.x above). Re-hook here. | ||
| 3258 | + hookBrandservicePublishTransfer(sTinkerCL, procName + "/tinker"); | ||
| 3259 | + hookAnyCgiCapture(sTinkerCL, procName + "/tinker"); | ||
| 3260 | + hookBrandserviceCdnUpload(sTinkerCL, procName + "/tinker"); | ||
| 3053 | // Pad-login: the login UIs (LoginPasswordUI/LoginHistoryUI/ | 3261 | // Pad-login: the login UIs (LoginPasswordUI/LoginHistoryUI/ |
| 3054 | // LoginUI) + bk run in Tinker CL — the base-CL hooks never | 3262 | // LoginUI) + bk run in Tinker CL — the base-CL hooks never |
| 3055 | // fire (0 N-VISIBLE). Register them here so the "以平板登录" | 3263 | // fire (0 N-VISIBLE). Register them here so the "以平板登录" |