贴图号自动发布日报 — 逆向进度记录(进行中)
目标:把 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/<hex>——
外层 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<cw3> 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 现状:提交发布的具体调用仍未定位,但基础设施已经完全打通
没找到"是哪次调用真正创建了这条贴图号动态",但已经排除了目前想到的所有候选,且证实:
- LSPosed 路线安全可靠(这次全程真机验证零故障),Frida spawn/attach 主进程那条路线(§3.6/3.7)不再需要——有更安全的替代方案。
js1.m.c()+hookAnyCgiCapture(通用 cgi 捕获)两个钩子都已在WeChatHook.java里长期驻留、可复用,改改过滤条件就能对着任意后续假设重新抓。- 真正的提交调用要么是:①落在我们观察窗口之外(用户操作比预想快,提交发生在按下"发布"瞬间,捕获时机没对齐——需要更精细的时间戳比对,或者干脆常驻抓一整晚不设窗口);②走了一条完全不同、不经过
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 cgibizh5webtransfer——同上,没有提交语义 - ❌
wxartrappsvr/route(表面最高频)——反编译证实是小程序心跳/保活,无关 - ❌
newsendmsg——时间戳对不上,无关背景消息 - ❌
m11.l0.d1()(webview 图片上传 CDN 服务)——零触发
真机验证过程本身完全安全(LSPosed 全程零故障,跟用户此前文字/图片/语音发送逆向经验一致),但核心问题仍未解决:提交发布的真实调用点还没找到。可能性:
- 提交发生在某个我们还没识别的第三条路径(Flutter 侧直接走 platform channel 调 native,完全绕开
TransferRequestInfo/modelbase.l/m11.l0这三层已知桥接)。 - 或者提交时机跟我们以为的不一致(比如"发布"按钮按下后有明显延迟才真正提交,我们的操作窗口理解有误差)。
本轮结束时决定:鉴于已经连续多轮真机验证都是负结果,且今天会话已经很长(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 <display-id>: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<r45.re7>(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, 待用户决定是否连同这份记录一起提交)。