【kernel exploit】CVE-2022-2588 Double-free 漏洞 DirtyCred 利用
影响版本:Linux v3.17 (commit) ~v5.19.1。 v5.19.2已修补。
测试版本:Linux-5.19.1 exploit及测试环境下载地址—https://github.com/bsauce/kernel-exploit-factory
编译选项:
CONFIG_BINFMT_MISC=y (否则启动VM时报错)
CONFIG_USER_NS=y (触发漏洞需要 User Namespace)
CONFIG_NET_CLS_ROUTE4=y (漏洞函数所在的模块)
CONFIG_DUMMY=y CONFIG_NET_SCH_QFQ=y (breezeO_o 提供的两个编译选项,触发poc需要用到)
CONFIG_NET_CLS_ACT / CONFIG_NET_CLS_BASIC (默认已开启)
CONFIG_NET_SCH_SFQ (exp中触发漏洞需用到 sfq随机公平队列)
CONFIG_NET_EMATCH_META (exp中堆喷对象时需要用到)
在编译时将.config
中的CONFIG_E1000
和CONFIG_E1000E
,变更为=y。参考
$ wget https://mirrors.tuna.tsinghua.edu.cn/kernel/v5.x/linux-5.19.1.tar.xz
$ tar -xvf linux-5.19.1.tar.xz
# KASAN: 设置 make menuconfig 设置"Kernel hacking" ->"Memory Debugging" -> "KASan: runtime memory debugger"。
$ make -j32
$ make all
$ make modules
# 编译出的bzImage目录:/arch/x86/boot/bzImage。
漏洞描述:和 CVE-2021-3715 (参见 BlackHat 2021-Europe-Your Trash Kernel Bug, My Precious 0-day 16页)类似,由于将 route4_filter
对象从链表中删除和释放时的检查条件不一致,导致该对象被释放后仍存于链表中,后面可以触发 Double-Free。需要 User Namespaces
才能触发。采用 DirtCred 方法进行提权。
补丁:patch
diff --git a/net/sched/cls_route.c b/net/sched/cls_route.c
index a35ab8c27866e..3f935cbbaff66 100644
--- a/net/sched/cls_route.c
+++ b/net/sched/cls_route.c
@@ -526,7 +526,7 @@ static int route4_change(struct net *net, struct sk_buff *in_skb,
rcu_assign_pointer(f->next, f1);
rcu_assign_pointer(*fp, f);
- if (fold && fold->handle && f->handle != fold->handle) {
+ if (fold) {
th = to_hash(fold->handle);
h = from_hash(fold->handle >> 16);
b = rtnl_dereference(head->table[th]);
保护机制:KASLR/SMEP/SMAP/KPTI
利用总结:exp中主要是两个函数完成漏洞利用, run_exp()
记为进程1,exploit()
记为进程2。进程之间是通过pipe进行通信,以确定运行顺序;注意每次新生成子进程,都要先绑定到 CPU0 上运行。喷射次数 middle
和 end
值可以适当调整,本文采用的是 middle=38 / end=38+40
。DirtyCred 的详细原理可参考 【bsauce读论文】 DirtyCred-内核凭证替换利用技术 和 【kernel exploit】CVE-2021-4154 错误释放任意file对象-DirtyCred利用。
进程1 | 进程2 |
---|---|
0. 绑定到CPU 0 上运行,设置子进程内存、工作目录、Namespace,启动进程2; | |
1. 去碎片化,打开10000个文件,消耗 filp cache,为 cross-cache 作准备; | |
2. 喷射 (middle+3)*32 kmalloc-192 & kmalloc-256;(和漏洞对象位于同一cache,便于进行 cross-cache 被 file 对象复用) | |
3. 分配1个 route4_filter 漏洞对象,还有1个kmalloc-256 的漏洞对象; | |
4. 再喷射 (end-middle-2)*32 kmalloc-192 & kmalloc-256; | |
5. 释放 (end-24)*32 kmalloc-192 & kmalloc-256; | |
6. 第1次释放漏洞对象 kmalloc-192 & kmalloc-256; | |
7. 释放 (end-middle+1) kmalloc-192 & kmalloc-256;(避免连续释放同一对象,触发内核 double-free 的检测) | |
8. 喷射 4000 个低权限 file 对象(通过打开 exp_dir/data 文件); | |
9. 第2次释放漏洞对象 kmalloc-192 & kmalloc-256 | |
10. 喷射 5000 个低权限 file 对象,采用 kcmp 调用检查是否和前4000个 file 重合,重合的两个 file 记为 overlap_a / overlap_b ; | |
11. 发起3个利用线程,线程1写入大量数据来占用文件锁;线程2往 overlap_a 写入恶意数据; | |
12. 线程3关闭 overlap_a / overlap_b ,喷射 4096*2 个高权限 file 对象(通过打开 /etc/passwd 文件);未区分CPU | |
13. 最后检查 /etc/passwd 文件是否被写入恶意数据。 |
1. 漏洞分析
1-1. 漏洞原理
漏洞分析:漏洞函数是 route4_change(),用于初始化和替换 route4_filter
对象。使用 handle
作为id来区分不同的 route4_filter
,如果存在某个 handle 之前已被初始化过(fold
变量非空),就会移除旧的 filter,添加新的 filter;否则直接添加新的 filter
。
先在 [4]
处将 old filter 从 list 中删除,再在 [6]
处释放 old filter。漏洞位于 [4]
处,检查条件是需同时满足旧filter 的 handle
非0且新旧 filter 的 handle
不相等,这和 [6]
处的条件不一致,[6]
只检查旧 filter 是否存在。因此,如果用户创建的 filter 的 handle 为0,则 old filter 在 [4]
处不会被移除,但是在 [6]
处会被释放。
漏洞触发序列:SYSCALL-write -> ksys_write() -> vfs_write() -> new_sync_write() -> call_write_iter() -> sock_write_iter() -> sock_sendmsg() -> sock_sendmsg_nosec() -> netlink_sendmsg() -> netlink_unicast() -> netlink_unicast_kernel() -> rtnetlink_rcv() -> netlink_rcv_skb() -> rtnetlink_rcv_msg() -> tc_new_tfilter() -> route4_change()
static int route4_change(struct net *net, struct sk_buff *in_skb,
struct tcf_proto *tp, unsigned long base, u32 handle,
struct nlattr **tca, void **arg, bool ovr,
bool rtnl_held, struct netlink_ext_ack *extack)
{
struct route4_filter *fold, *f1, *pfp, *f = NULL;
fold = *arg; // fold 来自上层函数,根据句柄调用 route4_get() 找到现有的 route4_filter
...
f = kzalloc(sizeof(struct route4_filter), GFP_KERNEL); // [0] 分配新的 route4_filter 对象
err = tcf_exts_init(&f->exts, net, TCA_ROUTE4_ACT, TCA_ROUTE4_POLICE); // route4_filter->exts.action 分配 256 字节的空间
...
if (fold) { // [1] fold 非空, 表示对应handle 的 filter 已存在, 将 old filter 的信息拷贝到 new filter 并在 [2] 初始化 new filter
f->id = fold->id;
f->iif = fold->iif;
f->res = fold->res;
f->handle = fold->handle;
f->tp = fold->tp;
f->bkt = fold->bkt;
new = false;
}
// initialize the new filter
err = route4_set_parms(net, tp, base, f, handle, head, tb, // [2] 初始化 new filter
tca[TCA_RATE], new, flags, extack);
if (err < 0)
goto errout;
// insert the new filter to the list
h = from_hash(f->handle >> 16); // [3] 将 new filter 插入到 list
fp = &f->bkt->ht[h];
for (pfp = rtnl_dereference(*fp);
(f1 = rtnl_dereference(*fp)) != NULL;
fp = &f1->next)
if (f->handle < f1->handle)
break;
tcf_block_netif_keep_dst(tp->chain->block);
rcu_assign_pointer(f->next, f1);
rcu_assign_pointer(*fp, f);
// remove fold filter from the list if fold exists
if (fold && fold->handle && f->handle != fold->handle) {// [4] 若存在 old filter, 则从 list 中移除
th = to_hash(fold->handle);
h = from_hash(fold->handle >> 16);
b = rtnl_dereference(head->table[th]);
if (b) {
fp = &b->ht[h];
for (pfp = rtnl_dereference(*fp); pfp;
fp = &pfp->next, pfp = rtnl_dereference(*fp)) {
if (pfp == fold) {
rcu_assign_pointer(*fp, fold->next); [5]// remove the old from the linked list
break;
}
}
}
}
...
// free the fold filter if it exists // [6] 释放 old filter
if (fold) {
tcf_unbind_filter(tp, &fold->res);
tcf_exts_get_net(&fold->exts);
tcf_queue_work(&fold->rwork, route4_delete_filter_work); // [7] 启动内核任务,调用 oute4_delete_filter_work() 释放 old filter
}
...
}
static inline int tcf_exts_init(struct tcf_exts *exts, struct net *net,
int action, int police)
{
··· ···
exts->actions = kcalloc(TCA_ACT_MAX_PRIO, sizeof(struct tc_action *), // 分配 32*8=256 大小的空间, 32个 tc_action 结构体指针
GFP_KERNEL);
··· ···
}
1-2. 漏洞对象
漏洞对象:有两个漏洞对象,我们用的是 kmalloc-256 这个对象,因为 file
对象的大小是 232,二者比较接近,便于进行 cross-cache
。
- route4_filter —— 大小为 144,属于
kmalloc-192
; - tc_action —— 大小为 192,漏洞对象并非
tc_action
对象,而是存储32个tc_action
结构体指针的空间,也即大小为 256 的空间,属于kmalloc-256
。
漏洞释放链:route4_delete_filter_work() -> __route4_delete_filter() -> tcf_exts_destroy()
static void route4_delete_filter_work(struct work_struct *work)
{
struct route4_filter *f = container_of(to_rcu_work(work),
struct route4_filter,
rwork);
rtnl_lock();
__route4_delete_filter(f); // <-------
rtnl_unlock();
}
static void __route4_delete_filter(struct route4_filter *f)
{
tcf_exts_destroy(&f->exts); // <-------
tcf_exts_put_net(&f->exts);
kfree(f); // 释放 tc_action 对象
}
void tcf_exts_destroy(struct tcf_exts *exts)
{
#ifdef CONFIG_NET_CLS_ACT // 编译时勾选 CONFIG_NET_CLS_ACT —— 默认是开启的
if (exts->actions) {
tcf_action_destroy(exts->actions, TCA_ACT_UNBIND);
kfree(exts->actions); // 释放 tc_action 对象
}
exts->nr_actions = 0;
#endif
}
EXPORT_SYMBOL(tcf_exts_destroy);
另一处释放链:route4_delete() -> route4_delete_filter_work()
static int route4_delete(struct tcf_proto *tp, void *arg, bool *last,
bool rtnl_held, struct netlink_ext_ack *extack)
{
struct route4_head *head = rtnl_dereference(tp->root);
struct route4_filter *f = arg;
struct route4_filter __rcu **fp;
struct route4_filter *nf;
struct route4_bucket *b;
...
fp = &b->ht[from_hash(h >> 16)];
for (nf = rtnl_dereference(*fp); nf;
fp = &nf->next, nf = rtnl_dereference(*fp)) {
if (nf == f) {
...
tcf_queue_work(&f->rwork, route4_delete_filter_work); // <--------
...
}
}
...
}
2. 漏洞利用
思路:由于已被释放的 fold
仍位于链表上,就可以再次释放 fold
,触发 route4_filer
对象的 Double-free,如果编译内核时开启了 CONFIG_NET_CLS_ACT
,那么 route4_filter->exts.actions
对象也会 Double-free。我们可以利用这两种漏洞对象来进行 DirtyCred 攻击,分别替换进程凭证(task credentials,利用 kmalloc-192)和文件凭证(open file credentials,利用 kmalloc-256)。
根据 DirtyCred 的思路,我们在文件写许可检查之后,替换 file
结构,就能往只有读许可的文件写数据。理论上,exp能通杀各个漏洞版本的内核(有些老版本内核中,msg_msg
隔离在 kmalloc-rcl-*
中,所以需要使用不同的堆喷对象)。作者测试后能在以下版本中提权:
- CentOS 8/Stream (4.18.0-80.el8.x86_64 ~ xxx)
- Debian 11 (5.10.0-8-amd64 ~ xxx)
- Fedora 33 (5.8.15-301.fc33.x86_64 ~ xxx)
- Manjaro 18 (xxx ~ xxx)
- RHEL 8 (4.18.0-80.el8.x86_64 ~ xxx)
- Ubuntu 17 (4.10.0-19-generic ~ xxx)
- Ubuntu 18 (xxx ~ xxx)
- Ubuntu 19 (5.0.0-38-generic ~ xxx)
- Ubuntu 20 (xxx ~ xxx)
问题:Double-free 触发崩溃。我们用到的漏洞对象是 kmalloc-256
,但漏洞同时也会把一个 kmalloc-192
释放两次,就会触发内核 Double-free 检测,导致崩溃。解决办法是,在两次释放 kmalloc-192
之间间隔释放同一页的其他 slab,就能避免崩溃。
static inline void set_freepointer(struct kmem_cache *s, void *object, void *fp)
{
unsigned long freeptr_addr = (unsigned long)object + s->offset;
#ifdef CONFIG_SLAB_FREELIST_HARDENED
BUG_ON(object == fp); // 检测 double-free
#endif
freeptr_addr = (unsigned long)kasan_reset_tag((void *)freeptr_addr);
*(void **)freeptr_addr = freelist_ptr(s, fp, freeptr_addr);
}
cross-cache:我们将释放某个 kmalloc-256
cache page,将该页归还给页管理器,然后分配 file
结构来复用该页(filp
cache)。步骤如下:
- (1)分配一堆
kmalloc-256
堆块,包含漏洞对象; - (2)利用漏洞第1次释放漏洞对象,并释放一堆
kmalloc-256
,以归还漏洞对象所在的页; - (3)分配大量低权限
file
对象来占据漏洞对象(cross-cache attack); - (4)利用漏洞第2次释放漏洞对象,堆喷高权限
file
对象来替换低权限file
对象。
测试截图:
3. exp适配
错误1:漏洞触发出错。在我编译的 Linux-v5.19.1 上测试exp总是失败,经过调试发现连漏洞都无法触发,在 tc_new_tfilter() tcf_proto_create()处就报错返回了,查阅后发现我这个内核版本中默认加载的流量控制队列只有以下几种:
$4 = 0xffffffff82b13f30 <pfifo_fast_ops+16> "pfifo_fast"
$5 = 0xffffffff82b14470 <pfifo_qdisc_ops+16> "pfifo"
$6 = 0xffffffff82b143b0 <bfifo_qdisc_ops+16> "bfifo"
$7 = 0xffffffff82b142f0 <pfifo_head_drop_qdisc_ops+16> "pfifo_head_drop"
$8 = 0xffffffff82b14170 <mq_qdisc_ops+16> "mq"
$9 = 0xffffffff82b13ff0 <noqueue_qdisc_ops+16> "noqueue"
$10 = 0xffffffff82b14230 <blackhole_qdisc_ops+16> "blackhole"
$11 = 0xffffffff82b14530 <qfq_qdisc_ops+16> "qfq"
并未加载 sfq随机公平队列 (exp中就是采用的这种队列),sfq
代码实现位于 net/sched/sch_sfq.c,查看 Makefile
文件才发现 sfq
对应的编译选项是 CONFIG_NET_SCH_SFQ
,编译时并未勾选这个选项,只有 CONFIG_NET_SCH_QFQ
默认是勾选上了的,所以需重新编译内核。
错误2:堆喷出错。 tcf_em_tree_validate() -> tcf_em_validate() 这里根据传入的 em_hdr->kind
(0x4)来寻找对应的 ops 结构,但是没找到。exp中赋值为 hdr->kind = TCF_EM_META
,搜索源码发现 TCF_EM_META
出现在 net/sched/em_meta.c,查看 Makefile
发现没有勾选 CONFIG_NET_EMATCH_META
选项。
参考
[漏洞分析] CVE-2022-2588 route4 double free内核提权
文档信息
- 本文作者:bsauce
- 本文链接:https://bsauce.github.io/2022/10/21/CVE-2022-2588/
- 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)