从两道kernelpwn题了解Dirtypipe零泄露提权的两种方法
0x00.前言
前几天,我从SCTF的kno_pus_revenge了解到了Dirtypipe的打法,觉得很奇妙,于是对Dirtypipe的来源进行了一系列的分析(
)。
明白其中原理之后,便可以通过零泄露的方式 ,拿下以下两道kernelpwn题。这种手法最巧妙的地方是不需要泄露任何地址,使用模板就可以通用于大部分0x400堆uaf题。
0x01.CISCN_2022_西南_cactus
函数分析
ida打开test.ko,可以看到fops定义了read、write、open、ioctl四个file函数。
module_open函数会申请一个0x100大小的buffer。
其中module_read和module_write都可往buffer中读和写不超过0x100个字节的数据,并都未进行上锁操作。
release函数则会进行buffer的释放和清空
因为module_open有对flag的check操作,所以我们不能同时open两次设备来构造uaf,并且由于module_release在调用前会等待module_read、module_write操作完毕,所以没法利用userfaultfd机制构造uaf。
以上这部分函数暂且抛弃。
module_ioctl函数则基于指令0x20、0x30、0x50,进行操作。
0x20指令申请0x400大小的堆块
0x30指令释放该堆块
0x50指令修改该堆块
以上操作都会基于edit_args结构体进行传参。
可以注意到,以上流程都是未加锁的,这就导致了可利用userfaultfd机制
进行条件竞争构造uaf,userfaultfd机制这里就不过多阐述。
漏洞利用
先前分析CVE Dirtypipe的文章中我有提到,当使用userfaultfd机制构造uaf时,是几乎可以从堆块的任意位置开始覆盖的。
再重温一遍构造手法:
先mmap申请两个连续的0x1000的内存,并让addr2在uffd_buf_hack前面,此时只初始化addr2+0xff0(即uffd_buf_hack-0x10)的数据,uffd_buf_hack仍然是缺页的。
倘若我们想要在0x28的偏移处写入1个字节的数据,就可以在write时设置buf为uffd_buf_hack-0x28
之后内核在执行copy_from_user时,前面0x28个字节都是正常拷贝的,当到了0x29字节时,试图从uffd_buf_hack拷贝数据,从而触发缺页异常,然后我们free掉原有堆块,pipe申请新堆块,此时继续缺页拷贝,就会只覆盖0x28偏移的数据。
因此我们可以在触发缺页异常之后free掉该堆块,并创建一个pipe,然后往pipe中写入内容从而利用pipe_buffer的alloc申请回我们free掉的堆块。这里我们将16个pipe_buffer都写满并读出,这样就可以将head和tail都变为0。(相当于循环队列绕了一圈回到原点)
然后我们打开想要修改的特权只读文件,这里我们选择poweroff文件。再而调用splice将poweroff文件的page放入到pipe名下。
然后我们再回到缺页处理程序,将堆块的0x18(flags位)修改为0x10(PIPE_BUF_FLAG_CAN_MERGE)。
这个时候调用write对pipe进行写入,pipe_write进行处理的时候发现head_pipe_buffer有PIPE_BUF_FLAG_CAN_MERGE标志,认为该page可写入,就会将数据写到poweroff文件的page中,从而修改文件内容。
这里需要注意,splice至少需要pipe读取file一个字节,所以在放入page时,会将offset设置至少为1,之后再去写入时也至少得从偏移1开始写,当然也可以通过修改loff_in往1之后的任意偏移写入数据。
以此我们成功修改了poweroff文件,执行命令exit后,init文件默认以root权限执行poweroff文件从而实现提权。
Exp
1 |
|
0x02.SCTF_2024_kno_puts
函数分析
ida打开ko文件,查看fops可以看到有open、write、ioctl的file函数
module_open函数啥也没干
module_write函数可以往ptr中写入不大于0x2e0大小的数据,且未上锁。
module_ioctl函数开头会有一个key的check。
开始读入0x20字节的随机值s2,然后将用户输入copy到v9再到s1中与s2比较,将比较结果存储到v8,其中v8由v10初始化。
爆破肯定是行不通的,可以看到这里读入读取了0x30个字节到v9,而v9只有0x20的大小,所以会溢出淹到v10,从而将v8初始化为1绕过检查。
0xFFF0指令申请得到0x400的堆,并将堆地址返回给用户。
0xFFF1指令释放申请的堆,且机会只有一次。
这里泄露的堆地址,但是对于我们Dirtypipe打法,这样的泄露都是多余的。
漏洞利用
还是老规矩,userfaultfd构造uaf,过程这里就不再描述了。
这次的打法会和先前不大一样,原因在于linux内核版本,5.4的内核不是使用PIPE_BUF_FLAG_CAN_MERGE标志进行写入check,而是检查的fops。
基于此,我们不能通过修改flags来写入page,转而构造puaf(page uaf)劫持file page。
通过分析我们可以知道struct page的大小为0x40,这很完美,因为每4个page结构体刚好大小为0x100。
这就意味着当喷射了大量的page结构体,如果我们只需要覆盖pipe_buffer的page结构体指针的首字节为0、0x40、0x80、0xc0中的某一个,就可以有3/4的概率让该page指向其他page,从而实现puaf。
步骤如下:
先创建pipe并调用write申请pipe_buffer从而喷射大量page,同时write也会写入我们的特征字符串,从而方便后续check。然后释放我们的堆块,再喷射大量page,此时我们堆块被复用,(多写入一个特征字符”0rb1t123”是为了防止page被提前释放)。
再回到缺页异常处理程序,修改堆覆盖page首字节为0,从而构造page重合。
此时我们需要进行check,通过特征字符串查找是哪两个page重合。当”0rb1t123”正常并且索引k异常时,就代表i和k两个pipe的page重合了。
前面我们已经写入0x14个字节了,这里我们再写入0x30个字节,让pipes[pn]的pipe_buffer->offset为0x44,后续写入就是从0x44开始写。然后我们关闭pipes[k],把page释放,此时就会构成puaf。
接下来就是puaf的利用了,我们知道在打开文件时,会从调用__alloc_file从filp_cachep申请堆块作为file结构体。
而当filp_cachep中的堆块被使用完之后,内核会从伙伴系统申请新的page作为新的堆块,这就意味着我们可以利用堆喷来实现让file的page与pipe_buffer的page重合,然后利用pipe_write修改file结构体的内容。
file结构体大致如下,其中f_mode(偏移0x44)表示文件打开模式,而我们的目标就是它。
我们大量打开poweroff文件,然后通过write修改文件结构体的f_mode为可写模式。
最后修改文件即可。
但是这种puaf的打法极其不稳定,因为程序执行结束,pipe会自动关闭,这就意味着page会被double free,系统很容易触发崩溃。
不过多跑几次还是能出结果的
Exp
1 |
|
0x03.总结
Dirtypipe起源于CVE,但不止步于CVE,它的奇妙延申利用手法让堆利用变得更加简单方便。特别是它的零泄露特性,让打法更加通用,不需要再去寻找gadget,构造rop,而是直白的修改文件实现提权。
当然Dirtypipe也有一些限制,例如稳定的flags修改提权需要特定的linux版本,而puaf打法又并不稳定(除非能修复pipe_buffer的page变回原page),且只适用于0x400堆。
总的来说,这样针对文件内容修改的打法给予了我很大启发,给我提供了新的思路,实在是妙不可言。
0x04.小trick
可以使用以下方式生成elf_shellcode,感受文件体积极致压缩的魅力。
64位
1 | ; 执行nasm -f bin -o tiny64 tiny64.asm |
32位
1 | ; 执行nasm -f bin -o tiny32 tiny32.asm |
- Title: 从两道kernelpwn题了解Dirtypipe零泄露提权的两种方法
- Author: 0rb1t
- Created at : 2024-10-12 10:27:53
- Updated at : 2024-11-19 22:16:02
- Link: https://redefine.ohevan.com/2024/10/12/从两道kernelpwn题了解Dirtypipe零泄露提权的两种方法/
- License: This work is licensed under CC BY-NC-SA 4.0.