kernel_pwn userfaultfd
userfaultfd的利用姿势是在realworld ctf的议题直播里跟着BrieflyX大佬学习的,对该方法很是好奇,赛后就调了下kstack。
userfaultfd是种page fault的处理方式。正常发生页缺失会引发异常,交给内核里的异常处理程序去解决,但userfaulted是让用户态的程序去处理自己的page fault。
userfaultfd具体的工作流程以及实现机制在BrieflyX的博客里讲的很清楚了,我主要记录下做kstack的过程来加深对该机制的理解。
kstack题目给出了源码,实现了push和pop操作,涉及的结构为:
1 | typedef struct _Element { |
owner标识所属进程的PID,value即存储的数据,fd指向下一个_Element结构体(以链表的方式组织)。
主要逻辑在proc_ioctl()
函数里:
1 | static long proc_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) |
逻辑也比较简单了,push先kmalloc一个sizeof(Element)
大小的内存,之后设置owner并通过头插法连入链表,之后调用copy_from_user
从用户区域读入数据到value,然后把该结点删除并kfree。
pop的话从头部查找,找到属于该进程的结点,之后将该结点value拷贝到用户区域,然后把zhe该结点删除并kfree。
看起来逻辑没什么问题,但存在的一个问题是整个过程没有加锁,并且对结点的删除操作是在copy_from_user()
和copy_to_user()
之后的。
那么如果带着利用userfaultfd构造race的想法再看这个逻辑的话如果在push时执行到copy_from_user()
,由于用户区域arg是需要userfaultfd的,则该线程就会阻塞在这里,此时交给用户态的fault_handler_thread
线程处理。该线程是用户可控的,如果再控制该线程执行pop操作,此时就会将之前push的结点里的value拷贝到用户区域。
因此就要想办法使得push时kmalloc拿到的chunk对应结点value的位置存放有kernel里的地址,这里用到的一个方法时利用shm_file_date
:
1 | struct shm_file_data{ |
该结构体总的大小和Element大小差不多,通过slab分配0x24大小的chunk存放。
通过调用shmget
建立一个共享内存对象,并shmat
将对象映射到调用进程的地址空间,之后通过shmdt
删除,则shm_file_data
也会被free,之后再通过push的kmalloc时就会拿到刚刚free的chunk,同时对应vaule的位置还残留着之前struct ipc_namespace *ns
的内容,该指针一般指向存在了的general namespace,该地址时kernel的地址,从而配合上面的pop到达leak地址的效果。
这里也提一下如何创建并注册一个需要通过userfaultfd的对象:
创建一个userfaultfd对象:
1 | uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK); |
之后mmap一块内存,mmap出来的内存只有在真正使用到时才会映射到物理内存,因此对mmap出来内存的第一个r/w操作会触发page fault。
1 | page_size = sysconf(_SC_PAGE_SIZE); |
然后将区域注册为userfaultfd处理机制:
1 | uffdio_register.range.start = (unsigned long)addr; |
leak完地址后继续利用userfaultfd的机制,pop一个需要userfaultfd的内存,在执行到copy_to_user()
时阻塞,此时在fault_handler_thread
线程中再执行一次pop,由于删除操作还未执行,因此处理的是同一个结点,在handler线程里将该结点删除后唤醒阻塞在copy_to_user()
的线程继续执行删除操作,就会导致一个结点被free两次造成double free
。
有了地址和double free
接下来就是想办法提权或拿flag了。要利用double free
的话要想办法修改chunk的前8个字节为目的地址,仅仅通过push的kmalloc是无法实现的这里用到了userfaultfd + setxattr
的组合拳。
1 | static long |
setxattr
源码里注意到[1]处kmalloc一个可控size的chunk,之后将value内容的size长度拷贝到kmalloc申请的chunk里,如果value处于两个页的交汇处,即value的内容在一个正常的页面的末尾,而在size-value_size
的部分在下一个页面,而该页面会触发page fault且
被注册为userfaultfd,则会导致在执行[2]copy_from_user()
时,处于正常页面的value_size
的内容会被正常拷贝,而之后到下一个页面时会触发userfaulted交给handler_thread
处理。
这里BrieflyX拿flag的方法是将modprobe_path - 8
的地址放在一个页的最后8个字节位置上,下一个页注册为userfaulted。
之后通过setxattr()修改double free的chunk头八个字节为modprobe_path - 8
的地址,之后在handler_thread线程里利用两次push取出modprobe_path - 8
的内存,并修改modprobe_path
为/tmp/x
。
x文件内容提前被设置system("echo -ne '#!/bin/sh\n/bin/chmod 777 /flag' > /tmp/x ");system("chmod +x /tmp/x");
,modprobe_path其实就是一个文件路径的字符串,该文件在系统试图执行一个无法执行(除elf和shell脚本#!)的程序后执行。
因此提前设置一个无法识别执行的脚本system("echo -ne '\\xff\\xff\\xff\\xff' > /tmp/dummy");system("chmod +x /tmp/dummy");
。
此时执行dummy文件,系统会报错,接着便会执行modprobe_path
的x文件,从而修改flag权限为777,之后system("cat /flag")
拿到flag。
userfaultfd就像是在copy_to/from_user()
处下了断点,形成一个稳定的race窗口,具有较强的通用型,这里的重点在于handler_thread
的构造,如何区分不同的情况并进行相应配合的操作。我也是看的BrieflyX大佬的exp学习的。
参考: