微信 MMTLS——抓包工具看不见的那条 80 端口连接,一球剥一层
代理抓包系列 · 第 4/4 篇
上一篇:《mitmweb——在浏览器里点着用 mitmproxy》
开头:工具箱齐了,微信还是一团黑
Proxifier 能把不认代理的程序押送到管子里,mitmproxy / mitmweb 把标准 HTTPS 变成了玻璃管。工具箱齐了,拿最常用的那个 App 练手——微信:
- mitmproxy / mitmweb 的流量列表里,微信的请求一条都不出现;
- 抓包工具里能看到微信在收发数据,但打开全是无意义的二进制;
- 想给它配代理?微信自己没有代理设置,真要走代理得靠 Proxifier 在外面硬拦——拦下来也进不了 mitmproxy 的眼。
浏览器、curl、绝大多数 App 都能开膛,为什么偏偏微信是铁板一块?
根因一句话:微信客户端和服务器之间跑的不是 TLS,是腾讯自家的 MMTLS——一个以 TLS 1.3 为底稿改出来的私有协议。mitmproxy 的全部手艺(CONNECT 隧道、现场签假证书、两头各握一次手)都建立在「客户端会说标准 TLS」这个前提上;前提没了,手艺再好也无从下手。
本篇不先背概念。实验从头到尾只有一条故事:本机微信此刻正开着的那条 TCP 连接。找到它、拍下它、逐字节拆它,每一球剥一层:
| 雪球 | 这一球解开的 | 当场能看见的效果 |
|---|---|---|
| 1 | 微信的网络骨架:一条长连接 | netstat 里微信主进程全网只有一条外连,端口竟是 80 |
| 2 | 线上跑的不是 TLS | 抓包里微信的包全以 17 f1 04 开头,而 TLS 以 16 03 开头 |
| 3 | 标准 TLS 1.3 长什么样 | openssl 对 github 一次成功握手;对微信服务器,对面直接挂断 |
| 4 | MMTLS 是什么、为什么造 | 记录头对照表;2016 年的三个动机 |
| 5 | 记录层怎么切 | Python 走通每一条记录的边界;规律性等长交换现形 |
| 6 | 它抄了哪些标准件 | 亲手跑 P-256 ECDH → HKDF → AES-128-GCM,与真实字节对上 |
| 7 | 组装方式哪里不同 | 两阶段握手、双 PSK、0-RTT、双层加密的整张图 |
| 8 | 安全账本 | Citizen Lab 的两条弱点、腾讯的回应、「到底安不安全」 |
| 9 | 收网:为什么两件套失效 | 三个前提逐一对照;想研究的人该去哪里 |
环境指纹(文中输出均来自本机实跑;协议内部结论注明了出处):
- Windows 10 Enterprise LTSC 2021(10.0.19044)+ Git Bash
- 微信 Windows 版 4.1.12.26(
H:\Program Files\Tencent\Weixin\Weixin.exe) - Wireshark/TShark 4.4.2、OpenSSL 3.5.7(Git Bash 自带)、Python 3.11.15 + cryptography 46.0.7
雪球 1:找到那条线——微信全网只有一条外连
先回答「抓谁」。微信不是浏览器,没有一目了然的域名可看;用进程视角扫一遍。先拿微信主进程的 PID(tasklist,本机输出):
Weixin.exe 13484 Console 1 315,288 K
Weixin.exe 41312 Console 1 56,580 K
...一串 Weixin.exe 里,内存最大的那个(13484)是主进程(其余是渲染、小程序等子进程;PID 每次开机都会变)。看它此刻的对外连接(Git Bash):
netstat -ano | awk '$NF==13484' | grep -v "127.0.0.1"本机输出,就一行:
TCP 172.16.248.125:27156 117.89.177.107:80 ESTABLISHED 13484三个值得瞪一眼的地方:
- 全网只有一条外连 TCP——收消息、发消息、朋友圈、小程序,此刻全押在这一条连接上;
- 端口是 80,不是 HTTPS 的 443;
- 对端
117.89.177.107是谁?解析一下long.weixin.qq.com(本机输出):
101.91.37.18
117.89.177.97
101.226.142.218同段机房。这条连接的官方名字叫 longlink(长连接):App 运行期间一直保持、服务器靠它把新消息推下来的通道。与之相对的 shortlink(短连接)是「一问一答就挂断」的 HTTP POST 式通道,用的时候才建——所以此刻 netstat 里看不到。
「长连接」第一次出现,钉个模型:不挂断的电话线 vs 挂断重拨的电话。微信收消息不轮询、不刷新,就是靠这条不挂断的线上服务器主动「推」。Citizen Lab 论文里的分工:Longlink 承载推送与保活,Shortlink 承载一次性请求。
线找到了。下一球把它拍下来看。
雪球 2:拍下它——17 f1 04,第一眼就不是 TLS
Wireshark 上场,只抓这条线,抓 150 秒(-i 5 是本机网卡编号,tshark -D 可查你的;-n 表示不要把 IP 反解成机器名,输出更干净):
tshark -i 5 -f "host 117.89.177.107" -a duration:150 -w wechat-longlink.pcapng150 秒里抓到了什么?把所有带载荷的帧的原始字节倒出来看前几个(-T fields -e tcp.payload 直接吐十六进制载荷):
tshark -r wechat-longlink.pcapng -n -Y "tcp.len>0" \
-T fields -e frame.time_relative -e tcp.srcport -e tcp.payload本机输出(10 帧中节选 6 帧,每帧只展示开头 8 字节;全量对账在雪球 5):
0.00 27156(→服务器) 17 f1 04 01 a2 45 c8 ...
0.08 80(→本机) 17 f1 04 00 58 39 06 ...
30.88 27156(→服务器) 17 f1 04 01 82 f6 3d ...
30.95 80(→本机) 17 f1 04 01 7a 05 25 ...
90.89 27156(→服务器) 17 f1 04 01 82 d6 3d ...
90.95 80(→本机) 17 f1 04 01 7a 30 3e ...每一帧、双向、无一例外,前三个字节都是 17 f1 04。
拿什么证明「这不是 TLS」?不用远求,铁证就在同一份抓包里——刚才抓包期间我用 openssl 探测过微信服务器(下一球详述),那次探测的帧也在文件里,它的开头是:
16 03 01 05 c8 ...对照着钉死:TLS 的每条记录头三个字节是「类型 + 版本」——类型 0x16=握手、0x17=应用数据;版本 03 01/03 03 是 TLS 自己的版本号。我那个 openssl 包 16 03 01 是一条标准 TLS 握手记录;微信的包类型字节同样是 0x17(应用数据),但版本字节是 f1 04——TLS 的版本表里从来没有这个值。
标准 TLS 记录头: 微信 MMTLS 记录头(本机实测):
┌──────┬─────────┐ ┌──────┬─────────┐
│ 类型 │ 03 xx │ │ 类型 │ f1 04 │
│ 1字节 │ 版本2字节│ │ 1字节 │ 版本2字节│
└──────┴─────────┘ └──────┴─────────┘
16 03 01 = TLS 握手 17 f1 04 = ?(雪球 4 揭晓)
17 03 03 = TLS 应用数据顺手一个佐证:让 Wireshark 自己认——tshark -G protocols 里搜 mmtls,0 个结果。Wireshark 4.4.2 至今没有 MMTLS 解析器,这些字节在它眼里就是裸 TCP 数据。(TLS 帧它会显示 TLSv1.3 Record Layer 之类的协议名,一屏就能看出区别。)
疑问升级了:类型字节 0x17 明明是 TLS 应用数据的编号,版本却不是 TLS——这协议跟 TLS 什么关系?先做个对照实验,看看「正主」长什么样。
雪球 3:标准 TLS 1.3 长什么样——以及微信服务器怎么对待它
两个命令,一通一断。
通的: 对一个支持 TLS 1.3 的站点握手,-brief 只要摘要:
openssl s_client -connect github.com:443 -tls1_3 -brief </dev/null本机输出(略去签名算法与证书验证两行):
CONNECTION ESTABLISHED
Protocol version: TLSv1.3
Ciphersuite: TLS_AES_128_GCM_SHA256
Peer certificate: CN=github.com
Peer Temp Key: X25519, 253 bits
DONE四行浓缩了 TLS 1.3 的全部要义,后面每球都会回到这张名片:
TLS_AES_128_GCM_SHA256——AES-128-GCM 加密 + 认证一体(AEAD);Peer Temp Key: X25519——临时椭圆曲线密钥交换(ECDHE),本次连接现生成、用完就丢;Peer certificate——服务器的证书,证明「我真是 github.com」;- 这一切在 1 个往返(1-RTT)内完成——TLS 1.3 的招牌改进。
断的: 换成微信的 longlink 服务器。它监听 80/8080/443(端口列表写在下发给客户端的配置里,Citizen Lab 抓到的下发值为 "80:8080:443"),对 443 试一次标准 TLS:
openssl s_client -connect long.weixin.qq.com:443 -tls1_3 -brief </dev/null本机输出:
Connecting to 117.89.177.107
...error:0A000126:SSL routines::unexpected eof while reading...unexpected eof——openssl 客整地递上了一张 TLS 1.3 ClientHello,对面一个字节没回,直接关闭了连接。雪球 2 抓包里我那帧 16 03 01 开头的记录,就是这次递上去的名片;它的下场是石沉大海。
(注意别说反了:long.weixin.qq.com:443 拒绝标准 TLS ≠ 端口没用。微信自己的客户端连这个端口时说的是 MMTLS——端口是载体,语言才是关键。)
结论摆在这了:微信的线上跑着一种「像 TLS 但不是 TLS」的东西。它是谁、从哪来?
雪球 4:MMTLS 是什么——TLS 1.3 的私房改版
MMTLS,MM = MicroMessenger(微信的内部代号),TLS = 它的底稿。多伦多大学公民实验室(Citizen Lab)2024 年 10 月的论文《Should We Chat, Too? Security Analysis of WeChat's MMTLS Encryption Protocol》把它完整逆向并审计了一遍,定性原话:MMTLS 是 TLS 1.3 的修改版,其中不少修改引入了弱点(雪球 8 细说)。
时间线先钉准(论文 + RFC 两头核对):
| 时间 | 事件 |
|---|---|
| 2016 及以前 | 微信只有业务层加密:登录前 RSA、登录后服务器下发 session_key 做 AES-CBC;最后不用 MMTLS 的版本是 v6.3.16(2016) |
| 2016–2017 | MMTLS 上线,包在业务层外面,形成双层加密 |
| 2018-08 | TLS 1.3 才正式定稿(RFC 8446)——微信造 MMTLS 时,它还只是草案 |
为什么 2016 年不等 TLS 1.3、要自己改一版?腾讯官方设计文档(《基于 TLS1.3 的微信安全通信协议 mmtls 介绍》,WeMobileDev 仓库)给出的动机,加上论文的补充,归纳成三条:
- 等不起:TLS 1.3 当时还是草案,微信等不了 IETF 的流程;
- 设备太杂:十几亿用户里大量低端 Android、嵌入式设备,需要协议足够小、足够省;
- 自家体系:要服务于微信自己的服务器选址、弱网重连、动态下发的策略,标准 TLS 的证书体系(CA 链)反而是负担(雪球 7 讲它怎么绕开的)。
理解 MMTLS 的正确姿势由此定型:零件基本是 TLS 的市售标准件,改的是组装方式。零件清单对照着看:
| 部件 | 标准 TLS 1.3 | MMTLS | 本机/出处 |
|---|---|---|---|
| 记录类型编号 | 15告警 16握手 17应用数据 | 照抄 15/16/17,另加 19(0-RTT ClientHello) | 雪球 2 实测 17 f1 04;Citizen Lab 仓库格式文档 |
| 版本字节 | 03 01~03 04 | 自造 f1 04 | 雪球 2 实测 |
| 记录头结构 | 类型(1)+版本(2)+长度(2) | 一模一样 | 雪球 5 实证 |
| 加密 | AEAD(AES-GCM 等) | AES-128-GCM(128 位密钥 + 96 位 nonce) | Citizen Lab 仓库 |
| 密钥交换 | ECDHE(曲线可选,如 X25519) | ECDHE,固定 P-256(secp256r1) | 论文 2A 节(注意:不是 X25519,网上常写错) |
| 密钥派生 | HKDF | HKDF,连标签都类似(如 handshake key expansion) | 论文 2A 节 |
| 证书 | CA 签名链 | 内置验签公钥 + 加密下发证书(两说,雪球 7) | 腾讯文档 vs 论文实测 |
那张表里「记录头结构一模一样」不是我抄文档抄来的——下一球用抓包亲手证明。
雪球 5:记录层逐字节——用十行 Python 切开每一条
雪球 2 只看了前三个字节。现在把整条记录的边界切出来。假设(来自上一张对照表):头 5 字节 = 类型(1) + 版本(2) + 长度(2),后面跟「长度」字节的密文。是骡子是马,让每帧字节自己走路——从第 0 字节起,按假设的头啃掉一条记录,再啃下一条,看边界能否不多不少正好走完:
# parse_records.py(节选;完整脚本见文末说明)
def walk(buf): # 头5字节:类型1 + 版本2 + 长度2
recs, i = [], 0
while i < len(buf):
if i + 5 > len(buf): return None
body_len = int.from_bytes(buf[i+3:i+5], "big")
recs.append((buf[i], buf[i+1:i+3].hex(), body_len))
i += 5 + body_len
if i > len(buf): return None # 边界越界 = 假设错误
return recs对抓包里全部 10 帧微信载荷跑一遍,本机输出:
t= 0.00s 客户端→服务器 总长 423B 前8字节: 17 f1 04 01 a2 45 c8 84
假设A:头5B,2B长度 ✅ 走通 1 条记录: [17 f104 len=418]
t= 0.08s 服务器→客户端 总长 93B 前8字节: 17 f1 04 00 58 39 06 15
假设A:头5B,2B长度 ✅ 走通 1 条记录: [17 f104 len=88]
t= 0.08s 客户端→服务器 总长 391B 前8字节: 17 f1 04 01 82 4a d6 08
假设A:头5B,2B长度 ✅ 走通 1 条记录: [17 f104 len=386]
t= 0.15s 服务器→客户端 总长 383B 前8字节: 17 f1 04 01 7a 3d 15 34
假设A:头5B,2B长度 ✅ 走通 1 条记录: [17 f104 len=378]
t= 30.88s 客户端→服务器 总长 391B 前8字节: 17 f1 04 01 82 f6 3d d0
假设A:头5B,2B长度 ✅ 走通 1 条记录: [17 f104 len=386]
t= 30.95s 服务器→客户端 总长 383B 前8字节: 17 f1 04 01 7a 05 25 ed
假设A:头5B,2B长度 ✅ 走通 1 条记录: [17 f104 len=378]
t= 90.89s 客户端→服务器 总长 391B 前8字节: 17 f1 04 01 82 d6 3d 7c
假设A:头5B,2B长度 ✅ 走通 1 条记录: [17 f104 len=386]
t= 90.95s 服务器→客户端 总长 383B 前8字节: 17 f1 04 01 7a 30 3e 05
假设A:头5B,2B长度 ✅ 走通 1 条记录: [17 f104 len=378]
(另有一对 t=136.93s 的 343B/141B,同样走通,略)十帧全部✅,边界不多不少。逐行解读,三条干货:
- 记录头坐实:
17 f1 04+ 2 字节长度。例如01 a2= 418,加上 5 字节头正好 423 字节,对上第一帧的总长。之前雪球 2 里那个看不懂的第 4 字节01,其实只是长度的高位字节——不是什么神秘字段; - 密文里含认证标签:AES-GCM 的每条密文末尾带 16 字节认证标签(雪球 6 亲手验),所以明文长度 = 记录长度 − 16;
- 流量有「心跳感」:客户端 391 字节 → 服务器 383 字节的等长成对交换,在 150 秒里出现 3 次,间隔约 31 秒、60 秒(不固定,自适应节奏)。论文说 longlink 靠 no-op 心跳保活,本机这份抓包里规律性的等长对答与此相符——但里面具体是什么业务消息,密文不可见,不猜。
一个重要的诚实声明:这次抓包没有抓到握手。长连接在抓包前就建立了,线上跑的全是 17 f1 04 应用数据记录。想抓握手得碰上微信新建连接(重启客户端或断网重连)——本篇没有重启你的微信,握手字节引用 Citizen Lab 配套仓库(wechat-security-report)的格式文档,出处随文标注。
雪球 6:它抄的三个标准件——亲手跑一遍
MMTLS 的零件都是标准件,这话空口无凭。三个零件各写十行 Python,跑给你看(曲线、标签、参数都按论文实测的来):
零件 1:ECDH 密钥交换(P-256)。 双方各现生成一对临时密钥,只交换公钥,两端各自算出同一个秘密——窃听者拿到全部交换字节也算不出来:
== 1) P-256 ECDH:双方各出一对临时密钥,交换公钥后算出同一个秘密 ==
客户端公钥(65字节, 非压缩点): 0402d8a9ea860267...
服务器公钥(65字节, 非压缩点): 0428d2e7f42b0bbe...
客户端算出的共享秘密: 99b592814a1e56ad40212ee737f04547c6707dfa88b1c43fde602cd6caa2f093
服务器算出的共享秘密: 99b592814a1e56ad40212ee737f04547c6707dfa88b1c43fde602cd6caa2f093
两边相等: True多巧都巧上了:65 字节、04 开头——正是 Citizen Lab 仓库 ClientHello 字节图里 00 41(0x41=65)那两个字节的公钥长度,非压缩 P-256 点的标准形状。MMTLS 的 ClientHello 里客户端一次性放出两把这样的公钥(字节图里 key index 1 和 2),意图下一球讲。
零件 2:HKDF 派生。 共享秘密不能直接当密钥,要加上双方的随机数(ClientRandom/ServerRandom)搅拌派生,标签用的就是论文里实测的 handshake key expansion:
== 2) HKDF:共享秘密 + 双方随机数,贴着真实标签派生会话密钥 ==
会话密钥(16字节/128位): fc28e720b41fb7abf9da2c47d590af06零件 3:AES-128-GCM。 加密 + 认证一体,密文末尾 16 字节是标签;MMTLS 里记录头和记录序号都参与运算(真实实现把它们作为 AAD,序号参与构造 nonce——演示里简化了):
== 3) AES-128-GCM:密文+16字节标签一体;序号参与 nonce,错一点就解不开 ==
明文: 老板:明早九点开会
密文(43字节): 790b9015055678405018f480e9ab64c23d7f6006ee03dc4349357c7498aa77dd8dc586814783a2f4bd07ca
对方解密: 老板:明早九点开会
篡改密文: 失败 (InvalidTag)
换序号重放: 失败 (InvalidTag)四十三字节密文 = 27 字节明文(9 个汉字 × 3 字节 UTF-8)+ 16 字节标签,账对得上。后两行是 GCM 的看家本领:改一个比特、或者把同一条密文换个序号重放,都直接 InvalidTag 拒收——这就是雪球 5 里「每条记录带长度」之外,记录层还有个递增序号的原因:防篡改、防重放,全指着它。
三个零件与线上的对应关系:雪球 5 抓到的每条 17 f1 04 记录,就是用零件 2 派生的密钥、零件 3 的方式封的包;而握手,就是双方用零件 1 商定密钥材料的过程。零件全是标准件——MMTLS 特殊在组装。
雪球 7:组装方式哪里不同——两阶段握手、双 PSK、0-RTT、双层加密
以下字节结构全部来自 Citizen Lab 配套仓库的格式文档(docs/mmtls-layer/mmtls-network-format.md),本机抓包只见到了应用数据层,前面已声明。
不同点 1:一次握手,两个阶段。 完整握手(1-RTT ECDHE 模式)的记录序列长这样:
客户端 服务器
16 f1 04 ClientHello ──────────────▶
(两个套件都带上:c0 2b ECDHE 版
+ 00 a8 PSK 版;两把 65 字节公钥;
若有旧票根,还附上 PSK 扩展)
◀────── 16 f1 04 ServerHello【明文】
ServerRandom + 服务器公钥
——证书的影子都没有
◀────── 16 f1 04 【加密】证书
◀────── 16 f1 04 【加密】新票根(session ticket)
◀────── 16 f1 04 【加密】ServerFinish
17 f1 04 加密的应用数据 ◀────────▶ 17 f1 04 加密的应用数据和 TLS 1.3 最大的区别一眼可见:服务器证书是在 ECDH 密钥加密的信道里发的,不像 TLS 把证书明文挂在 ServerHello 后面。为什么要这样?腾讯文档的设计说明 + 论文的分析:
- 阶段 1 匿名 ECDH 先建起加密信道(防旁观者收集证书、做指纹);
- 阶段 2 在信道里验服务器身份:ECDSA 签名(椭圆曲线数字签名算法——TLS 里服务器证明「我就是我」的同一类手段)覆盖
Client_Random + Server_Random + 服务器公钥,让签名和这一次握手一一绑定,防止签名被拿到别的连接里重放; - 官方文档说客户端内置验签公钥、不需要证书链;论文实测 8.0.x 里确实有一张加密下发的服务器证书被客户端验证——两说并存,出处都列在参考资料。
代价也明摆着:说不了标准 TLS 的工具,同样进不了这个信道。mitmproxy 面对标准 HTTPS 是「两头各握一次手、中间看明文」;面对 MMTLS,它递不出 ClientHello(格式不对),也没有可模仿的「证书链」可签。
不同点 2:会话恢复发两张票根。 PSK(Pre-Shared Key,预共享密钥)第一次出现,钉个模型:上次握手成功后服务器发的「下次免检凭据」——拿着它,重连时不必再跑完整 ECDHE。MMTLS 一次完整握手会下发两张(Citizen Lab 的实现里叫 PSK_ACCESS 和 PSK_REFRESH):一张短生命周期、安全性高,一张长生命周期、保可用性。此后:
| 模式 | 往返 | 前向保密 | 用在哪 |
|---|---|---|---|
| 1-RTT ECDHE | 1 | ✅ 有(临时密钥) | 长连接的完整握手 |
| 1-RTT PSK | 1 | ❌ 无(靠票根密钥) | 长连接重连 |
| 0-RTT PSK | 0 | ❌ 无 | 短连接:第一个包就带数据 |
腾讯文档对 0-RTT 的取舍说得很直白:0-RTT 的业务请求始终无法做到前向安全——性能和安全的经典交换。
不同点 3:0-RTT 的字节序列。 有票根的客户端发起短连接时,19 f1 04 开头的 ClientHello 只报 00 a8 一个套件(纯 PSK),后面紧跟着就是密文:
19 f1 04 ClientHello(PSK 版,附票根)
19 f1 04 【加密】扩展
17 f1 04 【加密】early data——第一个往返都没等,数据先走了
15 f1 04 【加密】结束标记注意 15(0x15 在 TLS 里是告警记录的编号)在这里被 MMTLS 征用为「early data 结束」标记——编号照抄 TLS,用法自己定,这个协议的作风贯穿始终。
不同点 4:MMTLS 里面还套着一层。 别以为剥开 MMTLS 就见明文——论文的核心发现之一是双层加密:
┌───────────────────────────────────────────┐
│ 业务层(更老,2016 年前是唯一的一层) │
│ pack()/unpack() 打包,Protobuf 载荷, │
│ 登录后用服务器下发的 session_key 加密 │
│ ┌───────────────────────────────────────┐ │
│ │ MMTLS 层(2016-2017 年包在外面) │ │
│ │ 17 f1 04 记录、AES-128-GCM、P-256 │ │
│ └───────────────────────────────────────┘ │
└───────────────────────────────────────────┘也就是说:就算未来某天 MMTLS 被解开了(比如你逆向了自己的客户端拿到会话密钥),里面还有业务层加密挡着。两层各管一段历史,谁也没废掉谁。
雪球 8:安全账本——弱在哪、扛得住什么、腾讯认了什么
Citizen Lab 2024 年 10 月这篇论文不是吓唬人,是逐条记账。账本摘要(每条都有论文章节背书,参考资料里给了入口):
MMTLS 层两条弱点:
- 确定性 IV:GCM 的 nonce 由计数器确定性派生(NIST 明确不建议)。单个用户没事;但十亿用户量级的 (key, IV) 池,论文援引 Bellare 与 Tackmann 的估算——生日碰撞式的暴力搜索「进入可行性范围」;
- 前向保密缺失:大量短连接流量走 0-RTT EarlyData(无前向保密);长连接一条活整个 App 生命周期,这条连接内的请求之间也无前向保密。票根密钥一旦泄露,可解密多条连接的 EarlyData。
业务层几条(利用前提是先攻破 MMTLS,或攻击腾讯内网——论文明确说后者纯属推测):元数据(用户 ID、请求 URI)在业务层是明文;genSignature 用 MD5+Adler32,可伪造;AES-CBC 存在 padding oracle 隐患(腾讯回应见下);session_key 身兼 IV 且全会话复用。
扛住了什么: 同样重要——论文原话,作者没能开发出彻底击破微信加密的攻击;配套 FAQ 面向用户的说法:「以今天已知的攻击技术,微信的加密协议不受其中任何一种的威胁」。0-RTT EarlyData 理论上可重放(披露信注明 may be susceptible to replay attacks),但综合评价是:防住被动窃听绰绰有余,弱于标准 TLS 1.3 的地方在工程余量,不在当下可利用性。论文 Discussion 的总评很扎嘴:「微信的协议看起来性能和安全都不如标准 TLS 1.3」,建议迁移到标准 TLS 或 QUIC+TLS(QUIC:基于 UDP 的新一代传输协议,HTTP/3 的底座,里面同样内嵌 TLS 1.3)。
腾讯的回应(论文附录,2024-04 披露、2024-05 回复):外层 MMTLS 认为是安全的;内层核心数据流量已切换到 AES-GCM,其余流量正从 AES-CBC 逐步切换。态度是继续使用和发展 MMTLS,不迁回标准 TLS。
时效性备注:论文审计的是手机端 8.0.x。桌面版 4.x 有没有改协议,至今没有公开分析——但本机 4.1.12.26 的线上实测(雪球 2/5)仍是 f1 04 记录,至少记录层延续着。另外同团队 2025 年用同一套 MMTLS 工具发表了微信追踪研究(《What WeChat Knows》,PoPETs 2025),协议本身的公开安全续作截至 2026-08 还没有。
雪球 9:收网——两件套为什么对微信无效,以及正路在哪
回到开头的痛点,现在能逐条说清了:
| mitmproxy 的前提 | 微信的现实 | 雪球出处 |
|---|---|---|
| 客户端发起 CONNECT、说标准 TLS | 微信直连 80 端口说 MMTLS,根本没有「HTTPS 代理」概念 | 球 1、3 |
| 有证书链可模仿、CA 可替换 | 无 CA 链:验签公钥内置 + 证书加密下发 | 球 7 |
| 解开 TLS 就见明文 | 里面还有业务层加密 | 球 7 |
| 客户端认系统信任库里的 CA | 信任不靠系统信任库,往里装 mitmproxy 的 CA 也没用 | 球 7 |
所以「让微信流量进玻璃管」这条路,在协议设计层面就被焊死了——这不是工具不行,是目标根本不说你的语言。这本身就是一个值得记住的安全设计样本:私有传输协议 + 双层加密 + 无证书体系,三件事分别废掉了「代理转发」「中间人解密」「CA 冒充」三类手段。
真有观察/研究微信流量的正当需求,正路是别人趟出来的:
- Citizen Lab 配套仓库 wechat-security-report(103★,2025-06 仍在更新):协议格式文档 + Frida 工具(Frida:动态插桩框架,能把脚本挂进运行中的 App 内部取数据)+ 手机端 8.0.49 的解密脚本——从自己设备上的客户端里取密钥,而不是攻击协议;
- 开源握手实现:duo/gommtls(Go,被引用最多)、ljc545w/pymmtls(Python,2025-10 仍在推送,还实现了本地长连接服务器)——能对服务器跑通 0xF104 版握手,说明这套协议在 2025 年末仍原样服役;
- 合规调试场景(公众号/小程序/企业微信的开发者接口)走的是标准 HTTPS 的开放平台 API,那才是 mitmproxy 能派上用场的地方。
章末
怎么记
| 你记住的 | 它长在哪一球 |
|---|---|
| 微信主进程全网一条外连 TCP,80 端口,longlink | 球 1 的 netstat |
17 f1 04 vs 16 03 xx:版本字节(第 2–3 字节)辨真伪 | 球 2 的对照图 |
| TLS 1.3 名片:ECDHE + AEAD + 证书 + 1-RTT | 球 3 的 -brief 四行 |
| 记录头 5 字节 = 类型(1)+版本(2)+长度(2),与 TLS 同构 | 球 5 的 Python 走边界 |
| ECDH/HKDF/AES-GCM 全是标准件,MMTLS 改的是组装 | 球 6 的三段演示 |
| 两阶段握手:证书进加密信道;双票根;0-RTT 无前向保密 | 球 7 的两张图两张表 |
| 弱点两条(确定性 IV、缺前向保密);但今日已知攻击解不开 | 球 8 的账本 |
| 私有协议 + 双层加密 + 无证书链 = 三类抓包手段全废 | 球 9 的对照表 |
历史包袱
- 2016 年前的单层时代:登录前 RSA、登录后 session_key + AES-CBC——业务层加密至今还在 MMTLS 里当内层,读老资料时注意区分「两层」各自的时间线(论文 Previous Versions 节)。
- TLS 1.3 的草案期:腾讯文档「TLS1.3 还是草案」是 2016 年语境;RFC 8446 于 2018-08 定稿。如今「等不起」的理由已消失,但腾讯明确表示继续用 MMTLS。
- 网上流传的「MMTLS 用 Curve25519」是错的——论文实测 P-256(65 字节非压缩点,
00 41长度字节为证)。读二手资料时,回到论文和配套仓库核对。
和系列其它篇
本系列:Proxifier(押送)→ mitmproxy(开膛)→ mitmweb(点着用)→ 本篇(边界)。
- 《mitmproxy》 / 《mitmweb》:本篇的痛点起点。它们的「两头握手」手艺只对标准 TLS 有效,本篇解释了为什么。
- 《Proxifier》:押送流量的手段对微信其实有效(TCP 层面拦得下来),但押到 mitmproxy 也开不了膛——「拦得下」和「看得见」是两回事。
- 《tcpdump 抓包》:Linux 侧的同类观测工具;本篇的 tshark 是它的 Wireshark 家族表亲。
小结与思考题
雪球滚完:找到那条 80 端口的线(1)→ 17 f1 04 验明正身(2)→ 标准 TLS 对照与被拒(3)→ MMTLS 的来历与零件表(4)→ 记录层逐字节(5)→ 三个标准件亲手跑(6)→ 组装差异:两阶段/双票根/0-RTT/双层(7)→ 安全账本(8)→ 两件套失效的原理(9)。一句话收拢:MMTLS 是「零件照抄、组装自定」的 TLS 1.3 私房改版,抓包工具看不见它,不是因为加密强,是因为它根本不说 TLS。
思考题(答案都在正文里):
- 雪球 5 的记录里,明文长度 = 记录长度 − 16。那 16 字节是什么?为什么雪球 6 里篡改一个比特都会
InvalidTag? - 0-RTT PSK 为什么注定没有前向保密?(提示:第一个包就带数据,此时谁还没参与密钥协商?)
- mitmproxy 对浏览器 HTTPS 有效的几个前提,微信分别用哪一招废掉的?(球 9 的表里有四个)
- 如果明天微信宣布迁到标准 TLS 1.3,mitmproxy 就能看见微信的流量了吗?(提示:别忘了球 7 的双层结构——里面还有一层)
参考资料
- 论文:Should We Chat, Too? Security Analysis of WeChat's MMTLS Encryption Protocol(Citizen Lab,2024-10-15)· 面向普通读者的 FAQ
- 配套仓库:citizenlab/wechat-security-report(协议格式文档
docs/mmtls-layer/、解密工具、抓包数据) - 腾讯官方设计文档:《基于 TLS1.3 的微信安全通信协议 mmtls 介绍》(WeMobileDev,2016 语境)
- RFC 8446(TLS 1.3,2018-08):datatracker 入口
- 开源实现:duo/gommtls、ljc545w/pymmtls(star 数与更新时间为 2026-08-19 GitHub API 实查)
- 本机版本指纹:微信 Windows 4.1.12.26、TShark 4.4.2、OpenSSL 3.5.7(mingw64)、Python 3.11.15 + cryptography 46.0.7、Windows 10 LTSC 19044;抓包与脚本存于
E:\DevCache\tmp\mmtls-lab\(crypto_demo.py / parse_records.py / wechat-longlink.pcapng)