作者:360漏洞研究院 劉永 王曉東 姚俊
原文鏈接:https://vul.360.net/archives/391
概述
眾所周知,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緩解措施。
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1916/
暫無評論