# 微信 8.0.74 内部发送 API 逆向全景(文字 / 语音 / 图片) > 通过 LSPosed 模块 `wechathook` 调用微信自己的内部发送 API,**无 UI 模拟点击**。 > 逆向自微信 **8.0.74(versionCode 3120)** on Pixel(Android 16 / A16)。 > 源码 `wechathook_src/WeChatHook.java`,构建 `rebuild_wechathook.sh`,发送 CLI `wechat_send.sh`。 ## TL;DR 能力矩阵 | 能力 | 状态 | 广播接口 | |---|---|---| | **文字(中英文)** | ✅ 完全可用,真机验证送达 | `SEND_MSG_INTERNAL --es to --es text <文字>` 或 `--es text_b64 ` | | **语音(TTS→SILK)** | ✅ 半自动 hot-swap(真机验证投递) | `ARM_VOICE_SWAP --es src_path --ei duration ` + 触发录音 | | **图片** | 🟢 消息建成+缩略图渲染+进管线,全图 CDN 上传是最后一公里 | `SEND_IMAGE_INTERNAL --es to --es path <路径>` | --- ## 0. 通用基础设施 ### 0.1 wxid 解析(nickname → username) 内部 API 认 **wxid(username)**,不是昵称。从解密的 `EnMicroMsg.db` 查: ``` key = MD5("1234567890ABCDEF" + uin)[:7] # imei 用微信默认值 本机: uin=215004919 → key=e03ce81 SQLCipher: cipher_compatibility=1, cipher_page_size=1024 uin 来源: /data/data/com.tencent.mm/shared_prefs/*.xml 里 last_login_uin ``` ```bash # Mac: brew install sqlcipher sqlcipher EnMicroMsg.db > PRAGMA key='e03ce81'; PRAGMA cipher_compatibility=1; PRAGMA cipher_page_size=1024; > SELECT username,nickname FROM rcontact WHERE nickname LIKE '%火星人%'; # → yibooo | 火星人 ``` 解密逻辑完整实现见 `lifelog-recorder/.../vision/WeChatDb.kt`。 ### 0.2 Tinker classloader 坑(关键) 微信真正跑的代码在 **Tinker 补丁** 里(`/data/user/0/com.tencent.mm/tinker/patch-*/dex/tinker_classN.apk`,~174MB), **混淆字段名与 base.apk 不同**。反编译要拉 tinker dex: ```bash adb shell su -c 'cp /data/user/0/com.tencent.mm/tinker/patch-*/dex/tinker_classN.apk /sdcard/tinkerN.apk' adb pull /sdcard/tinkerN.apk jadx --single-class --no-res -d out tinkerN.apk # 定位含某字符串的类:dexdump -d classesN.dex | 解析 Class descriptor + const-string ``` **hook 必须在 `sTinkerCL` 上做**(真实发送跑在 tinker CL,base CL hook 不触发)。 `App.onCreate` 里对每个 hook 都要 `hookXxx(sTinkerCL, procName+"/tinker")`。 ### 0.3 验证陷阱 - `am force-stop com.tencent.mm` 是 SIGKILL,**不 flush WCDB 加密 WAL**;sqlcipher 读不了加密 WAL → force-stop 后查主库永远停在旧 checkpoint。**验证送达靠实时 UI 截图 / 肉眼确认,别靠 force-stop 后查库**。 - LSPosed 日志文件会滚动(大 dump 触发),grep 前先 `ls -t` 找当前文件。 --- ## 1. 文字发送 ✅ ### 用法 ```bash # 英文/ASCII adb shell su -c 'am broadcast -a com.hermes.wechat.SEND_MSG_INTERNAL --es to yibooo --es text "hello"' # 中文(base64 避免 shell 损坏 UTF-8) B64=$(python3 -c "import base64;print(base64.b64encode('你好火星人'.encode()).decode())") adb shell su -c "am broadcast -a com.hermes.wechat.SEND_MSG_INTERNAL --es to yibooo --es text_b64 '$B64'" ``` ### 内部实现(`sendViaHighLevel`) ``` w11.r0(talker, content, type=1, flag=0, Object=null) # Object-构造器插入 f9 → com.tencent.mm.modelbase.z2.b(scene) # 入 NetSceneQueue(真实网络派发) ``` **关键点 / 踩过的坑**: - 必须用 **Object-构造器**(第5参 Object);long-构造器和 builder 路径 log OK 但**不入库**。 - `dk5.s5.Ni` **是发名片的**(`db.b` = `com.tencent.mm.ui.contact.db` 联系人卡片生成器),不是发文字! 用它会得到 type=42 空名片失败消息。别用。 - `w11.s1.b(IMAGE/TEXT,...)` 4参形式建的 r1 不完整,`n1.a()` NPE。别用于文字。 - `z2.b(m1)` 是 `IGetNetSceneQueue`,内部 `.d(scene)` 入队,绕开硬编码混淆字段名。 --- ## 2. 语音发送 ✅(半自动 hot-swap) 纯合成(newsendmsg/r0/z2.b)永远卡「发送中」——音频上传锁死在 `jg0.x.w` Kotlin 协程里。 **绕过方案**:让微信真录一段,在 CDN 上传步偷换 SILK 字节。 ``` 偷换点 = jg0.x.w(self, qi3.d0, w21.w0, int type=4, int durMs, w21.t silk, Continuation) arg5 w21.t 只有一个 byte[] 字段 = 明文 SILK 音频 广播 ARM_VOICE_SWAP 装弹 → input swipe 触发录音键长按 → 偷换在 hookVoiceProbe 的 w 分支 ``` 完整细节见 memory `wechat-voice-send-internals.md` 和 `tts_voice_swap.sh`。 --- ## 3. 图片发送 🟢(消息建成,CDN 上传是最后一公里) ### 用法(已能发出图片消息+缩略图,全图上传待完成) ```bash adb shell su -c 'am broadcast -a com.hermes.wechat.SEND_IMAGE_INTERNAL --es to filehelper --es path /sdcard/Pictures/x.png' ``` ### 完整发图链路(hook 真实发送三次抓取 + tinkerN.apk 反编译) ``` 真实发图 insert 栈: b80.u.oj(o70.k) ← 插库+发送 (返 ArrayList) ← b80.s.invokeSuspend ← = MicroMsg.ImgUpload.SendImgPreFeatureService (只做 prebuild) ← SendImgProxyUI / gallery 图片插件 b80.u (implements o70.e, = i95.n0.c(o70.e.class)): pj(o70.h) prebuild: 压缩 + 缩略图, stage 进 r6 文件夹 Bi(o70.h, String) 文件存在检查 (返 boolean) oj(o70.k) 插库 + 发送 (返 ArrayList) nj(o70.h, int) 进度回调 ``` ### 关键数据结构(字段都是单字母,混淆) ```java o70.k(r6 preBuildFolder) // 批量发送对象 a = r6 folder b = UUID clientId (auto) c = talker (toUserName) d = source (真实用 3) f = compressType g = List // 图片列表 i = HashMap extras o70.i(String imgPath, Uri, Long imgId) // 单张图片 a = 路径, b = Uri, c = imgId o70.h(r6) // PreBuildParams (pj 的输入) e = imgPath, g = talker, k = Uri, b(方法) = clientId c = source, f = imgId, h = compressType r6 folder = lp0.b.h0(gm0.j1.b().j() + "_MsgImgUploadFSC") // = g80.o.c7() ``` ### 已实现的重放(`sendImageViaAPI`) ``` 取 b80.u 插件 + r6 folder → 建 o70.k{c=talker, d=3, g=[o70.i(path, fileUri, 0)]} → 建 o70.h (同步字段: e=path, g=talker, k=fileUri, c=3, clientId) → Bi(h,"thumb.jpg") 否则 pj(h) + nj(h,10000) # prebuild → oj(k) # 插库+发送 真机: oj 返回 size=1, convertTo msgtype=3 触发, pt0.e0.h 触发, 缩略图渲染 ✅ 但全图 CDN 上传转圈不完成 ❌ ``` ### 剩余:图片 CDN 上传最后一公里 - `b80.s` 只是 prebuild service(`checkPreBuildImg`),**不含 oj**;图片字节 CDN 上传是独立异步服务(log tag `MicroMsg.ImgUpload`)。我们同步 pj+oj 没把它拉起来 / 没跑完。 - 试过 uri=null→fileUri、source 0→3,均未解决。imgId=0(真实是 MediaStore id)。 - **下一步**:① hook `MicroMsg.ImgUpload` 上传 NetScene(像语音摸 `w21.g1` 那样)看真实发图怎么触发上传;② 或让流程跑在 worker 线程/协程(真实 prebuild 在 worker,我们是同步 main);③ 或 imgId 用真实 MediaStore id(把文件插 MediaStore 拿 content://+id)。 --- ## 构建 / 部署 ```bash export ADB=/opt/homebrew/share/android-commandlinetools/platform-tools/adb export ANDROID_HOME=/opt/homebrew/share/android-commandlinetools export JAVA_HOME=/opt/homebrew/opt/openjdk@17 bash rebuild_wechathook.sh # 编译 + d8 + 签名 + pm install -r + 重启微信(无需 reboot) ``` ⚠️ 反复 `pm install -r` 模块可能搞断 LSPosed 注入 → 重启设备 + `pm install-existing com.tencent.mm` 修复(非破坏性,不掉登录)。 ## 相关文件 - `wechathook_src/WeChatHook.java` — hook 源码(2981 行,文字/语音/图片全在里面) - `backups/WeChatHook_*_text+voice+image.java` — 本文档对应的代码快照 - memory `wechat-voice-send-internals.md` — 更细的逆向记录 + 踩坑史 - `tts_voice_swap.sh` — 语音 hot-swap 端到端脚本