Speed Run r1rn kernel challenge note
Cross-Cache Attack
slub 相關的知識可以參考這
https://hackmd.io/@naup96321/BkeiK5Kexl
這邊簡單在介紹一次
我們可以知道,整個 OS 有相當多的 kmem_cache 這種 memory pool
他會嘗試去找對應大小 object 的 kmem_cache
其拿取的順序會從 kmem_cache_cpu 的 freelist,然後 active slab freelist,partial
然後去 kmem_cache_node partial
最後如果都沒有可用的,就會去 buddy system 分配 page 來用
analyze
他去創建了一個 device,裡面有定義一些 ioctl 這部分等等提到
之後去創建了一個 kmem_cache,其是一個大小為 0x200 obj 的結構體
1 |
|
kernel module 定義了一些 ioctl
就是基本的 alloc、write、free
1 |
|
這份 kernel driver 仔細看有一個 UAF
他在 free objs array 中某個 pointer 指向的 object 沒有去清空 pointer,因此可以去操作已被釋放的 object
1 | static struct obj *objs[OBJ_MAX]; |
首先是因為我們 UAF 的 kmem_cache 是一個自己創建的,利用價值比起其他結構是比較低的,因此希望可以通過 cross cache 來去嘗試分配到其他結構體 UAF,達到控制 RIP、任意讀寫等
其核心概念就是讓有 UAF 的 object,跟著整個 slab 一起被 buddy system 回收,再去 spray 想要分配到的 struct,來將這塊 page 被其他 kmem_cache 拿去用
attack
來看題目文件
1 |
|
看起來是幾乎保護都沒有開
題目給了 ext3,將其掛載起來編寫內容
1 | sudo mkdir /home/naup/Desktop/linux-kernel-exploitation/cross-cache-attack/sysfile |
接下來可以去看這個檔案
1 | ~ # ls /sys/kernel/slab/obj |
底下有很多很好用的資訊
objs_per_slab 代表一個 slab,有多少個 objects,這裡印出 8 ,代表有 8 個
另一個重要的是 cpu_partial
代表一個 CPU 本地可以去放置在 partial 中最大的 slab 數量
這邊印出來是 52
接下來我們來嘗試讓 UAF object 被 buddy system 回收
因此我們做一件事
先去分配 objs_per_slab * (cpu_partial + 1) 個
之後再分配一個 object 來去刷新 active object
確保整個 active slab 是我們可控制的
接下來我們有 objs_per_slab * (cpu_partial + 1) 個 full slab
接下來關注一件事,當 partial 滿的時候,他會去嘗試刷新整個 partial
其中所有 object 都被釋放狀態的 slab 會被 buddy system 回收
這裡的 slab 是一個 1 page 的 slab,也就是會進入 order 0 (0x200 * 8 = 1 page = 4kb)
但是如果讓連續的 page 被回收的話,會被 buddy system 合併到更高 order 的地方
讓我們 UAF page 被低 order slab 分配的機率下降
因此我們去釋放不連續的 page
讓雙數的 page 所有的 object 被 free 掉
單數的只 free 掉一塊
這樣在 partial 滿的狀態時候,他只會去把所有都 free 掉的回收
PS: 不是 page 被回收後都會進入到 buddy system freelist 中
buddy system 會維護以 page 為單位的記憶體分配,但根據需求的不同,上層子系統可能會請求 order-0、order-1 等不同大小的連續記憶體。為了處理碎片化,buddy system 內部會使用一個 PCP (per-cpu-page) 的結構做 page-level caching。這個結構有幾個特色:
每個 CPU 都會有一個
共有 14 個 list 來維護 cached page,分別是 4 種大小 (order-{0,1,2,3}) 乘以 3 種 migrate type (unmovable, movable, reclaimable) 共 12 種,再加上 THP (一種特別的 huge page 型態) 2 種
在釋放 (slab) cache 時,PCP 會先判斷該塊記憶體是否在 order 0 ~ 3 的範圍,如果是的話,就會先被 cache 到對應的 PCP list 內。反之亦然,請求 page 時也會先看 PCP list 是否有 page 可以用。
可以想像 cache 機制不會維護太多 page,所以當 page 超過一定的數量 (threshold) 時,就會真正的回到 buddy system。太少也會先從 buddy system 拿一些來用。
從這邊其實可以很清楚的觀察到這件事
https://elixir.bootlin.com/linux/v6.16/source/mm/page_alloc.c#L2692
去觀察 /proc/zoneinfo 也是個方法
1 | void show_zoneinfo() |
之後去 UAF 寫掉雙數的 object 第一塊
這樣就有大機率寫掉 seq
反正沒開 SMEP SMAP
控制執行流程後直接跳回 Userspace 跑 commit_cred(init_cred)
exploit
1 |
|
Dirty PageTable
analyze
基本上跟第一題一樣
1 | static long obj_alloc(int id) { |
不過多了一個 obj_read 可以用
並且啟動參數把 SMAP SMEP KASLR 都打開了
1 |
|
attack
首先是因為打開 SMEP SMAP 所以無法簡單的通過 control rip
來跳到 userspace 的 code 來去提權
所以這邊使用一個 data only 方式來打
通過去 spray mapping 產生的 PTE struct 來 cross cacch 去複寫
這個結構上的第二個 bits 是管理 R/W 權限的
當我們去 mapping 到 /etc/passwd 想要去複寫的時候,因為原本只有可讀所以無法改寫
但是通過 UAF 寫掉 R/W 位就可以改變權限讓 /etc/passwd 可寫
去改寫密碼來提權 openssl passwd -1 -salt naup naup96321
exploit
1 |
|
Dirty Cred
analyze
driver 功能變這樣
1 |
|
沒有 write 功能了
保護一樣是全開的狀態
1 |
|
attack
slab 一些東西不太一樣
稍微修改一下
在沒有辦法寫的狀況下,或許可以考慮做 dirty cred
現在有一個想法
如果我們可以 UAF cred 這個 struct
然後 spray,讓我們有一個自己低權限的 proecess UAF 後
去 free UAF 低權限的 process
之後再去 spary 高權限 process
來讓 cred 被拿走後被寫入高權限的資訊來提權我們可控的低權限 process
不過去 spray process 的 noise 非常多
但有方法可以降低這個 noise
可以參考這篇,通過設定一些 flags 可以大量去降低 noiseclone(CLONE_FILES | CLONE_FS | CLONE_VM | CLONE_SIGHAND)
https://www.willsroot.io/2022/08/reviving-exploits-against-cred-struct.html
exploit
exploit 穩定度好低,但是可以成功讀出 flag
可以在看怎麼提升穩定度
1 |
|
Dirty pipe
analyze
基本上跟上一題一樣
不過所有功能都有
不過他使用了
1 | static long obj_alloc(void) { |
所以現在有一個 general cache kmalloc-1k 的 kmem_cache UAF
pipe
可以嘗試 dirty pipe
先來介紹一下
pipe 是用於不同 process 之間通訊的一個東西
kernel 會創建一個 inode
並分配兩個 file descriptor 來去做為讀端及寫端
- pipe_inode_info 存放著該 pipe 的 metadata
- pipe_buffer 是用來存放 pipe 實際上傳遞的資料
https://elixir.bootlin.com/linux/v6.15.9/source/include/linux/pipe_fs_i.h#L86
1 | /** |
- head: 下一個寫入位置
- tail: 下一個讀取位置
- rd_wait / wr_wait
- 當 pipe 為空時,讀端進入 rd_wait 等待
- 當 pipe 為滿時,寫端進入 wr_wait 等待
- max_usage: ring buffer 可使用的最大 buffer 數
- readers / writers: 當前活躍的讀端與寫端數量
- files: 有多少個 struct file 引用這個 pipe(受 ->i_lock 保護)
- r_counter / w_counter: 計數器,追蹤端點操作,主要用於判斷狀態變化與喚醒邏輯
- poll_usage: 表示此 pipe 是否用於 epoll(因為 epoll 會造成更頻繁的 wakeup,需要特殊處理)
- tmp_page: 暫存釋放掉的 page
- bufs: 指向真正的環形緩衝區
https://elixir.bootlin.com/linux/v6.15.9/source/include/linux/pipe_fs_i.h#L26
1 | /** |
pipe_buffer 就是用來存資料的地方,每個 pipe 管理了一些 page 來存
這部分 read write 實作先不下 source code
先來看概念
https://bbs.kanxue.com/thread-286449.html
整個結構長這樣
剛剛提到
head 是一個 寫入 pointer
tail 是一個 讀取 pointer
pipe inode metadata 中的 bufs pointer 就管理了一個 0x10 個 pipe_buffer (使用上像是環形的)
並且有兩種狀態
緩衝區空:當 head == tail
表示沒有數據可讀,寫指標追上讀指標了,管道中沒有有效數據。
緩衝區滿:當 (head - tail) == N
表示寫指標比讀指標快 N 個緩衝槽位,管道已寫滿,不能再寫入數據,寫入方需要阻塞等待。
- pipe_write (寫入東西到 pipe_buffer)
- 通過 head & mask 來去找到當前要寫入的 page
- 第一次寫入設定
bufs[i]->flags被設置成PIPE_BUF_FLAG_CAN_MERGE或是PIPE_BUF_FLAG_PACKET(網路封包) copy_page_from_iter被 call,將 userspace 想傳的資料暫存在 kernel space 的 pipe_buffer 中- 更新 head 到下一個 page
- pipe_read (讀取東西到 userspace,另一個 process)
- 通過 tail & mask 來去找到當前要讀取的 page
copy_page_from_iter被 call,將資料從該 page 的buf->offset開始 copy,並更新buf->offset和buf->len(offset 是從哪裡開始讀,len 是讀多長)- 更新 tail 到下一個 page
- 若緩衝區為空,就代表沒事了
稍微 demo 一下用法大概長這樣
1 |
|
attack
如果有開啟 PIPE_BUF_FLAG_CAN_MERGE 可以在不超過一個 page 的狀況下,繼續去續寫 page
但如果我們去改寫 flags 成這樣然後把 len 改成 0 (offset 大概都是 0)
來讓 kernel pipe 誤認為該段可以追加寫入,可以寫入 /etc/passwd
可以看這段沒有去驗證當前權限直接將東西寫入
1 | if ((buf->flags & PIPE_BUF_FLAG_CAN_MERGE) && |
總結一句話,若你是直接對 file 做寫入那就是會去驗證檔案權驗
但是因為 flags 被改,誤認為這是追加寫入,讓我們可以去寫入 pipe_buffer
他會被映射到對應的 page 上(沒有檢查) 來做到任意寫入檔案
Dirty pipe 也可以看這個
https://dirtypipe.cm4all.com/
https://u1f383.github.io/linux/2024/08/16/linux-kernel-use-pipe-object-to-do-data-only-attack.html
exploit
1 |
|
pagejack
obj 是 0x400 大小 (用 kmalloc-1024)
這題提供兩個 ioctl
一個可以分配 chunk
一個可以去對 obj 做寫入
size 輸入完後會在最後補上 null
這是一個越界的 off by null
1 |
|
pagejack
pipe 相關資料可以參考這個
https://hackmd.io/@naup96321/HkS86zu_ex
pagejack 目標是放在 pipe_buffer array
pipe_inode_info 是 pipe 的 metadata
pipe_buffer 則是實際存放 pipe 資料的地方
一個 pipe_buffer 會指向一個 page 負責管理
1 | // |
pipe_buffer 裡面有 page pointer
如果能讓 page pointer 指向到同一張 page,就可以有 page level UAF (剛好 page_buffer 第一個元素是 page pointer,只需要有 off by one 或 off by null 就可以成功去作出 page level UAF)
1 | // https://elixir.bootlin.com/linux/v6.15.9/source/include/linux/pipe_fs_i.h#L17 |
attack
首先先 spray 一段 pipe_buffer
接下來 allocate 一個 victim object
之後再去 spray 一段 pipe_buffer
保證了 pipe_buffer 之中有一個可以用來做 overflow 的 obj
這邊稍微注意一下,因為我們不知道實際蓋到的 pipe 是哪一個
因此需要做一點標記
可以在每個 pipe_buffer 上寫入資訊,並每個都 +1
首先塞入 4 個作為識別
後面塞入一些垃圾做為 padding (8 bytes),不要讓 pipe_buffer 再讀取用於識別的 bytes 後,不會被 free 掉
為甚麼是 padding 4 + 8 bytes 呢? 原因是可以看到 struct file
當我們打開 file 後會自動分配一個 struct file
我們目標是將 /etc/passwd 改寫
而 /etc/passwd 本身是不可寫的,但如果能將 f_mode 改成可寫,就可以往檔案寫入東西
f_mode offset = 12
如果填入 12 bytes,下次寫入就可以直接寫
https://elixir.bootlin.com/linux/v6.13.12/source/include/linux/fs.h#L1035
1 | struct file { |
現在排好了 slub 布局以及 obj 後
就可以通過 off by null 去覆蓋掉 page pointer
接下來通過 read 來去看哪一個 pipe_buffer 現在不屬於原本的 page
去找到被改寫的 page 所屬的 pipe_buffer 後,以及現在被指向的 page 後 pipe_buffer
我們去釋放掉被指向的 pipe_buffer
這樣就有一個 page level 的 UAF 了 (struct page pointer)
接下來就去 spray /etc/passwd 的 file 結構體,讓 UAF 的 page 被拿走
然後去通過 write 對改寫的 page 做寫入,把 f_mode 寫成可寫
可以參考
https://elixir.bootlin.com/linux/v6.13.12/source/include/linux/fs.h#L116
1 | /* file is open for reading */ |
把他改寫為可以 write 後
就可以將 /etc/passwd 寫掉來提權了
PS: 當然實際上有可能發生我們 overlapping 的 page 是我們無法控制的,但在雜訊較低的環境中比較不容易發生,並且該覆蓋的記憶體與控制的 page 很相近,實際上非常有機會蓋到可控 page
最後稍微附註一下 slub 布局(kmalloc-1k)
通過 slub-dump 觀察到 (附註一下我這邊 Demo 為了識別方便改 padding 0x87)
可以觀察到通過 null byte 會 overwrite 低位
讓兩個 pipe_buffer 的 struct page pointer 指向同一塊
至於為甚麼 kmalloc-1k 沒有 Red zone 之類的我還沒研究
先放著
exploit
1 |
|