-
-
[原创]探索CVE-2026-31431的热补丁修复方案
-
发表于: 1天前 396
-
零、背景
在修复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;
/* Move high-order bits of sequence number back. */
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热补丁,是我不敢按官方方案做热补丁的主要原因。
做为一个临时的修补方案,我想内存还原法可以在不怎么影响性能、不影响业务逻辑的情况下,防御漏洞攻击。