Commit 67417267d50ebb5885b85ea6223c794dfaffbed0

Authored by yibo
1 parent b36fc045

research(wechat): 改用自主 UI 自动化复现发布流程,wxartrappsvr/route 也证伪

改用 adb input tap + screencap 自主驱动"我的作品→贴图号→发表"全流程,
不再需要用户手动操作,缩短每轮迭代成本。真机验证坑:可折叠机
screencap 默认拿错屏幕,需显式 -d <display-id>。

给 hookAnyCgiCapture 加了 r45.zt5(wxartrappsvr/route)专属 dump——
之前通用扁平字段扫描器看不到它的 LinkedList<re7> payload,误判成
"无内容"。真机确认这条路全是小程序 SDK 埋点上报(页面性能/console log
转发/事件上报),跟提交发布无关,第三个候选证伪。

意外发现:图片编辑步骤本身是个独立小程序(appid wx0b2ec076ec39c786,
page_path pages/image_editor/image_editor),此前一直当作纯 H5
webview——这是下次调查该跟进的新方向。核心提交调用仍未定位。
wechat_send_internal/BRANDSERVICE_PUBLISH_RESEARCH.md
@@ -299,6 +299,38 @@ cw3 { d=key, e=value } @@ -299,6 +299,38 @@ cw3 { d=key, e=value }
299 299
300 --- 300 ---
301 301
  302 +## 五、改用 `am`+UI 自动化自主复现发布流程(2026-07-27 下午续)
  303 +
  304 +用户提出关键改进:与其每轮都让用户在手机上手动操作、我在旁边被动等日志,不如**我自己直接用 `adb input tap`/`screencap` 驱动 UI**,全程自主复现"我的作品→贴图号→发表"整个流程,缩短每轮迭代的等待成本,也不再消耗用户注意力。
  305 +
  306 +### 5.1 UI 自动化关键坑:这台设备是可折叠机,`screencap` 默认拿错屏幕
  307 +
  308 +Pixel 10 Pro Fold 有两块屏(outer/cover 屏 + inner 屏),`adb exec-out screencap -p` 不带 `-d` 会警告"multiple displays"并默认拿第一个找到的(`4619827677550801152`,这次测得是纯黑——大概率是没在用的那块屏,或者是折叠态下未激活的屏)。**必须显式指定 `-d <display-id>`**:`dumpsys SurfaceFlinger --display-id` 列出两个 ID,`dumpsys display | grep mPrimaryDisplayDevice` 能看到当前哪个是 "Outer Display";这次机器当时是以 outer/cover 屏(1080×2364,display id `4619827677550801153`)在用,`input tap` 坐标也要按这块屏的分辨率算(截图工具返回图默认会整体缩放显示,需要按标注的缩放比例换算成真实坐标再传给 `input tap`)。
  309 +
  310 +### 5.2 自主复现全流程成功(无需用户参与),确认 `wxartrappsvr/route` 也不是提交点
  311 +
  312 +自主完整跑通"我→作品→(贴图号)发表→选图→下一步→填标题/正文→发表"全程(用中性的 AI 日报图,不涉真人照片),全程无需用户操作。两轮独立发布均确认**服务器真的接受了新文章**(`js1.m.c()` 抓到的预览页 URL 里 `chksm=` 每轮都不同,且发布后能在"作品"列表里看到新帖子),排除了"根本没发出去,我们看错了"这种可能。
  313 +
  314 +针对上一轮(§4)里最高频但内容读不出来的 `wxartrappsvr/route`(`r45.zt5`),本轮解开了它为什么读不出来的谜:**它的 payload 不是扁平字段,是一个 `LinkedList<r45.re7>`**(`zt5.d` 字段),每个 `re7{d=type int, e=protobuf.g 装的 body 字节串, f=route/action 字符串}`——用之前"扫描字段 0-25 的 getString/getInteger/getLong"的通用逻辑天生看不到 List 类型字段,这是之前"内容都是空"的真正原因,不是"这条路真的没数据"。
  315 +
  316 +给 `hookAnyCgiCapture` 加了 `r45.zt5` 专属 dump(读 `d` 字段的 LinkedList,逐个 `re7` 打印 `type(d)`/`route(f)`/`body(e.i())`),真机验证:**这条路的 payload 全是小程序 SDK 的埋点上报**——`type=1000`(页面性能指标:`costtime`/`staytime`/`targetAction`)、`type=1001`(`weapplog`,小程序 console.log 转发到日志系统)、`type=1005`(页面事件上报:`event_name`/`enter_scene`),`route(f)` 恒为 `wx0b2ec076ec39c786`(一个小程序 appid),`page_path` 恒为 `pages/image_editor/image_editor`。**这不是提交发布的调用,是小程序自身运行时的埋点上报,跟贴图号提交内容无关。**
  317 +
  318 +### 5.3 意外发现:图片编辑步骤本身是一个小程序(`appid=wx0b2ec076ec39c786`)
  319 +
  320 +这是本轮最有价值的新信息:从截图流程看,选完图后有一步"编辑"(`page_path: pages/image_editor/image_editor`)此前一直以为整条"编辑/填标题/发表"UI 都是纯 `TmplWebViewMMUI`(H5 网页),但埋点数据显示图片裁剪/编辑这一步实际上是**一个独立小程序**(appid `wx0b2ec076ec39c786`)渲染出来的,运行在 `appbrand*` 进程里(跟 §3 一开始的假设吻合,只是走了另一条我们没追的分支)。这意味着:贴图号发布流程可能是"原生 Flutter UI(选图) → 小程序(图片编辑) → H5 webview(标题/正文/发表)"三段拼接,每段用不同技术栈、不同网络通道,**"提交"这一步具体发生在哪一段,仍是未知数**——如果发生在小程序段,走的会是小程序专属的另一套 cgi 通道(`k2`/`y7`,回到最早 §一 反编译过的入口,但那条路当时已经确认在 appbrand 进程里挂了钩子却零触发,可能是挂错了具体哪个 appbrand 编号,或者小程序用的是另一套更底层的 native bridge,不经过 k2/y7)。
  321 +
  322 +### 5.4 现状(本轮结束):三个候选全部证伪,第四个新线索(小程序 image_editor)尚未跟进
  323 +
  324 +排除法进展更新:
  325 +- ❌ `js1.m.c()` / `bizh5webtransfer` —— 只读态
  326 +- ❌ `m11.l0.d1()` (CDN 上传服务)—— 零触发
  327 +- ❌ `wxartrappsvr/route`(`r45.zt5`/`re7`)—— 确认是小程序埋点上报,无关
  328 +- 🆕 图片编辑步骤是小程序 `wx0b2ec076ec39c786`,可能是下一个该深挖的方向,但还没有具体行动
  329 +
  330 +用户配合方式已改进(不再需要用户手动操作,可自主复现),今天已知的三条候选路径均已用真机数据证伪,核心提交调用仍未定位。建议下次接着来的方向:直接搜这个小程序 appid 相关的原生桥接代码(`k2`/`y7` 之外,可能还有别的小程序专属 jsapi 处理提交),或者接受当前"半自动"退路。
  331 +
  332 +---
  333 +
