零、背景
在修复CVE-2026-31431的过程中,同事希望能有一个热补丁方案,减少对现有业务的影响。 该方案不仅要防御攻击,还要避免影响现在和未来的业务。 所以就有了下面对热补丁方案的探索。
一、漏洞原理解析
具体原理,0xlane大佬已经分析的很详细了——Copy Fail 深度研究:Linux 页缓存漏洞的根因、利用与检测 这里只做简析: 该漏洞主要利用了2个点:
_aead_recvmsg 函数中,为了提升效率,将 输入源 直接用作 输出源
crypto_authenc_esn_decrypt 函数中,将 输出源 用于存放临时数据,且未还原完全。
二、涉及业务场景分析
1. 使用场景
基于 rfc4303 的描述,该数据的实际使用场景是IPsec的ESP(Encapsulating Security Payload)部分,用于应对重放攻击。
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Security Parameters Index (SPI) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Sequence Number |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+---
| IV (optional) | ^ p
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | a
| Rest of Payload Data (variable) | | y
~ ~ | l
| | | o
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | a
| | TFC Padding * (optional, variable) | v d
+-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+---
| | Padding (0-255 bytes) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| | Pad Length | Next Header |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Integrity Check Value-ICV (variable) |
~ ~
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
这是实际数据网络数据包的内容。
What What What
# of Requ'd Encrypt Integ is
bytes [1] Covers Covers Xmtd
------ ------ ------ ------ ------
SPI 4 M Y plain
Seq# (low-order bits) 4 M Y plain p
------ a
IV variable O Y plain | y
IP datagram [2] variable M or D Y Y cipher[3] |-l
TFC padding [4] variable O Y Y cipher[3] | o
------ a
Padding 0-255 M Y Y cipher[3] d
Pad Length 1 M Y Y cipher[3]
Next Header 1 M Y Y cipher[3]
Seq# (high-order bits) 4 if ESN [5] Y not xmtd
ICV Padding variable if need Y not xmtd
ICV variable M [6] plain
[1] M = mandatory; O = optional; D = dummy
[2] If tunnel mode -> IP datagram
If transport mode -> next header and data
[3] ciphertext if encryption has been selected
[4] Can be used only if payload specifies its "real" length
[5] See section 2.2.1
[6] mandatory if a separate integrity algorithm is used
这是解密时所需要的内容。
其中有一点需要指出的是:
网络中传输的 Sequence Number,只是 Seq# (low-order bits)
Seq# (high-order bits) 保存在各自的本地,在 Seq# (low-order bits) 溢出时,各自加1
因在加解密时需要,内核 esp_input_set_header 在这个函数会将 Seq# (high-order bits) 放到原数据中
经过解密算法的调整,就变成了应用数据的样子。
2. 具体应用
通过 strongwan 应用模拟使用场景,发现跟上面文档有些许出入,但流程是一样的。 先说 AEAD (Authenticated Encryption with Additional Data)—— 通过附加数据进行认证和加密。 该算法同时具备:认证和加密的功能。这样的好处除了保证数据的完整性和机密性外,认证机制还能防御密文攻击。 该算法的解密过程,分为两个部分:认证检查、密文解码。 而实际过程可能有所变化:比如 echainiv(authencesn(hmac(sha1),cbc(aes))) 其中算法解释:
echainiv: 该算法会从解密数据中提取加密向量。
authencesn:authenc的进阶版本,authenc就是 最基本的AEAD,整个解密数据就是认证和密文内容; 而 authencesn 在其基础上加了esn (AEAD wrapper for IPsec with extended sequence numbers); 就是加上了IPsec协议在传输过程中的序列号,就是上面 rfc4303 中提到的 Seq。 会多一步数据重新排列的过程,而此过程就是该漏洞需要利用的一环。
hmac(sha1):认证部分算法
cbc(aes):加解密部分算法
其中数据移动部分的示意图如下:
其中authencesn部分用python模拟的代码如下(其中数据是从模拟环境中抓取的):
import hmac
import hashlib
import struct
from Crypto.Cipher import AES
tb = lambda s: bytes .fromhex(s.replace(" " , "" ))
_UINT = struct.Struct("<I" )
UINT = lambda x: _UINT.pack(x)
class AlgKey (object ):
K_TMPL = ">II%ds"
def __init__ (self, typ, idd, keys ):
self .typ = typ
self .idd = idd
self .keys = keys
def to_bytes (self ):
s = self .K_TMPL % len (self .keys)
return struct.pack(s, self .typ, self .idd, self .keys)
data = bytes .fromhex("c082dfa20000000000000001a6017e0773cfd2552042e8c79de14c4a17ac32e6b7a98d64c6571a9b0dc37e0667b866b090e8e7e87e5db15c7fdf23b7d9234bae0f58e17811ceba4f09aa9b39b1efadd2c2c1945f5d75e7928baae128596fbaa5dc8ad902129d5e6bad7077a287365fd7272ad583caccabd56955219279c4bda766c56f6922daf876" )
IV = tb("a6 01 7e 07 73 cf d2 55 20 42 e8 c7 9d e1 4c 4a" )
IV = UINT(len (IV)) + IV
iv = IV[4 :]
RESULT = bytes .fromhex("521178d78d411de70b69e2ffb866411c0a1046100000403b00150001575e196a000000008e13020000000000101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f30313233343536370102030405060708090a0a04" )
def f_setkey ():
KEY = AlgKey(0x08000100 , 0x10 , tb("eb c8 87 75 dc 36 06 36 77 e9 5e 94 10 10 00 fc 0e 26 46 d4 64 bc 51 0e 47 47 18 4c f8 62 06 c2 74 b6 47 d1" )).to_bytes()
keylen = len (KEY)
rta_len, rta_type = struct.unpack("<HH" , KEY[:4 ])
param = KEY[4 :]
enckeylen = struct.unpack(">I" , param[:4 ])[0 ]
key = KEY[rta_len:]
keylen -= rta_len
authkeylen = keylen - enckeylen
authkey = key[:authkeylen]
enckey = key[authkeylen:]
print (authkeylen, bytes .hex (authkey))
print (enckeylen, bytes .hex (enckey))
return authkey, enckey
def f_valid (key, data ):
ihash = data[-ASIZE:]
data = data[:4 ] + data[8 :-ASIZE] + data[4 :8 ]
f_hash = hashlib.sha256
hmac_obj = hmac.new(key, digestmod=f_hash)
hmac_obj.digest_size = 32
hmac_obj.block_size = 64
hmac_obj.update(data)
r = hmac_obj.digest()
r = r[:ASIZE]
print ("F=" , bytes .hex (r))
print ("I=" , bytes .hex (ihash))
def f_decrypt (key, data, iv ):
print ("I:" , bytes .hex (IV))
print ("i:" , bytes .hex (data[8 :ALEN]))
data = data[ALEN:-ASIZE]
aes = AES.new(key, AES.MODE_CBC, iv)
r = aes.decrypt(data)
print ("D:" , bytes .hex (r))
print ("R:" , bytes .hex (RESULT))
assert r == RESULT
print ("PASSED" )
authkey, enckey = f_setkey()
f_valid(authkey, data)
f_decrypt(enckey, data, iv)
结论:通过详细分析 authencesn 部分的逻辑,最终可得出——被改写且未还原的Seq high数据,在后面的解密过程并未用到。
二、漏洞修复方案
1、 官方修复方案
官方补丁 主要是回滚了 “_aead_recvmsg 函数中 将 输入源 直接用作 输出源” 的代码,但这个方案无法做热补丁(涉及面太广,存在运行时数据等原因) 该方案可以作为后续升级内核时使用,在此就暂且不论。
2、 热补丁方案
热补丁方案只能通过修补 ——“crypto_authenc_esn_decrypt 函数中,将 输出源 用于存放临时数据,且未还原完全”——这一点来做。 查看近期其他几个类似漏洞 Dirty Frag 、Fragnesia 的官方修复方案,都是采用了和本漏洞 Copy Fail相同 的封堵策略,丝毫没有修改 crypto_authenc_esn_decrypt 的意思。 所以我们要动此函数,要更加谨慎才行。 通过分析漏洞机制,热补丁方案主要想了以下几种: ###2.1 内存平移 该方案的目标是尝试不修改最后的4字节,因为前面有未使用的4字节空间。 原作者设计代码时,应该是本着性能考量的目的,尽量减少内存拷贝。 通过上面的示意图,可以看到内存平移方案不会修改最后的4字节数据。 但依然还是要修改4字节数据,只是位置发生了移动。 攻击者,只需要扩大splice页的长度,就可以绕过此方案。 所以该方案还需要一个数据还原操作,来预防此问题。 这样的话还不如直接使用下面的 “内存还原” 方案。 PS: patchwork中的补丁 使用的是此方案
2.2 内存还原
该方案只是解决,原实现代码在最后只还原了aead associated data部分内存的问题。 猜想是因为,最终需要向用户态返回这部分数据,所以必须要还原。 我们需要再加一个操作,将最后4字节也还原,这样就达到了不修改原数据的目的,从而截断了攻击。
static int crypto_authenc_esn_decrypt_tail (struct aead_request *req,
unsigned int flags)
{
struct crypto_aead *authenc_esn = crypto_aead_reqtfm(req);
unsigned int authsize = crypto_aead_authsize(authenc_esn);
struct authenc_esn_request_ctx *areq_ctx = aead_request_ctx(req);
struct crypto_authenc_esn_ctx *ctx = crypto_aead_ctx(authenc_esn);
struct skcipher_request *skreq = (void *)(areq_ctx->tail +
ctx->reqoff);
struct crypto_ahash *auth = ctx->auth;
u8 *ohash = PTR_ALIGN((u8 *)areq_ctx->tail,
crypto_ahash_alignmask(auth) + 1 );
unsigned int cryptlen = req->cryptlen - authsize; <== [0 ]
unsigned int assoclen = req->assoclen;
struct scatterlist *dst = req->dst;
u8 *ihash = ohash + crypto_ahash_digestsize(auth); <== [1 ]
u32 tmp[2 ];
if (!authsize)
goto decrypt;
scatterwalk_map_and_copy(tmp, dst, 4 , 4 , 0 ); <== [2 ]
scatterwalk_map_and_copy(tmp + 1 , dst, assoclen + cryptlen, 4 , 0 ); <== [3 ]
scatterwalk_map_and_copy(tmp, dst, 0 , 8 , 1 ); <== [4 ]
if (crypto_memneq(ihash, ohash, authsize))
return -EBADMSG;
decrypt:
sg_init_table(areq_ctx->dst, 2 );
dst = scatterwalk_ffwd(areq_ctx->dst, dst, assoclen); <== [5 ]
skcipher_request_set_tfm(skreq, ctx->enc);
skcipher_request_set_callback(skreq, flags,
req->base.complete, req->base.data);
skcipher_request_set_crypt(skreq, dst, dst, cryptlen, req->iv); <== [6 ]
return crypto_skcipher_decrypt(skreq);
}
crypto_authenc_esn_decrypt_tail 函数是 crypto_authenc_esn_decrypt 最后还原数据的地方。 其中,[2] 和 [3] 是读取 低4位 和 高4低的数据,然后通过 [4] 将数据移动到最初始的位置。 我需要在下面加一个还原最后4字节的操作,数据在 [1] 处的 ihash 里。 但我们需要保证这块数据不会被后面的操作用到。 [5] 和 [6] 处限定了后面要使用的数据的范围。 通过上面的示意图,我们看到,后面所使用的数据并未使用到后面的 4字节数据,因为前面有强制性约束 authsize >=4。 至于为什么原作者不还原这4字节数据,个人认为是性能方面的考量(因为没必要),但也不排除可能是有的加密算法需要——这个需要后面跟原作者或相关内核开发者交流确认。 目前的暂时用于测试的补丁方案会是这样:
diff --git a/crypto/authencesn.c b/crypto/authencesn.c
index b60e61b..43b21bf 100644
--- a/crypto/authencesn.c
+++ b/crypto/authencesn.c
@@ -242,6 +242,8 @@ static int crypto_authenc_esn_decrypt_tail(struct aead_request *req,
scatterwalk_map_and_copy(tmp + 1, dst, assoclen + cryptlen, 4, 0);
scatterwalk_map_and_copy(tmp, dst, 0, 8, 1);
+ scatterwalk_map_and_copy(ihash, dst, assoclen + cryptlen, 4, 1);
+
if (crypto_memneq(ihash, ohash, authsize))
return -EBADMSG;
PS: 该方案存在一个可以绕过的地方,如果在认证阶段找到抛异常的路径,依然可以绕过。
三、总结
各家操作系统的修复方案,都没有推出CopyFail热补丁,是我不敢按官方方案做热补丁的主要原因。 做为一个临时的修补方案,我想内存还原法可以在不怎么影响性能、不影响业务逻辑的情况下,防御漏洞攻击。
最后于 3小时前
被wanglxi编辑
,原因: