ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

GCM模式加密认证原理及Wireshark抓包验证

GCM模式加密认证原理及Wireshark抓包验证

如果你用Wireshark抓过TLS或SSH的流量,一定见过这种场面:握手里还能看到明文的算法名称,比如TLS_AES_128_GCM_SHA256、aes128-gcm@openssh.com,可一旦进入传输阶段,密文栏里全是一串串十六进制,根本看不出门道。GCM模式这几个字天天见,但它到底是怎么把加密和认证同时做完的,很多同学其实说不上来。这东西不是只有密码学家才需要懂——你要是搞过TLS解密、排查过抓包、或者给通信协议选过加密方案,理解GCM就是绕不开的一课。今天我把它拆开揉碎讲清楚,先看懂原理,再带你在Wireshark里实际抓一把、验证一把结论。

1. 为什么TLS/SSH需要“加密”和“认证”一起上

1.1 只有加密并不安全,你还需要防篡改

先说个基础问题。很多人会把“加密”当成安全通信的全部,实际上加密只能解决机密性问题,也就是防偷听。它管不了另外两件事:完整性和真实性。

举个例子。你和服务器用AES-CBC或AES-CTR传数据,中间人虽然看不懂密文,但他可以把密文里的某些比特改掉。改了之后接收方解密出来可能是一片乱码,但更危险的情况是:对于某些协议,改动密文会导致解密后的明文产生可预测的变化,比如特定位置的字节被翻转。

这就是经典的密文翻转攻击(bit-flipping attack)。最惨痛的教材是一件真实发生过的事:TLS 1.0/1.1时代用CBC模式加独立MAC如果拼接顺序不对或处理不当,攻击者可以利用padding oracle逐字节猜出明文。后来整个社区花了很大力气才把这套坑填上。

所以TLS和SSH这类既要传机密数据、又要能确认“消息确实是对方发的、中途没被人动过”的协议,光有加密远远不够。你必须同时做到两件事:

  • 加密:别人读不懂。
  • 认证:收发双方都能验证消息没被篡改、且确实来自可信的对端。

1.2 老方案的坑:先加密再加MAC,拼接式方案问题多

早期方案是把加密和MAC分开做,也就是“加密算法 + HMAC”的组合。方案本身听起来合理:先算一个MAC附在密文后面,接收方先解密再校验MAC。但工程实现上有一堆问题。

先说拼接顺序。是先加密再MAC(Encrypt-then-MAC),还是先MAC再加密(MAC-then-Encrypt),还是同步进行,这三个选择在安全上有很大差异。选错了,或者实现里把“可能泄露padding错误”和“MAC校验失败”区分处理,就可能被攻击者利用后手。历史上多个TLS版本漏洞都出在“校验顺序”和“错误反馈”上,比如旧版TLS的padding oracle就是一例。

其次是性能。每个数据包要跑两套算法,一套加密一套MAC,CPU开销翻倍。对于TLS/SSH这种高频小包场景,这个开销完全不可忽略。

所以社区后来达成共识:与其让协议实现者自行组合加密和MAC,不如直接定义一个内置认证的加密方案,一把钥匙搞定两件事,这就是AEAD(Authenticated Encryption with Associated Data)大行其道的根本原因。而AEAD家族里,最典型、部署最广的就是今天的主角:GCM模式。

2. GCM模式是怎么做到“一份工,两件事”的

2.1 从CTR计数器模式说起

GCM全称是Galois/Counter Mode,伽罗华计数器模式。名字里藏着两个关键词:Counter说明它的加密部分是计数器模式,Galois说明它的认证部分用的是伽罗华域上的运算。

先说计数器模式。假设你要加密一段数据,把它按128比特切成若干块。CTR模式的做法是:用一个数字作为计数器,每加密一块,计数器加1。把计数器的值用AES加密一次,生成128比特的密钥流:

keystream_1 = E_K(counter_1) keystream_2 = E_K(counter_2) ...

然后密文 = 明文 XOR 密钥流。

解密完全一样:用同样的计数器值生成同样的密钥流,再和密文做一次XOR,就把明文捞回来了。CTR模式的好处是:每个数据块的加密互不依赖,可以并行计算,速度极快;而且解密不需要先等收到完整数据块,有流式处理的感觉。

GCM的加密部分就是这个CTR模式,只不过它在计数器前面加了一个随机数作为起点,这个随机数就是nonce(TLS里通常叫“salt + explicit nonce”或“write IV”,后面讲)。

2.2 GHASH:在二进制域里做“多项式求值”

真正让GCM独一无二的是它的认证部分,叫GHASH。名字里的Galois指的就是伽罗华域,GCM的认证就是在这个有限域上算出来的。

