本文章,共 9 章,由于篇幅较长,公众号将分三部分发出,比心❤
如今虚拟机广泛应用于个人或企业中。网络安全供应商通过使用不同的虚拟机,实现在一个可控且封闭的环境中分析恶意软件。一个问题自然而然的出现了:这些恶意软件会从虚拟机中逃逸出来并在宿主机上执行吗?
去年,安全公司CrowdStrike的Jason Geffner,已经发布了一系列QEMU漏洞,通过影响虚拟机软盘驱动代码实现让一个攻击者从虚拟机逃逸[1]到宿主机中。虽然这个漏洞已经在网络社区中引起了广泛关注——也许因为它有一个专有名称(VENOM)——但这已经不是第一次出现这种漏洞了。
2011年,Nelson Elhage[2]发布并成功利用了一个在QEMU虚拟机热插拔PCI设备的漏洞。 利用过程可参见[3]。
最近,奇虎360的刘旭和汪圣平在HITB 2016黑客大会上展示了KVM/QEMU的成功利用。他们在两种不同的网卡设备仿真模型RTL8139 和PCNET上,演示了两个漏洞(CVE-2015-5165 和CVE-2015-7504)的利用。在展示中,他们列举了在宿主机上运行代码的主要步骤,但没有提供任何漏洞利用或技术细节来再现逃逸过程。
在本文中,我们对CVE-2015-5165(一个内存泄露漏洞)和CVE-2015-7504(一个堆溢出漏洞)进行了深度分析和利用。同时利用这两个漏洞可以实现虚拟机逃逸并在目标宿主机上运行代码。我们讨论了在QEMU的网卡设备仿真上利用这两个漏洞的技术细节,并提供了继续利用QEMU未来出现的漏洞的通用技术。比如一个用来共享内存区域和共享代码的交互式bindshell。
KVM(Kernal-based Virtual Machine)是一个为用户空间程序提供整个虚拟化基础架构的内核模型。它允许运行多个Linux或Windows映像的虚拟机。
KVM的用户空间部分包含于处理主要设备仿真的主线QEMU(高速虚拟机)。
为了方便读者使用本文中示例代码,我们在这一节给出了搭建开发环境的主要步骤。
因为我们使用的漏洞已经被修补了,所以需要找到QEMU仓库的源并恢复到漏洞被修补之前。然后,我们配置QEM只支持x86_64并激活漏洞:
在我们的测试环境中,使用4.9.2版本的Gcc搭建QEMU。
剩下的,我们假设读者已经有一个Linux x86_64映像并可以执行以下命令:
我们分配2GB内存空间并创建两个网卡: RTL8139和PCNET。
我们在3.16单核x_86_64架构的Debian 7上运行QEMU。
为用户分配的物理内存实际上是QEMU虚拟地址空间中的一个mmap映射的私有区域。需要指出的是,为用户分配物理内存时PROT_EXEC参数是不可用的。
下图说明了用户内存和主机内存是如何共存的。
此外,QEMU为BIOS和ROM分配了一个内存区域。这些映射保存在QEMU的映射文件中:
QEMU中有两个地址转换层:
1. 从用户虚拟地址转换到用户物理地址。实验中,我们需要配置DMA(Direct Memory Access,直接内存存取)方式的网卡。例如,我们需要为传送缓冲区和接收缓冲区提供物理地址来正确配置网卡。
2. 从用户物理地址转换到QEMU虚拟地址空间。实验中,我们需要添加虚拟结构并得到它们在QEMU虚拟地址空间中的准确地址。
在x64系统中,一个虚拟地址由一个页地址(0-11位)和一个页号构成。在linux系统中,pagemap文件允许有CAP_SYS_ADMIN特权的用户空间进程发现每个虚拟页映射到哪个物理框架。为了便于在内核中记录,pagemap文件包含了每个虚拟页的一个64位的值。参考文章[5]:
为了将虚拟地址转换成物理地址,我们使用Nelson Elhage的代码[3]。下面的程序分配了一段缓冲区,用字符串“Where am I?”填充并打印其物理地址:
---[ mmu.c ]---
#include
#include
#include
#include
#include
#include
#include
#define PAGE_SHIFT 12
#define PAGE_SIZE (1 <
#define PFN_PRESENT (1ull <
#define PFN_PFN ((1ull <
int fd;
uint32_t page_offset(uint32_t addr)
{
return addr & ((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 <
}
int main()
{
uint8_t *ptr;
uint64_t ptr_mem;
fd = open("/proc/self/pagemap", O_RDONLY);
if (fd
perror("open");
exit(1);
}
ptr = malloc(256);
strcpy(ptr, "Where am I?");
printf("%s\n", ptr);
ptr_mem = gva_to_gpa(ptr);
printf("Your physical address is at 0x%"PRIx64"\n", ptr_mem);
getchar();
return 0;
}
如果在用户模式下运行以上代码并连接gdb到QEMU进程,可以看到我们的缓冲区位于分配给用户的物理地址空间上。更确切的说,输出的地址实际上就是基于用户物理内存基地址的偏移地址。
本文由 看雪翻译小组 Green奇 编译,来源 Mehdi Talbi和Paul Fariello@phrack
你可能对以下内容感兴趣:
更多优秀文章,长按下方二维码,“关注看雪学院公众号”查看!
看雪论坛:http://bbs.pediy.com/
微信公众号 ID:ikanxue
微博:看雪安全
投稿、合作:www.kanxue.com