# 贴图号自动发布日报 — 逆向进度记录(进行中) 目标:把 lifelog/wechat-viewer 生成的每日日报(图片 + 文字)自动发到微信"贴图号" (一种手机端管理的品牌服务号内容发布功能,入口在"我的作品")。 约束:跟 `RESEARCH.md`(消息发送)同一台设备(Pixel 10 Pro Fold, root + Magisk + frida-server 17.9.10 常驻 :27042),WeChat 8.0.74。 --- ## 一、已验证结论 ### 1. 网络层 MITM 是死路(别再试) 用 `phoneuse/mitm_proxy.py`(已在设备装 CA + iptables NAT 重定向 uid 10336 的 80/443 到本地代理) 抓包,贴图号发布 + 定位请求全部是 `POST szextshort.weixin.qq.com:443 /mmtls/`—— 外层 TLS 解密后 payload 还是微信自己的 MMTLS 二进制包,内容不可读。跟 `RESEARCH.md` 当年 "IM 消息走 MMTLS 不走 OkHttp"的结论一致,贴图号也一样,不是 Finder 那种明文 CGI。 **⚠️ 事故记录**:第一次全量拦截微信 uid 的 80/443 时,真实打断了用户当场发布(提示"上传图片失败", 后来确认是定位请求被搅坏)。所有 CA/iptables/mitm_proxy 改动已完全清理干净 (CA 从 `/system/etc/security/cacerts/` 删除,iptables 规则已 flush,进程已杀)。 **以后不要再用全量网络重定向的方式碰这个设备的微信流量。** ### 2. Activity 调用链(logcat 抓的,可信) ``` BizMyWorksFlutterViewActivity (plugin.brandservice.ui.flutter,Flutter 写的"我的作品"列表) → MediaTabPickerUI (plugin.gallery.picker.tab,原生选图) → AppBrandPluginUI (plugin.appbrand.ui,交给小程序宿主进程) → TmplWebViewMMUI (plugin.brandservice.ui.timeline.preload.ui,WebView 编辑器) → 回到 BizMyWorksFlutterViewActivity ``` ### 3. 反编译找到的关键 hook 点(重大进展) APK 反编译位置(设备上,Termux):`~/wechat_apk/decompiled`(apktool 3.0.2 反编译 `~/wechat_apk/base.apk`,从 `/data/app/~~xb1vAMX_YH46_JsgviBuTw==/com.tencent.mm-3EBNVMamk3aQotXae5mBQg==/base.apk` 拷贝,261MB,17 个 dex)。**这份反编译产物很大,没有也不应该进 git,留在设备上按需重新生成。** 搜索 `com/tencent/mm/plugin/brandservice/api/TransferRequestInfo` 的引用方,定位到: - **`com/tencent/mm/plugin/lite/jsapi/comms/k2.smali`** —— `LiteAppJsApiExtTransfer` (`lite` = 微信小程序框架内部代号)。方法 `a(String, JSONObject, boolean)` 是小程序 JS 调 `wx.xxx()` 通用透传 CGI 的原生实现:从传入的 `JSONObject` 里取 **`reqUrl`(明文 CGI 地址) / `cgiCmdId` / `reqBody`(明文请求体)/ `method`**,塞进 `TransferRequestInfo`, 再交给 `Ljs1/m;->c(TransferRequestInfo, callback)` 发出去(发出去之后才会被包进 MMTLS)。 - **`com/tencent/mm/plugin/lite/jsapi/comms/y7.smali`** —— `LiteAppJsApiWebTransfer`, 结构几乎一样,服务 WebView 侧(对应 `TmplWebViewMMUI`)。 **这意味着:任何小程序(包括贴图号背后那个)要发后端请求,都要先把明文 reqUrl/reqBody 过这两个方法之一。Hook 这两个方法 = 不用管 MMTLS,直接看到发布请求的明文内容,且只读打印 不改行为,不会像网络层 MITM 那样有破坏性副作用。** Hook 脚本已写好并验证能用:`wechat_send_internal/hook_lite_transfer.js` (`Java.perform` 里对 `k2`/`y7` 的 `a(String,JSONObject,boolean)` 做 overload hook, 打印三个参数后照常调用原方法,纯观察不拦截)。 --- ## 二、Frida attach 的坑(重要,省得下次重新踩) 微信这种巨型 App,`Java.perform` 第一次要枚举当前进程里所有已加载的类, 如果对着一个**已经跑了一段时间、加载了很多 class 的进程**去 attach, 枚举耗时会超过 frida CLI 脚本加载的默认超时(`Failed to load script: timeout was reached`)。 已经在这台设备上反复验证: - 对存量的 `com.tencent.mm:appbrand0/1/2`(不管是空闲的还是当前 TOP 前台的)attach,**全部超时**。 - 只有用 `frida -U -W '.*appbrand.*' -l script.js`(等一个**全新** spawn 的匹配进程, 从它刚诞生、类还没加载多少的时候就注入)才成功过一次: ``` Handling: Spawn(pid=22388, identifier="com.tencent.mm:appbrand1") Spawned `com.tencent.mm:appbrand1`. Resuming main thread! [hook] hooked com.tencent.mm.plugin.lite.jsapi.comms.k2.a(...) [hook] hooked com.tencent.mm.plugin.lite.jsapi.comms.y7.a(...) ``` - **`-W PATTERN` 的 PATTERN 是正则不是 glob**(`.*appbrand.*`,不能写 `*appbrand*`)。 - **`-W` 只逮一次**:撞见第一个匹配的 spawn 就处理,之后新冒出来的同名进程一律 "Ignoring", 不会连续抓多个。 微信这次的坑:**贴图号这个功能背后同时用了不止一个 appbrand 进程**(这次看到 appbrand0/1/2 三个都在跑),`-W` 只抓住了其中一个(appbrand1),但实际处理这次发布 JS 调用的 很可能是另外两个之一——它们当时已经不是"刚诞生"状态,attach 全部超时,没抓到任何 `reqUrl`/`reqBody` 输出。 --- ## 三、2026-07-27 新一轮验证结果(4 轮真机实测,关键发现:appbrand2 才是真正宿主) ### 3.1 三开 `-W` 会撞车导致目标进程崩溃(别再三开) 按原计划同时开 3 个 `frida -U -W '.*appbrand.*' -l hook_lite_transfer.js` 监听器,结果 这次发布流程**只**冒出了 1 个新 appbrand 进程(不是 3 个),3 个监听器同时抢着 `Handling`/`resume()` 同一个 spawn(pid 10481, appbrand0),导致目标进程真的崩了 (`Process crashed: Bad access due to invalid address`),钩子都没来得及装上。 **教训:一次只开 1 个监听器**,"多开几个提高命中率"这个假设本身是错的——是否有新进程 冒出跟开几个监听器无关,同一个 spawn 事件会被所有监听器同时抓到并竞争 resume, 多开只会增加竞争崩溃概率。所幸崩溃只杀掉 appbrand 子进程,不影响微信主进程/登录态, 用户当次发布仍然成功(微信自己重新拉起了 appbrand0)。 `yes | frida` 这个 keep-alive 技巧也有副作用:`y` 换行会被 frida REPL 当命令求值, 狂刷 `ReferenceError: 'y' is not defined`(无害但刷屏)。改用 `yes '' | frida ...` (空行)更干净。 ### 3.2 单开监听器成功装上钩子,但从未触发——因为挂错了进程 单开、清空 appbrand 后重试:`k2.a()`/`y7.a()` 都成功 hook 上(`appbrand1`, pid 21256), 但整场发布流程走完,**hook 里的 `console.log` 一次没触发过**。一度怀疑是方法签名/重载 不对,或者这条 CGI 通路假设整个错了。 ### 3.3 root cause:`frida -W` 只逮"当前进程池里第一个新冒出来的匹配 spawn",不是"这次发布用到的那个" 用 `adb logcat -v time ActivityTaskManager:I ActivityManager:I` 配合 hook 同时跑, 第 4 轮才看全整条链路的进程归属: ``` 11:04:01.764 Start proc 18899:com.tencent.mm:appbrand0 for Appbrand0IPCService ← 选图阶段起的,跟发布无关 11:10:08.241 START MediaTabPickerUI 11:10:16.225 START AppBrandPluginUI 11:10:20.128 Start proc 32030:com.tencent.mm:appbrand2 for Appbrand2IPCService ← 真正宿主 11:10:25.286 START TmplWebViewMMUI ← 紧跟在 appbrand2 起来之后 ``` `appbrand2`(pid 32030)几乎是紧挨着 `TmplWebViewMMUI`(真正装载发布用 WebView 编辑器的 Activity)启动前一刻才冒出来的,是它的宿主进程。但因为 `frida -W` 在流程更早的 `appbrand0` spawn 时就已经"用掉"了那一次捕获(这正是之前记录过的 `-W` 已知限制: "只逮一次,撞见第一个匹配的 spawn 就处理,之后新冒出来的同名进程一律 Ignoring"——见 `phoneuse` `TASKS.md` 相关记录),后面真正需要的 `appbrand2` spawn 被完全无视, hook 装在了一个跟这次发布毫不相干的进程上,自然永远不会触发。 **这不是"reqUrl/reqBody 假设错了",是"用 CLI 的 `-W` 这个一次性开关去应付一个进程池 一次流程里会冒出好几个同名子进程"这个用法本身跟场景不匹配。** ### 3.4 常驻 Python spawn-gating 脚本验证成功,但 hook 装上后仍然打不中 写了 `wechat_send_internal/multi_spawn_hook.py`(`device.enable_spawn_gating()` + `on('spawn-added', ...)`,逐个 spawn 都 attach+hook+resume,不像 CLI `-W` 那样只处理 第一个)。踩了一个新坑:`session.create_script()`(原生 Python binding)既不像 CLI 那样自动 bundle `frida-java-bridge`,也没有 `require()`——报 `ReferenceError: 'require' is not defined`。解法:本地起一个临时 npm 项目 `npm install frida-java-bridge`,写 `import Java from 'frida-java-bridge'` 的 entry.js, `frida-compile entry.js -o xxx_bundled.js -P gum -S` 编译成单文件 bundle, Python 脚本加载这个编译产物而不是原始 source。(`hook_lite_transfer.js` 保留原样给 CLI 用;`hook_lite_transfer_bundled.js` 是给 Python API 用的编译版,改了逻辑要两边 都同步 + 重新编译。) 修完这个坑后,`k2.a()`/`y7.a()` 在 appbrand0/1/2 三个进程上全部确认 hook 成功 (无报错),但**发布流程走完,三个进程加起来还是零次调用**。说明前面 §3.3 的 process 假设本身可能就是错的——不是"没挂上正确的 appbrand 编号",而是这次调用可能 根本不在任何 appbrand 进程里。 ### 3.5 反编译代码证实:真正的汇聚点是 `Ljs1/m;->c()`,很可能跑在主进程 翻了 `TmplWebViewMMUI.smali`:它只是 `TmplWebViewToolUI` → ... → `WebViewUI`(微信通用网页容器)的空壳子类,**跟小程序(`appbrand`/`lite`)体系完全 无关**,是走微信另一套更通用的 WebView JS-SDK 桥(`plugin/webview/luggage/jsapi/` 包,`luggage` 是内部代号)。搜 `TransferRequestInfo` 的全部引用方(比 §3.3 只搜 k2/y7 更全),找到 `com/tencent/mm/plugin/webview/luggage/jsapi/c.smali`(abstract 基类, 方法 `d(Context, String jsonStr, callback)`)——跟 k2/y7 结构几乎一样,从 JSON 里抠 `reqUrl`/`reqBody`/`cgiCmdId`/`h5Url` 建 `TransferRequestInfo`,但最后都殊途同归 调用同一个 sink:**`Ljs1/m;->c(TransferRequestInfo, callback)V`**(`js1/m.smali`)。 看了 `js1.m.c()` 的方法体:先判断 `Lcom/tencent/mm/sdk/platformtools/x2;->n()`, 是的话本地协程直接处理(`Ljs1/h`),不是的话通过 `Lcom/tencent/mm/ipcinvoker/f;->a("PROCESS_MAIN", requestInfo, ...)` **IPC 转发去 主进程**处理。`TmplWebViewMMUI` 本身大概率就跑在**主进程**(pid 29168,App 一直存活 不会重新 spawn),不在任何 appbrand 子进程——之前 logcat 看到的 `appbrand1`/`appbrand2` spawn(`MagicBrushServiceContextBase`/`Appbrand2IPCService`)现在看更像是同一时间段 凑巧冒出来的其它微信后台活动(图片编辑笔刷工具、某个通用 IPC 扩展服务),跟这次发布 未必有因果关系——之前的 process 归属推断是巧合关联,不是确证。 `Ljs1/m;->c()` 是比 k2/y7 更好的 hook 点:不管调用方是小程序侧还是 WebView-luggage 侧,到这一步 `TransferRequestInfo` 已经填好全部字段(`reqInfo.h.d`=reqUrl, `reqInfo.h.e`=reqBody, `reqInfo.h.f`=cgiCmdId, `reqInfo.h.h`=method, `reqInfo.e`=h5Url), 直接读字段就行,不用再解析原始 JSON。脚本:`wechat_send_internal/hook_transfer_sink.js` (原始版,entry)+ `hook_transfer_sink_bundled.js`(frida-compile 编译版,Python API 用)。 ### 3.6 ⚠️ 事故:spawn 主进程挂钩子导致微信"无法连接服务器",不要再直接这么干 `spawn_mm_and_hook.py`:`am force-stop com.tencent.mm` 全杀 → `device.spawn(["com.tencent.mm"])` (挂起态)→ attach + 装 `Ljs1/m;->c()` 钩子 → `resume()`。脚本本身跑起来一切正常 (hook 装上无报错),但**用户在手机上重新打开微信后,微信直接提示"无法连接服务器", 打不开**。Force-stop 全应用 + kill 掉我们的 frida 脚本 + 手动正常重新打开微信后 恢复正常——**确认是这次 spawn+hook 主进程的操作把微信启动流程搞坏了,不是设备网络 问题**(其间 `ping 8.8.8.8` 一直通)。 **教训**:之前对 appbrand 子进程做的"spawn 时挂起 → hook → resume"是安全的(那是 沙箱化的独立子进程,出问题最多子进程崩溃重启,不影响主进程/登录态,见 §3.1 尾注), 但**同样的手法用在主进程上风险明显更高**——主进程冷启动要做的初始化(网络栈/MMTLS 会话/一堆单例)顺序敏感,我们注入脚本 + spawn-gating 暂停的那个时间窗本身就可能 打乱这个顺序,哪怕脚本只做"pure observation"式的方法替换也可能触发这个问题 (具体是哪一步冲突的没有细查,代价太高不值得再试来定位)。**不要在没有更谨慎方案 之前重复"spawn 主进程 + 挂钩子"这个操作。** ### 3.7 第二次事故:换个注入时机(spawn 后立即 resume,隔 0.8s 再 attach)依然搞坏微信 `spawn_mm_and_hook.py` v2:不在 spawn-paused 状态下注入,而是 spawn 后立刻 `resume()`,隔 0.8 秒再对已经在跑的 pid 做正常 `device.attach()`。这次连 hook 本身 都没装成功——`Java.perform` 内部 attach JVMTI 环境时直接报 `Error: access violation accessing 0x0`(frida 自己的 JVMTI attach 逻辑挂了, 时机太早,ART 运行时还没就绪),我们的 hook 代码根本没跑到。 **但即便 hook 没装成功,微信依旧被搞坏了**——用户反馈"微信点不动"(界面卡死/无响应), 跟第一次的"无法连接服务器"症状不同,但同样需要 `am force-stop` 全杀 + 手动重新打开 才恢复正常。 **这排除了"是我们 hook 代码本身有 bug"这个解释**——两次事故一次 hook 装成功了 (§3.6,之后微信显示网络错误),一次 hook 压根没装上就已经 native 报错(这次, 微信直接卡死)。共同点只有"往微信主进程冷启动过程里做 frida attach 这个动作本身"。 不管注入时机多晚、hook 代码写不写东西,只要在主进程刚起来那段时间用 frida 碰它, 就有实质性概率把微信弄挂,且两次表现还不一样(无法连接服务器 / 界面卡死), 说明不是某个特定时序窗口的问题,而是这个操作本身对这台设备上这个微信版本 就是不安全的。 ### 3.8 结论:主进程注入这条路线到此为止 **不建议再尝试对微信主进程做任何形式的 frida spawn/attach**(不管是 spawn-paused 时机、resume 后延迟 attach,还是理论上的其它变体)。今天累计:6 轮真机手动发布 + 2 次主进程注入导致微信实际不可用(每次都要 force-stop 全杀 + 手动重开才恢复, 过程中用户体验是真实受影响的,不是纯理论风险)。风险已经从"可能浪费时间"上升到 "实际两次打断用户在用的微信",性价比不再支持继续在真机上试验变体。 `appbrand` 子进程的 spawn-gating hook(`multi_spawn_hook.py`,用 `hook_lite_transfer_bundled.js`)本身依然是安全的、已验证的技术(多次验证只影响 子进程,不影响主进程/登录态),可以留着给其它子进程场景复用;`Ljs1/m;->c()` 是不是真正 sink 也依然成立(反编译分析站得住),只是"怎么安全地够到跑在主进程里 的它"这个问题目前没有答案。 如果以后要再碰这条线,思路上可以考虑的方向(都没验证,下次再评估要不要试): - 不整个重启微信主进程,而是想办法在**已经在跑、且刚从后台切到前台**的主进程上 做非 spawn 式的常规 attach(拿掉"重启"这个变量,只保留"attach"这个变量, 分开验证到底是哪一步不安全)——但存量进程类枚举超时的老问题还在,且同样有 "只是 attach 就把微信弄挂"的风险,不能假设更安全。 - 完全放弃 hook 微信主进程,改成分析 MMTLS 外层协议本身能不能在不进程内注入的 情况下拿到明文(例如找有没有已知的 MMTLS 密钥导出/降级漏洞——目前没有调研过, 真实存在的可能性未知,可能是死路)。 - **退回"半自动"方案**:人工点发布,工具只负责自动生成图片+文案,不追求端到端 全自动。今天的两次真实故障之后,这个选项的性价比明显更高了,建议优先考虑。 --- ## 四、路线修正:改用 LSPosed(跟文字/图片/语音发送同一套机制),彻底解决主进程注入问题 用户指出关键盲点:文字/图片/语音发送逆向全程用的是 **LSPosed**(`xhs_lsposed_a16_fix/wechathook_src/WeChatHook.java`,Zygote fork 时随进程诞生自动挂钩子),从没出过问题;这次改用 Frida spawn/attach 才两次真实搞坏微信。根因很清楚:**LSPosed 挂钩发生在系统自己的进程创建路径里(Zygote fork),不是外部 ptrace 注入一个已经在跑/刚起来的进程**,没有"抢在类加载膨胀前注入"这种时序竞争,自然不会撞上 Frida 那两次事故的问题。 ### 4.1 改造 WeChatHook.java,加 `hookBrandservicePublishTransfer` 在 `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 这个注入方式上。 字段映射(现场验证): ``` TransferRequestInfo { d=type(int), e=h5Url, f=scope, h=y24(Lr45/y24;), n=needVerifySignature, i=is_security_check } y24 { d=reqUrl, e=reqBody(JSON串), f=cgiCmdId, h=method, g=LinkedList headers } cw3 { d=key, e=value } ``` ### 4.2 真机捕获:js1.m.c() 全程只看到 webview 支持性调用,没看到"提交发布" 真人走一遍"我的作品"发布流程(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 打,跟真正提交表单/上传图片的调用形态不同。 ### 4.3 排查了两个疑似候选,都证伪 加了 `hookAnyCgiCapture`(复用 `hookVoiceCgiCapture` 的通用 cgi 捕获逻辑,去掉 `uploadvoice` 过滤,改成不设限一律记录 `com.tencent.mm.modelbase.l.a()` 的每次调用)验证还有没有其它内部二进制 cgi 调用可能是真正的提交: - **`/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`),跟贴图号发布无关,是这次会话的干扰项,不是漏网的真身)。 - **`/cgi-bin/micromsg-bin/newsendmsg`(1 次)**:时间戳对不上(14:18:59,早于 14:59 左右的真实发布窗口),是无关的背景消息活动,不是这次发布触发的。 - **`getphotoworks` / `get_me_tab_pho_items`(各 1-2 次,用 `w71.o1`/`w71.p1` 通用信封,跟 `mp/profile`/`mp/acct` 共用同一对协议类)**:命名和触发时机(打开"我的作品"入口附近)都像是刷新"作品列表",不是新建。 - 全程扫了所有 `reqUrl` 里的 `action=` 参数值,只有 `get_*`/`batch_report`/`getcomment`/`send_status` 这类读/上报语义,没有任何 `publish`/`post`/`create`/`submit` 字样。 ### 4.4 现状:提交发布的具体调用仍未定位,但基础设施已经完全打通 **没找到"是哪次调用真正创建了这条贴图号动态"**,但已经排除了目前想到的所有候选,且证实: 1. LSPosed 路线安全可靠(这次全程真机验证零故障),Frida spawn/attach 主进程那条路线(§3.6/3.7)**不再需要**——有更安全的替代方案。 2. `js1.m.c()` + `hookAnyCgiCapture`(通用 cgi 捕获)两个钩子都已在 `WeChatHook.java` 里长期驻留、可复用,改改过滤条件就能对着任意后续假设重新抓。 3. 真正的提交调用要么是:①落在我们观察窗口之外(用户操作比预想快,提交发生在按下"发布"瞬间,捕获时机没对齐——需要更精细的时间戳比对,或者干脆常驻抓一整晚不设窗口);②走了一条完全不同、不经过 `TransferRequestInfo`/`modelbase.l` 的原生路径(类似图片消息发送 `b80.u.oj` 那种非 h5transfer 的二进制上传通道,更可能,因为提交要带图片二进制数据,用一个设计给"文本 JSON reqBody"的桥接机制传大图不合常理)。 **下一步(未做,需要用户决策)**:如果要继续,建议方向是①把 `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 链路里找。 ### 4.5 第二个候选也证伪:图片 CDN 服务 `m11.l0.d1()` 没有触发 顺着 `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 安装无误、微信全程正常。 真人再走一遍完整发布流程(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 服务)都被真机数据证伪**。 ### 4.6 现状总结(截至本轮) 排除法进展: - ❌ `js1.m.c()`(brandservice/webview JS-API transfer 汇聚点)——只见只读态调用 - ❌ `Ljs1/m;` 对应的 wire cgi `bizh5webtransfer`——同上,没有提交语义 - ❌ `wxartrappsvr/route`(表面最高频)——反编译证实是小程序心跳/保活,无关 - ❌ `newsendmsg`——时间戳对不上,无关背景消息 - ❌ `m11.l0.d1()`(webview 图片上传 CDN 服务)——零触发 真机验证过程本身完全安全(LSPosed 全程零故障,跟用户此前文字/图片/语音发送逆向经验一致),但**核心问题仍未解决**:提交发布的真实调用点还没找到。可能性: 1. 提交发生在某个我们还没识别的第三条路径(Flutter 侧直接走 platform channel 调 native,完全绕开 `TransferRequestInfo`/`modelbase.l`/`m11.l0` 这三层已知桥接)。 2. 或者提交时机跟我们以为的不一致(比如"发布"按钮按下后有明显延迟才真正提交,我们的操作窗口理解有误差)。 本轮结束时决定:鉴于已经连续多轮真机验证都是负结果,且今天会话已经很长(6+ 轮真人发布配合 + 2 次 Frida 主进程事故 + 多轮 LSPosed 验证),暂停在此,留给用户决定是否继续投入,还是改走"半自动"方案(人工点发布,工具只管生成图片+文案)。 --- ## 五、改用 `am`+UI 自动化自主复现发布流程(2026-07-27 下午续) 用户提出关键改进:与其每轮都让用户在手机上手动操作、我在旁边被动等日志,不如**我自己直接用 `adb input tap`/`screencap` 驱动 UI**,全程自主复现"我的作品→贴图号→发表"整个流程,缩短每轮迭代的等待成本,也不再消耗用户注意力。 ### 5.1 UI 自动化关键坑:这台设备是可折叠机,`screencap` 默认拿错屏幕 Pixel 10 Pro Fold 有两块屏(outer/cover 屏 + inner 屏),`adb exec-out screencap -p` 不带 `-d` 会警告"multiple displays"并默认拿第一个找到的(`4619827677550801152`,这次测得是纯黑——大概率是没在用的那块屏,或者是折叠态下未激活的屏)。**必须显式指定 `-d `**:`dumpsys SurfaceFlinger --display-id` 列出两个 ID,`dumpsys display | grep mPrimaryDisplayDevice` 能看到当前哪个是 "Outer Display";这次机器当时是以 outer/cover 屏(1080×2364,display id `4619827677550801153`)在用,`input tap` 坐标也要按这块屏的分辨率算(截图工具返回图默认会整体缩放显示,需要按标注的缩放比例换算成真实坐标再传给 `input tap`)。 ### 5.2 自主复现全流程成功(无需用户参与),确认 `wxartrappsvr/route` 也不是提交点 自主完整跑通"我→作品→(贴图号)发表→选图→下一步→填标题/正文→发表"全程(用中性的 AI 日报图,不涉真人照片),全程无需用户操作。两轮独立发布均确认**服务器真的接受了新文章**(`js1.m.c()` 抓到的预览页 URL 里 `chksm=` 每轮都不同,且发布后能在"作品"列表里看到新帖子),排除了"根本没发出去,我们看错了"这种可能。 针对上一轮(§4)里最高频但内容读不出来的 `wxartrappsvr/route`(`r45.zt5`),本轮解开了它为什么读不出来的谜:**它的 payload 不是扁平字段,是一个 `LinkedList`**(`zt5.d` 字段),每个 `re7{d=type int, e=protobuf.g 装的 body 字节串, f=route/action 字符串}`——用之前"扫描字段 0-25 的 getString/getInteger/getLong"的通用逻辑天生看不到 List 类型字段,这是之前"内容都是空"的真正原因,不是"这条路真的没数据"。 给 `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`。**这不是提交发布的调用,是小程序自身运行时的埋点上报,跟贴图号提交内容无关。** ### 5.3 意外发现:图片编辑步骤本身是一个小程序(`appid=wx0b2ec076ec39c786`) 这是本轮最有价值的新信息:从截图流程看,选完图后有一步"编辑"(`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)。 ### 5.4 现状(本轮结束):三个候选全部证伪,第四个新线索(小程序 image_editor)尚未跟进 排除法进展更新: - ❌ `js1.m.c()` / `bizh5webtransfer` —— 只读态 - ❌ `m11.l0.d1()` (CDN 上传服务)—— 零触发 - ❌ `wxartrappsvr/route`(`r45.zt5`/`re7`)—— 确认是小程序埋点上报,无关 - 🆕 图片编辑步骤是小程序 `wx0b2ec076ec39c786`,可能是下一个该深挖的方向,但还没有具体行动 用户配合方式已改进(不再需要用户手动操作,可自主复现),今天已知的三条候选路径均已用真机数据证伪,核心提交调用仍未定位。建议下次接着来的方向:直接搜这个小程序 appid 相关的原生桥接代码(`k2`/`y7` 之外,可能还有别的小程序专属 jsapi 处理提交),或者接受当前"半自动"退路。 --- ## 四、现场状态(写这份记录时) - 设备上所有 mitm/iptables/CA 改动均已清理(见上文事故记录),微信网络恢复正常。 - 设备上遗留:`~/wechat_apk/`(含 base.apk + decompiled/,可保留复用,不小,按需清理)。 - 本次会话未留任何后台 frida/mitm 进程在跑(已逐一 kill 确认)。 - `wechat_send_internal/hook_lite_transfer.js` 已提交到仓库(本次新增,还未 git add/commit, 待用户决定是否连同这份记录一起提交)。