Introduction
本漏洞产生的原因是Kernel版本<4.10.6的XFRM模块中存在一段越界写,使得攻击者可以覆盖cred中的值从而本地提权。
XFRM
XFRM(transform)是Linux中实现IPsec协议的模块。
IPsec
IPSec是通过分组加密认证来保护IP协议的一类协议,主要包含认证头(Authentication Header,AH)、封装安全载荷(Encapsulating Security Payload,ESP)和网络密钥交换(Internet Key Exchange, IKE)。其中AH和ESP都需要用到安全协商/安全关联(Security Associstion, SA)。SA一般定义了IPsec双方的IP地址、IPsec协议、加密算法、密钥、模式、抗重放窗口等。
NetLink
对于XFRM模块,用户和内核的交互是由NetLink来实现的。如果不深究细节的话我觉得可以把NetLink简单的理解成ioctl over socket。用户将想要调用的函数对应的常数设置为nlmsg_type
然后将msg
发送到kernel,XFRM收到消息后会根据nlmsg_type
来调用对应的函数。下图是调用xfrm_new_ae
的一个例子:
Vulnerability
XFRM模块用xfrm_state
结构来保存SA,xfrm_state
中存在一个变长结构体xfrm_replay_state_esn
1
2
3
4
5
6
7
8
9struct xfrm_replay_state_esn {
unsigned int bmp_len;
__u32 oseq;
__u32 seq;
__u32 oseq_hi;
__u32 seq_hi;
__u32 replay_window;
__u32 bmp[0];
};bmp_len
表示bmp
数组的长度,replay_window
表示bmp
的索引范围。这个结构体由xfrm_add_sa
函数初始化,具体调用顺序为xfrm_add_sa() -> xfrm_state_construct() -> xfrm_alloc_replay_state_esn() / xfrm_update_ae_params()
,除此之外xfrm_new_ae
函数也会调用xfrm_update_ae_params()
来更新xfrm_replay_state_esn
。 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17static void xfrm_update_ae_params(struct xfrm_state *x, struct nlattr **attrs,
int update_esn)
{
...
struct nlattr *re = update_esn ? attrs[XFRMA_REPLAY_ESN_VAL] : NULL;
...
if (re) {
struct xfrm_replay_state_esn *replay_esn;
replay_esn = nla_data(re);
memcpy(x->replay_esn, replay_esn,
xfrm_replay_state_esn_len(replay_esn));
memcpy(x->preplay_esn, replay_esn,
xfrm_replay_state_esn_len(replay_esn));
}
...
}xfrm_update_ae_params()
的代码可以发现这个函数可以将来自用户空间的数据复制到replay_esn
当中。
在xfrm_new_ae()
的代码中可以看到其在调用xfrm_update_ae_params()
更新esn之前先调用了xfrm_replay_verify_len()
做了一个检查,如果检查通过就更新esn。 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23static int xfrm_new_ae(struct sk_buff *skb, struct nlmsghdr *nlh,
struct nlattr **attrs)
{
struct net *net = sock_net(skb->sk);
struct xfrm_state *x;
struct km_event c;
int err = -EINVAL;
...
struct nlattr *re = attrs[XFRMA_REPLAY_ESN_VAL];
...
err = xfrm_replay_verify_len(x->replay_esn, re);
if (err)
goto out;
spin_lock_bh(&x->lock);
xfrm_update_ae_params(x, attrs, 1);
spin_unlock_bh(&x->lock);
...
out:
xfrm_state_put(x);
return err;
}
但是在xfrm_replay_verify_len()
当中并没有对replay_window的检测,这就导致了数组越界的问题。
通过分析对于replay_window
的操作可以发现这里的数据越界在xfrm_replay_advance_esn()
中可以被攻击者用于一段区间置零或者1bit写。
有兴趣详细了解以上代码的选手可以翻阅:
Exploit
首先fork
大量进程来往堆上喷射大量cred
结构体,这些进程很快退出,在kmem_cache
上留下大量free chunk。通过调试可以发现本内核的cred_jar
是kmalloc-192
。由于SLUB的特性,如果此时我们申请大小为128到192的xfrm_replay_state_esn
结构,其就很有可能和之后分配的cred
相邻。
所以我们紧接着调用xfrm_add_sa()
分配xfrm_replay_state_esn
结构体并调用xfrm_new_ae()
来修改replay_window
使其越界。
然后再往堆上大量喷射cred
结构体。
此时我们触发OOB,这样xfrm_replay_advance_esn()
就能越界写到cred
中的uid
等字段从而提权。
Debug Details
在前面分配replay_esn
的时候会伴生一个preplay_esn
,后者虽然没什么用但是会影响堆结构。分配完之后堆结构大概是这样:
图中绿色区域就是bmp[(replay_window - 1) >> 5]
能索引到的范围,因为我们还没有修改replay_window
,所以此时的索引范围是合法的。
在触发xfrm_new_ae()
来修改replay_window
再喷射大量cred
之后,我们的堆结构变成了这样:
在左边的内存窗口中可以看到堆上连续排布的replay_esn
, preplay_esn
和cred
结构体,此时bmp[(replay_window - 1) >> 5]
已经越界并可以改到cred的头部的uid
等字段。下面触发OOB验证前面的分析:
可以看到原本的绿色区域被置零,这里用红色标记。原本cred
头部存放的uid
等均被修改为0,拿到root。
Reproducing Environment (for WSL)
Compile linux-4.4.20
edit makefile: add -fno-pie
1 | KBUILD_CFLAGS := -Wall -Wundef -Wstrict-prototypes -Wno-trigraphs \ |
启用一些config (也可以像我一样直接抄ET的作业
1 | CONFIG_INET_AH=y |
1 | make |
Image使用syzkaller创建 1
2
3wget https://raw.githubusercontent.com/google/syzkaller/master/tools/create-image.sh -O create-image.sh
chmod +x create-image.sh
./create-image.sh
qemu启动 1
2
3
4
5
6
7
8qemu-system-x86_64 \
-m 2048 \
-gdb tcp::1234 \
-net nic -net user,hostfwd=tcp:127.0.0.1:2222-:22 \
-display none -serial stdio -no-reboot \
-hda ./stretch.img \
-kernel ./bzImage \
-append "root=/dev/sda console=ttyS0"
如果虚拟机网卡不是eth0,比如我执行dmesg | grep -i eth0
发现enp0s3: renamed from eth0
,运行sed -i 's/eth0/enp0s3/g' /etc/network/interfaces
替换即可。