x86内存虚拟化技术

内存虚拟化会带来的问题


  • ring aliasing
  • ring compression
  • 需两次转换,而MMU仅能执行一次转换
  • GOS和HOS频繁切换导致的TLB失效
  • 安全方面,必须保证各个域之间的隔离,以及GOS和HOS空间的隔离(address compression)

MMU半虚拟化


介绍

由VMM在其中牵线搭桥,将中间地址层(客户物理地址)通过由VMM保管的一个转换表(机器地址–>客户物理地址)消除。也就是说,经过VMM的努力后,GOS所拥有的客户页表中的地址不再是客户物理地址,而是机器地址,因此MMU能够像在原来操作系统中一样直接完成由虚拟地址到机器地址的转换工作。

效率

效率高,但需要修改内核。

安全

通过分段,分页机制将GOS和VMM的空间隔离,保证了GOS和VMM的隔离之后,VMM便是可信任的,故可以通过VMM空间中的P2M来控制访问权限,进而将GOS之间隔离。异常可以通过hypercall机制来被VMM感知并处理。

影子页表(Shadow Page Table)


介绍

在全虚拟化或引入硬件虚拟化的虚拟机系统中,对于没有修改过系统内核的GOS,其虚拟地址到机器地址的转换必须使用影子页表(Shadow Page Table)来实现。为了能够消除客户物理地址层,使得MMU能够利用剩下的虚拟地址和机器地址完成地址转换。与半虚拟化技术将这个映射关系更新到GOS 的页表项不同,影子页表技术则是为GOS 的每个页表维护一个“影子页表”,并将合成后的映射关系(虚拟地址–>机器地址)写入到这个“影子页表”中,GOS 的页表内容则保持不变。最后,VMM 将影子页表交给MMU 进行地址转换。

安全

与上一种方法大体相同,只是访问控制改为由影子页表控制。大部分的异常监控仍然由软件完成。

效率

不需要修改内核,但是效率低下。
首先是时间开销,由于GOS 构造页表时不会主动通知VMM,VMM 必须等到GOS 发生缺页时通过分析缺页原因,再为其补全影子页表。此过程中VMM 需要通过模拟MMU 遍历GOS的页表,方能获得GOS 所维护的地址映射关系(虚拟地址–>客户物理地址),这种间接的手段要比半虚拟化低效很多。另外,由于每次缺页都会造成上下文切换,会导致TLB频繁失效,造成更大的性能损失。
其次是空间开销,VMM 需要支持多个虚拟机同时运行,而每个虚拟机的GOS 通常会为其上运行的每个进程都创建一套页表系统,因此影子页表的空间开销会随着进程的数量的增多而迅速增大,而GOS 的进程数量是VMM不可控的。减小空间开销的一种方法是只为当前进程的页表维护影子页表,这样做虽然将空间开销限制在了常数级别,但是却大大增加了上下文切换的时间开销:VMM 需要在GOS 的每个进程切换时重构新进程的所有影子页表。在空间开销和时间开销中做出权衡的方法是使用影子页表缓存(Shadow Page Table Cache),即VMM 在内存中维护部分最近使用过的影子页表,只有当影子页表在缓存中找不到时,才构建一个新的,但仍然不能有一个质的改变。

硬件虚拟化(VT-x)


介绍

VMX指令集通过引入了一个可以由VMM通过VMCS来监控其各种行为的非根操作环境(non-root)来解决CPU虚拟化中ring aliasing和ring compression的问题,提供了陷入机制,可以监控各种敏感指令。由于这部分由硬件实现,故大大提高了效率。
对内存虚拟化的支持:引入了EPT(extended page table)。
EPT 技术在原有客户机页表对客户机虚拟地址到客户机物理地址映射的基础上,又引入了 EPT 页表来实现客户机物理地址到宿主机物理地址的另一次映射,这两次地址映射都是由硬件自动完成。客户机运行时,客户机页表被载入 CR3,而 EPT 页表被载入专门的 EPT 页表指针寄存器 EPTP。EPT 页表对地址的映射机理与客户机页表对地址的映射机理相同,下图 1 出示了一个页面大小为 4K 的映射过程:

图 1.EPT 页表转换

在客户机物理地址到宿主机物理地址转换的过程中,由于缺页、写权限不足等原因也会导致客户机退出,产生 EPT 异常。对于 EPT 缺页异常,KVM 首先根据引起异常的客户机物理地址,映射到对应的宿主机虚拟地址,然后为此虚拟地址分配新的物理页,最后 KVM 再更新 EPT 页表,建立起引起异常的客户机物理地址到宿主机物理地址之间的映射。对 EPT 写权限引起的异常,KVM 则通过更新相应的 EPT 页表来解决。
此外VT-x技术还引入了VCPUID的概念,实现了带tag的TLB,降低了由TLB失效所带来的性能损失。

安全

