qemu-pwn-seccon-2018-q-escape

2019-12-03 约 953 字 预计阅读 5 分钟

声明:本文 【qemu-pwn-seccon-2018-q-escape】 由作者 raycp 于 2019-12-03 09:32:57 首发 先知社区 曾经 浏览数 353 次

感谢 raycp 的辛苦付出!

描述

官方的描述如下:

q-escape

We developed a new device named CYDF :)
Ubuntu 16.04 latest
nc q-escape.pwn.seccon.jp 1337

将文件下下来,目录如下:

$ ll
-rw-rw-r--  1 raycp raycp 1.7M Aug 21 08:03 initramfs.igz
drwxr-xr-x  6 raycp raycp 4.0K Oct 22  2018 pc-bios
-rwxr-xr-x  1 raycp raycp  28M Oct 22  2018 qemu-system-x86_64
-rwxr-xr-x  1 raycp raycp  256 Oct 22  2018 run.sh
-rw-------  1 raycp raycp 7.9M Oct 22  2018 vmlinuz-4.15.0-36-generic

run.sh中的内容是:

#!/bin/sh
./qemu-system-x86_64 \
        -m 64 \
        -initrd ./initramfs.igz \
        -kernel ./vmlinuz-4.15.0-36-generic \
        -append "priority=low console=ttyS0" \
        -nographic \
        -L ./pc-bios \
        -vga std \
        -device cydf-vga \
        -monitor telnet:127.0.0.1:2222,server,nowait

可以知道设备名称是cydf-vga以及在本地的2222端口开启了qemu monitor。

分析

首先仍然是sudo ./run.sh把虚拟机跑起来,我的环境是ubuntu18,报了下面的错误:

./qemu-system-x86_64: error while loading shared libraries: libcapstone.so.3: cannot open shared object file: No such file or directory

解决方案:

sudo apt-get install libcapstone3

虚拟机跑起来的同时把qemu-system-x86_64拖进ida进行分析,查找cydf-vga相关函数:

查看cydf_vga_class_init函数,知道了它的device_id0xB8vendor_id0x1013class_id0x300。同时根据字符串Cydf CLGD 54xx VGA去搜索,进行相应比对,找到了该设备是Cirrus CLGD 54xx VGA Emulator改过来的。Cirrus在qemu中源码路径为./hw/display/cirrus_vga.c

先在虚拟机中查看设备信息,根据设备id等信息,可以知道它是最后一个00:04.0 Class 0300: 1013:00b8

/ # 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 0300: 1013:00b8

由于它里面的lspci不支持-v等参数,所以要看它的内存以及端口空间,可以去读取它的resource文件,可以看到它有三个mmio空间:

/ # cat /sys/devices/pci0000\:00/0000\:00\:04.0/resource
0x00000000fa000000 0x00000000fbffffff 0x0000000000042208
0x00000000febc1000 0x00000000febc1fff 0x0000000000040200
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x00000000febb0000 0x00000000febbffff 0x0000000000046200

另一个方法是telnet 127.0.0.1 2222连上它的monitor,可以看到相应的地址空间:

info pci
...
Bus  0, device   4, function 0:
    VGA controller: PCI device 1013:00b8
      BAR0: 32 bit prefetchable memory at 0xfa000000 [0xfbffffff].
      BAR1: 32 bit memory at 0xfebc1000 [0xfebc1fff].
      BAR6: 32 bit memory at 0xffffffffffffffff [0x0000fffe].
      id ""

一个奇怪的问题是在cydf_init_common函数中看到了三个注册I/O函数:

memory_region_init_io(&s->cydf_vga_io, owner, &cydf_vga_io_ops, s, "cydf-io", 0x30uLL);
...
memory_region_init_io(&s->low_mem, owner, &cydf_vga_mem_ops, s, "cydf-low-memory", 0x20000uLL);
...
memory_region_init_io(&s->cydf_mmio_io, owner, &cydf_mmio_io_ops, s, "cydf-mmio", 0x1000uLL);

