4. Hyper-V虚拟机与宿主机的数据传输
Hyper-V虚拟机和宿主机之间的数据传输是Hyper-V安全研究中一个最重要的部分,弄清楚数据传输的方法,流程,才能清楚地知道Hyper-V的攻击面,并且能根据数据的流向和处理分析出函数的功能和某些重要结构体的成员分布,为逆向工作减轻了工作量。同时,也为复现漏洞的过程提供了帮助。
下面,我们从虚拟机到宿主机的顺序,依次介绍数据的流向。这些顺序分别是:虚拟机内核层,Hypervisor层,Windows内核层,Windows应用层。其中,Hypervisor层我们不做过多的介绍,只介绍这层大致的功能。原因是Hypervisor层的代码多是有关VT指令的处理,要完全弄清楚内部的实现,需要大量时间做逆向工作,而且攻击面较窄,不容易出现问题,Hypervisor层代码也很少有解析从虚拟机传来数据的过程。就Windows内核层和Windows应用层的数据传输来说,他们有很多解析虚拟机传来数据的操作,拥有广泛的攻击面。所以,我们着重介绍其他3个方面的数据传输。不过,有兴趣的读者也可以自行逆向、调试Hypervisor层代码,一方面能增加对VT技术的理解,另一方面能了解虚拟化原理相关的知识,但是在本书中就不多赘述。
在介绍数据传输的过程中,希望读者能根据书中介绍自行调试一遍,以加深对数据传输流程的理解。
4.1. 数据传输流程概览
在介绍具体的数据传输流程之前,我们先对它进行一个大体的认识,这样在之后的分层介绍时有一个整体的概览,到时候回看本小节的内容可以帮助您理清思路。
如图1-31,描述了顺序为虚拟机到宿主机的数据传输过程,下面几个小节也是根据这个框架分层展开介绍的。
图1-31
图1-31描述了整个数据传输的大致流程,通过这幅图我们能了解Hyper-V的整体数据传输的原理。
如果有数据要从虚拟机发送,那么会将数据填入环形内存(buffer_ring),然后经由VMBus调用Hypercall通知Hypervisor层有数据到来;如果宿主机发来数据,则通过虚拟机中断,调用中断处理程序中对应的回调函数(callback),回调函数会从收到的环形内存(buffer_ring)中读取数据,并加以解析。
Hypervisor层用于处理VM-Exit事件,触发宿主机和虚拟机中断来通知宿主机和虚拟机有数据到来。
Windows内核层同样使用中断方法获取到Hypervisor层发来的数据,在通过Windows nt模块分发到vmbusr模块中,然后根据数据的类型分发到vmbkmclr模块中,vmbkmclr再分发到相应虚拟设备驱动中,或者通知用户态程序有数据来临;如果有数据从VMBus发到虚拟机的话,则会调用Hypercall产生一个VM-Exit事件,陷入Hypervisor层处理。
Windows用户态的某些Dll用于模拟显示,鼠标键盘,动态内存,集成服务等设备。用户态和内核的通信主要通过调用Writefile/Readfile读写不同设备对应的命名管道,如果用户态要发送数据到内核,调用Writefile直接将数据写入命名管道;如有数据从内核传到用户态,内核会通知用户态程序需要接收数据,用户态程序调用Readfile函数,从命名管道中读取数据。
在下面的介绍中,我们会详细介绍图中的内容,并且会着重介绍Windows内核部分的数据传输。
4.2. 虚拟机操作系统层
我们先从虚拟机内核部分说起,这里还是用Linux 内核代码作为介绍。笔者使用的Linux内核版本是4.7.2,可能不同版本的Linux内核代码会稍有不同。
Hyper-V设备在Linux内核中的位置如表1-6。
在介绍虚拟机内核部分的数据传输时,我们以hyperv_fb设备为例,分别介绍数据传出和传入虚拟机的流程。
这个hyperv_fb设备是虚拟显示设备,可以理解为它虚拟出来一个vga显示器,用于传输和控制虚拟机显示的画面。这个模块对应的Linux内核模块文件为./linux-4.7.2/drivers/video/fbdev/hyperv_fb.c,下面我们从这个文件开始,解读虚拟机内核部分的数据传输。
从宿主机接收数据
在Linux虚拟机中,每当宿主机发来数据都会以中断的方法通知虚拟机进行处理。图1-32中,显示了Linux虚拟机所有注册的中断。图中的倒数第五行就是Hyper-V在Linux内核中注册的中断,每当宿主机有数据发过来时,Hypervisor层会触发这个中断来通知内核需要接收数据,并且对数据进行处理。
下面我们以hyperv_fb设备进行整个数据接收过程的分析。
图1-32
先从hyperv_fb驱动中出发,当有数据传入虚拟机时,会调用回调函数synthvid_receive,那么我们看一下这个回调函数是如何知道数据到达了呢。在驱动初始化过程中,synthvid_connect_vsp函数调用vmbus_open函数来注册hyperv_fb驱动收到数据时的回调函数,如下代码所示。
下面我们来看看vmbus_open函数中到底做了什么操作,代码如下。
vmbus_open函数中,主要操作是新建了一个channel,这个操作在驱动初始化的时候完成,所以这个channel也就是对应该设备建立的。在上面的代码中,函数将回调函数synthvid_receive的地址放入newchannel的onchannel_callback字段,这里初始化当数据传来时调用的回调函数。下面,我们继续研究是谁调用了vmbus_channel结构中onchannel_callback字段中存储的回调函数。
在此之前,我们先了解下中断处理函数是如何注册以及调用的。文件./linux-4.7.2/drivers/hv/vmbus_drv.c是hv_vmbus模块的源文件之一,其中的vmbus_bus_init函数是用来初始化VMBus驱动,并且注册Hyper-V在Linux虚拟机之中的驱动,如下面简化过的代码所示。
vmbus_bus_init函数通过调用hv_setup_vmbus_irq函数来注册Hyper-V在Linux虚拟机中的中断处理程序。函数hv_setup_vmbus_irq的原型如下面的代码所示,其源文件的位置为./linux-4.7.2/arch/x86/kernel/cpu/mshyperv.c。
通过hv_setup_vmbus_irq函数,注册了中断处理函数hyperv_callback_vector,并且将handler赋给全局变量vmbus_handler,这里的vmbus_handler中指向的就是上文中的vmbus_isr函数。下面我们继续查看hyperv_callback_vector的定义,在./linux-4.7.2/arch/x86/entry/entry_64.S文件中我们发现这么一段定义,如下面的代码所示。
这句语句说明hyperv_callback_vector中断处理函数实际上是hyperv_vector_handler函数的别名,我们再来查看hyperv_vector_handler函数的定义,如下面代码所示,定义这个函数的文件位置为./linux-4.7.2/arch/x86/kernel/cpu/mshyperv.c。
从上面的代码中可以看出,当中断来临时,函数hyperv_vector_handler会调用vmbus_handler来进行处理,从上文可知,vmbus_handler指向的就是函数vmbus_isr。那么也就是说,当Hypervisor层发来数据时,便会调用vmbus_isr函数用于对通知的处理。
vmbus_isr函数简化的原型如下面代码所示,定义函数所在文件位置为./linux-4.7.2/drivers/hv/vmbus_drv.c。
上面的代码中,使用了tasklet_schedule函数来调用hv_context.event_dpc[cpu]注册的延迟函数。下面我们看看hv_context.event_dpc[cpu]描述符中注册了什么函数,代码所示,所在文件为./linux-4.7.2/drivers/hv/hv.c。
上述代码中,hv_synic_alloc函数是用作初始化tasklet的函数,其中初始化了hv_context.event_dpc[cpu]描述符,并注册软中断函数为vmbus_on_event。vmbus_on_event函数的简化原型如下所示,文件为./linux-4.7.2/drivers/hv/connection.c。
函数vmbus_on_event调用process_chn_event函数,process_chn_event函数如下所示,文件位置为./linux-4.7.2/drivers/hv/connection.c。
上述代码中,注释部分调用了vmbus_channel结构中onchannel_callback字段的回调函数。这里的onchannel_callback的值就是上文中的onchannelcallback变量的值,即回调函数synthvid_receive的地址。也就是说,每当有数据到达虚拟机,便会经过这么多层函数的传递,然后调用hyperv_fb驱动特定channel的回调函数,执行hyperv_fb驱动接收数据的函数synthvid_receive。
以hyperv_fb驱动为例,当宿主机传来数据时,经过中断处理程序的分发,调用特定channel对应的回调函数处理发来的数据,并且不同的设备使用的回调函数不同,比如hyperv_fb驱动使用的是synthvid_receive函数。那么下面我们来看看synthvid_receive函数中是如何将数据从宿主机中读取出来的,如下面代码所示。
从上面的代码可以看出,synthvid_receive函数调用了vmbus_recvpacket函数来获取从宿主机发来的数据。vmbus_recvpacket函数如下,文件位置为./linux-4.7.2/drivers/hv/channel.c。
在vmbus_recvpacket函数中,先调用了hv_ringbuffer_read函数读出环形内存中的数据,如果读满一个环形了,那就调用vmbus_setevent函数通知宿主机读取数据完成。若在这之后还有数据,则要通知虚拟机再去读取数据,再次执行从中断处理程序开始的流程;若没有数据则不通知虚拟机读数据。如果没有读完一整个环形内存,那么函数便直接返回。hv_ringbuffer_read函数主要是读取环形内存的操作,读者可以自行去查看这部分代码,这里就不再赘述,vmbus_setevent函数我们会在下文着重介绍。然后,虚拟机读取完宿主机传来的数据,解析,然后继续传递给内核其他函数加以处理。到这,虚拟机就算完成了数据的接收。
到此为止,我们便介绍完虚拟机是如何从宿主机接收数据的流程。
发送数据至宿主机
我们还是以hyperv_fb驱动为例子,探索虚拟机是如何将数据发送到宿主机的。在hyperv_fb驱动中,发送数据到宿主机的函数为synthvid_send。下面是synthvid_send函数的原型,源文件位置为./linux-4.7.2/drivers/video/fbdev/hyperv_fb.c。
上面的代码调用了函数vmbus_sendpacket函数将msg指向的数据发送至宿主机,vmbus_sendpacket紧接着调用了vmbus_sendpacket_ctl函数,下面是vmbus_sendpacket_ctl函数的原型,文件位置为./linux-4.7.2/drivers/hv/channel.c。
上面的代码是将要发送的数据,即buffer指向的内存填入环形内存中,然后通过函数vmbus_setevent通知宿主机去读取环形内存中的内容。函数hv_ringbuffer_write便是将数据填入环形内存的函数,读者可以自行去查阅这部分代码,这里不再赘述。如果环形内存写满,则会调用vmbus_setevent函数来通知宿主机。由于虚拟机向宿主机传递数据是通过通知宿主机读取环形内存的方式,所以我们主要探究虚拟机是如何通知宿主机。函数vmbus_setevent原型如下,文件位置为./linux-4.7.2/drivers/hv/channel.c。
从上面的代码中可以看出,vmbus_setevent函数通知宿主机的方法有两种。一种是利用monitorpage,在它的trigger_group数组成员中的pending成员置位来实现通知宿主机;另一种是继续调用vmbus_set_event函数,vmbus_set_event函数使用hypercall通知宿主机。从实际使用中,一般来说网卡(hv_netvsc),硬盘(hv_storvsc)设备会使用monitorpage的方法通知宿主机,而集成服务(hv_utils),键盘(hyperv_keyboard),鼠标(hid_hyperv),动态内存(hv_balloon),视频(hyperv_fb)设备会使用hypercall方式通知宿主机。下面我们分别介绍这两种通知宿主机的方式。
1.monitorpage方式
如上面的代码所示,monitorpage通知宿主机的方法十分直接,直接在特定内存位置置位即可。下面我们来看一下monitorpage的初始化过程,初始化操作在vmbus_connect函数中,文件位置为./linux-4.7.2/drivers/hv/connection.c。
然后,通过调用vmbus_negotiate_version函数,将刚刚分配的monitor_pages地址发送到宿主机中,完成了monitorpage的注册过程。之后,如果需要通知宿主机,便可直接在monitorpage特定内存位置置位即可
2.hypercall方式
Hypercall方式通知虚拟机通过vmbus_setevent函数调用vmbus_set_event函数实现,下面我们来看看vmbus_set_event函数中的内容,代码如下。文件位置为./linux-4.7.2/drivers/hv/connection.c。
从上面的代码可以看出,vmbus_set_event函数继续调用hv_do_hypercall函数来通知宿主机。hv_do_hypercall函数代码如下,文件位置为./linux-4.7.2/drivers/hv/hv.c。
上面的代码中,有一句嵌入汇编代码,直接运行了汇编语句call hypercall_page,hypercall_page是由hv_context.hypercall_page赋值而来,下面我们来探寻hypercall_page的初始化过程和其中的内容。在hv_init函数中,完成了对hypercall_page的初始化,代码片段如下,文件位置为./linux-4.7.2/drivers/hv/hv.c。
其中,hypercall_msr结构如下。
由上面代码可知,hypercall_page是由读写msr寄存器来告知宿主机虚拟机中的hypercall_page的位置。虚拟机内核在读写msr寄存器时,实际上每次读写都被hypervisor层的代码截获,然后通过读写的内容进行不同的操作,这里便是将hypercall_page指向的内存写入数据,写入的数据如下。
从hypercall_page中过的数据可知,hv_do_hypercall函数实际上是调用了vmcall汇编指令,产生VM-Exit事件,陷入hypervisor层进行处理,hypervisor层处理完成后再通知宿主机数据到来,宿主机驱动再对虚拟机数据进行处理。也就是说,每次调用vmbus_set_event函数最终都会运行vmcall汇编指令陷入hypervisor层进行处理。
综上,虚拟机收发数据都是通过VMBus进行传输的,并且需要hypervisor层处理和转发。以上便是虚拟机内核层的数据收发过程。
static int synthvid_connect_vsp(struct hv_device *hdev)
{
struct fb_info *info = hv_get_drvdata(hdev);
struct hvfb_par *par = info->par;
int ret;
ret = vmbus_open(hdev->channel, RING_BUFSIZE, RING_BUFSIZE,
NULL, 0, synthvid_receive, hdev);
if (ret) {
pr_err("Unable to open vmbus channel\n");
return ret;
}
.....
return 0;
error:
vmbus_close(hdev->channel);
return ret;
}
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课