异常的监控与捕获很大程度上交给了速度较快的硬件。由硬件来监控GOS的异常行为,由于硬件与软件之间存在天然的隔离,故硬件的监控是可信任的,在监控能到异常之后,会将控制权交由VMM来采取相应的措施。总的来说,是在软硬件天然隔离的基础上保证了软件与硬件的协作监控的可信。而这个协作监控,又保证了其上GOS之间,以及VMM与GOS之间的有效隔离。

效率

解决了三层地址的转换问题,避免了频繁的GOS和HOS上下文切换,且访问控制也由硬件监控。在TLB中引入了VCPUID,即带tag的TLB,可以减少因TLB失效而带来的性能损失。但是相应地,一旦发生TLB miss,其开销会非常大,因为硬件必须走过两个完整的页表结构。

使用virtio-serial实现guest OS与host的高效通信

Virtio简介


virtio是kvm/Linux中的io半虚拟化解决方案,其前端驱动运行在Guest OS的内核中,且已经被整合入Linux Kernel. 其后端驱动运行在qemu中负责接处理来自前端驱动的IO请求。由于virtio使用的是半虚拟化机制,使用virtio进行io可以达到几乎接近native的性能。目前virtio已经成为kvm
具体介绍可以查看:
http://www.ibm.com/developerworks/cn/linux/l-virtio/
http://www.ibm.com/developerworks/cn/linux/1402_caobb_virtio/
http://www.linux-kvm.org/page/Virtio/

Virtio-serial简介


virtio有一种有趣的应用:用于实现Guest与Host的高效通信。可以通过在Guest中虚拟出一个虚拟串口,并将该串口通过qemu上的后端驱动,经由host上的IPC机制,暴露给host上的其它应用。这样可以方便的实现Guest与Host之间的高效通信。该方式相对基于网络通信方式来说,具有效率高(没有封包解包过程),占用资源少,安全性高的优点。
可以参照:
http://blog.csdn.net/hbsong75/article/details/9451929

Libvirt简介


libvirt是一套免费、开源的支持Linux下主流虚拟化工具的C函数库,其旨在为包括Xen在内的各种虚拟化工具提供一套方便、可靠的API。当前主流Linux平台上默认的虚拟化管理工具virt-manager,virt-install等均基于libvirt开发而成。
libvirt 库是一种实现 Linux 虚拟化功能的 Linux API,它支持各种虚拟机监控程序,包括 Xen 和 KVM,以及 QEMU 和用于其他操作系统的一些虚拟产品。他对各种不同的hypervisor提供了较为统一的管理方式以及API。
libvirt使用xml来对各个域进行描述,具体到当前的目标,我们可以通过在xml中添加virtio-serial的方式来在guest中创建虚拟串口。此外,libvirt还支持由qemu的命令行参数生成相应的xml,因此由现有qemu虚拟机创建一个libvirt域是非常方便的。一有了描述域的xml之后,便可以通过libvirt的命令行管理工具virsh来创建,启动,停止该域。
详情可见libvirt官网:
http://libvirt.org/

AF_UNIX socket简介


通过libvirt的channel机制可以将guest里面的串口映射到host中的一个AF_UNIX socket文件中。
AF_UNIX类型的socket实质上是一种本机的IPC机制,其工作方式为直接将一个进程的用户空间数据copy到另一个进程的空间中,是一种效率很高的IPC。
可以参考:
https://en.wikipedia.org/wiki/Unix_domain_socket
http://man7.org/linux/man-pages/man7/unix.7.html

具体实现


环境安装

源码编译qemu(典型的./configure;make;make install过程,附带解决一些确实的依赖,都是routine),编译时按需打开相应支持,并在qemu上安装好一个ubuntu 14.04 LTS。
安装libvirt:
从git获取源码 git clone git://libvirt.org/libvirt.git
执行./autogen。若出现错误,则根据错误提示安装缺失的相应依赖(印象中有perl的XML::XML_Parser,libdevmap-dev, libxml2-dev等等,依赖较多,需要些耐心,不过都是些routine)
然后便是常规的make -jnproc; sudo make install

创建libvirt域

工作目录为/home/ubuntu/vms/,ubuntu_img是硬盘镜像文件

把qemu的参数写到一个文件里面
echo “/usr/local/bin/qemu-system-x86_64 -m 512 –drive file=/home/ubuntu/vms/ubuntu_img,format=raw,index=0,media=disk –boot order=d –enable-kvm” > qemu.args

从该文件创建xml
virsh domxml-from-native qemu-argv qemu.args > vm1.xml

修改域的名字
vm1

修改图形显示为vnc方式(不知道为什么libvirt认不出type=’gtk’, 所以只好用vnc了)

并添加两个channel,将virtio-serial映射为host上的两个AF_UNIX socket文件, vm.ctl和vm.data

<channel type='unix'>
<source mode='bind' path='/home/ubuntu/vms/vm.ctl'/>
<target type='virtio' address='virtio-serial' port='0'/>
</channel>

<channel type='unix'>
<source mode='bind' path='/home/ubuntu/vms/vm.data' />
<target type='virtio' address='virtio-serial' port='1' />
</channel>

<controller type='virtio-serial' index='0' ports='16' />