可以看到函数中注册了0x30大小的PMIO,0x20000大小的MMIO以及0x1000大小的MMIO。但是为啥在设备中只看到了BAR10x1000大小的MMIO空间,其余两个去哪里了?

cirrus_vga.c中有下面两行注释:

/* Register ioport 0x3b0 - 0x3df */
...
/* memory access between 0xa0000-0xbffff */

cat /proc/iomemcat /proc/ioports查看相应的MMIO和PMIO:

/ # cat /proc/iomem
...
000a0000-000bffff : PCI Bus 0000:00
...
04000000-febfffff : PCI Bus 0000:00
...
  febc1000-febc1fff : 0000:00:04.0

/ # cat /proc/ioports
...
  03c0-03df : vga+
...

因此另外两个0x30大小的PMIO空间以及0x20000大小的MMIO空间看起来似乎是vga的地址空间,根据师傅们的writeup以及Mapping of Display Memory into CPU Address Space Addressing details可以知道,地址000a0000-000bffff确实是vga的空间。

有了源码的参考看起来会方便很多,接下来对比二者,以找到题目中什么地方被修改了。经过比对,最主要的变化是在cydf_vga_mem_write函数,同时在CydfVGAState结构体中加入了两个字段:

000133D8 vs              VulnState_0 16 dup(?)
000134D8 latch           dd 4 dup(?)

VulnState的定义为:

00000000 VulnState_0     struc ; (sizeof=0x10, align=0x8, copyof_4201)
00000000                                         ; XREF: CydfVGAState/r
00000000                                         ; CydfVGAState_0/r
00000000 buf             dq ?                    ; offset
00000008 max_size        dd ?
0000000C cur_size        dd ?
00000010 VulnState_0     ends

接下来看cydf_vga_mem_write函数存在区别的部分主要的内容是什么(漏洞是什么):

void __fastcall cydf_vga_mem_write(CydfVGAState *opaque, hwaddr addr, uint64_t mem_value, uint32_t size)
{
  ...

  if ( !(opaque->vga.sr[7] & 1) )
  {
    vga_mem_writeb(&opaque->vga, addr, mem_value);
    return;
  }
  if ( addr <= 0xFFFF )
  {
    ...
  }
  if ( addr - 0x18000 <= 0xFF )
  {
    ...
  }
  else
  {
    v6 = 205 * opaque->vga.sr[0xCC];
    LOWORD(v6) = opaque->vga.sr[0xCC] / 5u;
    cmd = opaque->vga.sr[0xCC] - 5 * v6;
    if ( *(_WORD *)&opaque->vga.sr[0xCD] )      // cmd = sr[0xcc]%5
      LODWORD(mem_value) = (opaque->vga.sr[0xCD] << 16) | (opaque->vga.sr[0xCE] << 8) | mem_value;                                                                          // idx=sr[0xcd]
    if ( (_BYTE)cmd == 2 )                      // cmd 2 printf buff
    {
      idx = BYTE2(mem_value);
      if ( idx <= 0x10 )
      {
        v25 = (char *)*((_QWORD *)&opaque->vga.vram_ptr + 2 * (idx + 0x133D));
        if ( v25 )
          __printf_chk(1LL, v25);
      }
    }
    else
    {
      if ( (unsigned __int8)cmd <= 2u )
      {
        if ( (_BYTE)cmd == 1 )                  // cmd 1 vs buff[cur_size++]=value, cur_size < max_size
        {
          if ( BYTE2(mem_value) > 0x10uLL )
            return;
          v8 = (__int64)opaque + 16 * BYTE2(mem_value);
          vs_buff = *(_QWORD *)(v8 + 0x133D8);  // 0x133d8 vuln_state buff
          if ( !vs_buff )
            return;
          cur_size = *(unsigned int *)(v8 + 0x133E4);// 0x133e4 cur_size
          if ( (unsigned int)cur_size >= *(_DWORD *)(v8 + 0x133E0) )// 0x133e0 max_size
            return;
LABEL_26:
          *(_DWORD *)(v8 + 0x133E4) = cur_size + 1;
          *(_BYTE *)(vs_buff + cur_size) = mem_value;
          return;
        }
        goto LABEL_35;
      }
      if ( (_BYTE)cmd != 3 )
      {
        if ( (_BYTE)cmd == 4 )                  // cmd 4 vs buff[cur_size++]=value, no cur_size check
        {
          if ( BYTE2(mem_value) > 0x10uLL )
            return;
          v8 = (__int64)opaque + 16 * BYTE2(mem_value);
          vs_buff = *(_QWORD *)(v8 + 0x133D8);
          if ( !vs_buff )
            return;
          cur_size = *(unsigned int *)(v8 + 0x133E4);
          if ( (unsigned int)cur_size > 0xFFF )
            return;
          goto LABEL_26;
        }
LABEL_35:
        v20 = vulncnt;
        if ( vulncnt <= 0x10 && (unsigned __int16)mem_value <= 0x1000uLL )// cmd 0 vs buff[vulcnt]=malloc(value)
        {
          mem_valuea = mem_value;
          ptr = malloc((unsigned __int16)mem_value);
          v22 = (__int64)opaque + 16 * v20;
          *(_QWORD *)(v22 + 0x133D8) = ptr;
          if ( ptr )
          {
            vulncnt = v20 + 1;
            *(_DWORD *)(v22 + 0x133E0) = mem_valuea;
          }
        }
        return;
      }
      if ( BYTE2(mem_value) <= 0x10uLL )        // cmd 1 set max_size
      {
        v23 = (__int64)opaque + 16 * BYTE2(mem_value);
        if ( *(_QWORD *)(v23 + 0x133D8) )
        {
          if ( (unsigned __int16)mem_value <= 0x1000u )
            *(_QWORD *)(v23 + 0x133E0) = (unsigned __int16)mem_value;
        }
      }
    }
  }
}

最主要的区别是增加了0x10000-0x18000地址空间的处理代码,通过代码可以看到增加的功能为vs的处理代码,opaque->vga.sr[0xCC]cmdopaque->vga.sr[0xCD]为idx,功能描述如下:

  1. cmd为0时,申请value&0xffff空间大小的堆,并放置vs[vulncnt]中,同时初始化max_size
  2. cmd为1时,设置idx所对应的vs[idx]max_sizevalue&0xffff
  3. cmd为2时,printf_chk(1,vs[idx].buff)
  4. cmd为3时,当cur_size<max_size时,vs[idx].buff[cur_sizee++]=value&0xff
  5. cmd为4时,vs[idx].buff[cur_sizee++]=value&0xff

漏洞主要有两个地方:

  • 一个是堆溢出。cmd为4时,可以设置max_size,对max_size没有进行检查也没有对堆块进行realloc,后续按这个size进行写,导致溢出。
  • 另一个是数组越界。idx最多可以为0x10,即最多可以寻址vs[0x10],而vs大小只有16,即vs[0xf]。vs[0x10]则士后面的latch[0],导致会越界访问到后面的latch数组的第一个元素。

还有要解决的问题就是如何触发漏洞代码。除了addr之外,还需要使得(opaque->vga.sr[7]&1 ==1)以绕过前面的if判断、设置opaque->vga.sr[0xCC]来设置cmd以及设置opaque->vga.sr[0xCD]设置idx。

在代码中可以找到cydf_vga_ioport_write函数中可以设置opaque->vga.sraddr0x3C4vuluevga.srindex;当addr0x3C5时,valuevga.sr[index]的值。从而可以通过cydf_vga_ioport_write设置vga.sr[7]vga.sr[0xCC]以及vga.sr[0xCD]

还需要说明的是可以通过cydf_vga_mem_read函数来设置opaque->latch[0]latch[0]刚好是vs越界访问到的元素。

