Linux 2021 最新檔案系統層中的size_t-to-int 漏洞
2021-12-19由 飛魚在浪嶼 發表于 農業
路徑覆蓋檔案是什麼
更多網際網路精彩資訊、工作效率提升關注【飛魚在浪嶼】(日更新)
摘要
=========================== =========================================
一個Linux核心檔案系統層的size_t- to-int轉換漏洞:透過建立、掛載、刪除
總路徑長度超過1GB的深層目錄結構,非特權本地攻擊者可以將10位元組的字串“//deleted”寫入 vmalloc() 核心緩衝區的開頭下方,正好偏移量 -2GB-10B的位置。
利用了這種不受控制的越界寫入,並得了預設安裝的 Ubuntu 20。04 的完全 root 許可權,
Ubuntu 20。10、Ubuntu 21。04、Debian 11 和 Fedora 34 工作站;其他Linux 發行版當然容易受到攻擊,並且可能會被利用。
我們的漏洞利用需要大約 5GB 的記憶體和 1M 的 inode;
可從以下網址獲得:https : //www。qualys。com/research/security-advisories/ 詳細介紹
分析
===================================================================
Linux 核心的 seq_file 介面產生包含記錄序列的虛擬檔案(例如,/proc 中的許多檔案是
seq_files,而記錄通常是行)。每個記錄都必須適合一個seq_file 緩衝區,因此可以根據需要擴大它,方法是
在第 242 行將其大小加倍(seq_buf_alloc() 是 kvmalloc() 的簡單包裝器):
———————————————————————————————————— 168 ssize_t seq_read_iter(struct kiocb *iocb, struct iov_iter *iter) 169 { 170 struct seq_file *m = iocb->ki_filp->private_data; 。。。 205 /* grab buffer if we didn‘t have one */ 206 if (!m->buf) { 207 m->buf = seq_buf_alloc(m->size = PAGE_SIZE); 。。。 210 } 。。。 220 // get a non-empty record in the buffer 。。。 223 while (1) { 。。。 227 err = m->op->show(m, p); 。。。 236 if (!seq_has_overflowed(m)) // got it 237 goto Fill; 238 // need a bigger buffer 。。。 240 kvfree(m->buf); 。。。 242 m->buf = seq_buf_alloc(m->size <<= 1); 。。。 246 }————————————————————————————————————
這個大小乘法本身並不是一個漏洞,因為m->size 是一個 size_t(一個無符號的 64 位整數,在 x86_64 上),並且系統會在這個乘法溢位整數 m->size之前就耗盡記憶體。
不幸的是,這個 size_t 也被傳遞給 一個度量用 int型別作為引數的函式(有符號的 32 位整數)而不是 size_t 的函式。例如,show_mountinfo() 函式(在第 227 行呼叫以格式化
/proc/self/mountinfo 中的記錄)呼叫 seq_dentry()(在第 150 行),它
呼叫 dentry_path()(在第 530 行),它呼叫prepend()(在第 387 行):
————————————————————————————————————135 static int show_mountinfo(struct seq_file *m, struct vfsmount *mnt)136 {。。。150 seq_dentry(m, mnt->mnt_root, “ \t\n\\”);———————————————————————————————————— 523 int seq_dentry(struct seq_file *m, struct dentry *dentry, const char *esc) 524 { 525 char *buf; 526 size_t size = seq_get_buf(m, &buf); 。。。 529 if (size) { 530 char *p = dentry_path(dentry, buf, size);————————————————————————————————————380 char *dentry_path(struct dentry *dentry, char *buf, int buflen)381 {382 char *p = NULL;。。。385 if (d_unlinked(dentry)) {386 p = buf + buflen;387 if (prepend(&p, &buflen, “//deleted”, 10) != 0)———————————————————————————————————— 11 static int prepend(char **buffer, int *buflen, const char *str, int namelen) 12 { 13 *buflen -= namelen; 14 if (*buflen < 0) 15 return -ENAMETOOLONG; 16 *buffer -= namelen; 17 memcpy(*buffer, str, namelen);————————————————————————————————————
結果,如果一個無特權的本地攻擊者建立、掛載和刪除總路徑長度超過 1GB 的深層目錄結構,如果攻擊者 呼叫open() 和 read()函式操作/proc/self/mountinfo,則:
- 在 seq_read_iter() 中,使用vmalloc() 分配了2GB 緩衝區(第242行),並
呼叫 show_mountinfo()(第 227 行);
- 在 show_mountinfo() 中,使用空的 2GB 緩衝區呼叫 seq_dentry()(第 150 行);
- 在 seq_dentry() 中,呼叫dentry_path() 攜帶引數2GB 大小(第 530 行);
- 在 dentry_path() 中,因此 int buflen 為負數 (INT_MIN,-2GB),p 指向 vmalloc()分配的 緩衝區下方 -2GB 的負偏移量(第 386 行),並呼叫 prepend()(第 387 行);
- 在 prepend() 中,*buflen 減去10 個位元組並變成一個大但是是正的整數(第 13 行),*buffer 減少 10 位元組,指向vmalloc()ated 緩衝區下方 -2GB-10B 的偏移量(第 16 行),
寫入 10 位元組大小的字串“//deleted”(第 17 行)。
漏洞利用概述
===================================================================
1/ mkdir () 一個總路徑長度超過 1GB的深層目錄結構(大約 1M 個巢狀目錄),我們將其繫結掛載到非特權使用者名稱空間中,然後 rmdir() 將其掛載。
2/ 建立一個執行緒,它 vmalloc() 訪問一個小的 eBPF 程式(透過BPF_PROG_LOAD),然後我們阻止這個執行緒(透過 userfaultfd 或 FUSE),在核心進行 JIT 編譯之前,eBPF 程式已經透過核心 eBPF 驗證程式的驗證
3/ 我們在我們的非特權使用者名稱空間中 open() /proc/self/mountinfo ,並開始 read() 我們繫結掛載目錄的長路徑,從而將字串“//deleted”寫入正好 -2GB 的偏移量-10B 低於
vmalloc() 化緩衝區的開頭。
4/ 安排這個“//deleted”字串覆蓋經過驗證的eBPF程式的一條指令(從而使
核心eBPF驗證器的安全檢查無效),並將這個不受控制的越界寫入轉化為資訊暴露,並進入有限但受控的越界寫入。
5/透過重用 Manfred Paul 漂亮的 btf 和map_push_elem 技術,將這個有限的越界寫入轉換為核心記憶體的任意讀寫:https ://www。thezdi。com/blog/2020/4/8 /cve-2020-8835-linux-kernel-privilege-escalation-via-improper-ebpf-program-verification
6/ 使用這個任意讀取來定位核心記憶體中的 modprobe_path[] 緩衝區,並使用任意寫入來替換
這個緩衝區的內容(預設為“/sbin/modprobe”)和我們自己的可執行檔案的路徑,從而獲得完整的 root 許可權。
漏洞利用細節
====================================================================
a/ 建立了一個總路徑長度超過1GB的目錄:理論上,需要建立超過1GB/256B=4M的巢狀目錄(NAME_MAX 為 255);實際上,show_mountinfo() 將我們長目錄中的每個 ’\\‘ 字元替換為 4 位元組字串“\\134”,因此我們只需要建立 1M 巢狀目錄。
b/ 我們填補了所有大的 vmalloc 漏洞:
在幾個非特權使用者名稱空間中繫結掛載 (MS_BIND)長目錄的各個部分,並
透過 read()ing /proc/self/mountinfo 來 vmalloc() 分配 大 seq_file 緩衝區。
例如,我們在漏洞利用中 vmalloc() 佔用了 768MB 的大緩衝區。
c/ vmalloc() 使用了兩個 1GB 緩衝區和一個 2GB 緩衝區(透過在三個不同的使用者名稱空間中繫結掛載我們的長目錄,並透過 read()ing
/proc/self/mountinfo),檢查“//deleted “ 確實寫入在 2GB 緩衝區開頭下方的 -2GB-10B 偏移量(即,第一個 1GB 緩衝區開頭上方的 8182B——“XXX”是保護頁):
”//deleted“ | 4KB v 1GB 4KB 1GB 4KB 2GB——-|——-|——-+——————-|——-|————————-|——-|————————-| 。。。 |XXX| seq_file buffer |XXX| seq_file buffer |XXX| seq_file buffer |——-|——-|——-+——————-|——-|————————-|——-|————————-| | | | | \——<——<——<——<——<——<——<——/ 8182B -2GB-10B
d/ 填寫所有小的 vmalloc 漏洞:vmalloc()透過傳送 () 大量 NETLINK_USERSOCK 訊息來消耗各種小套接字緩衝區。例如,在漏洞利用中 vmalloc() 佔用了 256MB 的小緩衝區。
e/ 建立了 1024 個使用者空間執行緒;每個執行緒開始將 eBPF程式載入到核心中,但是(透過 userfaultfd 或 FUSE)阻塞了核心空間中的每個執行緒(在第 2101 行),在 eBPF 程式
實際上被 vmalloc() 化之前(在第 2162 行):
————————————————————————————————————2076 static int bpf_prog_load(union bpf_attr *attr, union bpf_attr __user *uattr)2077 {。。。。2100 /* copy eBPF program license from user space */2101 if (strncpy_from_user(license, u64_to_user_ptr(attr->license),。。。。2161 /* plain bpf_prog allocation */2162 prog = bpf_prog_alloc(bpf_prog_size(attr->insn_cnt), GFP_USER);————————————————————————————————————
f / vfree第一個 1GB seq_file 緩衝區(其中“//deleted”被越界寫入),立即解除對所有 1024 個執行緒的阻塞;eBPF 程式被 vmalloc() 放入剛剛 vfree() 的 1GB 孔中:
4KB 1GB 4KB 1GB 4KB 2GB——-|——-|————————-|——-|————————-|——-|————————-| 。。。 |XXX| eBPF programs |XXX| seq_file buffer |XXX| seq_file buffer |——-|——-|————————-|——-|————————-|——-|————————-|
g/ 接下來,(再次透過 userfaultfd 或 FUSE)阻止我們的一個執行緒(在第 12795 行)在核心 eBPF驗證程式。驗證其 eBPF 程式之後但在核心對其進行 JIT 編譯之前:
——- —————————————————————————— ————————- 12640 int bpf_check(struct bpf_prog **prog, union bpf_attr *attr, 12641 union bpf_attr __user *uattr) 12642 { 。。。。。12795 print_verification_stats(環境);—————————————————————————— ————————————
h/ 最後,用越界的“//deleted”字串覆蓋了這個 eBPF 程式的指令(再次透過我們的 2GB seq_file 緩衝區),因此使核心 eBPF 驗證器的安全檢查無效:
”//deleted“ | 4KB v 1GB 4KB 1GB 4KB 2GB——-|——-|——-+——————-|——-|————————-|——-|————————-| 。。。 |XXX| eBPF programs |XXX| seq_file buffer |XXX| seq_file buffer |——-|——-|——-+——————-|——-|————————-|——-|————————-| | | | | \——<——<——<——<——<——<——<——/ 8182B -2GB-10B
首先,變換這種不受控制的 eBPF 程式損壞為資訊洩露。第一個未損壞的 eBPF 程式被
核心 eBPF 驗證器認為是安全的、
Manfred Paul 的 btf和 map_push_elem 技術,將這種有限的越界讀寫轉換為核心記憶體的任意讀寫:
- 透過任意核心讀取,我們定位符號“__request_module”,從而找到函式__request_module(),反彙編這個函式,
並從
“if (!modprobe_path[0])”指令中提取 modprobe_path[] 的地址。
- 使用任意核心寫入,我們用我們自己的可執行檔案的路徑覆蓋modprobe_path[](預設為“/sbin/modprobe”)的內容,並呼叫 request_module()(透過建立一個 netlink 套接字),
它執行 modprobe_path,就是我們自己的可執行檔案。
緩解措施
========================== ==========================================
重要說明:以下緩解措施僅阻止我們特定的漏洞利用(但可能存在其他漏洞利用技術);至徹底修復這個漏洞,核心必須打補丁。
- 將 /proc/sys/kernel/unprivileged_userns_clone 設定為 0,以防止攻擊者在使用者名稱空間中掛載長目錄。但是,攻擊者可能會透過 FUSE 掛載一個長目錄;還沒有完全探索這種可能性,因為無意中在 systemd 中偶然發現了CVE-2021-33910:如果攻擊者 FUSE-mount 一個長目錄(超過 8MB),那麼 systemd 會耗盡其堆疊,崩潰,從而導致整個操作崩潰系統(核心恐慌)。
- 將 /proc/sys/kernel/unprivileged_bpf_disabled 設定為 1,以防止攻擊者將 eBPF 程式載入到核心中。然而攻擊者可能會損壞其他 vmalloc() 化物件(例如,執行緒堆疊),但尚未調查這種可能性。