LibVMI是一个专注于读写虚拟机内存的自省库,它能够监视虚拟机底层的运行细节并将其还原。LibVMI支持对Xen及KVM虚拟化平台上的运行虚拟机进行自省操作,针对KVM虚拟化平台,LibVMI对QEMU进行修改以提供虚拟机物理内存的读写接口,基于LibVMI的自省程序通过本地Unix socket与QEMU进行通信,实现对特定物理地址内容的读写。本篇文章针对Libvmi读取虚拟机特定地址内容的原理进行分析。

(一)总体设计

Libvmi的总体设计如下图所示:

图中VMI Application基于LibVMI提供的相关函数进行虚拟机自省操作,如读取目标虚拟机某个虚拟地址的内容,或者获取目标虚拟机中的进程链表等。以读取目标虚拟机某个GPA(Guest Physical Address,客户机物理地址)的内容为例,简述在KVM虚拟化平台下使用LibVMI进行自省操作的流程:

第一步:VMI Application创建一个VMI实例,并给出需要读取的目标虚拟机名称及对应的GPA,交由LibVMI处理;

第二步:LibVMI基于当前所处的虚拟化平台初始化VMI实例及相关驱动函数,然后使用QMP(Qemu Monitor Protocol,qemu虚拟机协议)向qemu-kvm-patch发送创建Unix socket的命令,用于和目标虚拟机所在的qemu进程进行通信;

第三步:qemu-kvm-patch接收到QMP命令后,创建一个unix socket,作为服务器端监听来自LibVMI的请求;

第四步:LibVMI向socket server端发出连接请求,并按照相关格式发送内存读写请求,如发送读取某GPA的请求;

第五步:qemu-kvm-patch接收来自LibVMI的连接请求,并解析请求格式,调用qemu中的相关函数进行自省操作,读取目标虚拟机中对应GPA的内容,返回给Libvmi;

第六步:LibVMI接收qemu-kvm-patch返回的数据结果,将其返回至VMI Application,此次自省操作结束。

具体地,qemu-kvm-patch中使用qemu源码中提供的cpu_physical_memory_map()及cpu_physical_memory_unmap()函数进行目标虚拟机中特定地址内容的获取。上述函数通过qemu中维护的管理目标虚拟机内存的相关结构体,完成虚拟机GPA到HVA(Host Virtual Address,宿主机虚拟地址)的转换,进而读取HVA对应的内容,返回给LibVMI。对于目标虚拟机特定进程GVA(Guest Virtual Address,客户机虚拟地址)内容的获取,需首先获取该进程页表的页目录基地址,进行页表的逐级遍历,遍历过程中涉及的各级页表的GPA读取同之前所述过程,逐级遍历得到GVA对应的最终GPA并读取内容返回。可知libvmi与qemu之间的通信主要是通过qmp及本地socket实现。需注意的是不同于大多数文章描述的libvmi通过EPT页表实现地址翻译,实际上libvmi实现的地址翻译过程为:首先使用虚拟机进程的页表完成gva到gpa的转换,随后通过qemu中的函数实现gpa到hva的转换,之后直接读取该hva处的内容。(这里插一句,与qemu中对应的,kvm中的系列函数kvm_read_guest_virt,之前也一直以为是通过ept页表,实际上原理类似,也是先通过虚拟机进程页表完成gva到gpa的转换,之后根据kvm中的kvm_memslot完成gpa到hva的转换,之后读取内容,至少从源码实现上来看是这样)

(二)Libvmi读取虚拟机特定地址内容原理

(1)地址转换原理及相关函数

qemu中GPA到HVA的转换过程:为了在虚拟机退出时,能够顺利根据物理地址找到对应的HVA地址,qemu会有一个AddressSpaceDispatch结构,用来在AddressSpace中进行位置的找寻,AddressSpaceDispatch结构中有一个PhysPageMap,保存了一个GPA->HVA的一个映射,通过多层页表实现,其最后一级页表指向一个MemoryRegionSection,根据MemoryRegionSection可得到其所属的MR,然后获得实体MR指向的RAMBlock,由此得到对应的hva。当kvm exit退到qemu之后,通过AddressSpace->AddressSpaceDispatch-> map查找对应的MemoryRegionSection,继而找到对应的主机HVA。

获取进程页表

libvmi实现了虚拟机GVA到GPA的转换,GPA到HVA的转换由qemu完成,然后读取hva处的内容。在libvmi中实现gva到gpa转换时,首先根据pid确定使用的进程页表,为0时表示使用内核页表,若不为0则根据mm offset及pgd offset得到进程对应页表基地址,具体实现在linux_pid_to_pgd()中:首先通过内核符号表得到内核页表的虚拟地址(32位内核符号为swapper_pg_dir,64位为init_level4_pgt),然后通过startup_32(内核镜像入口点,地址在内核链接时生成)和phys_startup_32的差值得到内核一致映射区物理地址与虚拟地址的offset,64位则通过startup_64和phys_startup_64得到:一般而言32位下偏移为0xc0000000,64位下偏移为0xffffffff80000000:

