USMA:用户态映射攻击
概述
众所周知,ROP是一种主流的Linux内核利用方式,它需要攻击者基于漏洞来寻找可用的gadgets,然而这是一件十分耗费时间和精力的事情,并且有时候很有可能找不到合适的gadget。此外由于CFI(控制流完整性校验)利用缓解措施已经被合并到了Linux内核主线中了,所以随着后续主流发行版的跟进,ROP会变得不再可用。
这篇博客主要介绍一种叫做USMA(User-Space-Mapping-Attack),跨平台通用的利用方法。它允许普通用户进程可以映射内核态内存并且修改内核代码段,通过这个方法,我们可以绕过Linux内核中的CFI缓解措施,在内核态中执行任意代码。下面此文会介绍一个漏洞,然后分别使用ROP和USMA两种方法完成对这个漏洞的利用,最后总结一下USMA的优势。
漏洞
漏洞出现在Linux内核中的packet socket模块,这个模块可以让用户在设备驱动层接受和发送raw packets,并且为了加速数据报文的拷贝,它允许用户创建一块与内核态共享的环形缓冲区,具体的创建操作是在packet_set_ring()这个函数中实现的。
/net/packet/af_packet.c
4292 static int packet_set_ring(sk, req_u, closing, tx_ring)
4294 {
4317 if (req->tp_block_nr) {
4362 order = get_order(req->tp_block_size);
4363 pg_vec = alloc_pg_vec(req, order);
4366 switch (po->tp_version) {
4367 case TPACKET_V3:
4369 if (!tx_ring) {
4370 init_prb_bdqc(po, rb, pg_vec, req_u);
4371 }
4390 }
4391 }
4414 if (closing || atomic_read(&po->mapped) == 0) {
4417 swap(rb->pg_vec, pg_vec);
4418 if (po->tp_version <= TPACKET_V2)
4419 swap(rb->rx_owner_map, rx_owner_map);
4435 }
4450 out_free_pg_vec:
4451 bitmap_free(rx_owner_map);
4452 if (pg_vec)
4453 free_pg_vec(pg_vec, order, req->tp_block_nr);
4456 }
packet_set_ring()通过用户传递的tp_block_nr(行4317)和tp_block_size(行4362)来决定分配的环形缓冲区的大小,如果packet socket的版本为TPACKET_V3,那么在init_prb_bdqc()的调用中(行4370),packet_ring_buffer.prb_bdqc.pkbdq就会持有一份pg_vec的引用(行584)。
/net/packet/af_packet.c
573 static void init_prb_bdqc(po, rb, pg_vec, req_u)
577 {
578 struct tpacket_kbdq_core *p1 = GET_PBDQC_FROM_RB(rb);
579 struct tpacket_block_desc *pbd;
583 p1->knxt_seq_num = 1;
584 p1->pkbdq = pg_vec;
603 prb_init_ft_ops(p1, req_u);
604 prb_setup_retire_blk_timer(po);
605 prb_open_block(p1, pbd);
606 }
如果用户传递的tpacket_req.tp_block_nr等于0,那么就没有新的pg_vec会被分配,并且旧的pg_vec会被释放(行4453),但是packet_ring_buffer.prb_bdqc.pkbdq仍然保留着被释放的pg_vec的引用。如果我们此时将packet socket的版本切换为TPACKET_V2并且再次设置缓冲区,那么保存在pkbdq,被释放的pg_vec会被当做rx_owner_map再次被释放(行4451),因为packet_ring_buffer是一个联合体,pkbdq(行18)和rx_owner_map(行74)的内存偏移是一样的。
/net/packet/internal.h
59 struct packet_ring_buffer {
60 struct pgv *pg_vec;
73 union {
74 unsigned long *rx_owner_map;
75 struct tpacket_kbdq_core prb_bdqc;
76 };
77 };
17 struct tpacket_kbdq_core {
18 struct pgv *pkbdq;
19 unsigned int feature_req_word;
20 unsigned int hdrlen;
21 unsigned char reset_pending_on_curr_blk;
22 unsigned char delete_blk_timer;
52 struct timer_list retire_blk_timer;
53 };
ROP
ROP的利用分为两个步骤:
- 泄露内核地址,绕过KASLR。
- 劫持PC,通过gadget修改进程的cred。
这两个步骤要各自触发一次漏洞,通过选择不同的目标结构体,分别达到上述的目的。
信息泄露
/include/linux/msg.h
9 struct msg_msg {
10 struct list_head m_list;
11 long m_type;
12 size_t m_ts; /* message text size */
13 struct msg_msgseg *next;
14 void *security;
15 /* the actual message follows immediately */
16 };
这里选择msg_msg结构体作为目标结构体,原因有以下两点:
- 它含有m_ts成员(行12),这个成员用来描述结构体下面跟着的缓冲区长度。
- 普通用户可以读取缓冲区的内容。
通过pg_vec double free的漏洞,在第一次释放pg_vec之后,使用msg_msg进行堆喷,之后再次释放pg_vec,使用msg_msgseg进行堆喷来修改msg_msg的m_ts成员,这样在copy_msg函数中就可以有一次越界读的机会(行128)。
/ipc/msgutil.c
118 struct msg_msg *copy_msg(src, dst)
119 {
121 size_t len = src->m_ts;
127 alen = min(len, DATALEN_MSG);
128 memcpy(dst + 1, src + 1, alen);
129
130 for (dst_pseg = dst->next, src_pseg = src->next;
131 src_pseg != NULL;
132 dst_pseg = dst_pseg->next, src_pseg = src_pseg->next) {
133
134 len -= alen;
135 alen = min(len, DATALEN_SEG);
136 memcpy(dst_pseg + 1, src_pseg + 1, alen);
137 }
142 return dst;
143 }
如果将timerfd_ctx结构体通过堆风水布局在double free的pg_vec后面,如下图所示,那么就可以将timerfd_ctx结构体的内容读取到用户态中。
通过泄露timerfd_ctx结构体中的function函数指针(行121)以及wqh等待队列头(行38),就可以得到内核代码段的地址以及timerfd_ctx的堆地址。
/fs/timerfd.c
31 struct timerfd_ctx {
32 union {
33 struct hrtimer tmr;
34 struct alarm alarm;
35 } t;
38 wait_queue_head_t wqh;
47 };
/include/linux/hrtimer.h
118 struct hrtimer {
119 struct timerqueue_node node;
120 ktime_t _softexpires;
121 enum hrtimer_restart (*function)(struct hrtimer *);
127 };
劫持PC
整个劫持PC进行rop的步骤如下:
- 再次触发一次double free,第一次释放pg_vec后,选择pipe_buffer进行占位。
- 再次释放pg_vec,使用msg_msgseg进行堆喷,修改pipe_buffer的ops成员指向刚刚泄露地址的timerfd_ctx。
- 释放timerfd_ctx,使用msg_msgseg进行堆喷,伪造出一个pipe_buf_operations。
- 选择通过ops中的release函数指针劫持PC,当pipe被close时,release函数指针就会被调用。
/include/linux/pipe_fs_i.h
95 struct pipe_buf_operations {
103 int (*confirm)(struct pipe_inode_info *, struct pipe_buffer *);
109 void (*release)(struct pipe_inode_info *, struct pipe_buffer *);
119 bool (*try_steal)(struct pipe_inode_info *, struct pipe_buffer *);
124 bool (*get)(struct pipe_inode_info *, struct pipe_buffer *);
125 };
通过release函数指针的定义可以看到,pipe_buffer作为函数的第二个参数且pipe_buffer内存内容可以被控制,那么通过以下的gadget来将栈迁移到pipe_buffer上。
push rsi; jmp qword ptr [rsi + 0x39];
pop rsp; pop r15; ret;
add rsp, 0xd0; ret;
pop rdi; ret; // 0
prepare_kernel_cred;
pop rcx; ret; // 0
test ecx, ecx; jne 0xd8ab5b; ret;
mov rdi, rax; jne 0x798d21; xor eax, eax; ret;
commit_creds;
mov rsp, rbp; pop rbp; ret;
可以看到上述的gadgets十分复杂,要是在不同的内核版本中编写通用的exploit的话,工作量会非常大。
USMA
USMA这个利用方法的原理,其实来自于这个漏洞本身。如之前所说的,为了加速数据在用户态和内核态的传输,packet socket可以创建一个共享环形缓冲区,这个环形缓冲区通过alloc_pg_vec()创建。
/net/packet/af_packet.c
4291 static struct pgv *alloc_pg_vec(struct tpacket_req *req, int order)
4292 {
4293 unsigned int block_nr = req->tp_block_nr;
4294 struct pgv *pg_vec;
4295 int i;
4296
4297 pg_vec = kcalloc(block_nr, sizeof(struct pgv), GFP_KERNEL | __GFP_NOWARN);
4301 for (i = 0; i < block_nr; i++) {
4302 pg_vec[i].buffer = alloc_one_pg_vec_page(order);
4305 }
4308 return pg_vec;
4314 }
可以看到pg_vec实际上是一个保存着连续物理页的虚拟地址的数组,而这些虚拟地址会被packet_mmap()函数所使用,packet_mmap()将这些内核虚拟地址代表的物理页映射进用户态(行4502),这样普通用户就能在用户态对这些物理页直接进行读写。
/net/packet/af_packet.c
4458 static int packet_mmap(file, sock, vma)
4460 {
4491 for (rb = &po->rx_ring; rb <= &po->tx_ring; rb++) {
4495 for (i = 0; i < rb->pg_vec_len; i++) {
4496 struct page *page;
4497 void *kaddr = rb->pg_vec[i].buffer;
4500 for (pg_num = 0; pg_num < rb->pg_vec_pages; pg_num++) {
4501 page = pgv_to_page(kaddr);
4502 err = vm_insert_page(vma, start, page);
4503 if (unlikely(err))
4504 goto out;
4505 start += PAGE_SIZE;
4506 kaddr += PAGE_SIZE;
4507 }
4508 }
4509 }
4517 return err;
4518 }
如果通过漏洞将存储在pg_vec的虚拟地址进行覆写,更改为内核代码段的虚拟地址,那么vm_insert_page()就能将内核代码段的内存页插入到用户态的虚拟地址空间中。值得一提的是,vm_insert_page函数实际上调用validate_page_before_insert()函数对传入的page做了校验。
/mm/memory.c
1753 static int validate_page_before_insert(struct page *page)
1754 {
1755 if (PageAnon(page) || PageSlab(page) || page_has_type(page))
1756 return -EINVAL;
1757 flush_dcache_page(page);
1758 return 0;
1759 }
检查page是否为匿名页,是否为Slab子系统分配的页,以及page是否含有type,而内存页的type总共有以下四种。
/include/linux/page-flags.h
718 #define PG_buddy 0x00000080
719 #define PG_offline 0x00000100
720 #define PG_table 0x00000200
721 #define PG_guard 0x00000400
PG_buddy为伙伴系统中的页,PG_offline为内存交换出去的页,PG_table为用作页表的页,PG_guard为用作内存屏障的页。可以看到如果传入的page为内核代码段的页,以上的检查全都可以绕过。
为了避免vm_insert_page()返回err(行4503),必须得控制pg_vec中所有的虚拟地址为合法的可插入的内核态虚拟地址,我们可以使用fuse+setxattr或者ret2dir来控制pg_vec中的所有内存。
在这个漏洞利用中,我们选择将pg_vec中保存的虚拟地址通过漏洞篡改为__sys_setresuid函数所在的内核代码段页的虚拟地址,从而在用户态中对权限校验逻辑进行更改(行659),使得普通用户也能设置自己的uid,从而达到提权的目的。
/kernel/sys.c
631 long __sys_setresuid(uid_t ruid, uid_t euid, uid_t suid)
632 {
659 if (!ns_capable_setid(old->user_ns, CAP_SETUID)) {
660 if (ruid != (uid_t) -1 && !uid_eq(kruid, old->uid) &&
661 !uid_eq(kruid, old->euid) && !uid_eq(kruid, old->suid))
662 goto error;
663 if (euid != (uid_t) -1 && !uid_eq(keuid, old->uid) &&
664 !uid_eq(keuid, old->euid) && !uid_eq(keuid, old->suid))
665 goto error;
666 if (suid != (uid_t) -1 && !uid_eq(ksuid, old->uid) &&
667 !uid_eq(ksuid, old->euid) && !uid_eq(ksuid, old->suid))
668 goto error;
669 }
694 }
最后,可以在alloc_pg_vec()中看到,block_nr是用户传入的,那么pg_vec的大小也是用户可控的(行4297),这就意味着pg_vec可以占据不同大小的slab,从而将各种堆上的问题转化为对内核代码段进行覆写。
总结
通过USMA这种方式,我们可以大幅提高利用编写的效率,对漏洞要求大大降低,克服了gadget可获得性限制,并且绕过现有的最新的CFI缓解措施。