copy fail笔记
漏洞信息
无需条件竞争,稳定提权。只需要732字节大小的python脚本即可实现2017年后的大部分发行版提权。
该漏洞是 authencesn 模块的一个逻辑漏洞,从 AF_ALG 以及 splice() 导致4字节的 page-cache 写入。
exp:
1 | #!/usr/bin/env python3 |
漏洞原理
以下内核源码均为 6.12.94 版本
AF_ALG 是一种 socket 类型,其会将内核的 crypto 子系统暴露在普通用户空间。普通用户可以打开,绑定一个任意的AEAD(Authenticated Encryption with Associated Data)模板,然后调用加解密操作。
漏洞的核心原因位于 splice()。其会在文件描述符以及管道直接传递数据,不是拷贝,而是使用page cache,同时增加引用。当一个用户 splice 一个文件到管道,然后再到 AF_ALG 管道,该 socket 的输入 scatterlist 会持有内核中该文件的 page cache 页的直接引用。该页不存在冗余或重复,scatterlist 指向相同的物理页。
对于 AEAD 解密,输入格式为:AAD(associated authenticated data) || ciphertext || authentication_tag. 在 algif_aead.c, 其recvmsg()函数为 aead_recvmsg。该函数会在内部完成以上解密操作,意味着其会将 scatterlist 作为输入和输出的缓冲区。
1 | static struct proto_ops algif_aead_ops = { |
AAD 和加密数据通过 memcpy_sglist 拷贝输入 scatterlist 到输出缓冲区。page cache页仅用于读取。但是 authentication tag 没有被拷贝。内核会保留这些 scatterlist 条目并将它们链接到输出 scatterlist 的尾部使用 sg_chain().
1 | static int _aead_recvmsg(struct socket *sock, struct msghdr *msg, |
1 | Input SGL: AAD || CT || Tag |
输出 scatterlist 现在包含两部分:用户的 recvmsg 缓冲区(AAD+ciphertext),链接后的 tag pages,仍然指向目标文件的原始 page cache。内核设置了 req->src = req->dst,都指向了以上缓冲区头部。
1 | src ----+ |
以上内存布局是漏洞的根本成因,其将page cache页放入了可写的scatterlist,与合法的可写区域仅靠offset来分离。该设计假定了每个 AEAD 算法会限制其写入到正确位置,但是不会做检查。
有一个 AEAD 算法存在问题。 authencesn
内核 AEAD API 定义了一个清晰的输出规范:
the destination buffer receives AAD || plaintext, exactly assoclen + (cryptlen - authsize) bytes.
authencesn 是一个 IPsec 协议提供 ESN 支持的 AEAD wrapper。IPSec 使用64位序列号,被分割为高4字节和低4字节。根据有线协定,高字节会被忽略。在 HMAC 计算中,authencesn 需要重排这些字节: seqno_hi在hash_input头部,seqno_lo在尾部。
其在crypto_authenc_esn_decrypt函数中执行以上操作:
1 | /* Move high-order bits of sequence number to the end. */ |
在src==dst条件下第三次调用scatterwalk_map_and_copy时,会向 assoclen+cryptlen 偏移位置写入4字节,越过了 AEAD tag 位置。导致了越界写入。crypto_authenc_esn_decrypt_tail 会读取 seqno_lo 重新构造 AAD, 但是dst[assoclen+cryptlen] 处的原始字节被写入且未被恢复。
至此,可以实现4字节 page cache 写入。
- page cache 是任意普通用户可读的文件。
- 偏移可以是上一次 spliced 文件数据的 authsize。可以进行任意控制。
- 内容为 seqno_lo,其是 AAD 的第4-7字节,可以通过 sendmsg() 构造。