本文记录一次IPsec VPN检测时ESP解密失败问题的定位过程。涉及到IPsec VPN协议、加密算法、内核xfrm框架等知识。
问题背景描述
先交代下问题背景,公司新开发的IPsec VPN产品去过检测。在协议检测时,出现检测平台发送过来的ESP包在解密失败的问题,检测算法是SM4-SM3。
我并没有参与IPsec产品的开发,之前对于IPsec也是丝毫不了解。我在这样的情况下协助进行问题定位。这个问题的排查的难点在于对方是黑盒,你只能根据己方这边的信息进行分析定位。
以下分别为检测平台的ESP抓包和我们的ipsec程序自己跟自己互通时的ESP抓包。可以看到检测平台的ESP包长度要短4个字节。
ESP规范
首先想到的肯定是ESP包的格式问题,是不是某些字段的理解跟协议有出入。于是查阅相关规范。
国密规范中ESP的描述如下:
国密规范有些地方讲的不清楚,更精确的描述参看rfc2406-esp。
ESP包格式分析
结合规范及实际的抓包分析,ESP包的格式应如下:
1 | |ESP头(SPI+Index)|IV|原IP头|原协议头|载荷数据|padding|ESP尾(填充长度+下一个头)|ESP认证数据| |
其中padding的长度,要保证加密范围的长度是块加密算法块长度(SM4为16)的整数倍,即加上ESP尾之后的长度是block长度的整数倍。
密文数据跟认证数据的长度尚无法判断。
原因分析
ESP包的长度不对,可能的原因有:
- ESP包的格式跟我们理解的不一样,可能还有其他字段
- 实际使用的算法并非SM4-SM3
- 包长度因主动或被动的原因被截短了(因为IP包头中的长度是正确的,所以应该是主动截断)
第1种可能的原因,再三的确认了规范,确认对ESP包格式的理解是没有问题的
第2种可能的原因,因为SM1和SM4算法长度是一样的,只可能是摘要算法用的不对,而规范里只支持SM3和SHA1算法。取最后32字节为ESP认证数据,对前面的认证范围数据手动进行HMAC(SM3)计算,计算结果跟最后32字节不一致。取最后20字节为ESP认证数据,对前面的认证范围数据手动进行HMAC(SHA1)计算,计算结果跟最后20字节不一致。
第3种可能的原因,因为检测平台的包比我们程序自己互通时正常的包短了4个字节。取最后28字节为ESP认证数据,对前面的认证范围数据手动进行HMAC(SM3)计算,计算结果跟最后28字节不一致。
问题确认
HMAC认证
问题陷入了僵局,看来只能从代码本身入手进行分析了。正在对这个长度问题困扰的时候,一个偶然的机会用最后12字节作为认证数据可以验证通过。
验证过程如下,协商出的密钥信息如下:
接收到的ESP包原始数据如下:
手动验证HMAC通过:
自此确认了ESP包长度不对的原因,是因为ESP认证数据截短到了12字节。之所以抓包比对ESP包长度只相差了4字节,是因为我们的代码中有bug,完整性的长度icv_size设置成了16,而实际应该是32字节。
也是因为这个原因,导致我们一开始误以为认证长度是截短为28字节。
接下来就是修改内核代码了,将SM3算法的认证长度截短到12字节,出现ESP包接收之后死机的现象,怀疑可能是解密出的包格式不对。
SM4解密
确认了认证数据长度之后,密文范围也就确定了。手动解密得到原文数据如下:
又尝试了其他几个ESP包密文数据,解出来的原文也是这种格式
对比正常的ping的抓包:
ping包原文的格式应该是没有问题的,具体的ping包格式如下:
所以解密的包格式应该是没有问题的,问题应该是其他地方引入的。经过一番折腾,后来发现系统死机是因为之前的残留的修改没有删除所致。重新拿一个干净的内核源码修改后正常。
内核代码修改
在xfrm_algo.c
中修改算法描述,将icv_truncbits
改成96
然后xfrm_user.c
中构造xfrm_state
的时候,将xfrm_algo_desc
中的值赋给xfrm_algo_auth
结构体。
esp.c
中esp_init_authenc
在初始化的时候,设置认证的长度到crypto_aead
结构体中。
然后在后续ESP包认证解密的时候,使用crypto_aead_authsize
获取认证长度。
代码的定位过程
内核中加密相关函数调用,都是通过回调的方式,通过其中的成员配合container_of
宏获取到实际的算法结构体指针。以crypto_aead
为例。其中的base是一个算法转换实例
1 | 154 struct crypto_aead { |
再看crypto_tfm
的结构,其中的__crt_alg
是实际的算法实现结构体的指针
1 | 572 struct crypto_tfm { |
然后通过container_of
宏,可以获取到该指针对应的上层算法aead_alg
结构体的指针
1 | 196 static inline struct aead_alg *crypto_aead_alg(struct crypto_aead *tfm) |
即通过aead_alg
中base成员的地址,获取aead_alg
结构体本身的地址。
1 | 136 struct aead_alg { |
所以如下的解密操作,最终会调用到crypto_authenc_decrypt
中(因为使用的是加密和认证独立的算法)
1 | 355 static inline int crypto_aead_decrypt(struct aead_request *req) |
对于其他的结构体也是类似。例如通过ahash_request
中base获取到crypto_tfm
的指针,然后通过container_of
宏获取到crypto_ahash
的指针。
1 | 54 struct ahash_request { |