简单理解GHASH:密钥K经AES加密一个固定的0值,得到128比特的哈希子密钥H。然后,把附加认证数据AAD、密文、以及它们的长度信息都切成128比特的块,按照下面这个规则迭代:

Y_0 = 0 Y_i = (Y_{i-1} XOR 消息块_i) * H

最终算出的Y_last再加上一步异或操作,就是GCM的认证标签Tag。这里的乘法和加法可不是普通算术,而是在二元伽罗华域GF(2^128)上进行的运算:加是XOR,乘是“无进位多项式乘法取模一个固定约简多项式”。

如果你把迭代公式展开,会发现它本质上是:

GHASH = X1 * H^m XOR X2 * H^(m-1) XOR ... XOR Xm * H

这就是个以H为变量的多项式。H是只有通信双方知道的秘密值,所以攻击者猜不出散列结果,也伪造不了标签。而接收方只要用同样的H、同样的AAD和密文重新算一遍,看看算出的结果和收到的Tag是否一致,就能确定消息有没有被篡改。

这个过程有两个工程优点:

  • 支持并行和流水线。伽罗华域的乘法硬件化之后非常快,现代CPU里的PCLMULQDQ指令就是专门干这个的。配合AES-NI加速,一块支持AES-GCM的网卡或CPU可以在转发层面直接完成加解密,这也是为什么TLS/SSH都愿意用它。
  • 认证不改变密文长度。GHASH是对“已经加密好的密文”再做运算,所以密文长度和明文一样,只是额外追加一段Tag。

2.3 完整流程:输入、输出、Tag

把整个GCM加密流程串起来就是这样:

输入: K(密钥)、IV/nonce(96比特)、P(明文)、AAD(附加认证数据) 输出: C(密文,与P等长)、T(Tag,128比特,可截断) 1. H = E_K(0^128) # 哈希子密钥 2. J0 = IV || 0^31 || 1 # 96比特IV时的计数器初值 3. 对每个明文块P_i: keystream_i = E_K(J0 + i) # 计数器从2开始递增 C_i = P_i XOR keystream_i 4. GHASH处理: AAD(补齐) || C(补齐) || len(AAD) || len(C) 5. T = GHASH结果 XOR E_K(J0) # 用J0加密一次作为掩码

AAD指的是“只认证不加密”的数据,比如TLS记录头、SSH的包长度字段。这个东西很关键:它让协议能对“那些必须明文传输的元数据”做完整性保护,比如确保包长度字段不会被中间人偷偷改掉。

这里再说一个很多人忽略的点:GCM的Tag默认可以截断,比如TLS里用16字节的完整Tag,有些应用为了省带宽会用8字节或者更短。短Tag不是免费的,安全性会随着Tag长度缩短而下降。实践中能用满16字节就用满。

3. 在TLS和SSH里看到GCM的身影

3.1 从TLS 1.2到TLS 1.3:GCM成为标配

TLS 1.2时代,GCM是以“套餐”形式出现的。你看Server Hello里常见到这类名字:

Cipher Suite说明
TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256ECDHE做密钥交换,AES-128-GCM做加密认证,SHA256做握手摘要
TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384256位密钥版本
TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256证书用ECDSA时常见

到了TLS 1.3,整个密码套件规则被重写。TLS 1.3里默认的套件本身全部是AEAD,不再允许独立“加密+MAC”的旧组合,TLS_AES_128_GCM_SHA256、TLS_AES_256_GCM_SHA384就是其中最核心的成员。你如果看TLS 1.3的握手,会发现套件名字短了很多,因为密钥交换、签名和AEAD都各自独立协商,不再绑死。

TLS 1.2里GCM的nonce是怎么构造的呢?它分成两部分,4字节的盐(salt)加8字节的显式随机数(explicit nonce),合起来是96比特的IV。盐在握手时随机生成,随机数每个记录都重新生成。TLS 1.3就不一样了,nonce是一个固定的write IV和一个单调递增的序列号做异或。序列号每发一个记录加1,从而保证同一密钥下每个记录的nonce都不同。

TLS 1.3里AAD也变了。TLS 1.2的AAD是把序列号、内容类型、版本、明文长度拼在一起。TLS 1.3简化为每个记录头那5个字节,也就是内容类型、版本、密文长度。这意味着密文长度都被绑进认证里了,中间人哪怕想改包长都不行。

3.2 SSH里的GCM:连包长度都被认证

SSH这边,OpenSSH提供了aes128-gcm@openssh.com和aes256-gcm@openssh.com这两个常见算法名。SSH的传输协议结构是:

uint32 包长度 byte 填充长度 byte[] 负载 byte[] 随机填充

早期SSH加密模式里,包长度字段通常是明文的,因为接收端要先知道包有多长,才能决定读多少字节再解密。但明文长度字段意味着中间人至少能观察到流量的包大小模式,还可能对长度值动手脚。

