记录一个IPsec VPN检测时ESP包解密失败问题的定位过程

本文记录一次IPsec VPN检测时ESP解密失败问题的定位过程。涉及到IPsec VPN协议、加密算法、内核xfrm框架等知识。

问题背景描述

先交代下问题背景,公司新开发的IPsec VPN产品去过检测。在协议检测时,出现检测平台发送过来的ESP包在解密失败的问题,检测算法是SM4-SM3。

我并没有参与IPsec产品的开发,之前对于IPsec也是丝毫不了解。我在这样的情况下协助进行问题定位。这个问题的排查的难点在于对方是黑盒,你只能根据己方这边的信息进行分析定位。

以下分别为检测平台的ESP抓包和我们的ipsec程序自己跟自己互通时的ESP抓包。可以看到检测平台的ESP包长度要短4个字节。

检测平台ESP抓包

自己互通ESP抓包

ESP规范

首先想到的肯定是ESP包的格式问题,是不是某些字段的理解跟协议有出入。于是查阅相关规范。

国密规范中ESP的描述如下:

ESP规范1

ESP规范2

ESP规范3

国密规范有些地方讲的不清楚,更精确的描述参看rfc2406-esp

ESP包格式分析

结合规范及实际的抓包分析,ESP包的格式应如下:

1
2
3
|ESP头(SPI+Index)|IV|原IP头|原协议头|载荷数据|padding|ESP尾(填充长度+下一个头)|ESP认证数据|
|<----------------------加密范围-------------------->|
|<---------------------------------认证范围----------------------------->|

其中padding的长度,要保证加密范围的长度是块加密算法块长度(SM4为16)的整数倍,即加上ESP尾之后的长度是block长度的整数倍。

ESP格式1

密文数据跟认证数据的长度尚无法判断。

原因分析

