diff --git a/wechat_send_internal/BRANDSERVICE_PUBLISH_RESEARCH.md b/wechat_send_internal/BRANDSERVICE_PUBLISH_RESEARCH.md index bcc611e..1d7cd24 100644 --- a/wechat_send_internal/BRANDSERVICE_PUBLISH_RESEARCH.md +++ b/wechat_send_internal/BRANDSERVICE_PUBLISH_RESEARCH.md @@ -239,6 +239,66 @@ API 用)。 --- +## 四、路线修正:改用 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 验证),暂停在此,留给用户决定是否继续投入,还是改走"半自动"方案(人工点发布,工具只管生成图片+文案)。 + +--- + ## 四、现场状态(写这份记录时) - 设备上所有 mitm/iptables/CA 改动均已清理(见上文事故记录),微信网络恢复正常。 diff --git a/xhs_lsposed_a16_fix/wechathook_src/WeChatHook.java b/xhs_lsposed_a16_fix/wechathook_src/WeChatHook.java index c27bc7a..92d0810 100644 --- a/xhs_lsposed_a16_fix/wechathook_src/WeChatHook.java +++ b/xhs_lsposed_a16_fix/wechathook_src/WeChatHook.java @@ -183,6 +183,23 @@ public class WeChatHook implements IXposedHookLoadPackage { // Capture the real uploadvoice cgi request (this device uses inline-cgi, not CDN). hookVoiceCgiCapture(cl, lpparam.processName); hookVoiceCgiResult(cl, lpparam.processName); + + // 贴图号发布逆向(wechat_send_internal/BRANDSERVICE_PUBLISH_RESEARCH.md §3.5): + // both the mini-program (k2/y7) path and the WebView "luggage" JS-API path + // funnel into this one sink before dispatch (local coroutine or IPC to + // PROCESS_MAIN). Read-only: dumps TransferRequestInfo/y24 fields, does not + // touch args or return value. LSPosed-installed (Zygote-time), NOT a live + // Frida spawn/attach — that approach broke WeChat's main-process cold start + // twice in one session (see doc §3.6/3.7); this hook exists precisely to + // avoid repeating that mistake. + hookBrandservicePublishTransfer(cl, lpparam.processName); + // Broad catch-all: js1.m.c() only ever saw plain webview traffic for this flow + // (§3.9), so also watch the generic internal-cgi builder unfiltered in case the + // real publish submission is a native NetScene call instead. + hookAnyCgiCapture(cl, lpparam.processName); + // Image submission for brandservice: js1.m.c()/bizh5webtransfer never showed a + // submit call, traced instead to a separate CDN service (§4 of doc). + hookBrandserviceCdnUpload(cl, lpparam.processName); } // ── WCDB hook ────────────────────────────────────────────────────────────── @@ -1525,6 +1542,79 @@ public class WeChatHook implements IXposedHookLoadPackage { } catch (Throwable t) { XposedBridge.log("[WeChatHook] hookVoiceCgiResult failed in " + procName + ": " + t); } } + // ── 贴图号发布 sink: js1.m.c(TransferRequestInfo, zq1.g0) ──────────────────── + //反编译确认(见 BRANDSERVICE_PUBLISH_RESEARCH.md §3.5): brandservice/webview + // 的 luggage JS-API 层(com.tencent.mm.plugin.webview.luggage.jsapi.c.d())和 + // 小程序侧的 k2/y7 都把 reqUrl/reqBody/cgiCmdId 填进 TransferRequestInfo 后调 + // 这一个方法。字段映射: TransferRequestInfo{d=type, e=h5Url, f=scope, h=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})}。 + private static void dumpFieldsByName(Object obj, String[] names, StringBuilder sb, String indent) { + if (obj == null) { sb.append(indent).append("(null)\n"); return; } + for (String name : names) { + try { + java.lang.reflect.Field f = obj.getClass().getDeclaredField(name); + f.setAccessible(true); + Object v = f.get(obj); + String vs = v == null ? "null" : v.toString(); + if (vs.length() > 4000) vs = vs.substring(0, 4000) + "…(truncated)"; + sb.append(indent).append(name).append(" = ").append(vs).append("\n"); + } catch (Throwable t) { + sb.append(indent).append(name).append(" = \n"); + } + } + } + + private void hookBrandservicePublishTransfer(ClassLoader cl, String procName) { + try { + Class sinkCls = XposedHelpers.findClass("js1.m", cl); + final Class reqInfoCls = XposedHelpers.findClass( + "com.tencent.mm.plugin.brandservice.api.TransferRequestInfo", cl); + int hooked = 0; + for (java.lang.reflect.Method m : sinkCls.getDeclaredMethods()) { + Class[] pt = m.getParameterTypes(); + if (pt.length != 2 || !reqInfoCls.isAssignableFrom(pt[0])) continue; + m.setAccessible(true); + XposedBridge.hookMethod(m, new XC_MethodHook() { + @Override protected void beforeHookedMethod(MethodHookParam p) { + try { + Object reqInfo = p.args[0]; + StringBuilder sb = new StringBuilder( + "[WeChatHook] BRANDSVC-SINK js1.m.c() called in " + procName + "\n"); + if (reqInfo == null) { sb.append(" reqInfo = null\n"); XposedBridge.log(sb.toString()); return; } + dumpFieldsByName(reqInfo, new String[]{"d", "e", "f", "n", "i"}, sb, " "); + Object h; + try { + java.lang.reflect.Field hf = reqInfo.getClass().getDeclaredField("h"); + hf.setAccessible(true); + h = hf.get(reqInfo); + } catch (Throwable t) { h = null; } + if (h == null) { + sb.append(" h (y24, reqUrl/reqBody/cgiCmdId container) = null\n"); + } else { + sb.append(" h (y24):\n"); + dumpFieldsByName(h, new String[]{"d", "e", "f", "h"}, sb, " "); + try { + java.lang.reflect.Field gf = h.getClass().getDeclaredField("g"); + gf.setAccessible(true); + Object headers = gf.get(h); + sb.append(" g (headers) = ").append(headers == null ? "null" : headers.toString()).append("\n"); + } catch (Throwable t) { sb.append(" g (headers) = \n"); } + } + XposedBridge.log(sb.toString()); + } catch (Throwable t) { + XposedBridge.log("[WeChatHook] BRANDSVC-SINK dump err: " + t); + } + } + }); + hooked++; + } + XposedBridge.log("[WeChatHook] hookBrandservicePublishTransfer: hooked js1.m.c x" + hooked + " in " + procName); + } catch (Throwable t) { + XposedBridge.log("[WeChatHook] hookBrandservicePublishTransfer failed in " + procName + ": " + t); + } + } + private void hookVoiceCgiCapture(ClassLoader cl, String procName) { try { Class lCls = XposedHelpers.findClass("com.tencent.mm.modelbase.l", cl); @@ -1573,6 +1663,118 @@ public class WeChatHook implements IXposedHookLoadPackage { } catch (Throwable t) { XposedBridge.log("[WeChatHook] hookVoiceCgiCapture failed in " + procName + ": " + t); } } + // 贴图号发布探测: js1.m.c() 抓到的全是 mp.weixin.qq.com 网页态调用(真实测过,见 + // BRANDSERVICE_PUBLISH_RESEARCH.md §3.9),没有一次带 protobuf reqBody/cgiCmdId 的—— + // 说明真正的"提交发布"根本不走 brandservice/webview 那条 JS-API 桥,大概率是原生侧 + // (Flutter platform channel 之后的 Kotlin/Java 代码)直接建的内部 cgi 调用,类似文字/ + // 语音发送那条 NetSceneQueue 路子。与其继续在混淆的 Flutter 桥接代码里找具体是哪个类, + // 直接照抄 hookVoiceCgiCapture 但去掉 "uploadvoice" 过滤——modelbase.l.a() 是所有内部 + // 二进制 cgi 调用的公共构造入口,不管发起方是谁,只要真是这条路子就跑不掉这个方法。 + private void hookAnyCgiCapture(ClassLoader cl, String procName) { + try { + Class lCls = XposedHelpers.findClass("com.tencent.mm.modelbase.l", cl); + final Class pbCls = XposedHelpers.findClass("com.tencent.mm.protobuf.f", cl); + XposedBridge.hookMethod(lCls.getDeclaredMethod("a"), new XC_MethodHook() { + @Override protected void beforeHookedMethod(MethodHookParam p) { + try { + Object self = p.thisObject; + String url = null; + java.util.List pbs = new java.util.ArrayList<>(); + for (Class c = self.getClass(); c != null && !c.equals(Object.class); c = c.getSuperclass()) { + for (java.lang.reflect.Field f : c.getDeclaredFields()) { + if (java.lang.reflect.Modifier.isStatic(f.getModifiers())) continue; + f.setAccessible(true); + Object v; try { v = f.get(self); } catch (Throwable t) { continue; } + if (v instanceof String && ((String) v).contains("cgi-bin")) url = (String) v; + else if (v != null && pbCls.isInstance(v)) pbs.add(v); + } + } + if (url == null) return; // no gate otherwise — log every internal cgi call + StringBuilder sb = new StringBuilder("[WeChatHook] ANYCGI url=" + url + " pbCount=" + pbs.size()); + int idx = 0; + for (Object req : pbs) { + sb.append("\n -- pb#").append(idx++).append(" ").append(req.getClass().getName()).append(" --"); + for (int i = 0; i <= 25; i++) { + Object val = null; String kind = ""; + try { Object c = XposedHelpers.callMethod(req, "getCustom", i); + if (c != null) { val = "custom:" + c.getClass().getName(); kind = "C"; } } catch (Throwable t) {} + if (val == null) { try { Object s = XposedHelpers.callMethod(req, "getString", i); if (s != null) { val = s; kind = "S"; } } catch (Throwable t) {} } + if (val == null) { try { Object iv = XposedHelpers.callMethod(req, "getInteger", i); if (iv != null && ((Integer) iv) != 0) { val = iv; kind = "I"; } } catch (Throwable t) {} } + if (val == null) { try { Object lv = XposedHelpers.callMethod(req, "getLong", i); if (lv != null && ((Long) lv) != 0L) { val = lv; kind = "L"; } } catch (Throwable t) {} } + if (val != null) { + String vs = String.valueOf(val); + if (vs.length() > 200) vs = vs.substring(0, 200) + "…"; + sb.append("\n [").append(i).append("](").append(kind).append(")=").append(vs); + } + } + } + XposedBridge.log(sb.toString()); + } catch (Throwable t) { XposedBridge.log("[WeChatHook] ANYCGI err: " + t); } + } + }); + XposedBridge.log("[WeChatHook] hookAnyCgiCapture: hooked modelbase.l.a() (unfiltered) in " + procName); + } catch (Throwable t) { XposedBridge.log("[WeChatHook] hookAnyCgiCapture failed in " + procName + ": " + t); } + } + + // 贴图号发布图片提交(BRANDSERVICE_PUBLISH_RESEARCH.md §4):js1.m.c()/bizh5webtransfer + // 那条纯文本 transfer 链路里从没见过提交调用——反编译顺着 + // LuggageUploadMediaFileManager$UploadMediaFileTask.z() 往下追,图片走的是完全独立的 + // CDN 提交服务:Lm11/l0;->d1(Lk70/h0;ZJ)Lm11/b0;(m11.l0 单例经 Lm11/b1;->Di() 拿到, + // 跟消息发图 v70.w.oj/b80.u.oj 是两套不同的服务)。k70.h0 = 提交参数(本地文件路径等), + // m11.b0 = CDN 提交结果(约 40 个混淆字段,字段语义未知,先全量 dump 出来看)。 + private static void dumpAllFields(Object obj, StringBuilder sb, String indent) { + if (obj == null) { sb.append(indent).append("(null)\n"); return; } + for (java.lang.reflect.Field f : obj.getClass().getDeclaredFields()) { + if (java.lang.reflect.Modifier.isStatic(f.getModifiers())) continue; + try { + f.setAccessible(true); + Object v = f.get(obj); + String vs = v == null ? "null" : v.toString(); + if (vs.length() > 300) vs = vs.substring(0, 300) + "…"; + sb.append(indent).append(f.getName()).append(" (").append(f.getType().getSimpleName()) + .append(") = ").append(vs).append("\n"); + } catch (Throwable t) { + sb.append(indent).append(f.getName()).append(" = \n"); + } + } + } + + private void hookBrandserviceCdnUpload(ClassLoader cl, String procName) { + try { + Class l0Cls = XposedHelpers.findClass("m11.l0", cl); + final Class h0Cls = XposedHelpers.findClass("k70.h0", cl); + int hooked = 0; + for (java.lang.reflect.Method m : l0Cls.getDeclaredMethods()) { + if (!"d1".equals(m.getName())) continue; + Class[] pt = m.getParameterTypes(); + if (pt.length == 0 || !h0Cls.isAssignableFrom(pt[0])) continue; + m.setAccessible(true); + XposedBridge.hookMethod(m, new XC_MethodHook() { + @Override protected void beforeHookedMethod(MethodHookParam p) { + try { + StringBuilder sb = new StringBuilder( + "[WeChatHook] BRANDSVC-CDN m11.l0.d1() called in " + procName + "\n k70.h0 params:\n"); + dumpAllFields(p.args[0], sb, " "); + XposedBridge.log(sb.toString()); + } catch (Throwable t) { XposedBridge.log("[WeChatHook] BRANDSVC-CDN before err: " + t); } + } + @Override protected void afterHookedMethod(MethodHookParam p) { + try { + StringBuilder sb = new StringBuilder( + "[WeChatHook] BRANDSVC-CDN m11.l0.d1() returned in " + procName + "\n m11.b0 result:\n"); + dumpAllFields(p.getResult(), sb, " "); + XposedBridge.log(sb.toString()); + } catch (Throwable t) { XposedBridge.log("[WeChatHook] BRANDSVC-CDN after err: " + t); } + } + }); + hooked++; + } + XposedBridge.log("[WeChatHook] hookBrandserviceCdnUpload: hooked m11.l0.d1 x" + hooked + " in " + procName); + } catch (Throwable t) { + XposedBridge.log("[WeChatHook] hookBrandserviceCdnUpload failed in " + procName + ": " + t); + } + } + private void hookVoiceUpload(ClassLoader cl, String procName) { try { Class g1 = XposedHelpers.findClass("w21.g1", cl); @@ -3050,6 +3252,12 @@ public class WeChatHook implements IXposedHookLoadPackage { // Capture the real uploadvoice cgi request in Tinker CL. hookVoiceCgiCapture(sTinkerCL, procName + "/tinker"); hookVoiceCgiResult(sTinkerCL, procName + "/tinker"); + // js1.m may live in a Tinker DEX too (base-CL hook installed + // but confirmed it never fires — same "base CL vs Tinker CL" + // gotcha as f9/w11.s1/jg0.x above). Re-hook here. + hookBrandservicePublishTransfer(sTinkerCL, procName + "/tinker"); + hookAnyCgiCapture(sTinkerCL, procName + "/tinker"); + hookBrandserviceCdnUpload(sTinkerCL, procName + "/tinker"); // Pad-login: the login UIs (LoginPasswordUI/LoginHistoryUI/ // LoginUI) + bk run in Tinker CL — the base-CL hooks never // fire (0 N-VISIBLE). Register them here so the "以平板登录"