创建并启动vm1
virsh create vm1.xml

实现通信

用remmina(或者其它vnc客户端)连上去之后,可以发现在guest的/dev 下有如下两个文件
vport0p1 vport0p2
可以直接通过文件io来读写,我们尝试一下往vport0p1里面写字符串,看主机能否收到
这是客户端的代码,运行时要记得加sudo,因为该程序直接读写了设备文件

#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main()
{
     int fd0;
     char *greetings = "Hi there!\n";

     fd0 = open("/dev/vport0p1", O_RDWR);
     for(;;) {
         write(fd0, greetings, strlen(greetings) + 1);
         sleep(1);                                                                     //一秒写一次
     }

     return 0;
}

这是host端的代码,用来通过UNIX domain socket接收从guest中发来的消息,这里我们尝试一下读vport0p1对应的vm.ctl

#include <unistd.h>
#include <sys/types.h>
#include <sys/un.h>
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main()
{
    int sock;
    struct sockaddr_un addr;      // AF_UNIX的地址结构是sockaddr_un
    char buffer[512];

    sock = socket(AF_UNIX, SOCK_STREAM, 0);  //创建一个 UNIX domain socket
    addr.sun_family = AF_UNIX;
    strcpy(addr.sun_path, "vm.ctl");

    if(-1 == connect(sock, (struct sockaddr *)&addr, sizeof(addr)))
        perror("aha:");

    while(read(sock, buffer, 512)) {
        printf("data: %s\n", buffer);
        getchar();
    }

    return 0;
}

运行之后,敲击一次回车可以看到一条信息,说明信道畅通。

Buffer Over Flow

This is not a passage, but just some pieces of reminders.

Definition


A buffer overflow happens when more data is written to or read from a buffer than the buffer can hold.

real examples


In fact the first self-propagating Internet worm—1988’s Morris Worm—used a buffer overflow in the Unix finger daemon to spread from machine to machine.
And just this May, a buffer overflow found in a Linux driver left (potentially) millions of home and small office routers vulnerable to attack.
heart-bleed

stack it up


function have address, which are almost fixed
the memory layout of a program, virtual address, libs, kernel space, and so on
esp, eip, ebp, return address and so on
code inject, shell code

an attacker’s toolkit


instructions can contain zero-bytes : convert into equivalent sequences that avoid the problem byte
return address can contain zero-bytes(because they always lay in the lower part of the address space) : use “call esp” from elsewhere, may from the library or other functions(trampolining)
“NOP sled”

blame c


gets(), strcpy(), strcat() and even strncpy(), strncat() are unsafe

Fixing the leaks


safe runtime environments
lots of awful c code still in use:legacy, performance, low level operations, many libs depends on C(even C# and friends)
and so are buffer overflows
some tools to analyze source code and running programs :
AddressSantizer Valgrind, but they require active involvement of the developer
some systems to make it harder to exploit overflows :
W^X (“write exclusive-or execute”), DEP (“data execution prevention”), NX (“No Xecute”), XD (“eXecute Disable”), EVP (“Enhanced Virus Protection,” a rather peculiar term sometimes used by AMD), XN (“eXecute Never”), efficient and cost little, can be complemented by hardware, can be applied to existing programs retroactively just by updating the operating system to one that supports it(although hard for things like JVM and .NET), has been mainstream since 2004

Beyond NX


system()(the so-called return-to-libc technique) : useful , but sometimes system() don’t take arguments from stacks, and calling multiple function is hard
a number of ways to extend return-to-libc : nonetheless limited
return-oriented-programming (ROP) : using gadgets , each gadget follows a particular pattern: it performs some operation (putting a value in a register, writing to memory, adding two registers, etc.) followed by a return instruction, some times you can find a Turing-complete set of these, these instructions can even be used to change the current state of the pages, turning them into excutable

Getting random


Address Space Layout Randomization (ASLR) : it randomizes the position of the stack and the in-memory location of libraries and executables
it’s useful but hard to apply to current systems : it’s ok for dlls, but hard for linux and exes, (compatibility, performance)
but the range the address can be is limited on x86 : total memory is limited, libs must stay as close as they can, code always start at the beginning of a page, etc
thus, if the chance is 1/256, you can try it out for 256 times and you’ll succeed
the situation is better on x64, guessing is almost impossible
browsers : javascript, flash(both contain JIT), PDF plugin, Microsoft’s Office browser plugins(old version didn’t enable ASLR)

A never-ending war


Powerful protective systems such as ASLR and NX raise the bar for taking advantage of flaws and together have put the days of the simple stack buffer overflow behind us, but smart attackers can still combine multiple flaws to defeat these protections.
Microsoft’s EMET (“Enhanced Mitigation Experience Toolkit”) includes a range of semi-experimental protections that try to detect heap spraying or attempts to call certain critical functions in ROP-based exploits. But in the continuing digital arms war, even these have security techniques that have been defeated.
the difficulty (and hence cost) of exploiting flaws goes up with each new mitigation technique—but it’s a reminder of the need for constant vigilance.