得到内核页表的虚拟地址及page_offset之后,就可以减去page_offset得到内核页表的物理地址。如果无法直接得到page_offset,也可以先假设几个偏移(如libvmi中猜测为0xC0000000,0x80000000,0x40000000),那么页表物理地址= 页表虚拟地址 – 假设偏移。以页表物理地址作为内核页表基址,遍历页表得到页表虚拟地址对应的物理地址记为true_phys,如果true_phys与减去偏移得到的物理地址一致,说明猜测的偏移正确,否则错误并尝试下一个偏移。内核页表中也存储了内核一致映射区的相关表项,对于一致映射区,直接使用_pa(),也就是减去偏移page_offset的方式也可以得到对应的物理地址,但是页表是用于硬件寻址的,而__pa()则用于软件计算得到物理地址,因此页表中有必要存储一致映射区对应的表项。以32位为例,内核页表swapper_pg_dir在初始化的时候会把页目录的前768项空出来(768的原因:以32位10-10-12分页为例,则一共有1024个页目录项,一个页目录项指向1024个页表项,故一个页目录项表示的大小为4M,那么768项则表示3G,因此为768项),也就是只初始化了3G以上的空间,一致映射区域中虚拟地址到物理地址的映射也就会放到768项以后的页目录中。进程页表的768项之后的内容和swapper_pg_dir相同,因为内核空间是共享的。在操作3G+896M以上的虚拟内存时,只会更改swapper_pg_dir的映射信息,当别的进程访问到这些页面的时候会发生缺页,在缺页处理中会与swapper_pg_dir同步。

得到虚拟机内核页表的物理地址gpa后,就可以通过qemu将其转换为hva并读取对应的内容,也就可以遍历虚拟机的内核页表。init_task进程的虚拟地址由内核符号表得到,通过内核页表将其转换为gpa,然后根据task offset及pid offset遍历进程链表(进程描述符task_struct是存放在内核空间的,因此遍历进程链表的对应地址转换都可以通过内核页表实现),得到指定pid的进程描述符,然后根据mm_offset得到进程对应的mm_struct结构体之后根据pgd_offset得到mm. pgd的虚拟地址,然后将其转换成GPA,该转换过程由函数vmi_translate_kv2p实现,该函数中依然使用的是内核页表,说明mm_struct结构体是存放在内核空间的。

遍历虚拟机进程页表得到gpa

得到指定进程对应的页基址后,由函数vmi_pagetable_lookup_cache()完成gva到gpa的转换,该函数中如果v2p存在于建立的缓存中,则直接读取,否则根据模式调用相关函数,如v2p_pae或v2p_ia32等,该函数中进行虚拟机各级页表的转换,如函数get_pdpi()函数中,根据页表基地址加上偏移,读取该gpa处的内容。对于gva到gpa的转换,具体地,对每一级页表,首先根据gva获取其在gfn处的偏移,读取该GPA处的内容,得到下一级页表的页框号,然后根据gva得到下一级页表的offset,同样读取gpa的内容得到下一级页表的页框号,如此进行到最后一级页表。因此由一个gva得到最终的gpa的过程中需要多次读取gpa,即涉及到多次GPA到hva的转换。

读取gpa处的内容

对gpa内容的读取调用函数vmi_read_64_pa() ->vmi_read_pa() ->vmi_read() ->vmi_read_page()->drive_read_page()->kvm_read_page()->memory_cache_insert()->get_memory_data()->vmi->get_data_callback(),该回调函数设置为get_data,在kvm_setup_live_mode函数中调用函数memory_cache_init()函数将get_data初始化为kvm_get_memory_patch(),因此最终调用的是kvm_get_memory_patch()(gdb方式下为kvm_get_memory_native())kvm_get_memory_patch函数与qemu patch中的connection_handle()关联,当req.type为1时,读取对应地址的内容,调用函数connection_read_memory() -> cpu_physical_memory_map() ->address_space_map() -> address_space_translate-> address_space_translate_internal函数完成gpa到hva的转换,然后读取hva处的内容,由此得到gpa处的内容,一级级读取得到最终的gpa及其对应的内容。可知对于读取不同进程的虚拟地址,由于根据pid可以得到不同进程的页表,只要对应的gpa到hva有映射,hva有映射(无映射也会通过缺页中断将其映射到物理内存吧),就可以读取对应的内容。