SSH一旦切到GCM套件,情况就不一样了:包长度字段不再加密,但它作为AAD参与GHASH认证。这样接收端仍然可以先把长度字段读出来、分配好缓冲区,又能确认这个长度值没有被改过。这算是一种兼顾效率和安全的折中设计。每个GCM加密后的SSH包,末尾会追加16字节的Tag。

SSH的GCM nonce一般也是12字节,其中一部分是固定值,一部分是64位的数据包序号。这个设计保证了即便传输海量数据,同一个密钥下nonce也不会重复。

3.3 三个关键参数:nonce、AAD、Tag分别管什么

用TLS/SSH的现场来理解这哥仨,大约是这样:

  • nonce:每一条记录都必须独一无二。它不保密,可以明文出现在网络里,但绝不能重复。GCM的安全强度完全建立在“密钥一定,nonce不重”的前提上。一旦nonce重复,等于CTR模式的密钥流重复,两个包的密文一异或,明文关系直接暴露;GHASH这边也会出大事,攻击者有机会解出H,整个认证体系崩塌。
  • AAD:保护那些“不想加密但又怕被篡改”的字段。TLS记录头、SSH包长度这些元数据都由AAD兜底。
  • Tag:认证的落点。发送方生成它,接收方本地重算,比对上就收下,比不上立即断掉连接并报警。Tag出现任何一位不匹配,都说明消息被改过,或者解密密钥对不上。

4. Wireshark抓包实操:把GCM的现场还原出来

4.1 搭建抓包环境与解密配置

光看协议树上的套件名不算本事,真正有价值的是亲眼看到GCM的nonce、Tag这些现场证据。这边我用TLS做主线。

第一步,让流量可控。抓包建议用回环接口,比如访问本机起的HTTPS服务,或者用浏览器访问一个你信任的HTTPS站点。抓回环包不会混入别人的广播、重传等杂音,分析效率高很多。

第二步,准备好密钥日志文件。GCM套件在两种情况下可以解出明文:TLS 1.3几乎只能靠密钥日志;TLS 1.2如果是ECDHE这类现代密钥交换,单靠服务器私钥也解不了,也得靠密钥日志。做法是:

# 在你的终端里配置环境变量 export SSLKEYLOGFILE=/tmp/sslkeys.log # 启动一个支持该参数的浏览器,或者用curl发请求 curl -k https://127.0.0.1/ > /dev/null # 抓包的同时让流量产生 tshark -i lo -w /tmp/tls.pcapng & curl --cacert ./cert.pem https://127.0.0.1/ > /dev/null

第三步,在Wireshark里把密钥日志填进去。路径在:Preferences → Protocols → TLS →(Pre)-Master-Secret log filename。选定后重新打开抓包文件,TLS记录就能解密了。

4.2 在握手里找出GCM套件与证据

抓包文件打开后,先看Server Hello。展开握手信息,你会看到类似:

Handshake Protocol: Server Hello Cipher Suite: TLS_AES_128_GCM_SHA256 (0x1301)

这就是TLS 1.3默认的GCM套件。再往上看,来自客户端/服务器的密钥交换消息都藏在加密扩展里,这些内容只有在解密后才能看到。

这时候你可以用tshark更直接地观察GCM相关字段:

tshark -r /tmp/tls.pcapng -o tls.keylog_file:/tmp/sslkeys.log -Y "tls.handshake.type==2" -V | grep -E "Cipher Suite|Nonce|Salt"

正常的话能看到TLS 1.2对应套件的salt和explicit nonce信息,以及TLS 1.3里握手的随机数。在解密后的TLS记录里,Wireshark在展开“Decrypted TLS”时还会显示每一条记录使用的nonce。你数一下每条记录的nonce是不是按序列号变了,就能验证TLS 1.3“write IV XOR序列号”的nonce构造规律。

4.3 看解密后的应用数据与Tag

解密成功后,Wireshark的应用层解析会顺带上线。TLS记录里那些“Application Data”会直接显示出HTTP/2帧头或者HTTP/1.1的明文请求行:

Frame └─ TLS └─ Decrypted TLS └─ HTTP ├─ GET / HTTP/1.1 └─ Host: ...

这时候GCM的Tag怎么找?它不在对话树里显眼位置,因为它本来就是密文的一部分。Wireshark把每条TLS记录的密文解析为一个二进制字段,你可以点击记录,看包字节区——在每个Application Data记录的最后16个字节,就是那条记录对应的GCM Tag。有些Wireshark版本在解析TLS记录时会直接把“GCM Tag”字段列在协议树里,你只要点开带AEAD套件的记录就能看到。

