作者: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的利用分為兩個步驟:

  1. 泄露內核地址,繞過KASLR。
  2. 劫持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結構體作為目標結構體,原因有以下兩點:

  1. 它含有m_ts成員(行12),這個成員用來描述結構體下面跟著的緩沖區長度。
  2. 普通用戶可以讀取緩沖區的內容。

通過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的步驟如下:

  1. 再次觸發一次double free,第一次釋放pg_vec后,選擇pipe_buffer進行占位。
  2. 再次釋放pg_vec,使用msg_msgseg進行堆噴,修改pipe_buffer的ops成員指向剛剛泄露地址的timerfd_ctx。
  3. 釋放timerfd_ctx,使用msg_msgseg進行堆噴,偽造出一個pipe_buf_operations。
  4. 選擇通過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緩解措施。


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1916/