(2)qemu patch的作用

简单地说,就是为了qemu进程和vmi进程能够通信:图源

vmi实例在初始化时:vmi_init_complete() ->vmi_init() ->driver_init() ->driver_kvm_setup() -> vmi->driver.init_ptr() 实质为kvm_init(),通过libvirt连接到了本地qemu虚拟机监控程序,完成了相关函数指针的赋值。根据平台初始化驱动相关函数之后,通过驱动中定义的函数初始化vmi实例:vmi_init_complete() ->vmi_init() ->driver_init_vmi() ->kvm_init_vmi() ->kvm_setup_live_mode(),该函数进行本地通信socket的相关创建:

函数kvm_setup_live_mode() ->exec_memory_access() 向qemu虚拟机发送qmp命令建立一个本地socket,用于libvmi用户程序与qemu进程之间的通信。QMP是一种基于JSON格式的传输协议,可使用QMP与一个QEMU虚拟机实例进行交互,还可以通过修改qemu添加自定义的qmp命令用于交互,libvmi中则使用了这种方式。 Libvmi发送pmemaccess的命令,参数为对应本地socket的文件路径,qemu-patch中则添加了该命令的处理函数,即qmp_pmemaccess(),该函数根据传递的文件路径创建一个本地服务器端socket,然后监听客户端的请求。

服务端socket创建成功后,libvmi调用函数kvm_setup_live_mode() -> memory_cache_init,该函数将vmi->get_data_callback初始化为kvm_get_memory_patch,此后 vmi实例则使用该函数获取虚拟机内容,然后函数kvm_setup_live_mode()调用init_domain_socket创建客户端socket并连接到qemu的服务器端,将客户端的socket描述符保存在kvm_instance_t结构体中。服务端创建线程接受连接请求,线程执行的函数为memory_access_thread,该函数中会接受客户端的连接请求,然后调用函数connection_handler()对具体的读写请求进行处理,接收到关闭请求就关闭服务端对应的socket描述符。

当vmi实例需要读取虚拟机对应gpa的内容时,使用函数kvm_get_memory_patch()通过read/write向kvm_instance_t实例中保存的客户端socket描述符写入请求,请求包括读内存/写内存,指定gpa及读/写长度。Qemu服务端处理函数connection_handler()中为一个while(1)循环,通过连接描述符读取请求并解析,如果是读内存操作则调用connection_read_memory()进行读取,并将读取的内容写入连接描述符对应的buffer中,如果是写请求则调用connection_write_memory()对内存写,返回是否写入成功,如果是退出请求则返回并关闭服务端socket描述符。之后函数kvm_get_memory_patch()从客户端描述符对应的缓冲中读取服务端写入的数据,得到对应gpa的内容,完成一次通信。退出请求的发送:vmi_destroy() ->driver_destroy() ->kvm_destroy() ->destroy_domain_socket()发送退出请求,然后服务端客户端断开连接。每次服务端通过一个线程来处理一个客户端的连接请求及数据发送,所以可以同时运行多个vmi进程,实验对同一个虚拟机同时运行获取进程链表和模块链表的实例是可行的。

(3)偏移量的获取

对于上述过程中涉及的相关偏移量,如mm_offset等。Libvmi将自省过程需要的偏移量从虚拟机中加载内核模块得到,然后写入配置文件中,vmi_get_config函数使用lex/yacc或者flex/bison对配置文件进行解析,然后将结果返回到一个哈希表中,初始化vmi实例的时候将哈希表中的内容赋值给结构体linux_instance_t:

VMI应用程序中通过函数vmi_get_offset得到结构体linux_instance_t中存储的偏移量。

关于lex/yacc: https://segmentfault.com/a/1190000000396608

以上则为Libvmi读取虚拟机特定地址内容的设计及实现。

参考:Libvmi源码,qemu源码