如果你想验证Tag确实参与认证,一个有意思的测试是:拿到一个解密后的TLS记录,用脚本把密文区域的任意一个字节改掉,重新计算CRC或者直接保存成pcapng再发给Wireshark。注意这里别真发到公网,就在本机做。改完再打开,你会发现原本该正常显示的明文记录可能解析失败,或者TLS协议栈直接报标签不匹配。这说明接收端一旦发现GHASH算出来的Tag对不上,立刻就会丢弃整条记录。

SSH这边抓包稍微糙一点。你在KEXINIT里能定位到算法里面带aes128-gcm@openssh.com,但握手结束后所有应用数据都显示成“Encrypted packet”。要验证Tag其实也简单:SSH的GCM包结构是“4字节明文长度 + 加密数据 + 16字节Tag”。你找到一条encrypted packet的原始字节,数一数末尾16字节,正好能和包总长度对上账,这就是GCM认证标签的落点。

5. 典型翻车现场与排查手册

5.1 nonce复用:安全上不可逆的灾难

做了上面实验,你对nonce的敏感度应该有感觉了。我在实际排查里见过最可惜的案例:某自研网关,开发同学图省事,把nonce写死在配置里,只换密钥不换nonce。结果某次安全测试里,流量直接被分析出明文规律,加密形同虚设。

原因前面提过:GCM的加密是CTR模式,密钥流由密钥和nonce共同决定。两个包用了同一个nonce和同一个密钥,就产生了完全相同的密钥流。你把这两个密文一异或:

C1 XOR C2 = (P1 XOR keystream) XOR (P2 XOR keystream) = P1 XOR P2

只要知道其中一段明文,另一段明文直接算出来。更致命的是,攻击者还能伪造认证标签,因为GHASH的H值在nonce碰撞时也能被解出来。所以GCM工程实现的第一铁律就是:同一密钥下,nonce绝对不允许重复。上线前必须把nonce生成逻辑当安全边界来审。

5.2 Wireshark解不开TLS?先排查这四件事

解密失败是高频问题,按下面顺序查基本能解决:

  1. 密钥日志路径对不对。有些同学配置好了Wireshark,但浏览器是后来才启动的,密钥日志里压根没有这次连接的会话密钥。必须保证抓包、密钥日志记录、流量产生三者是同一个时间段。
  2. 客户端日志功能没生效。浏览器或curl没有正确把环境变量传进去,日志文件一直是空的。可以先手动跑一次cat $SSLKEYLOGFILE确认有内容。
  3. 密钥交换方式不支持私钥解密。如果你只配置了服务器私钥,还想解开ECDHE或TLS 1.3的流量,那是注定失败——现代套件里每次会话的密钥都是临时协商的,静态私钥根本拿不到会话密钥。这时候要么上密钥日志,要么用支持导出会话密钥的方案。
  4. 会话恢复场景。有些TLS会话是客户端和服务端通过session ticket恢复的,如果密钥日志里没有ticket对应的会话密钥,这部分流量依然解不开。

另外,抓包过滤器建议直接用:

tls.handshake.type == 1 || tls.handshake.type == 2

先筛出握手,再把域名过滤加上,比如tls.handshake.extensions.server_name == "你的目标域名",能省掉一屏幕无关流量。

5.3 实战建议:GCM的边界与替代方案

GCM确实快、安全、主流,但它也不是万能的。几条实操建议供参考:

  • 超长消息要警惕。GCM在同一个密钥下加密的数据量越大,理论安全余量越低。一般建议单密钥加密不超过2^32条消息,或者不超过几十GB级的数据量。海量文件传输时,该切分就切分,该rekey就rekey。
  • 硬件加速别浪费。配服务器的密码学库时,确认AES-NI和PCLMULQDQ指令集是开着的。不开加速,GCM的GHASH纯软件算,慢到你怀疑人生;开了之后,AES-GCM能在普通CPU上跑到好几Gbps,这也是它碾压很多老方案的关键。
  • 没AES加速的场合换ChaCha20-Poly1305。TLS 1.3和SSH里都能看到ChaCha20-Poly1305这个AEAD选项。它在软件实现上比AES-GCM更友好,移动端低端CPU也跑得动,所以现在主流协议都会把它作为候选。
  • 出于谨慎,真实的调试过程里千万别为了“验证”GCM而去修改变量。我见过有人拿真实生产包的密文做改包测试,结果密钥轨迹乱了,连排查用的明文都被污染了。自己用本地抓包数据折腾就好。

最后分享一个我常用的习惯:每次调完协议栈,顺手抓一个TLS 1.3 + AES-GCM的包,确认密钥日志能解开、Tag字段能找到、记录nonce在按序增长,然后再谈上线。这一套能挡住一大半“加密套接字看着通、实际安全配置是坏的”的隐性事故。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进