WECHAT_SEND_API.md 7.92 KB

微信 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 <wxid> --es text <文字> 或 --es text_b64 <base64>
语音(TTS→SILK) ✅ 半自动 hot-swap(真机验证投递) ARM_VOICE_SWAP --es src_path <SILK> --ei duration <ms> + 触发录音
图片 🟢 消息建成+缩略图渲染+进管线,全图 CDN 上传是最后一公里 SEND_IMAGE_INTERNAL --es to <wxid> --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
# 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:

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 <cls> --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. 文字发送 ✅

用法

# 英文/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 上传是最后一公里)

用法(已能发出图片消息+缩略图,全图上传待完成)

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)     进度回调

关键数据结构(字段都是单字母,混淆)

o70.k(r6 preBuildFolder)          // 批量发送对象
  a = r6 folder
  b = UUID clientId (auto)
  c = talker (toUserName)
  d = source (真实用 3)
  f = compressType
  g = List<o70.i>                 // 图片列表
  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)。

构建 / 部署

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 端到端脚本