作者: f-undefined團隊 v1n3gar
原文鏈接:https://mp.weixin.qq.com/s/JPbwYA2sS9jCMMgwBxONjg

知識點

(1)使用 msg_msg 構造任意寫來篡改 modprobe_path通過 FUSE 來處理頁錯誤(克服5.11版本之后用戶沒有userfaultfd權限的問題,肯定有一大波CTF題將要效仿)。

(2)由于漏洞對象位于 8-page,已經不能用常規的堆噴(slub allocator)來利用了,得利用頁噴射(buddy system),作者分析了伙伴系統的源碼,可以學習頁噴知識(主要采用 ring_buffer 進行頁噴和頁風水,值得學習)。某種程度上來說本漏洞是一種 cross-cache overflow,從一個頁溢出覆蓋到下一個頁上的cache(因為頁上可以含有cache也可以是單純的頁)。

(3)采用新的彈性對象來泄露信息,也即user_key_payload彈性對象,由于長度變量和數據在一起,所以不擔心溢出時覆蓋到指針(限制是只能分配最多200個最長20000字節)。

影響版本:Linux-v5.17-rc8 以前,v5.17-rc8已修補。

測試版本:Linux-v5.16.14 exploit及測試環境下載地址

編譯選項:所有和 INET6 / TUNNEL / XFRM / CONFIG_NET_KEY / CONFIG_NF_SOCKET_IPV6 相關的選項都勾上y,特別是以下選項。

CONFIG_XFRM_ESP=y
CONFIG_INET_ESP=y
CONFIG_INET_ESP_OFFLOAD=y
CONFIG_INET6_ESP=y
CONFIG_INET6_ESP_OFFLOAD=y

在編譯時將.config中的CONFIG_E1000CONFIG_E1000E,變更為=y。參考

$ wget https://mirrors.tuna.tsinghua.edu.cn/kernel/v5.x/linux-5.16.14.tar.xz
$ tar -xvf linux-5.16.14.tar.xz
# KASAN: 設置 make menuconfig 設置"Kernel hacking" ->"Memory Debugging" -> "KASan: runtime memory debugger"。
$ make -j32
$ make all
$ make modules
# 編譯出的bzImage目錄:/arch/x86/boot/bzImage。

漏洞描述:位于目錄 net/ipv4/esp4.cnet/ipv6/esp6.c 中的 IPsec ESP transformation 代碼存在堆溢出。漏洞自2017年引入(cac2661c53f3 / 03e2a30f6a27)。

本漏洞能夠在最新的 Ubuntu 21.10 上提權,來自于pwn2own 2022,能夠影響 Ubuntu / Fedora / Debian。首次進行 page-level heap fengshui 和 cross-cache overflow(環境:4G內存,2 CPU)。

補丁patch 補丁引入 ESP_SKB_FRAG_MAXSIZE,大小為 32768 ,也就是 8-page,如果 allocsize 大于8頁,則跳轉到 COW

diff --git a/include/net/esp.h b/include/net/esp.h
index 9c5637d41d951..90cd02ff77ef6 100644
--- a/include/net/esp.h
+++ b/include/net/esp.h
@@ -4,6 +4,8 @@

 #include <linux/skbuff.h>

+#define ESP_SKB_FRAG_MAXSIZE (PAGE_SIZE << SKB_FRAG_PAGE_ORDER)
+
 struct ip_esp_hdr;

 static inline struct ip_esp_hdr *ip_esp_hdr(const struct sk_buff *skb)
