书接上文。。。
______________________________________________________________________________________________
4.3. Hypervisor层
本节的内容主要有两点,一方面是虚拟机执行了vmcall指令之后发生的事情,另一方面是虚拟机和宿主机之间的内存映射关系。下面我们就这两点进行讨论。
在开始介绍之前,我们需要一份Intel® 64 and IA-32 Architectures Software Developer’s Manual Volume 3B: System Programming Guide, Part 2文档,这份文档可以直接从Intel官方下载,这个文档是Intel VT技术的详细介绍与使用。
vmcall指令的处理
经过上面小节的介绍,我们发现虚拟机内核可以通过执行vmcall指令将通知发送给Hypervisor层,下面我们来介绍在执行vmcall之后的处理。
通过查阅Intel文档,我们得知在发生VM-Exit事件之后,使用vmread读取0x4402字段的数据可以将VM-Exit事件产生的原因代码取出来,并且VM-Exit原因代码为0x12代表VM-Exit发生的原因为执行vmcall指令。知道了这些,我们便可以通过搜索IDA中的关键字来寻找读取0x4402字段的代码,IDA中结果如下。
然后在WinDbg中分别对上面三个地址设置断点,然后继续运行系统。注意,设置断点需要在调试hypervisor的WinDbg窗口中操作。最终发现只有第一个断点被访问,逆向之,部分代码如下。
通过上面的代码可知,代码在.text:FFFFF8000021962E处读取出VM-Exit的原因,然后之后通过判断原因代码的值进行操作。代码位置.text:FFFFF80000219D00处如果VM-Exit原因代码为0x12(vmcall造成的VM-Exit),便开始运行对应VM-Exit原因代码的操作。之后,便是一些分发操作和数据处理,然后触发Windows宿主机内核对应的中断处理程序并交给Windows宿主机内核继续进行处理。
宿主机与虚拟机内存映射
Hyper-V中的宿主机和虚拟机之间的内存映射是通过EPT(Extended-Page Table)实现的,要弄清楚宿主机和虚拟机的内存映射关系首先要弄清楚Hyper-V是如何使用EPT的。
通过查阅Intel文档,得知通过调用vmwrite指令读取0x201A字段获得的值为EPT Pointer(full),那么我们通过使用上面的办法,在IDA中搜索”201AH”看看哪里使用了这个值。
和上面一样,对前三个地址设备断点,然后继续运行代码,会发现没有一个断点被访问,这时,我们需要开启或者重启虚拟机才能触发断点,因为EPT初始化是在虚拟机启动过程中执行的。重启虚拟机,发现只有.text:FFFFF800002A0B35地址被访问了,代码如下。
我们通过WinDbg调试查看通过vmwrite指令在0x201A字段写入了什么数据。
我们在Linux内核中写入一些数据,并且把这些数据的地址转换为物理地址并打印出来。在内核日志中显示如下。
我们在写了一个Linux内核模块,主要内容为将一个字符串的物理地址打印到内核log中,将它加载进内核后运行上面代码便出现了” AAAAAAAAAAAA......”字符串在虚拟机中的物理地址。
下面我们在WinDbg中通过调试来确定虚拟机物理地址和宿主机物理地址的关系。
在hv+0x2a0b35处断下时rax值为EPTP,把它作为!vtop指令的dirbase参数,将虚拟机中的物理地址作为!vtop中的VirtualAddress参数,便可得到虚拟机物理地址对应宿主机中的物理地址,上面调试过程中,这个地址为0x1972ee000。通过查看物理地址内存命令!db 0x1972ee000,可以看到这段内存和虚拟机中的内容是一样的。
我们以后便可使用上面的方法将虚拟机中的物理地址映射到宿主机物理中。
4.4. Windows内核层
在介绍这部分内容之前,这里先要声明Windows操作系统的版本,笔者的被调试机的Windows版本为Windows10企业版Build 14393.rs1_release.160715-1616,不同的Windows操作系统版本在逆向和调试时可能代码和偏移会有所不同,所以这里我们以我的被调试机为准,下面的逆向和调试过程中,都使用的是这个版本的Windows系统。
每当虚拟机发来数据时,hypervisor层都会触发注册在Windows内核中的中断,WinDbg运行!idt命令的部分结果如下。
在上面的结果中,列出了五个函数。这五个函数用作处理从hypervisor层发来的通知,并且最终都会调用nt! HvlRouteInterrupt函数。nt! HvlRouteInterrupt函数是用来将各类的通知分发到不同的函数中,再到其他模块中进行下一步的处理。下面是nt! HvlRouteInterrupt函数的原型。
由上面的代码中可知,这段函数以rcx寄存器中的值为引索在HvlpInterruptCallback表中查表,然后调用表中函数。下面我们通过动态调试查看表中的数据,并总结出rcx分别对应着上文中哪些中断处理函数。nt! HvlpInterruptCallback表中的数据如下。
通过在函数nt! HvlRouteInterrupt下断点,每次查看rcx寄存器的值以及栈回溯,总结出如下结论,如表1-7。
在笔者的Windows10系统中,只使用了上文中5个中断处理函s数的前三个,剩余两个函数在实际的使用中没有被调用过,并且在nt!HvlpInterruptCallback表中偏移0x18,0x20为nt!EmpCheckErrataList函数,说明后面两个回调函数并没有初始化。即中断处理函数nt!KiVmbusInterrupt2和nt!KiVmbusInterrupt3虽然被注册但是在笔者的操作系统版本中并没有使用。
我们继续查看函数vmbusr!XPartEnlightenedIsr的内部实现,它的代码如下。
从上面的代码中可以看出,函数vmbusr!XPartEnlightenedIsr通过调用KeInsertQueueDpc函数来插入一个DPC结构体,它的作用是延迟调用DPC结构体中保存的函数地址。下面是vmbusr!XPartEnlightenedIsr调用KeInsertQueueDpc时的DPC结构体内容。
从WinDbg结果可知,函数vmbusr!XPartEnlightenedIsr将vmbusr!ParentRingInterruptDpc函数和vmbusr!ParentInterruptDpc函数插入到延迟调用队列中。
到此为止,宿主机收到从hypervisor层发来的通知后,进程会经历以上流程运行到winhvr! WinHvOnInterrupt函数,vmbusr!ParentInterruptDpc函数和vmbusr!ParentRingInterruptDpc函数中。在这里,winhvr! WinHvOnInterrupt函数和vmbusr!ParentInterruptDpc函数并非用于Hyper-V虚拟设备数据传输之间的运行过程,攻击面较小。所以我们主要介绍vmbusr!ParentRingInterruptDpc函数之后的流程,由于篇幅有限,读者们可以自行对剩余两个函数进行逆向研究。
下面我们以网卡设备(vmswitch.sys)为例子,介绍数据在Windows内核中的传递过程。下面我们先从vmbusr!ParentRingInterruptDpc函数开始,它的代码如下。
从vmbusr!ParentRingInterruptDpc函数的代码中可以看到,在ParentRingInterruptDpc+5C位置有一句跳转到rax寄存器的中的值位置的语句。下面通过调试,查看ParentRingInterruptDpc+55处rcx中地址的内容。
上面的结果说明vmbusr!ParentRingInterruptDpc函数使用了两个函数分发,分别调用了函数vmbkmclr!KmclpVmbusManualIsr和函数vmbkmclr!KmclpVmbusIsr。这两个函数不同之处在于:vmbkmclr!KmclpVmbusIsr是用于处理虚拟机通过monitorpage方式将数据发送给宿主机的函数;vmbkmclr!KmclpVmbusManualIsr是用于处理虚拟机通过hypercall方法将数据发送给宿主机的函数。从上文中的介绍我们知道,虚拟机中不同的设备使用固定的方式来通知宿主机,所以这里也可以得出结论:网卡(hv_netvsc),硬盘(hv_storvsc)设备会使用vmbkmclr!KmclpVmbusIsr函数进行之后的处理,而集成服务(hv_utils),键盘(hyperv_keyboard),鼠标(hid_hyperv),动态内存(hv_balloon),视频(hyperv_fb)设备会使用vmbkmclr!KmclpVmbusManualIsr函数进行之后的处理。vmbkmclr!KmclpVmbusManualIsr这个分支用作准备用户态使用的数据并通知Windows用户态程序读写完成,所以这个分支会在下面的Windows应用层小节中介绍,本小节主要以Hyper-V虚拟网卡设备在宿主机中的驱动(vmswitch.sys)为例做Windows内核层的介绍,故小节剩余部分主要介绍vmbkmclr!KmclpVmbusIsr分支。
这里也许会有人有个疑问,每当代码运行到vmbusr!ParentRingInterruptDpc函数时,我如何知道是虚拟机中的哪个设备发来的通知,如果不知道是什么设备又该如何单一的调试一个虚拟设备,也许其他设备发来的通知一样会被WinDbg截获到,会对调试造成一定的困扰。所以,在介绍vmbkmclr!KmclpVmbusIsr这条分支之前,我们需要在代码运行到vmbusr!ParentRingInterruptDpc函数时判断是哪个虚拟机中的设备发来的通知。
为了分别不同的虚拟机中设备,笔者先修改了Linux内核。在Linux内核中,每个虚拟设备都会通过vmbus_sendpacket函数将数据发送至宿主机,所以只要找到每个虚拟设备代码中调用vmbus_sendpakcet的函数,然后在这个函数中添加一段代码,下面以hyperv_fb.c作为例子。
保存文件,然后重新编译内核,重启虚拟机。然后开机选择刚才编译过的内核,进入系统后,输入如下命令。
上面的命令运行的结果说明,hyperv_fb设备的connection_id的值为0x10005。我们在Linux驱动源码中添加了将vmbus_channel结构体中offermsg.connection_id成员变量的值打印出来的功能,是为了通过connection_id来确定不同的虚拟设备发送的数据。像上面举的例子一样,如果是hyperv_fb驱动发送出去的数据,connection_id一定是0x10005。
既然确定通过connection_id来标记不同设备的数据,那么在宿主机驱动中,又该如何通过connection_id的值知道是哪个设备发送的数据呢?我们通过下面的调试过程来弄清楚这件事。首先先在函数ParentRingInterruptDpc+0x55处设置断点。
WinDbg在ParentRingInterruptDpc+0x55处成功断下,这时我们查看rbp+0x38位置处的数据,会发现地址0x ffff8c8f2c234338处的4字节数据便是虚拟机驱动中connection_id的值。我们可以通过这种办法判断当运行到ParentRingInterruptDpc函数时传输着什么虚拟设备的数据,方便之后跟踪调试特定设备的数据传输流程,减小调试的难度。
言归正传,我们继续介绍vmbkmclr!KmclpVmbusIsr函数之后的流程。在笔者的Linux虚拟机环境中,虚拟网卡的设备是hv_netvsc,它发送数据时vmbus_channel结构体中offermsg.connection_id的值为0x10008,这个值可能在不同的机器上会不一样。下面我们要跟踪网卡数据在宿主机驱动中的流向,首先先要逆向vmbkmclr!KmclpVmbusIsr函数,代码如下。
通过WinDbg单步跟踪,我们发现vmbkmclr!KmclpVmbusIsr函数会调用函数vmbkmclr!InpFillAndProcessQueue。继续单步跟踪函数vmbkmclr!InpFillAndProcessQueue发现,vmbkmclr!InpFillAndProcessQueue函数中会直接调用vmswitch模块中的函数。在InpFillAndProcessQueue+16E处会调用vmswitch!VmsVmNicPvtKmclProcessPacket函数,这个函数vmswitch模块用于处理发来的数据;在InpFillAndProcessQueue+2CA处调用vmswitch!VmsVmNicPvtKmclProcessingComplete函数,这个函数也同样用于处理虚拟机发来的数据。这样,便从VMBus总线过渡到vmswitch之类的虚拟设备驱动中。之后在vmswitch驱动中的操作便是解析数据,并对解析后的数据进行下一步操作。
以上便是Hyper-V将虚拟机中数据传递给Windows内核中Hyper-V驱动组件(如vmswitch.sys)的流程,为了便于理解,我们将以上的流程概括成图1-33。
图1-33
4.5. Windows应用层
这一小节内容为Hyper-V把虚拟机中数据传递给Windows用户态下的Hyper-V组件的流程,我们会以hyperv_fb设备为例子,介绍数据从Linux虚拟机中的hyperv_fb设备到Hyper-V用户态组件vmuidevices.dll的过程。这个小节名字虽为Windows应用层,但实际上很大部分介绍还是在Windows内核态下。
首先我们继续上面小节的部分,介绍vmbkmclr!KmclpVmbusManualIsr这个分支之后的流程。vmbkmclr!KmclpVmbusManualIsr代码如下。
函数vmbkmclr!KmclpVmbusManualIsr调用函数vmbusr!PipeEvtChannelSignalArrived,下面为函数vmbusr!PipeEvtChannelSignalArrived的代码。
从上面vmbusr!PipeEvtChannelSignalArrived函数的代码可以很简洁的看出,它主要调用了一个函数,即vmbusr!PipeProcessDeferredIosAndUnlock。我们继续跟进这个函数的内容,它的代码如下。
通过上面的代码,可以看出代码运行到这个函数,会先将从虚拟机传来的数据拿出来,放在驱动的缓冲区中,这个过程调用的函数是vmbusr!PipeProcessDeferredReadWrite。取完数据之后便调用vmbusr!PipeCompleteIrpList函数,vmbusr!PipeCompleteIrpList函数中会调用函数IofCompleteRequest,用户态readfile函数返回,随后用户态组件处理虚拟机发来的数据。这个过程比较像Hyper-V的Linux内核驱动里__vmbus_recvpacket函数,笔者在逆向时确实也受到Linux驱动的启发。
我们以vmuidevices.dll组件举例,当Windows内核态中vmbusr!PipeCompleteIrpList函数调用函数IofCompleteRequest之后,vmuidevices.dll文件中的vmuidevices!VMBusPipeTransportImpl<VMBusPipeIO,VMBusPipeServerDisposition>::IoOperation函数中调用的readfile函数便会返回,代码继续运行。此时readfile的lpBuffer参数指向的内存中便是从虚拟机发来的数据,vmuidevices.dll组件之后会对这些数据进行解析,并根据数据进行不同的操作。
以上,便是数据从虚拟机发送至宿主机中的用户态组件的过程。和上面小节一样,我们同样用一张图总结,如图1-34。
图1-34
Address Function Instruction
------- -------- -----------
.text:FFFFF80000219629 sub_FFFFF800002194F0 mov eax, 4402h
.text:FFFFF8000029F434 sub_FFFFF8000029F418 mov eax, 4402h
.text:FFFFF800002B6387 sub_FFFFF800002B5F68 mov eax, 4402h
然后在WinDbg中分别对上面三个地址设置断点,然后继续运行系统。注意,设置断点需要在调试hypervisor的WinDbg窗口中操作。最终发现只有第一个断点被访问,逆向之,部分代码如下。
.text:FFFFF80000219629 ; --------------------------------------------------------
.text:FFFFF80000219629 loc_FFFFF80000219629:; CODE XREF: sub_FFFFF800002194F0+126
.text:FFFFF80000219629 mov eax, 4402h
.text:FFFFF8000021962E vmread rcx, rax
.text:FFFFF80000219631 mov [rbp+57h+var_A8], rcx
.text:FFFFF80000219635
.text:FFFFF80000219635 loc_FFFFF80000219635:; CODE XREF: sub_FFFFF800002194F0+137
.text:FFFFF80000219635 movzx edx, cx
.text:FFFFF80000219638 mov r8, 0EC0191DFFDF400h
.text:FFFFF80000219642 xor edx, r15d
.text:FFFFF80000219645 mov dword ptr [rdi+4], 25h
.text:FFFFF8000021964C bt r8, rdx
.text:FFFFF80000219650 jnb short loc_FFFFF8000021967F
.text:FFFFF80000219652 mov eax, cs:dword_FFFFF80000624F70
.text:FFFFF80000219658 test al, 1
.text:FFFFF8000021965A jz short loc_FFFFF8000021966E
.text:FFFFF8000021965C mov rax, gs:17010h
.text:FFFFF80000219665 mov r8d, [rax+2C8h]
.text:FFFFF8000021966C jmp short loc_FFFFF8000021967B
.text:FFFFF8000021966E ; --------------------------------------------------------
.text:FFFFF80000219D00 ; --------------------------------------------------------
.text:FFFFF80000219D00 loc_FFFFF80000219D00:; CODE XREF: sub_FFFFF800002194F0+3AD
.text:FFFFF80000219D00 cmp edx, 12h
.text:FFFFF80000219D03 jnz loc_FFFFF80000219E37
.text:FFFFF80000219D09 mov rax, [rdi+30h]
.text:FFFFF80000219D0D mov dword ptr [rax+130h], 6
.text:FFFFF80000219D17 mov rax, [rdi+88h]
.text:FFFFF80000219D1E cmp dword ptr [rax+0CF8h], 3
.text:FFFFF80000219D25 jnz short loc_FFFFF80000219D55
.text:FFFFF80000219D27 mov r10b, 1
.text:FFFFF80000219D2A mov [rdi+18h], r10b
.text:FFFFF80000219D2E mov rax, [rdi+88h]
.text:FFFFF80000219D35 mov rcx, [rax+28h]
.text:FFFFF80000219D39 mov rax, [rcx+20h]
.text:FFFFF80000219D3D mov [rdi+10h], rax
.text:FFFFF80000219D41 xor edx, edx
.text:FFFFF80000219D43 mov rcx, rdi
.text:FFFFF80000219D46 call sub_FFFFF800002BC17C
.text:FFFFF80000219D4B test al, al
.text:FFFFF80000219D4D jz loc_FFFFF80000219DEF
.text:FFFFF80000219D53 jmp short loc_FFFFF80000219D5C
.text:FFFFF80000219D55 ; ------------- ------------------------------------------
通过上面的代码可知,代码在.text:FFFFF8000021962E处读取出VM-Exit的原因,然后之后通过判断原因代码的值进行操作。代码位置.text:FFFFF80000219D00处如果VM-Exit原因代码为0x12(vmcall造成的VM-Exit),便开始运行对应VM-Exit原因代码的操作。之后,便是一些分发操作和数据处理,然后触发Windows宿主机内核对应的中断处理程序并交给Windows宿主机内核继续进行处理。
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)