【kernel exploit】CVE-2021-4154 错误释放任意file对象-DirtyCred利用

2022/10/17 Kernel-exploit 共 15658 字,约 45 分钟

【kernel exploit】CVE-2021-4154 错误释放任意file对象-DirtyCred利用

影响版本:Linux v5.13.4 以前,v5.13.4 已修补。评分8.8

测试版本:Linux-5.13.3 exploit及测试环境下载地址—https://github.com/bsauce/kernel-exploit-factory

编译选项

在编译时将.config中的CONFIG_E1000CONFIG_E1000E,变更为=y。参考

$ wget https://mirrors.tuna.tsinghua.edu.cn/kernel/v5.x/linux-5.13.3.tar.xz
$ tar -xvf linux-5.13.3.tar.xz
# KASAN: 设置 make menuconfig 设置"Kernel hacking" ->"Memory Debugging" -> "KASan: runtime memory debugger"。
$ make -j32
$ make all
$ make modules
# 编译出的bzImage目录:/arch/x86/boot/bzImage。

漏洞描述kernel/cgroup/cgroup-v1.ccgroup1_parse_param() 函数(通过fsconfig 系统调用触发)存在类型混淆,导致UAF漏洞。可以调用syscall fsconfig 设置任意的 fd,最终关闭该文件后 fd 对应 file 对象会被释放。这样我们就能释放任意一个文件描述符对应的 file 结构。本文采用两种方法实现利用,一是DirtyCred,二是构造ROP。对比两种方法,DirtyCred方法的优点是跨内核版本通用,不需要适配,缺点是需要覆写特权文件来提权,所以在docker等容器中无法提权;ROP的优点是可以任意读写内核内存并执行任意代码,缺点是对不同内核版本的适配很麻烦。

补丁patch 添加检查,若 param->type != fs_value_is_string,则报错退出。


