Linux內核eBPF RINGBUF越界訪問漏洞(CVE-2021-3489)利用分析

發布時間 2022-03-01

近年來,在PWN2OWN比賽Ubuntu桌面系統破解項目中,Linux內核eBPF機制一直是熱門的攻擊面。本文分析的CVE-2021-3489是在PWN2OWN 2021比賽中使用的漏洞,該eBPF漏洞和以往的邏輯驗證漏洞不同,漏洞出現在新引入的eBPF RINGBUF功能中,導致內存訪問越界,可實現越界讀寫達到權限提升。


BPF環形緩沖區及映射


eBPF提供多種類型的映射,環形緩沖區映射就是其中之一。該實現的動機之一是通過在CPU之間共享環形緩沖區來更有效地利用內存。單個RINGBUF環形緩沖區作為 BPF_MAP_TYPE_RINGBUF類型的BPF映射實例呈現給BPF程序。還提供多個BPF_CALL接口函數,其中bpf_ringbuf_output()功能為允許將數據從一個地方復制到環形緩沖區,bpf_ringbuf_reserve()/bpf_ringbuf_commit()/bpf_ringbuf_discard()這組函數將整個過程分為兩個步驟。首先,預留固定數量的空間。如果成功,則返回指向環形緩沖區數據區域內數據的指針,BPF程序可以像使用數組/哈希映射內的數據一樣使用該指針。一旦準備好,這塊內存要么被提交,要么被丟棄。discard與commit類似。


在創建BPF_MAP_TYPE_RINGBUF映射時,內核將分配兩個內存區域。一個是 bpf_ringbuf_map 結構,類似于其他的映射類型,另一個是bpf_ringbuf結構。該結構定義如下圖所示:


代碼文件.png


其中,pages是內存分配的所有頁面集合,consumer_pos為消費者計數器,producer_pos為生產者計數器,分別放在相鄰的單獨的頁面中,在該漏洞修復前,這兩個頁面均可以通過MMAP映射到用戶空間進行讀寫操作的。bpf_ringbuf_alloc()函數是實現bpf_ringbuf并初始化的,實現代碼如下所示:


代碼文件.png


調用bpf_ringbuf_area_alloc()函數分配bpf_ringbuf,然后初始化rb->spinlock,rb->waitq和rb->work,最后設置rb->mask,rb->consumer_pos和rb->producer_pos。bpf_ringbuf_area_alloc()函數是用來具體分配ringbuf內存區域的,該實現如下代碼所示:


代碼文件.png


第一個參數data_sz為申請分配內存的大小,nr_meta_pages為元數據頁面數,包含一個不可映射頁面和兩個可映射頁面,分別為consumer_pos和producer_pos,nr_data_pages為實際申請分配內存所需的內存頁面數,nr_pages為nr_meta_pages和nr_data_pages之和,pages用于存放所有頁面集合,內存分配如下代碼所示:


代碼文件.png

調用bpf_map_area_alloc()函數分配pages指針數組,用于存放即將分配的內存頁面。然后循環調用alloc_pages_node()函數分配頁面并存放在pages中,注意到nr_data_pages是雙份的。實際內存布局如下所示:


示例圖.png

最后,調用vmmap()函數將pages中的頁面映射到連續虛擬內存空間中,如下代碼所示:


代碼文件.png

漏洞原理與修復補丁


該漏洞發生在__bpf_ringbuf_reserve()函數中,該函數可以返回指向環形緩沖區數據區域內數據的指針,但是并沒有判斷訪問長度大小,導致可以越界訪問數據。該函數關鍵實現如下代碼所示:


代碼文件.png


參數size為訪問長度,首先判斷size是否大于0x3fffffff,但是并沒有判斷len是否大于ringbuf的data_sz,即訪問的范圍是否大于實際分配的ringbuf內存范圍。然后對size+8上限取整為len,接下來取出rb->producer_pos,通過prod_pos+len計算出new_prod_pos。


代碼文件.png


行333,首先判斷新的生產者位置不超過ringbuf的data_sz-1,確保ringbuf內存空間是充足的。然后通過rb->data+prod_pos計算出hdr的位置,最后將rb->producer_pos更新為new_prod_pos,返回hdr+8位置的指針,如下代碼所示:


代碼文件.png


根據前文分析,rb->consumer_pos和rb->producer_pos所在頁面是可映射的,是可控的且沒有檢查,size訪問長度也是可控的,因此可以構造如下條件達到大范圍越界訪問,令producer_pos = 0,consumer_pos= 0x3fffffff和size=0x3fffffff。這三個變量可以繞過所有檢查,最后計算出的new_prod_pos為0x3fffffff+8,這是個很大的范圍。


該漏洞修復補丁有兩部分,第一部分是加上了和data_sz大小的判斷,防止訪問長度超出實際分配的空間范圍,如下代碼所示:


代碼文件.png


不允許對rb->producer_pos所在內存頁面進行寫映射。


漏洞利用過程


(1)通過堆噴構造連續內存布局,給越界讀寫提供場景

連續創建多個size=0x1000000的ringbuf,這里mapfd的ringbuf和victimfd的ringbuf是連續的,中間間隔一個頁面的guard page


代碼文件.png

(2)通過eBPF指令構造出越界讀寫原語

通過eBPF指令訪問mapfd的ringbuf,并調用bpf_ringbuf_reserve()函數獲取mapfd的ringbuf->data指針。這里SIZE為0x30000000大于實際分配的內存空間。


代碼文件.png


偏移size*2+0x1000跳過mapfd的ringbuf,再偏移8處是victimfd的ringbuf->wait_queue_head->list_head,偏移40處是ringbuf-> irq_work->func,初始化bpf_ringbuf時,func為bpf_ringbuf_notify,因此可以計算出內核基地址。


(4)劫持返回地址執行代碼

通過大量fork子進程,在victimfd內存后面得到連續的task_struct內存布局。


代碼文件.png


同時,子進程和父進程通過pipe進行通信,這個過程中會調用__x64_sys_read和ksys_read函數并處于阻塞狀態,然后不斷搜索thread內核棧,搜索到這兩個函數返回地址將其修改成commit_creds和prepare_kernel_cred,在父進程中解除阻塞狀態便可劫持流程進行執行任意代碼。      


綜上利用過程,通過精心構造可實現對該漏洞的提權效果。


        

參考鏈接: 

https://flatt.tech/reports/210401_pwn2own/