Copy Fail (CVE-2026-31431) 漏洞原理与 AF_ALG AEAD API 详解教学文档
本文档基于一篇关于 Copy Fail 内核漏洞的技术文章,旨在详细解析该漏洞的原理、相关的 AF_ALG 套接字接口及其AEAD(带关联数据的认证加密)API的实现机制。通过学习,您将理解如何利用内核加密模块中的一个逻辑错误,向只读文件的页缓存中写入数据。
第一部分:漏洞概述 (Copy Fail - CVE-2026-31431)
漏洞本质:
Copy Fail 是 Linux 内核身份验证加密模块 (authencesn) 中存在的一个逻辑错误。它允许非特权用户(普通用户)向任意可读文件的页缓存中写入 4 字节的任意数据,并且可以多次写入。
关键特性与影响:
- 内存写入,非持久化:内核不会将被写入的文件页标记为“脏页”(dirty page),因此不会触发写回操作,硬盘上的原始文件数据不会被修改。
- 运行时影响:当进程访问该文件时,读取的是内存中的页缓存。由于页缓存中的数据已被篡改,这意味着内存中文件内容的损坏会影响依赖此文件运行的程序,可能导致拒绝服务、权限提升或其他意外行为。
- 利用代码:公开的漏洞利用(Exploit)C语言版本位于 GitHub: https://github.com/tgies/copy-fail-c
第二部分:AF_ALG 内核加密套接字接口
AF_ALG 是 Linux 内核提供的一个 Netlink 套接字族,用于在用户空间和内核空间的加密 API 之间建立通信桥梁。它允许用户空间程序通过标准的套接字系统调用(如 socket, bind, setsockopt, sendmsg, recvmsg)来请求内核执行加密操作。
主要功能:
- 支持多种加密算法,包括:
- 消息摘要(如 HMAC, CMAC)
- 对称密码
- AEAD 密码
- 随机数生成器
- 原地操作(In-place Operation):
send/write的输入缓冲区和recv/read的输出缓冲区可以是同一块内存区域,这有助于提升性能并减少内存拷贝。
内核文档:详细使用方式可参考内核官方文档:https://www.kernel.org/doc/html/v6.1/crypto/userspace-if.html
第三部分:与 AF_ALG API 的交互方式
用户必须通过 socket 系统调用来与内核加密 API 交互。
关键步骤:
- 创建控制套接字:使用
socket(AF_ALG, SOCK_SEQPACKET, 0)创建一个控制套接字 (ctrl_sock)。 - 绑定算法:填充
struct sockaddr_alg结构体,指定算法族 (salg_family=AF_ALG)、算法类型(如salg_type="aead")和具体算法名称(如salg_name="authencesn(hmac(sha256),cbc(aes))")。然后通过bind()将此套接字与算法绑定。 - 配置参数:对于 AEAD 等算法,需要使用
setsockopt()在SOL_ALG层级设置密钥 (ALG_SET_KEY) 和认证标签大小 (ALG_SET_AEAD_AUTHSIZE)。 - 创建操作套接字:通过对控制套接字调用
accept(),生成一个用于实际数据加密/解密操作的套接字 (op_sock)。 - 数据操作:通过
send()/write()向操作套接字发送待处理的数据,通过read()/recv()获取处理结果。
第四部分:AEAD (Authenticated Encryption with Associated Data) 原理
AEAD 是一种同时提供机密性和完整性/认证的加密模式。
核心输入:
- Key (密钥) 和 IV (初始化向量):用于加解密的秘密信息。
- Plaintext (明文):需要被加密保护的核心数据。
- AAD (关联数据, Associated Data):不需要加密(保持明文),但必须参与完整性校验的数据(例如数据包头部、协议版本号)。
输出:
- Ciphertext (密文):加密后的乱码数据。
- Tag / MAC (认证标签):基于密钥、明文、AAD等计算出的消息认证码,用于验证数据完整性。
工作流程:
- 加密:输入
[AAD | Plaintext],输出[AAD | Ciphertext | Auth Tag]。 - 解密:输入
[AAD | Ciphertext | Auth Tag]。内核使用密钥、IV 和 AAD 进行解密并验证 Tag。如果验证成功,输出[AAD | Plaintext];如果 Tag 不匹配,则返回错误(如-EBADMSG)。
第五部分:内核中 AEAD API 的实现与漏洞根源
5.1 整体生命周期
用户态通过 AF_ALG 套接字接口与内核 AEAD 交互的流程如下:
- 绑定算法:创建基础套接字并通过
bind指定算法。 - 设置密钥:通过
setsockopt将密钥传递给内核,保存在套接字上下文中。 - 建立会话:对基础套接字调用
accept(),返回用于实际操作的新套接字。 - 发送与接收数据:通过
sendmsg发送数据,通过recvmsg接收处理结果。
5.2 数据在内核中的流转 (aead_sendmsg 和 af_alg_sendmsg)
- 用户数据通过
sendmsg传入内核。 - 内核函数
af_alg_sendmsg负责管理这些数据:- 使用散列表 (Scatterlist, sg) 来高效管理可能不连续的内存页。
- 数据被拷贝到内核空间,并添加到散列表 (
ctx->tsgl_list) 中。 - 如果用户设置了
MSG_MORE标志,则暂不触发加密/解密操作。
5.3 漏洞触发的关键:af_alg_sendpage 与只读文件页
漏洞利用的核心在于 splice() 系统调用与 af_alg_sendpage 函数的结合。
- splice 管道操作:利用
splice()将只读文件的页缓存挂载到管道中。 - 页移动到套接字:再将管道中的数据移动到
AF_ALG套接字。此过程会调用af_alg_sendpage。 - 零拷贝(Zero-Copy)引用:
af_alg_sendpage不是将文件数据拷贝到新的内核页,而是通过get_page()增加只读文件页的引用计数,并直接将该页的指针 (page) 挂载到散列表 (sgl->sg) 中。此时,一个本应只读的物理页,被添加到了可写的散列表里,为后续写入埋下伏笔。
5.4 解密过程与越界写入 (_aead_recvmsg)
当用户调用 recvmsg 触发解密时,内核调用 _aead_recvmsg。
- SGL(散列表)管理:涉及三个主要的 SGL:
- TX SGL:包含用户传入的完整数据
[AAD | Ciphertext | Auth Tag]。 - RX SGL:内核分配,用于存放输出(解密后的明文)。
- TAGX (areq->tsgl):专门为 Tag 数据创建的 SGL。
- TX SGL:包含用户传入的完整数据
- 数据重组:
- 将 TX SGL 中的 AAD 和 Ciphertext 拷贝到新分配的 RX SGL 中。
- 通过
af_alg_pull_tsgl释放 TX SGL 中 AAD 和 Ciphertext 所占页的引用,仅保留 Tag 所在物理页的引用,并将其指针赋给areq->tsgl。 - 使用
sg_chain将 RX SGL 的尾部链接到areq->tsgl,形成一个逻辑上连续但物理上包含只读页的缓冲区。
- 执行解密:调用
crypto_aead_decrypt(),最终跳转到具体算法实现(如crypto_authenc_esn_decrypt)。
5.5 漏洞触发点:ESN扩展序列号的写入
在 authencesn 算法的解密函数中,为了处理 64 位扩展序列号(ESN,用于 IPsec),内核需要将高 32 位序列号临时拼接到数据中参与 HMAC 计算,计算后再丢弃。
scatterwalk_map_and_copy(tmp + 1, dst, assoclen + cryptlen, 4, 1);
这行代码是关键:
dst:指向接收缓冲区,即之前链接好的 RX SGL,其尾部指向包含 Tag 数据的只读文件页。assoclen + cryptlen:这个偏移量正好指向 Tag 数据的开始位置。- 操作:将
tmp+1(即 ESN 的高 32 位)的 4 字节数据,写入到dst缓冲区中assoclen + cryptlen的位置。
漏洞产生:由于 Tag 数据所在的物理页是来自只读文件的页缓存,并且通过 af_alg_sendpage 以零拷贝方式引入,内核在解密过程中错误地向这个只读页执行了写入操作,从而完成了对只读文件页缓存的越权修改。由于内核不认为此页是脏页,所以不会写回磁盘,但内存中的内容已被永久改变,直到该页被释放。
总结
Copy Fail 漏洞的根源在于 Linux 内核加密子系统中 AF_ALG 接口对 AEAD 算法的实现存在缺陷。af_alg_sendpage 允许只读文件页以零拷贝方式进入加密上下文,而随后的 authencesn 解密流程在处理 ESN 时,未检查目标内存页的可写性,便直接向该页执行了内存写入操作。这打破了页缓存的只读保护,使得非特权用户能够篡改任意可读文件在内存中的映像,从而可能破坏应用程序逻辑,构成安全威胁。