diff --git a/kernel/cgroup/cgroup-v1.c b/kernel/cgroup/cgroup-v1.c
index ee93b6e895874..527917c0b30be 100644
--- a/kernel/cgroup/cgroup-v1.c
+++ b/kernel/cgroup/cgroup-v1.c
@@ -912,6 +912,8 @@ int cgroup1_parse_param(struct fs_context *fc, struct fs_parameter *param)
 	opt = fs_parse(fc, cgroup1_fs_parameters, param, &result);
 	if (opt == -ENOPARAM) {
 		if (strcmp(param->key, "source") == 0) {
+			if (param->type != fs_value_is_string)
+				return invalf(fc, "Non-string source");
 			if (fc->source)
 				return invalf(fc, "Multiple sources not supported");
 			fc->source = param->string;

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

利用总结

  • DirtyCred 方法:成功。
    • (1)准备工作:创建 ./exp_dir 目录和可写文件./exp_dir/data,创建软连接 ./exp_dir/uaf -> ./exp_dir/data
    • (2)循环运行子进程 namespace_sandbox_proc() 进行提权,检查 /etc/passwd 是否被篡改;
    • (3)打开文件 ./exp_dir/uaf (文件描述符为uaf_fd),触发漏洞释放 uaf_fd 对应的 file 结构;
    • (4)线程1—— slow_write() 打开文件 ./exp_dir/uaf (文件描述符为fd),写入大量数据来占据读写锁
    • (5)线程2—— write_cmd() 往文件描述符 uaf_fd 写入恶意数据(例如 hacker:x:0:0:root:/:/bin/sh);
    • (6)线程3(父线程)—— spray_files() 通过打开大量的 /etc/passwd喷射高权限 file 对象,替换uaf_fd 对应的 file 结构。每打开一次 /etc/passwd,都调用 kcmpsyscall-kcmp 默认开启了 CONFIG_KCMP,就可以使用)检查当前打开的文件描述符和 uaf_fd 是否对应同一文件,判断是否替换成功。
  • ROP 方法:本方法在测试过程中还是会有问题,没能成功提权(在通过漏洞的悬垂指针释放 file 对象时报错,kernel BUG at mm/slub.c:4205!,报错代码在 kfree())。经过调试发现,cross-cache 并没有成功,file cache page 并没有被 msg_msg 复用,导致 (1-5) 没能成功泄露 magic 值。
    • (1)泄露 heap magic 值
      • (1-1)喷射file对象:打开5000个文件,打开漏洞文件 uaf_fd,再打开5000个文件;
      • (1-2)释放 uaf_fd 附近的400个文件,以释放一整个 filp cache page;
      • (1-3)喷射800个 msg_msg -> msg_msgseg(填充字节 A),也即 kmalloc-4k -> kmalloc-512进行 cross-cache 利用,其中某个 msg_msgseg 会占据 uaf_fd 对应的 file 对象;
      • (1-4)触发漏洞再次释放 uaf_fd 对应的 file 对象(称为漏洞对象),这样就会在漏洞对象上写入 magic 值;
      • (1-5)利用 msg_msg 读取漏洞对象上的magic值(这个和漏洞对象重叠的消息下标记为 msg_id);
    • (2)泄露内核基址
      • (2-1)喷射 100 个 pipe_buffer,利用 fcntl 调用调整分配大小为 8*40 = 320(位于 kmalloc-512),占据漏洞对象;
      • (2-2)通过重叠的 msg_msg,泄露pipe_buffer->pagepipe_buffer->ops
    • (3)泄露堆地址:释放下标为 msg_id+1 的消息;遍历 100 个 pipe_buffer,每次调用 fcntl 会先分配新的 pipe_buffer (分配大小为 10*40=400)再释放掉原先的 pipe_buffer,如果恰好释放了漏洞对象,那么通过 msg_msg 读取漏洞对象,就能泄露下标为 msg_id+1 的消息的地址。问题是,分配新的 pipe_buffer 时会不会占用下标为 msg_id+1 的消息呢???如果会占用,那么只能说泄露了某个 kmalloc-512 的堆块地址(记录为 leaked kmalloc-512
    • (4)布置ROP并劫持控制流
      • 问题是,这时的 pipe_buffer 大小只有512字节,不足以放下ROP、ops结构、保存rbp等。
      • (4-1)再次喷射100个 pipe_buffer,每喷射1个,都通过 msg_msg 读取漏洞对象来检查漏洞对象是否被 pipe_buffer 占用,如果被占用则记录 pipe_bufffer 的下标——pipe_victim_idx。问题是,会不会有某个 pipe_buffer 占据了刚才泄露地址的堆块 leaked kmalloc-512 ???那么就不能在这个堆块上放置伪造的 pipe_buffer->ops 结构了。
      • (4-2)伪造 pipe_buffer->ops 结构和 ROP 链,喷射100个 msg_msg,希望能够喷到刚才泄露地址的堆块 leaked kmalloc-512
      • (4-3)伪造 pipe_buffer->ops 指针、pivot gadget,通过 msg_msg 释放漏洞对象,然后喷射100个 msg_msg 来篡改 pipe_buffer
      • (4-4)通过关闭下标为 pipe_victim_idxpipe_buffer,触发劫持控制流。

1. 漏洞分析

触发代码

    int fscontext_fd = fsopen("cgroup");
    int fd_null = open("/dev/null, O_RDONLY);
    int fsconfig(fscontext_fd, FSCONFIG_SET_FD, "source", fd_null);
    close_range(3, ~0U, 0);

漏洞:cgroup v1 的 cgroup1_parse_param() 需要一个标记为 “source” 的字符串参数,但同时 “source” 也能指定一个文件描述符,而 cgroup1_parse_param() 没有区分传入的是字符串(param->type == fs_value_is_string )还是文件描述符(param->type == fs_value_is_file),就直接把 param->stringparamfs_parameter 结构,其中 param->stringparam->file 是用union 存储的,所以空间重叠,存在类型混淆漏洞)赋值给了 fc->source。如果我们调用 fsconfig 时传入的是 fd_null,后面触发执行 put_fs_context() 时会释放 fc->source,从而实际释放了 fd_null 对应的 file 对象,之后关闭 fd_null 时触发UAF。

调用路径SYSCALL-fsconfig -> vfs_fsconfig_locked() -> vfs_parse_fs_param() -> cgroup1_parse_param()

SYSCALL_DEFINE5(fsconfig,
		int, fd,
		unsigned int, cmd,
		const char __user *, _key,
		const void __user *, _value,
		int, aux)
{
    struct fs_parameter param = {
		.type	= fs_value_is_undefined,
	};
    ...
    switch (cmd) {
    ...
    case FSCONFIG_SET_STRING:
		param.type = fs_value_is_string; 					// [1] 设置 fs_parameter 结构
		param.string = strndup_user(_value, 256);
		param.size = strlen(param.string);
		break;
    ...
    case FSCONFIG_SET_FD: 									// [2] 设置 fs_parameter 结构
		param.type = fs_value_is_file;
		param.file = fget(aux);
		break;  
    ...
    }
    ret = mutex_lock_interruptible(&fc->uapi_mutex);
	if (ret == 0) {
		ret = vfs_fsconfig_locked(fc, cmd, &param); 		// <--- [3] 最终调用 cgroup1_parse_param() 漏洞函数, 传入 param 结构
		mutex_unlock(&fc->uapi_mutex);
	}
    ...
}

struct fs_parameter {
	const char		*key;		/* Parameter name */
	enum fs_value_type	type:8;		/* The type of value here */
	union {
		char		*string;
		void		*blob;
		struct filename	*name;
		struct file	*file; 							// file 指针和 string 指针位于 union 结构中,所以重叠了
	};
	size_t	size;
	int	dirfd;
};

int cgroup1_parse_param(struct fs_context *fc, struct fs_parameter *param)
{
	struct cgroup_fs_context *ctx = cgroup_fc2context(fc);
	struct cgroup_subsys *ss;
	struct fs_parse_result result;
	int opt, i;

	opt = fs_parse(fc, cgroup1_fs_parameters, param, &result);
	if (opt == -ENOPARAM) {
		if (strcmp(param->key, "source") == 0) {
			if (fc->source)
				return invalf(fc, "Multiple sources not supported");
			fc->source = param->string; 	// [4] 漏洞!!! 没有检查 param->type 类型, 可能将 param->file 赋值给 fc->source
			param->string = NULL;
			return 0;
		}
		for_each_subsys(ss, i) {
			if (strcmp(param->key, ss->legacy_name))
				continue;
			if (!cgroup_ssid_enabled(i) || cgroup1_ssid_disabled(i))
				return invalfc(fc, "Disabled controller '%s'",
					       param->key);
			ctx->subsys_mask |= (1 << i);
			return 0;
		}
		return invalfc(fc, "Unknown subsys name '%s'", param->key);
	}
	...
}

2. DirtyCred 利用方法

DirtyCred exp适用版本

  • CentOS 8 kernels higher than linux-4.18.0-305.el8
  • Debian 11 kernels higher than 5.10.0-8
  • Fedora 31/32/33 kernels higher than 5.3.7-301.fc31
  • Ubuntu 18/20 kernels higher than 5.4.0-84 and 5.11.0-37.41

2-1. 利用思路

思路来源:DirtyCred 方法的灵感来自于 Jann Horn 的 double-put exploit,该漏洞和 CVE-2021-4154 类似,释放某个 file 结构但仍存有对该结构的引用。Jann Horn 的利用方法是,先打开一个具有写权限的非特权文件,然后写入恶意内容,在检查写许可和实际写这个时间窗口之间,用特权 file 结构来替换低权限 file 结构(例如打开 /etc/crontab/etc/passwd 文件,就会分配一个特权 file 对象),这样就会将恶意内容写入特权文件。

时间窗口:看以下写函数中,vfs_writev()(通过 writev 系统调用来触发)先检查文件的写许可,然后调用 do_readv_writev() 准备io并进行实际写。do_readv_writev() 进行实际写之前会先调用 rw_copy_check_uvector() 访问用户数据(本函数作用是处理用户空间的io), Jann Horn 就是利用FUSE在这里暂停的。

注意,为什么选取 writev 这个调用来写文件呢?后缀 v 表示 vector,向量写,用户可以通过 iovec 结构一次传递多个用户地址,这样内核在实际写文件之前就先从用户读取这个 iovec 结构,那就可以在读取 iovec 结构的时候暂停内核。普通的 write 可能就直接写文件了,根本不给你暂停的机会。

ssize_t vfs_writev(struct file *file, const struct iovec __user *vec,
           unsigned long vlen, loff_t *pos)
{ 
    if (!(file->f_mode & FMODE_WRITE)) 						// [1] 检查是否具有写许可
        return -EBADF;
    if (!(file->f_mode & FMODE_CAN_WRITE)) 	
        return -EINVAL;

    return do_readv_writev(WRITE, file, vec, vlen, pos); 	// [2] 调用 do_readv_writev() 进行实际写
}

static ssize_t do_readv_writev(int type, struct file *file,
                   const struct iovec __user * uvector,
                   unsigned long nr_segs, loff_t *pos)
{
    size_t tot_len;
    struct iovec iovstack[UIO_FASTIOV];
    struct iovec *iov = iovstack;
    ssize_t ret;
    io_fn_t fn;
    iov_fn_t fnv;
    iter_fn_t iter_fn;

    // userfault here  										// [3] 读取用户传入的 iovec 结构, 可以暂停
    ret = rw_copy_check_uvector(type, uvector, nr_segs,
                    ARRAY_SIZE(iovstack), iovstack, &iov);
    if (ret <= 0)
        goto out;

    tot_len = ret;
    ret = rw_verify_area(type, file, pos, tot_len); 		// [4] 实际写
    if (ret < 0)
        goto out;
    // perform writing
    ...
}

问题:在 Jann Horn 公开exp之后,内核 v4.13 以后就修补了这个问题,将 rw_copy_check_uvector() 调用移到了许可检查的前面。这样 Jann Horn 的方法就不可用了。

static ssize_t vfs_writev(struct file *file, const struct iovec __user *vec,
           unsigned long vlen, loff_t *pos, rwf_t flags)
{
    struct iovec iovstack[UIO_FASTIOV];
    struct iovec *iov = iovstack;
    struct iov_iter iter;
    ssize_t ret;

    // preparing io, where kernel could be paused using userfault
    ret = import_iovec(WRITE, vec, vlen, ARRAY_SIZE(iovstack), &iov, &iter); 	// [1] 先读取 iovec 结构
    if (ret >= 0) {
        file_start_write(file);
        ret = do_iter_write(file, &iter, pos, flags); 							// [2] do_iter_write()
        file_end_write(file);
        kfree(iov);
    }
    return ret;
}

static ssize_t do_iter_write(struct file *file, struct iov_iter *iter,
        loff_t *pos, rwf_t flags)
{
    size_t tot_len;
    ssize_t ret = 0;

    // checking permission
    if (!(file->f_mode & FMODE_WRITE)) 										 	// [3] 再检查写许可
        return -EBADF;
    if (!(file->f_mode & FMODE_CAN_WRITE))
        return -EINVAL;

    tot_len = iov_iter_count(iter);
    if (!tot_len)
        return 0;
    ret = rw_verify_area(WRITE, file, pos, tot_len);
    if (ret < 0)
        return ret;

    // performing writing 														// [4] 最后进行实际写入
    if (file->f_op->write_iter)
        ret = do_iter_readv_writev(file, iter, pos, WRITE, flags);
    else
        ret = do_loop_readv_writev(file, iter, pos, WRITE, flags);
    if (ret > 0)
        fsnotify_modify(file);
    return ret;
}

2-2. 利用文件系统锁来暂停内核

方法:文件系统不允许多个线程同时写入同一文件,所以采用锁机制来保证只有1个线程进行实际写。所以可以利用多线程竞争写来延长时间窗口。

  • 线程1打开可写文件,写入大量数据,这样就能 长时间占用锁;
  • 线程2打开同一文件,等待写入恶意数据;
  • 线程3触发漏洞,将线程2的低权限 file 结构释放,并用高权限 file 结构替换(打开 /etc/passwd 即可)。
   Thread 0: slow write               Thread 1: cmd write                      Thread 3: exploit
    __fdget_pos (no lock)            __fdget_pos (bypass lock)                             |
        |                                  |                                               |
        |                                  |                                               |
        \/                                \/                                               |
 ext4_file_write_iter (lock inode)    ext4_file_write_iter (wait for lock)                 |
        |                                  |                                               |
        |                                  |                                               |
        \/                                 |                                               \/
   normal write                            |                                      replace the file structure
        |                                  |
        |                                  |
        \/                                 |
write done, release inode lock             |
                                          \/
                                   get inode lock and then write
                                           |
                                          \/
                                        write done

问题:在检查写许可之前会调用 __fdget_pos(),其中也会获取 file->f_pos_lock 锁,可能导致线程2卡在检查写许可之前(这样就无法增大写许可检查和实际写之间的时间窗口)。看以下代码,若当前 file 包含 FMODE_ATOMIC_POS flag 且 refcount 大于1,就会有锁来避免多个线程竞争写入同一文件。本利用中由于在线程1和2中都打开了同一文件,所以至少有3个refcount,所以我们需要移除 FMODE_ATOMIC_POS flag 来避免去争夺锁。

unsigned long __fdget_pos(unsigned int fd)
{
    unsigned long v = __fdget(fd);
    struct file *file = (struct file *)(v & ~3);

    if (file && (file->f_mode & FMODE_ATOMIC_POS)) {
        if (file_count(file) > 1) {
            v |= FDPUT_POS_UNLOCK;
            mutex_lock(&file->f_pos_lock);
        }
    }
    return v;
}

解决:在 open 调用中,只要文件是常规文件,就会设置 FMODE_ATOMIC_POS flag,作者查看内核代码后发现,如果打开软连接文件就不会设置 FMODE_ATOMIC_POS flag,这样就能避免卡在 __fdget_pos() 函数中。

    /* POSIX.1-2008/SUSv4 Section XSI 2.9.7 */
    if (S_ISREG(inode->i_mode) || S_ISDIR(inode->i_mode))
        f->f_mode |= FMODE_ATOMIC_POS;

文件系统锁:如下所示,ext4_file_write_iter() 会对 inode 上锁,避免多个线程同时写入同一文件,这个锁恰好在写许可检查与实际写之间。线程1先获得锁并写入大量数据,线程2写入同一文件(确保不会在__fdget_pos()中卡住),在函数 ext4_file_write_iter() 中获取 inode 锁时暂停,线程3触发漏洞释放线程2的file 结构,并用特权 file 结构替换,等线程2获得锁后会往特权文件写入恶意数据。

调用路径.write_iter -> ext4_file_write_iter() -> ext4_buffered_write_iter()

static ssize_t ext4_buffered_write_iter(struct kiocb *iocb,
					struct iov_iter *from)
{
	ssize_t ret;
	struct inode *inode = file_inode(iocb->ki_filp);

	if (iocb->ki_flags & IOCB_NOWAIT)
		return -EOPNOTSUPP;

	ext4_fc_start_update(inode);
	inode_lock(inode); 												// [1] 获取 inode 锁
	...
	ret = generic_perform_write(iocb->ki_filp, from, iocb->ki_pos);	// [2] 实际写 generic_perform_write
	...
	inode_unlock(inode); 											// [3] 释放 inode 锁
	return ret;
}

2-3. 测试结果

如下图所示,成功往 /etc/passwd 文件写入了 "hi:x:0:0:root:/:/bin/sh\n",将当前用户 hi (密码为 lol)修改成了root权限。

succeed

3. ROP 利用方法

3-1. Cross-cache 覆写

方法:先打开10000个文件,释放400个文件(包括漏洞 file 对象),有概率将一整个 filp cache page 释放后还给页管理器;然后堆喷 struct msg_msgseg 占用 filp cache page。注意,堆喷对象所在的 slab cache 对应的 page size 最好和 filp cache 一样,这样能提高可靠性。

3-2. 绕过 FREELIST_HARDENED

方法:以往大家认为绕过 FREELIST_HARDENED 需要泄露一个 xor'ed freelist pointer 和一个堆地址,这样才能计算出 magic 值。但其实,我们可以不用泄露某个已知的堆地址,就能计算出 magic 值,进而泄露堆地址。参考 How AUTOSLAB Changes the Memory Unsafety Game

static inline void set_freepointer(struct kmem_cache *s, void *object, void *fp)
{
    unsigned long freeptr_addr = (unsigned long)object + s->offset;

#ifdef CONFIG_SLAB_FREELIST_HARDENED
    BUG_ON(object == fp); /* naive detection of double free or corruption */
#endif

    *(void **)freeptr_addr = freelist_ptr(s, fp, freeptr_addr);
}

若 current freelist pointer 为0,表示 freelist 中没有对象,这里 fp 也为0,那么 xor’ed freelist pointer 就为 magic_value ^ 0,直接就等于 magic 值本身,这样就能绕过 FREELIST_HARDENED

作者在利用中,完成 cross-cache 之后,通过漏洞再次释放 file 结构,就会将 xor’ed value 留在堆上,可通过读取 msg_msg 内容来泄露。

泄露堆地址:以上已经获得了 magic值,可继续通过以下步骤泄露堆地址。

  • 申请对象 obj_A 来占据被漏洞释放的堆块;
  • 释放同一cache中的对象 obj_B,然后再次释放 obj_A
  • 这样 obj_A 上的 freelist 指针就等于 address_of_obj_B ^ magic_value,泄露 obj_A 的内容就能泄露 obj_B 的堆地址。

3-3. 利用 pipe_buffer 绕过KASLR并劫持控制流

方法:参考 Andy’s write-up 如何利用 pipe_buffer 绕过KASLR并劫持控制流(ROP构造也可以参考),问题是 pipe_buffer 默认分配位于 kmalloc-1k,而我们需要一个位于 kmalloc-512 的弹性对象。看以下代码,可以通过 syscall fcntl 来控制 nr_slots 变量,进而控制 pipe_buffer 的分配大小,本例中作者使用的是 0x8 和 0xa,这个功能在 Android 和通用内核中很常见。

pipe_buffer默认分配路径pipe() -> do_pipe2() -> __do_pipe_flags() -> create_pipe_files() -> get_pipe_inode() -> alloc_pipe_info() —— 默认分配大小为0x370(默认16个page,16*0x28=0x370)。

fcntl 修改分配大小SYSCALL-fcntl -> do_fcntl() -> pipe_fcntl() -> pipe_set_size() -> pipe_resize_ring() 传入的flag标记为 F_SETPIPE_SZ,赋值语句是 nr_slots = size >> PAGE_SHIFT,所以传入的size为 0x8000,这样才能使 nr_slots = 8,这样分配出来的堆块大小为 8 * sizeof(pipe_buffer) == 8 * 0x28 == 320,位于 kmalloc-512。 注意,从以下代码可以看出,每调用一次 fcntl 就会重新分配一次 pipe_buffer 不管分配大小是否相等。

struct pipe_buffer {
	struct page *page;
	unsigned int offset, len;
	const struct pipe_buf_operations *ops;	// offset 0x10
	unsigned int flags;
	unsigned long private;
};

static long pipe_set_size(struct pipe_inode_info *pipe, unsigned long arg)
{
	unsigned long user_bufs;
	unsigned int nr_slots, size;
	long ret = 0;

	size = round_pipe_size(arg);
	nr_slots = size >> PAGE_SHIFT; 				// [1] 修改 nr_slots 值,所以传入的 size 为 0x8000
	...
	ret = pipe_resize_ring(pipe, nr_slots);		// [2] 调用 pipe_resize_ring() 重新分配 
	...
}

int pipe_resize_ring(struct pipe_inode_info *pipe, unsigned int nr_slots)
{
	struct pipe_buffer *bufs;
	unsigned int head, tail, mask, n;

	mask = pipe->ring_size - 1;
	head = pipe->head;
	tail = pipe->tail;
	n = pipe_occupancy(pipe->head, pipe->tail);
	if (nr_slots < n)
		return -EBUSY;

	bufs = kcalloc(nr_slots, sizeof(*bufs),		// [3] 分配 pipe_buffer
		       GFP_KERNEL_ACCOUNT | __GFP_NOWARN);
	... 
	kfree(pipe->bufs);							// [4] 释放原先的 pipe->bufs
	pipe->bufs = bufs; 							// [5] 替换 pipe->bufs 指针
	pipe->ring_size = nr_slots;
	...
}

4. 补充

4-1. file 对象分配

file 对象是从专属cache(filp cache)分配的。

void __init files_init(void)
{
	filp_cachep = kmem_cache_create("filp", sizeof(struct file), 0,
			SLAB_HWCACHE_ALIGN | SLAB_PANIC | SLAB_ACCOUNT, NULL);
	percpu_counter_init(&nr_files, 0, GFP_KERNEL);
}

$ sudo cat /proc/slabinfo
# name            <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> : tunables <limit> <batchcount> <sharedfactor> : slabdata <active_slabs> <num_slabs> <sharedavail>
filp              107391 111104    256   64    4 : tunables    0    0    0 : slabdata   1736   1736      0
    
john@ubuntu:~/Desktop/tmp/CVE-2021-4154/linux-5.13.3$ pahole -C file ./vmlinux
struct file {
	union {
		struct llist_node  fu_llist;             /*     0     8 */
		struct callback_head fu_rcuhead __attribute__((__aligned__(8))); /*     0    16 */
	} f_u __attribute__((__aligned__(8)));           /*     0    16 */
	struct path                f_path;               /*    16    16 */
	struct inode *             f_inode;              /*    32     8 */
	const struct file_operations  * f_op;            /*    40     8 */
	spinlock_t                 f_lock;               /*    48     4 */
	enum rw_hint               f_write_hint;         /*    52     4 */
	atomic_long_t              f_count;              /*    56     8 */
	/* --- cacheline 1 boundary (64 bytes) --- */
	unsigned int               f_flags;              /*    64     4 */
	fmode_t                    f_mode;               /*    68     4 */
	struct mutex               f_pos_lock;           /*    72    32 */
	loff_t                     f_pos;                /*   104     8 */
	struct fown_struct         f_owner;              /*   112    32 */
	/* --- cacheline 2 boundary (128 bytes) was 16 bytes ago --- */
	const struct cred  *       f_cred;               /*   144     8 */
	struct file_ra_state       f_ra;                 /*   152    32 */
	u64                        f_version;            /*   184     8 */
	/* --- cacheline 3 boundary (192 bytes) --- */
	void *                     f_security;           /*   192     8 */
	void *                     private_data;         /*   200     8 */
	struct hlist_head *        f_ep;                 /*   208     8 */
	struct address_space *     f_mapping;            /*   216     8 */
	errseq_t                   f_wb_err;             /*   224     4 */
	errseq_t                   f_sb_err;             /*   228     4 */

	/* size: 232, cachelines: 4, members: 21 */
	/* forced alignments: 1 */
	/* last cacheline: 40 bytes */
} __attribute__((__aligned__(8)));

4-2. 常用命令

常用命令

# ssh连接与测试
$ ssh -p 10021 hi@localhost             # password: lol
$ ./exploit

# scp 传文件
$ scp -P 10021 ./exploit hi@localhost:/home/hi      # 传文件
$ scp -P 10021 hi@localhost:/home/hi/trace.txt ./   # 下载文件
$ scp -P 10021 ./exploit.c ./get_root.c ./exploit ./get_root  hi@localhost:/home/hi

ftrace调试:注意,QEMU启动时需加上 no_hash_pointers 启动选项,否则打印出来的堆地址是hash之后的值。trace中只要用 %p 打印出来的数据都会被hash,所以可以修改 TP_printk() 处输出时的格式符,%p -> %lx

# host端, 需具备root权限
cd /sys/kernel/debug/tracing
echo 1 > events/kmem/kmalloc/enable
#echo 1 > events/kmem/kmalloc_node/enable
echo 1 > events/kmem/kfree/enable
echo 1 > events/kmem/kmem_cache_alloc/enable
echo 1 > events/kmem/kmem_cache_free/enable

# ssh 连进去执行 exploit

cat /sys/kernel/debug/tracing/trace > /home/hi/trace.txt

# 下载 trace
scp -P 10021 hi@localhost:/home/hi/trace.txt ./ 	# 下载文件

参考

Markakd/DirtyCred - github

DirtyCred: Escalating Privilege in Linux Kernel - ACM CCS 2022

【bsauce读论文】 DirtyCred-内核凭证替换利用技术

文档信息

Search

    Table of Contents