ESP包的长度不对,可能的原因有:

  1. ESP包的格式跟我们理解的不一样,可能还有其他字段
  2. 实际使用的算法并非SM4-SM3
  3. 包长度因主动或被动的原因被截短了(因为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字节作为认证数据可以验证通过。

验证过程如下,协商出的密钥信息如下:

完整性验证-key

接收到的ESP包原始数据如下:

完整性验证-esp

手动验证HMAC通过:

完整性验证-hmac

自此确认了ESP包长度不对的原因,是因为ESP认证数据截短到了12字节。之所以抓包比对ESP包长度只相差了4字节,是因为我们的代码中有bug,完整性的长度icv_size设置成了16,而实际应该是32字节。

也是因为这个原因,导致我们一开始误以为认证长度是截短为28字节。

接下来就是修改内核代码了,将SM3算法的认证长度截短到12字节,出现ESP包接收之后死机的现象,怀疑可能是解密出的包格式不对。

SM4解密

确认了认证数据长度之后,密文范围也就确定了。手动解密得到原文数据如下:

SM4解密-1

又尝试了其他几个ESP包密文数据,解出来的原文也是这种格式

SM4解密-2

SM4解密-3

对比正常的ping的抓包:

普通ping抓包

ping包原文的格式应该是没有问题的,具体的ping包格式如下:

ping包格式

所以解密的包格式应该是没有问题的,问题应该是其他地方引入的。经过一番折腾,后来发现系统死机是因为之前的残留的修改没有删除所致。重新拿一个干净的内核源码修改后正常。

内核代码修改

xfrm_algo.c中修改算法描述,将icv_truncbits改成96

SM3认证截断96

然后xfrm_user.c中构造xfrm_state的时候,将xfrm_algo_desc中的值赋给xfrm_algo_auth结构体。

截断长度赋值

esp.cesp_init_authenc在初始化的时候,设置认证的长度到crypto_aead结构体中。

初始化时设置认证长度

然后在后续ESP包认证解密的时候,使用crypto_aead_authsize获取认证长度。

获取认证长度

代码的定位过程

内核中加密相关函数调用,都是通过回调的方式,通过其中的成员配合container_of宏获取到实际的算法结构体指针。以crypto_aead为例。其中的base是一个算法转换实例

1
2
3
4
5
6
  154 struct crypto_aead {
155 unsigned int authsize;
156 unsigned int reqsize;
157
158 struct crypto_tfm base;
159 };

再看crypto_tfm的结构,其中的__crt_alg是实际的算法实现结构体的指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
572 struct crypto_tfm {
573
574 u32 crt_flags;
575
576 union {
577 struct ablkcipher_tfm ablkcipher;
578 struct blkcipher_tfm blkcipher;
579 struct cipher_tfm cipher;
580 struct compress_tfm compress;
581 } crt_u;
582
583 void (*exit)(struct crypto_tfm *tfm);
584
585 struct crypto_alg *__crt_alg;
586
587 void *__crt_ctx[] CRYPTO_MINALIGN_ATTR;
588 };

然后通过container_of宏,可以获取到该指针对应的上层算法aead_alg结构体的指针

1
2
3
4
5
  196 static inline struct aead_alg *crypto_aead_alg(struct crypto_aead *tfm)
197 {
198 return container_of(crypto_aead_tfm(tfm)->__crt_alg,
199 struct aead_alg, base);
200 }

即通过aead_alg中base成员的地址,获取aead_alg结构体本身的地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  136 struct aead_alg {
137 int (*setkey)(struct crypto_aead *tfm, const u8 *key,
138 unsigned int keylen);
139 int (*setauthsize)(struct crypto_aead *tfm, unsigned int authsize);
140 int (*encrypt)(struct aead_request *req);
141 int (*decrypt)(struct aead_request *req);
142 int (*init)(struct crypto_aead *tfm);
143 void (*exit)(struct crypto_aead *tfm);
144
145 const char *geniv;
146
147 unsigned int ivsize;
148 unsigned int maxauthsize;
149 unsigned int chunksize;
150
151 struct crypto_alg base;
152 };

所以如下的解密操作,最终会调用到crypto_authenc_decrypt中(因为使用的是加密和认证独立的算法)

1
2
3
4
5
6
7
8
9
355 static inline int crypto_aead_decrypt(struct aead_request *req)
356 {
357 struct crypto_aead *aead = crypto_aead_reqtfm(req);
358
359 if (req->cryptlen < crypto_aead_authsize(aead))
360 return -EINVAL;
361
362 return crypto_aead_alg(aead)->decrypt(req);
363 }

对于其他的结构体也是类似。例如通过ahash_request中base获取到crypto_tfm的指针,然后通过container_of宏获取到crypto_ahash的指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
   54 struct ahash_request {
55 struct crypto_async_request base;
56
57 unsigned int nbytes;
58 struct scatterlist *src;
59 u8 *result;
60
61 /* This field may only be used by the ahash API code. */
62 void *priv;
63
64 void *__ctx[] CRYPTO_MINALIGN_ATTR;
65 };

158 struct crypto_async_request {
159 struct list_head list;
160 crypto_completion_t complete;
161 void *data;
162 struct crypto_tfm *tfm;
163
164 u32 flags;
165 };

201 struct crypto_ahash {
202 int (*init)(struct ahash_request *req);
203 int (*update)(struct ahash_request *req);
204 int (*final)(struct ahash_request *req);
205 int (*finup)(struct ahash_request *req);
206 int (*digest)(struct ahash_request *req);
207 int (*export)(struct ahash_request *req, void *out);
208 int (*import)(struct ahash_request *req, const void *in);
209 int (*setkey)(struct crypto_ahash *tfm, const u8 *key,
210 unsigned int keylen);
211
212 unsigned int reqsize;
213 bool has_setkey;
214 struct crypto_tfm base;
215 };
-------------本文结束感谢您的阅读-------------

欢迎关注我的其它发布渠道