uint64_t __fastcall cydf_vga_mem_read(CydfVGAState *opaque, hwaddr addr, uint32_t size)
{
  ...
  latch = opaque->latch[0];
  if ( !(_WORD)latch )
  {
    v4 = (opaque->vga.sr[7] & 1) == 0;
    opaque->latch[0] = addr | latch;            // set latch low dword
    if ( !v4 )
      goto LABEL_3;
    return vga_mem_readb(&opaque->vga, addr);
  }
  v4 = (opaque->vga.sr[7] & 1) == 0;
  opaque->latch[0] = (_DWORD)addr << 16;        // set latch high word
  if ( v4 )
    return vga_mem_readb(&opaque->vga, addr);
    ...

利用

漏洞已经清楚了,利用则可以利用数组越界漏洞来实现任意地址写。具体原理为:可以通过cydf_vga_mem_read函数将opaque->latch[0]设置成想要写的任意地址;再将opaque->vga.sr[0xCD](idx)设置成0x10,再往vs[0x10]写数据时即实现了往任意地址(latch[0]中的地址)写数据。

在代码中存在qemu_log函数,关键代码如下:

int qemu_log(const char *fmt, ...)
{

  ...
  if ( qemu_logfile )
  {
   ...
    ret = vfprintf(qemu_logfile, fmt, va);
  ...
  }
...
}

且因为程序没有开PIE,结合上面的qemu_log函数,可以做到只利用任意地址写就能实现任意命令执行。具体利用的步骤则如下:

  1. 往bss段数据中写入要执行的命令cat /root/flag
  2. 将该bss地址写入到全局变量qemu_logfile中。
  3. vfprintf函数got表覆盖为system函数的plt表地址。
  4. printf_chk函数got表覆盖为qemu_log函数的地址。
  5. 利用cmd为2时,触发printf_chk,最终实现system函数的调用,同时参数也可控。

最后一个问题,该如何去交互。以往都是用户态打开对应的resource0文件进行映射,实现mmio的访问。但是这次000a0000-000bffff地址空间不知道该打开哪个文件去映射。访问该地址空间才可以实现对cydf_vga_mem_write以及cydf_vga_mem_read的访问。

这时我们可以利用/dev/mem文件,dev/mem是物理内存的全映像,可以用来访问物理内存,用mmap来访问物理内存以及外设的IO资源,是实现用户空间驱动的一种方法。具体可以man mem去查看详情。

调用cydf_vga_ioport_write去设置opaque->vga.sr[]以及opaque->vga.sr_index,有两种方式(exp中使用的是前者)可以实现对cydf_vga_ioport_write函数的调用:

一种是利用访问febc1000-febc1fff地址空间,触发cydf_mmio_write从而实现对 cydf_vga_ioport_write的调用。

void __fastcall cydf_mmio_write(CydfVGAState *opaque, hwaddr addr, uint64_t val, unsigned int size)
{
  if ( addr > 0xFF )
    cydf_mmio_blt_write(opaque, addr - 0x100, val);
  else
    cydf_vga_ioport_write(opaque, addr + 0x10, val, size);
}

一种是直接利用PMIO,out类指令以及in类指令直接对相应的0x3b0 - 0x3df端口进行访问,实现对该函数的调用。

小结

即使做完了这题,对于vga设备的原理还是不太了解,还是有很多的事值得去做、需要去做。

感觉这部分应该有不少是我理解错误了的或者没考虑到的,欢迎各位师傅对我进行指导。

相关文件与脚本链接

参考链接

  1. 使用 monitor command 监控 QEMU 运行状态
  2. Linux中通过/dev/mem操控物理地址
  3. Mapping of Display Memory into CPU Address Space
  4. SECCON2018_online_CTF/q-escape
  5. seccon 2018 - q-escape
  6. q-escape - SECCON 2018
  7. cirrus_vga.c

关键词:[‘安全技术’, ‘CTF’]


author

旭达网络

旭达网络技术博客,曾记录各种技术问题,一贴搞定.
本文采用知识共享署名 4.0 国际许可协议进行许可。

We notice you're using an adblocker. If you like our webite please keep us running by whitelisting this site in your ad blocker. We’re serving quality, related ads only. Thank you!

I've whitelisted your website.

Not now