diff --git a/net/ipv4/esp4.c b/net/ipv4/esp4.c
index e1b1d080e908d..70e6c87fbe3df 100644
--- a/net/ipv4/esp4.c
+++ b/net/ipv4/esp4.c
@@ -446,6 +446,7 @@ int esp_output_head(struct xfrm_state *x, struct sk_buff *skb, struct esp_info *
    struct page *page;
    struct sk_buff *trailer;
    int tailen = esp->tailen;
+   unsigned int allocsz;

    /* this is non-NULL only with TCP/UDP Encapsulation */
    if (x->encap) {
@@ -455,6 +456,10 @@ int esp_output_head(struct xfrm_state *x, struct sk_buff *skb, struct esp_info *
            return err;
    }

+   allocsz = ALIGN(skb->data_len + tailen, L1_CACHE_BYTES);
+   if (allocsz > ESP_SKB_FRAG_MAXSIZE)
+       goto cow;
+
    if (!skb_cloned(skb)) {
        if (tailen <= skb_tailroom(skb)) {
            nfrags = 1;
diff --git a/net/ipv6/esp6.c b/net/ipv6/esp6.c
index 7591160edce14..b0ffbcd5432d6 100644
--- a/net/ipv6/esp6.c
+++ b/net/ipv6/esp6.c
@@ -482,6 +482,7 @@ int esp6_output_head(struct xfrm_state *x, struct sk_buff *skb, struct esp_info
    struct page *page;
    struct sk_buff *trailer;
    int tailen = esp->tailen;
+   unsigned int allocsz;

    if (x->encap) {
        int err = esp6_output_encap(x, skb, esp);
@@ -490,6 +491,10 @@ int esp6_output_head(struct xfrm_state *x, struct sk_buff *skb, struct esp_info
            return err;
    }

+   allocsz = ALIGN(skb->data_len + tailen, L1_CACHE_BYTES);
+   if (allocsz > ESP_SKB_FRAG_MAXSIZE)
+       goto cow;
+
    if (!skb_cloned(skb)) {
        if (tailen <= skb_tailroom(skb)) {
            nfrags = 1;

保護機制:KASLR / SMEP / SMAP

利用總結:主要利用過程位于 loop() 函數:

(1)初始化:設置CPU affinity,設置漏洞socket(文件描述符存在 r[1]);

(2)緩解噪聲(避免 order-2 從 order-3 取頁 或者 order-2 的頁合并到 order-3,影響到漏洞對象的排布):

  • (2-1)耗盡 order-0/1/2 的 freelist:噴射0x1000個大小為0x1000的 ring_buffer
  • (2-2)分配 (10*100*2) 個 4-page,釋放一半:采用 ring_buffer
  • (2-3)釋放 (2-1) 中堆噴的對象;

(3)泄露 msg_msg->next(嘗試9次):堆上布局3個相鄰的對象—— vul object -> user_key_payload -> msg_msg

  • (3-1)耗盡 order-3 的freelist,使得堆排布時從 order-4 取頁,保證8-page 相鄰:噴射 0x2000 個大小為 0x8000 的 ring_buffer
  • (3-2)耗盡 kmalloc-4k,使得分配 user_key_payload 時從 buddy system 取頁(order-3):調用 setxattr() 分配 0x50*8 個 kmalloc-4k;
  • (3-3)分配3個連續的 8-page 占位對象:采用 ring_buffer 對象;
  • (3-4)釋放第2個占位對象,分配1個8-page slab(分配1個 user_key_payload 和7個 setxattr());
  • (3-5)噴射 100 個 seq_operations 對象,便于之后泄露內核基址;
  • (3-6)釋放第3個占位對象,分配1個8-page slab(噴射16個 msg_msg,位于 kmalloc-4k / kmalloc-32);
  • (3-7)釋放第1個占位對象,分配漏洞對象,觸發越界寫來修改 user_key_payload->datalen
  • (3-8)通過 user_key_payload 進行越界讀,泄露 msg_msg->next

(4)泄露內核基址(嘗試50次):堆上布局2個相鄰的對象——vul object -> msg_msg

  • (4-1)耗盡 kmalloc-4k,使得分配 msg_msg 時從 buddy system 取頁(order-3):堆噴0x100個大小為0x1000 的 ring_buffer
  • (4-2)耗盡 order-3 的freelist,使得堆排布時從 order-4 取頁,保證8-page 相鄰:噴射 0x100 個大小為 0x8000 大小的 ring_buffer
  • (4-3)分配8*2個連續的 8-page 占位對象(占位對象):采用 ring_buffer 對象;
  • (4-4)釋放第2個占位對象,分配1個8-page slab (分配 9 個 msg_msg,位于 kmalloc-4k / kmalloc-32);
  • (4-5)釋放第1個占位對象,分配漏洞對象,觸發越界寫來修改 msg_msg->m_ts & msg_msg->next (改成上一步泄露的 msg_msg->next);
  • (4-6)通過 msg_msg 進行越界讀,泄露 seq_operations->start / stop / next 指針;

(5)篡改 modprobe_path 提權(嘗試50次):堆上布局2個相鄰的對象——vul object -> msg_msg。 - (5-1)設置 FUSE,頁錯誤處理地址為 fuse_evil_addr = 0x1339000(FUSE的 evil_read_pause() 函數在處理頁錯誤時,會往該地址寫入字符串 /tmp/get_rooot\x00,也即提權程序); - (5-2)耗盡 order-3 的freelist,使得堆排布時從 order-4 取頁,保證8-page 相鄰:噴射 0x100 個大小為 0x8000 大小的 ring_buffer; - (5-3)分配2個連續的 8-page 占位對象(占位對象):采用 ring_buffer 對象; - (5-4)釋放第2個占位對象,分配1個8-page slab (分配 9 個 msg_msg,位于 kmalloc-4k / kmalloc-32)(注意,用戶message地址設置為 fuse_evil_addr-8 == 0x1339000-8,以便在內核拷貝消息時觸發頁錯誤而暫停); - (5-5)釋放第1個占位對象,分配漏洞對象,觸發越界寫來修改 msg_msg->next(改成 modprobe_path-8); - (5-6)通過寫pipe來通知 FUSE 的 evil_read_pause() 函數,結束頁錯誤處理,使得 msg_msg 消息完成拷貝,篡改 modprobe_path; - (5-7)執行錯誤binary文件觸發modprobe,完成提權。


1. 漏洞分析

簡介:漏洞來自 Linux esp6 crypto 模塊,接收緩沖區是 8-page,但發送者可以發送大于 8-page 的數據,導致頁溢出。

1-1 漏洞對象創建

漏洞對象創建esp6_output_head() 負責創建 receive buffer,allocsize 變量不重要,因為 skb_page_frag_refill() 會默認分配 8-page 內存(order-3 pages)。

調用棧sendmsg() -> __sys_sendmsg() -> ___sys_sendmsg() -> ____sys_sendmsg() -> sock_sendmsg() -> sock_sendmsg_nosec() -> rawv6_sendmsg() -> rawv6_push_pending_frames() -> ip6_push_pending_frames() -> ip6_send_skb() -> ip6_local_out() -> dst_output() -> xfrm6_output() -> NF_HOOK_COND() -> __xfrm6_output() -> xfrm_output() -> xfrm_output2() -> xfrm_output_resume() -> dst_output() -> ip6_output() -> NF_HOOK_COND() -> ip6_finish_output -> __ip6_finish_output -> ip6_finish_output2() -> neigh_output() -> neigh_hh_output() -> dev_queue_xmit() -> __dev_queue_xmit() -> validate_xmit_skb() -> validate_xmit_xfrm() -> esp6_xmit() -> esp_output_head() 34層,太復雜了。。。

int esp6_output_head(struct xfrm_state *x, struct sk_buff *skb, struct esp_info *esp)
{
        ...
        struct page_frag *pfrag = &x->xfrag;          // x->xfrag->page = vul object
        int tailen = esp->tailen;
        allocsize = ALIGN(tailen, L1_CACHE_BYTES);

        spin_lock_bh(&x->lock);

        if (unlikely(!skb_page_frag_refill(allocsize, pfrag, GFP_ATOMIC))) {    // [1]
            spin_unlock_bh(&x->lock);
            goto cow;
        }
        ...
}

bool skb_page_frag_refill(unsigned int sz, struct page_frag *pfrag, gfp_t gfp)
{
        if (pfrag->offset + sz <= pfrag->size)
        return true;
    ...
    if (SKB_FRAG_PAGE_ORDER &&
        !static_branch_unlikely(&net_high_order_alloc_disable_key)) {

        pfrag->page = alloc_pages((gfp & ~__GFP_DIRECT_RECLAIM) |               // [2] 
                      __GFP_COMP | __GFP_NOWARN |
                      __GFP_NORETRY,
                      SKB_FRAG_PAGE_ORDER);
        ...
    }
    ...
    return false;
}

1-2 越界寫

漏洞對象越界null_skcipher_crypt() 函數中,內核拷貝了 N-page 數據,導致OOB。

static int null_skcipher_crypt(struct skcipher_request *req)
{
    struct skcipher_walk walk;
    int err;

    err = skcipher_walk_virt(&walk, req, false);

    while (walk.nbytes) {
        if (walk.src.virt.addr != walk.dst.virt.addr)
            // out-of-bounds write
            memcpy(walk.dst.virt.addr, walk.src.virt.addr,
                   walk.nbytes);
        err = skcipher_walk_done(&walk, 0);
    }

    return err;
}

調用棧... -> esp6_xmit() -> esp6_output_tail() -> crypto_aead_encrypt() -> crypto_authenc_encrypt() -> crypto_authenc_copy_assoc() -> crypto_skcipher_encrypt() -> null_skcipher_crypt() 可以看到,esp6_xmit() 先調用 esp6_output_head() 分配漏洞對象的 8-page 內存(地址存放在x->xfrag->page),再調用 esp6_output_tail() 將該內存賦值給 req.dst 并最后觸發OOB。

static int esp6_xmit(struct xfrm_state *x, struct sk_buff *skb,  netdev_features_t features)
{
    int err;
    int alen;
    struct esp_info esp;
    bool hw_offload = true;

    ... ...
    esp.tailen = esp.tfclen + esp.plen + alen;          // esp.tailen   ->   allocsize

    if (!hw_offload || !skb_is_gso(skb)) {
        esp.nfrags = esp6_output_head(x, skb, &esp);    // [1] alloc the vulnerable object, saved at x->xfrag->page
        if (esp.nfrags < 0)
            return esp.nfrags;
    }
    ... ...
    err = esp6_output_tail(x, skb, &esp);               // [2] trigger OOB
    ... ...
}

int esp6_output_tail(struct xfrm_state *x, struct sk_buff *skb, struct esp_info *esp)
{
    ... ... 
    if (!esp->inplace) {
        int allocsize;
        struct page_frag *pfrag = &x->xfrag;
        ... ...
        page = pfrag->page;
        get_page(page);
        /* replace page frags in skb with new page */
        __skb_fill_page_desc(skb, 0, page, pfrag->offset, skb->data_len);
        pfrag->offset = pfrag->offset + allocsize;
        spin_unlock_bh(&x->lock);

        sg_init_table(dsg, skb_shinfo(skb)->nr_frags + 1);
        err = skb_to_sgvec(skb, dsg,
                       (unsigned char *)esph - skb->data,
                       assoclen + ivlen + esp->clen + alen);
    ... ...

    aead_request_set_crypt(req, sg, dsg, ivlen + esp->clen, iv);       // [2-1]  dsg  =  x->xfrag->page
    aead_request_set_ad(req, assoclen);

    ... ...
    err = crypto_aead_encrypt(req);        // [2-2] req->dst = dsg
    ... ...
}
EXPORT_SYMBOL_GPL(esp6_output_tail);

static int null_skcipher_crypt(struct skcipher_request *req)
{
    struct skcipher_walk walk;
    int err;

    err = skcipher_walk_virt(&walk, req, false);            // [3] walk->dst = req.dst

    while (walk.nbytes) {
        if (walk.src.virt.addr != walk.dst.virt.addr)
            memcpy(walk.dst.virt.addr, walk.src.virt.addr,  // [4]  trigger OOB
                   walk.nbytes);
        err = skcipher_walk_done(&walk, 0);
    }

    return err;
}

漏洞缺陷:作者利用時,發送 16-page 數據,可以溢出 8-page,問題是 esp_output_fill_trailer() 會根據消息長度和所用協議類型,在末尾添加幾個字節(對我們來說是垃圾數據)。

static inline void esp_output_fill_trailer(u8 *tail, int tfclen, int plen, __u8 proto)
{
    /* Fill padding... */
    if (tfclen) {
        memset(tail, 0, tfclen);
        tail += tfclen;
    }
    do {
        int i;
        for (i = 0; i < plen - 2; i++)
            tail[i] = i + 1;
    } while (0);
    tail[plen - 2] = plen - 2;
    tail[plen - 1] = proto;
}

2. Buddy system 知識

說明:分析伙伴系統的原理是研究 page-level heap fengshui 的前提。

2-1 page allocator

頁分配器的知識可以參見 page_alloc.c 源碼。

簡介Linux page allocator 管理內核底層的物理頁,SLUB / SLAB / SLOB 內存分配器都在 Page allocator 之上。例如,當內核耗盡所有 kmalloc-4k slab之后,內存分配器會向 Page allocator 申請內存,由于 kmalloc-4k 位于 8-page slab (order 3),所以 Page allocator 會申請 8-page 內存給內存分配器。

存儲結構:Page allocator 采用 free_area 結構(zone->free_area 數組,長度為 MAX_ORDER == 1,所以最大order為11)來保存空閑頁,也就是個保存不同 order/size 頁的數組,采用 order 來區分不同大小的頁(例如,N-order 表示大小為 PAGE_SIZE<<3 的頁;order-0 就表示大小為 PAGE_SIZE 的頁)。free_area 中每個 order 都對應一個 free_list,從 free_list 分配或將頁釋放后放入 free_list

struct free_area {
    struct list_head    free_list[MIGRATE_TYPES];
    unsigned long       nr_free;
};

cache 與頁分配:不同的slab如果耗盡了會申請不同 order 的頁,例如,kmalloc-256 會從 order-0 申請頁,而 kmalloc-512 會從 order-1 申請頁,kmalloc-4k 會從 order-3 申請頁。

1-understand-free_area

split page:如果 free_list 中沒有空閑頁,則 lower-order free_areahigher-order free_area 取頁,higher-order free_area 將頁一分為二,然后 lower-order free_area 將頁返回給申請者(例如 alloc_pages())。例如,當 order-2(4-page)的 free_list 耗盡之后,就從 order-3 申請頁,order-3 的頁分成兩個 4-page 頁,位于低地址的 4-page 返回給申請者,高地址的 4-page 保存在 order-2free_list 中供下次申請。原理如下所示:

2-higher-order-split-to-lower-order

merge page:如果 free_list 中有很多空閑頁,頁分配器會整合兩個相鄰的、order相同的頁,并放入 higher-order free_area 。還是以剛才的例子來看,假設 order-3 被分成兩個 order-2 的頁,其中一個存放在 order-2free_list,只要被分配的頁又被釋放回 order-2free_list,頁分配器會檢查新釋放的頁在同一 free_list 中是否存在相鄰的頁(這倆就被稱為 buddy),存在的話就將這倆合并后放入 order-3

3-lower-order-merge-to-higher-order

對應源碼:以下代碼展示了頁分配器如何從 free_area 中選取頁以及如何從 higher-order 中取頁。

static __always_inline
struct page *__rmqueue_smallest(struct zone *zone, unsigned int order,
                        int migratetype)
{
    unsigned int current_order;
    struct free_area *area;
    struct page *page;


    for (current_order = order; current_order < MAX_ORDER; ++current_order) {
        // Pick up the right order from free_area
        area = &(zone->free_area[current_order]);
        // Get the page from the free_list
        page = get_page_from_free_area(area, migratetype);
        // If no freed page in free_list, goes to high order to retrieve
        if (!page)
            continue;
        del_page_from_free_list(page, zone, current_order);
        expand(zone, page, order, current_order, migratetype);
        set_pcppage_migratetype(page, migratetype);
        return page;
    }

    return NULL;
}

static inline struct page *get_page_from_free_area(struct free_area *area,
                        int migratetype)
{
    return list_first_entry_or_null(&area->free_list[migratetype],
                    struct page, lru);
}

2-2 shaping heap

頁風水目標:現在討論下如何為 OOB write 布局堆結構。現在已知在頁分配器中,每種 order 的頁都保存在 free_area->free_list 中。由于不能保證在同一 free_list 中的兩個頁是連續的,所以即便連續申請2個同一order的頁,這2個頁可能相隔很遠。為了更好的控制堆布局,我們需要確保 free_list 中所有的頁是連續的。首先耗盡目標order的 free_list,迫使其向 higher-order 取頁,這樣取過來的頁會被劃分成兩段連續的內存。

緩解噪聲(保證連續):有些內核進程也會分配和釋放頁,影響了堆布局。回到本漏洞中來,我們的目標是布局連續的 order-3 的頁,但是可能會有 order-3 的頁被劃分到 order-2 或者有 order-2 的頁被整合到 order-3。為了緩解噪聲影響,可以采取以下步驟:

(1)耗盡 order 0, 1, 2 的 free_list;(采用socket中的 ring_buffer 來堆噴—頁風水

(2)分配大量的 order-2 對象 (假定為N個),這樣,order-2 會向 order-3 取頁;(分10個進程,每個進程噴200個 4-page 大小的 ring_buffer

(3)釋放第2步中一半的對象,這樣,有 N/2 個對象會存入 order-2 的 free_list

(4)釋放第1步所有的對象;

第3步中,釋放一半的 order-2 就避免其發生整合而被存入 order-3,這樣 order-2 的 free_list 中就有 N/2 個頁可以使用了,之后就不會從 order-3 取頁或者整合到 order-3 了。避免我們構造連續的 8-page 時受到影響。

4-mitigate_noise


3. 漏洞利用

3-1 泄露方法

思路一(失敗):利用 msg_msg,覆寫 msg_msg->m_ts 構造越界讀。但是測試時發現,漏洞的垃圾字節會覆寫 msg_msg->next 指針(m_tsnext 相鄰),導致越界讀失敗。

思路二:利用 user_key_payload 結構(從 ELOISE 論文中找到)。可以看到,這個結構的 datalen 長度和數據在一起,這樣即便垃圾字節會填到末尾,也不會破壞到指針了。

struct user_key_payload {
    struct rcu_head rcu;        // rcu指針可以被設置為NULL
    unsigned short  datalen;    /* length of this data */
    char        data[] __aligned(__alignof__(u64)); /* actual data */
};

key長度限制:Ubuntu 上默認會限制key的數量和長度。問題是導致溢出的對象位于 8-page,我們在漏洞對象后面也要布置一個 8-page 對象(暫時稱為 victim slab),而在Ubuntu上,只有 kmalloc-2k / kmalloc-4k / kmalloc-8k 會從 order-3 取頁。所以至少要使key被分配在 kmalloc-2k 上,才能使key位于 8-page 的頁中。

$ sudo cat /proc/sys/kernel/keys/maxbytes 
20000
$ sudo cat /proc/sys/kernel/keys/maxkeys 
200

victim個數限制:可以用8個 kmalloc-4k 對象來填充victim slab,采用長度為 2049 的 user_key_payload 即可。這樣 user_key_payload 總長度為 2049*8=16392,由于限制最多 20000 字節的key,只剩下 1 個 user_key_payload 可用 — ((20000-16392)/2049 = 1),所以最多可以布置2個 victim slab,條件非常嚴苛。

5-weak-page-fengshui

增大victim個數:可以每個 victim slab 放一個 user_key_payload 對象,剩下的空間填充其他對象,user_key_payload 可以在 victim slab 中任意位置,因為本漏洞可以溢出覆蓋整個 victim slab。這樣,我們就可以噴9個 victim slab 了,增大的泄露的成功幾率。

6-strong-page-fengshui

3-2 泄露內核基址

方法:其實本可以直接在 victim slab 后面放一個包含內核指針的對象,但是作者很想嘗試 post 中通過篡改 msg_msg->next 進行任意讀寫的技術。先通過 user_key_payload 越界讀來泄露 msg_msg->next 指針,然后偽造 msg_msg->m_ts & msg_msg->nextmsg_msg->security 在Ubuntu上沒用,可覆蓋為0)進行任意讀。

泄露msg_msg->next:堆上布局3個相鄰的對象—— vul object -> user_key_payload -> msg_msg,注意 msg_msg->next 指向 kmalloc-32,并堆噴大量的 struct seq_operations 對象。觸發越界寫來篡改 user_key_payload->datalen,通過 user_key_payload 越界讀來泄露 msg_msg->next 指針。為了增大成功幾率,可以創建9對這種布局(3個相鄰對象的堆布局)。

泄露內核基址:堆上布局2個相鄰的對象——vul object -> msg_msg,觸發越界寫來篡改 msg_msg->m_ts & msg_msg->next ,通過 msg_msg 越界讀來泄露 struct seq_operations 對象上的函數指針(因為之前泄露的 msg_msg->next 指向 kmalloc-32,而kmalloc-32 上已經噴射了很多 struct seq_operations 對象)。

總體步驟:(1)~(8)泄露 msg_msg->next ,(9)~(12)泄露內核基址。

(1)分配大量8-page 頁來耗盡 order-3free_list,這樣 order-3 就會從 order-4 取頁,保證內存連續性;

(2)分配3個連續的 8-page dumy 對象(占位對象);(占位對象采用 ring_buffer

(3)釋放第2個占位對象,分配1個8-page slab,其中包含1個 user_key_payload 對象和7個其他對象(這7個對象采用多個子線程調用setxattr()來堆噴);

(4)釋放第3個占位對象,分配1個8-page slab,填滿大小在4056~4072之間的 msg_msg,使得 msg_msgseg 位于 kmalloc-32;

(5)噴射大量的 struct seq_operations,和第4步的 msg_msgseg 位于同一cache;

(6)釋放第1個占位對象,分配漏洞對象,觸發越界寫來修改 user_key_payload->datalen

(7)如果第(6)步成功,就能通過 user_key_payload 進行越界讀;

(8)如果第(7)步成功,就能泄露出 msg_msg->next 指針;

(9)分配2個連續的 8-page dumy 對象(占位對象);

(10)釋放第2個占位對象,分配1個8-page slab,填滿 msg_msg

(11)釋放第1個占位對象,分配漏洞對象,觸發越界寫來篡改 msg_msg->m_ts & msg_msg->next

(12)如果第(11)步成功,就能越界讀來泄露 struct seq_operations 對象上的函數指針。

3-3 提權

任意寫:還是利用 msg_msg 來進行任意寫。由于普通用戶需要 specific capability 才能使用userfaultfd,可以采用 CVE-2022-0185 中的介紹的FUSE方法來進行任意寫。通過FUSE可以實現用戶空間文件系統,然后映射我們的內存地址,只要有讀寫訪問到該地址就可以調用我們的頁錯誤處理函數,這樣可以控制當 msg_msg->next 被篡改之后,再允許 copy_from_user() 繼續訪問用戶空間的數據。

7-arb_write

提權:利用任意寫來篡改 modprbe_path 提權。后面方法和 CVE-2022-0185 一樣。將 modprbe_path 改為 /tmp/get_rooot (運行chmod u+s /bin/bash),這樣提權后只要運行 /bin/bash 即可提權。

(1)分配2個連續的 8-page dumy 對象(占位對象);

(2)映射消息內容到FUSE,釋放第2個占位對象,分配1個8-page slab,填滿 msg_msg,線程會暫停在 copy_from_user()

(3)釋放第1個占位對象,分配漏洞對象,觸發越界寫來篡改 msg_msg->nextmodprobe_path 地址;

(4)執行一個錯誤格式的binary 觸發 modprobe;

(5)打開 /bin/bash 即可提權。

8-succeed

exp說明

  • 原exp中前1271行是設置環境,不重要(作者說,環境設置部分的代碼是syzkaller自動生成的,非常復雜;只有利用部分,也即 loop() 函數是作者寫的,我們需要重點研究該函數),需要用到幾個符號:single_start / single_next / single_stop / modprobe_path,在原exploit的111行修改即可。

  • exp中 main 函數設置完環境之后,調用 clone() 創建子進程執行 loop() 函數,loop() 函數實現主要利用過程。參見 clone()分析,不同于 fork() / vfork()clone() 克隆生成的子進程繼續運行時不以調用處為起點,轉而去調用以參數func所指定的函數;當函數func返回或者是調用 exit()(或者 _exit())之后,克隆產生的子進程就會終止,父進程可以通過 wait() 一類函數來等待克隆子進程;調用者必須分配一塊大小適中的內存空間供子進程的棧使用,同時將這塊內存的指針置于參數 child_stack 中。

  #define _GNU_SOURCE
  #include <sched.h>
  int clone(int (*func)(void*),void *child_stack,int flags,void *func_arg,....
             /*pid_t *ptid,struct user_desc *tls,pid_t *ctid*/);
                                       Return process ID of child on success,or -1 on error
  • 頁噴射對象:進行頁風水和頁占位的對象是ring_buffer,因為其size設置很靈活,適合頁噴射。

參考

CVE-2022-27666: Exploit esp6 modules in Linux kernel

exploit

[漏洞分析] CVE-2022-27666 IPV6 ESP協議頁溢出內核提權


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