Libvmi实现分析的更多相关文章

  1. alias导致virtualenv异常的分析和解法

    title: alias导致virtualenv异常的分析和解法 toc: true comments: true date: 2016-06-27 23:40:56 tags: [OS X, ZSH ...

  2. 火焰图分析openresty性能瓶颈

    注:本文操作基于CentOS 系统 准备工作 用wget从https://sourceware.org/systemtap/ftp/releases/下载最新版的systemtap.tar.gz压缩包 ...

  3. 一起来玩echarts系列(一)------箱线图的分析与绘制

    一.箱线图 Box-plot 箱线图一般被用作显示数据分散情况.具体是计算一组数据的中位数.25%分位数.75%分位数.上边界.下边界,来将数据从大到小排列,直观展示数据整体的分布情况. 大部分正常数 ...

  4. 应用工具 .NET Portability Analyzer 分析迁移dotnet core

    大多数开发人员更喜欢一次性编写好业务逻辑代码,以后再重用这些代码.与构建不同的应用以面向多个平台相比,这种方法更加容易.如果您创建与 .NET Core 兼容的.NET 标准库,那么现在比以往任何时候 ...

  5. UWP中新加的数据绑定方式x:Bind分析总结

    UWP中新加的数据绑定方式x:Bind分析总结 0x00 UWP中的x:Bind 由之前有过WPF开发经验,所以在学习UWP的时候直接省略了XAML.数据绑定等几个看着十分眼熟的主题.学习过程中倒是也 ...

  6. 查看w3wp进程占用的内存及.NET内存泄露,死锁分析

    一 基础知识 在分析之前,先上一张图: 从上面可以看到,这个w3wp进程占用了376M内存,启动了54个线程. 在使用windbg查看之前,看到的进程含有 *32 字样,意思是在64位机器上已32位方 ...

  7. ZIP压缩算法详细分析及解压实例解释

    最近自己实现了一个ZIP压缩数据的解压程序,觉得有必要把ZIP压缩格式进行一下详细总结,数据压缩是一门通信原理和计算机科学都会涉及到的学科,在通信原理中,一般称为信源编码,在计算机科学里,一般称为数据 ...

  8. ABP源码分析一:整体项目结构及目录

    ABP是一套非常优秀的web应用程序架构,适合用来搭建集中式架构的web应用程序. 整个Abp的Infrastructure是以Abp这个package为核心模块(core)+15个模块(module ...

  9. HashMap与TreeMap源码分析

    1. 引言     在红黑树--算法导论(15)中学习了红黑树的原理.本来打算自己来试着实现一下,然而在看了JDK(1.8.0)TreeMap的源码后恍然发现原来它就是利用红黑树实现的(很惭愧学了Ja ...

随机推荐

  1. 【原创】大叔问题定位分享(18)beeline连接spark thrift有时会卡住

    spark 2.1.1 beeline连接spark thrift之后,执行use database有时会卡住,而use database 在server端对应的是 setCurrentDatabas ...

  2. web页面实现文件下载的几种方法

    今天碰到文件下载的一些问题,本着知其然也要知其所以然的精神,站在巨人的肩膀上深入学习和测试了一下,抛砖引玉,现在总结结论如下: 1)标准URL下载方式可以通过在web页面中嵌入 url超级链接,标准的 ...

  3. 【python】随机数用法

    全文拷贝自:Python随机数用法 random.seed(int) 给随机数对象一个种子值,用于产生随机序列. 对于同一个种子值的输入,之后产生的随机数序列也一样. 通常是把时间秒数等变化值作为种子 ...

  4. 在python中使用print()时,raw write()返回无效的长度:OSError: raw write() returned invalid length 254 (should have been between 0 and 127)

    写出一个不是code的bug,很烦恼,解决了挺长时间,都翻到外文来看,不过还是解决了,只尝试了一种简单可观的方法,希望对大家有用 我正在使用Django与Keras(tensorflow)来训练一个模 ...

  5. Java桥接模式

    定义:将抽象部分与它的具体实现部分分离,使他们都可以独立的变化 通过组合的方式建立两个类之间联系,而不是继承 类型:结构型 适用场景: 抽象和具体实现之间增加更多的灵活性 一个类存在两个(或多个)独立 ...

  6. 如何利用docker 构建golang线上部署环境

    公司最近开发了一个项目是用golang 写的,现在要部署到线上环境去,又不想在服务器上装单独的golang,决定用docker 封装下,直接打到镜像里面,然后就直接在hub.docker.com上面搜 ...

  7. numpy安装-【老鱼学numpy】

    要玩numpy,就得要安装numpy. 安装python 3.6.3 64位 首先需要安装python,安装python的具体方法这里就不细讲了. 可以到官网上下载相应的python版本就可以了,目前 ...

  8. 分布式session解决——Spring-data-redis

    1.如果没有集成shiro来管理session,可以直接使用spring-session 2.若集成了shiro,需要Spring-data-redis (或 shiro-redis) 3.nginx ...

  9. 理解ActivityManagerService

    --摘自<Android进阶解密> *AMS家族* 1.Android 7.0的AMS家族 2.AMP和AMS进行通信 3.(Android 7.0)AMP是AMN的内部类,它们都实现了I ...

  10. X Open Cup named after E.V. Pankratiev. European Grand Prix

    A. Arithmetic Rectangle 对于一行或者一列的情况可以递推求出最大值. 对于至少一行或者一列的情况,可以定义四个格子一组横向和纵向的相等关系,然后悬线法求最大子矩阵. 时间复杂度$ ...