GCM模式加密认证原理及Wireshark抓包验证
如果你用Wireshark抓过TLS或SSH的流量一定见过这种场面握手里还能看到明文的算法名称比如TLS_AES_128_GCM_SHA256、aes128-gcmopenssh.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。但工程实现上有一堆问题。先说拼接顺序。是先加密再MACEncrypt-then-MAC还是先MAC再加密MAC-then-Encrypt还是同步进行这三个选择在安全上有很大差异。选错了或者实现里把“可能泄露padding错误”和“MAC校验失败”区分处理就可能被攻击者利用后手。历史上多个TLS版本漏洞都出在“校验顺序”和“错误反馈”上比如旧版TLS的padding oracle就是一例。其次是性能。每个数据包要跑两套算法一套加密一套MACCPU开销翻倍。对于TLS/SSH这种高频小包场景这个开销完全不可忽略。所以社区后来达成共识与其让协议实现者自行组合加密和MAC不如直接定义一个内置认证的加密方案一把钥匙搞定两件事这就是AEADAuthenticated 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模式只不过它在计数器前面加了一个随机数作为起点这个随机数就是nonceTLS里通常叫“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/nonce96比特、P明文、AAD附加认证数据 输出: C密文与P等长、TTag128比特可截断 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.3GCM成为标配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-gcmopenssh.com和aes256-gcmopenssh.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.type2 -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-gcmopenssh.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先排查这四件事解密失败是高频问题按下面顺序查基本能解决密钥日志路径对不对。有些同学配置好了Wireshark但浏览器是后来才启动的密钥日志里压根没有这次连接的会话密钥。必须保证抓包、密钥日志记录、流量产生三者是同一个时间段。客户端日志功能没生效。浏览器或curl没有正确把环境变量传进去日志文件一直是空的。可以先手动跑一次cat $SSLKEYLOGFILE确认有内容。密钥交换方式不支持私钥解密。如果你只配置了服务器私钥还想解开ECDHE或TLS 1.3的流量那是注定失败——现代套件里每次会话的密钥都是临时协商的静态私钥根本拿不到会话密钥。这时候要么上密钥日志要么用支持导出会话密钥的方案。会话恢复场景。有些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在按序增长然后再谈上线。这一套能挡住一大半“加密套接字看着通、实际安全配置是坏的”的隐性事故。