libvmi虚拟机自省源码分析(一)

来源:互联网 发布:淘宝官方正品女装 编辑:程序博客网 时间:2024/05/16 11:56

Libvmi概况

LibVMI是一个C库,它提供了对正在运行中的底层虚拟机的运行细节进行监视的功能,监视的功能是由观察内存细节,陷入硬件事件和读取CPU寄存器来完成的。这种方式被称作虚拟机自省(virtual machine introspection)

Libvmi原理

libvmi所提供的种种功能中,最主要的是内存自省功能,内存自省能允许用户从dom0监控(也就是读取内存数据)以及控制(也就是改写内存数据)操作系统。

用libvmi获取linux内核符号表

这里写图片描述

上图是通过libvmi获取内核符号的流程,主要有以下过程组成:
1. 应用程序请求查看内核符号。
2. libvmi通过系统的System.map获取内核符号的虚拟地址。
3. 找到虚拟地址所对应的内核页目录,并获取对应的页表。
4. 通过页表找到正确的数据页。
5. 数据页被返回给libvmi。
6. libvmi将数据返回给vmi应用程序。

用libvmi获取linux内核符号表的源码分析

首先来分析example下的map-symbol.c程序,这个程序可以在内存中找到指定的内核符号,并将内核符号所在的内存页打印出来。在分析源码的时候,我们尽可能减少无关紧要的代码,而只保留关键部分的代码进行分析。

/*map-symbol.c*/#define PAGE_SIZE 1 << 12int main(int argc, char **argv){    /*创建一个vmi的实例*/    vmi_instance_t vmi;    ……    /*初始化vmi的实例*/    if (vmi_init(&vmi, VMI_AUTO | VMI_INIT_COMPLETE, name) ==    VMI_FAILURE) {        printf("Failed to init LibVMI library.\n");        goto error_exit;    }    /*获取符号在内存页中的起始地址 */    if (PAGE_SIZE != vmi_read_ksym(vmi, symbol, memory, PAGE_SIZE)) {        printf("failed to get symbol's memory.\n");        goto error_exit;    }    vmi_print_hex(memory, PAGE_SIZE);    vmi_destroy(vmi);    return 0;}

总体上来看对符号内存进行dump的过程主要有以下几点:

  • 初始化vmi
  • 获取符号表,并经过内核地址翻译的过程
  • 打印(dump)内存页
  • 释放vmi资源

以上过程中的初始化是一个很重要的步骤, vmi_instance_t vmi是对应初始化的实例,下面我们就来分析它的初始化过程。vmi_init函数经过一层封装,实际上后最后会调用vmi_init_private()函数,这个函数会设置vmi的cache(vmi为了提高系统的运行效率用哈希表构建了三种映射作为缓存,分别是pid–>DTB,DTB是一个物理地址,Symbol–>虚拟地址,虚拟地址–>物理地址),设置运行模式,连接到xen/kvm等虚拟机,获得虚拟机内存大小,操作系统类型,内存布局等信息,这些信息都会在后面的代码中起到很大的作用,后续很多功能都直接依赖于初始化后的vmi中保存的一系列信息。附录一中列出的vmi_instance_t的具体定义及其解释。接下来查找symbol对应的虚拟地址,由于linux的内核符号的虚拟地址已经在系统的System.map中规定了,而且在vmi初始化的过程中就读取了guest的System.map,因此在这个文件中对符号名进行匹配就可以获取到符号的虚拟地址了,下面是这个过程的代码,可以看到,代码先是在sym–>虚拟地址的缓存中进行查找,当没有找到后才去system.map中读取,并且将读取到的结果缓存到符号cache中。这样就获取到了一个符号的虚拟地址,接下来还要进行读取这个虚拟地址内容的过程。

addr_t vmi_translate_ksym2v (vmi_instance_t vmi, char *symbol){    addr_t ret = 0;    addr_t base_vaddr = 0;      if (VMI_FAILURE == sym_cache_get(vmi, base_vaddr, 0, symbol, &ret)) {        if (VMI_OS_LINUX == vmi->os_type) {            if (VMI_FAILURE                    == linux_system_map_symbol_to_address(vmi, symbol, &ret)) {                ret = 0;            }        }        if (ret) {            sym_cache_set(vmi, base_vaddr, 0, symbol, ret);        }    }    return ret;}

虚拟地址转换成物理地址

虚拟地址转换成物理地址的功能由所以主要的内存转换工作主要是由vmi_translate_kv2p(vmi, vaddr + buf_offset)完成,这段代码会读取cr3寄存器里的值,然后根据这个值查找系统的页表进行一次四级页表的查询操作,获得gpa然后将这个值交给底层虚拟机(读取内存函数,该函数会根据当前运行的虚拟机是KVM还是XEN自动调用对应的物理内存读取接口函数),底层虚拟机一般会经过ept或npt进行映射,最后读取到物理内存的内容。其实原理上还是经过了一次四级页表的walk。
enter image description here

ept映射物理内存主要的的流程是:
1. CPU首先查找Guest CR3指向的L4页表;
2. 由于Guest CR3给出的是GPA,CPU需要查EPT页表;
3. 如果EPT页表中不存在该地址对应的查找项,则Guest Mode产生EPT Violation异常由VMM来处理;
4. 获取L4页表地址后,CPU根据GVA和L4页表项的内容,来获取L3页表项的GPA;
5. 如果L4页表中GVA对应的表项显示为缺页,那么CPU产生Page Fault,直接交由Guest Kernel处理,注意这里不会产VM-Exit;
6. 获得L3页表项的GPA后,CPU同样查询EPT页表,过程和上面一样;
7. L2,L1页表的访问也是如此,直至找到最终于与GPA对应的HPA。

完成地址转换后就要对转换出的地址中的内容进行读取

c
void * driver_read_page(vmi_instance_t vmi, addr_t page){
driver_instance_t ptrs = driver_get_instance(vmi);
if (NULL != ptrs && NULL != ptrs->read_page_ptr) {
return ptrs->read_page_ptr(vmi, page);
}
else {
dbprint
("WARNING: driver_read_page function not implemented.\n");
return NULL;
}
}

整体上的代码调用树为
c
vmi_read_page (vmi_instance_t vmi, addr_t frame_num){
driver_read_page(vmi_instance_t vmi, addr_t page){
instance->read_page_ptr = &xen_read_page
xen_read_page(vmi_instance_t vmi, addr_t page){
addr_t paddr = page << vmi->page_shift;
return memory_cache_insert(vmi, paddr);
}
}
}

从以上可以看出,经过一番调用之后,最终memory_cache_insert(vmi, paddr)提供了读取内存的功能,可以看出来libvmi的思路,就是先从cache中读取,如果cache中不存在的话,就把映射到的内容insert进cache中。然后继续经过一系列调用,最终通过xc_map_foreign_range实现内存的读取,这是一个xen的”系统调用”。接下来对比分析一下bitvisor中读取物理内存的这个过程:
“`c
void read_hphys_b (u64 phys, void *data, u32 attr)
—-static void * hphys_mapmem (u64 phys, u32 attr, uint len, bool wr)
——–void * mapmem (int flags, u64 physaddr, uint len)
————static void * mapped_hphys_addr (u64 hphys, uint len, int flags)
—-void unmapmem (void *virt, uint len)


“`

0 0