302 ## 四、现场状态(写这份记录时) 334 ## 四、现场状态(写这份记录时)
303 335
304 - 设备上所有 mitm/iptables/CA 改动均已清理(见上文事故记录),微信网络恢复正常。 336 - 设备上所有 mitm/iptables/CA 改动均已清理(见上文事故记录),微信网络恢复正常。
xhs_lsposed_a16_fix/wechathook_src/WeChatHook.java
@@ -1694,6 +1694,49 @@ public class WeChatHook implements IXposedHookLoadPackage { @@ -1694,6 +1694,49 @@ public class WeChatHook implements IXposedHookLoadPackage {
1694 int idx = 0; 1694 int idx = 0;
1695 for (Object req : pbs) { 1695 for (Object req : pbs) {
1696 sb.append("\n -- pb#").append(idx++).append(" ").append(req.getClass().getName()).append(" --"); 1696 sb.append("\n -- pb#").append(idx++).append(" ").append(req.getClass().getName()).append(" --");
  1697 + // wxartrappsvr/route (r45.zt5) is a generic RPC multiplexer: field
  1698 + // `d` is a LinkedList<r45.re7>, each re7{d=type int, e=protobuf.g
  1699 + // body bytes, f=route/action String}. Flat getString/getInteger
  1700 + // scanning below never sees this (it's a List, not a scalar field),
  1701 + // which is why earlier passes thought this call carried no payload.
  1702 + // Dump it explicitly so we can see what's actually being routed.
  1703 + if (req.getClass().getName().equals("r45.zt5")) {
  1704 + try {
  1705 + java.lang.reflect.Field lf = req.getClass().getDeclaredField("d");
  1706 + lf.setAccessible(true);
  1707 + java.util.LinkedList<?> items = (java.util.LinkedList<?>) lf.get(req);
  1708 + sb.append("\n zt5.d LinkedList<re7> size=").append(items.size());
  1709 + int ridx = 0;
  1710 + for (Object item : items) {
  1711 + String typeStr = "?", routeStr = "?", bodyStr = "?";
  1712 + try {
  1713 + java.lang.reflect.Field df = item.getClass().getDeclaredField("d");
  1714 + df.setAccessible(true);
  1715 + typeStr = String.valueOf(df.get(item));
  1716 + } catch (Throwable t) {}
  1717 + try {
  1718 + java.lang.reflect.Field ff = item.getClass().getDeclaredField("f");
  1719 + ff.setAccessible(true);
  1720 + Object fv = ff.get(item);
  1721 + routeStr = fv == null ? "null" : fv.toString();
  1722 + } catch (Throwable t) {}
  1723 + try {
  1724 + java.lang.reflect.Field ef = item.getClass().getDeclaredField("e");
  1725 + ef.setAccessible(true);
  1726 + Object ev = ef.get(item);
  1727 + if (ev != null) {
  1728 + Object asStr = XposedHelpers.callMethod(ev, "i");
  1729 + bodyStr = asStr == null ? "null" : asStr.toString();
  1730 + if (bodyStr.length() > 500) bodyStr = bodyStr.substring(0, 500) + "…";
  1731 + }
  1732 + } catch (Throwable t) { bodyStr = "<err: " + t + ">"; }
  1733 + sb.append("\n re7#").append(ridx++)
  1734 + .append(" type(d)=").append(typeStr)
  1735 + .append(" route(f)=").append(routeStr)
  1736 + .append(" body(e.i())=").append(bodyStr);
  1737 + }
  1738 + } catch (Throwable t) { sb.append("\n zt5 dump err: ").append(t); }
  1739 + }
1697 for (int i = 0; i <= 25; i++) { 1740 for (int i = 0; i <= 25; i++) {
1698 Object val = null; String kind = ""; 1741 Object val = null; String kind = "";
1699 try { Object c = XposedHelpers.callMethod(req, "getCustom", i); 1742 try { Object c = XposedHelpers.callMethod(req, "getCustom", i);