通过扩展功能启用寄存器(EFER)进行系统调用SYSTEMCALL Hook
翻译自:https://revers.engineering/syscall-hooking-via-extended-feature-enable-register-efer/
作者:Daax Rynd
翻译:Lonely
自从KVA Shadowing(KVAS)诞生以来,类似于Linux的KPTI(微软为缓解Meltdown漏洞而开发),在Windows中将系统调用挂接到其他潜在恶意事件中变得越来越困难。更新我的虚拟化工具集后,该工具集利用syscall挂钩策略来协助进行控制流分析,当尝试添加对任何启用KVAS的Windows版本的支持时,我遇到了麻烦。这是由于Windows将syscall处理程序映射KiSystemCall64Shadow
到内核影子页表。因此,在尝试使用LSTAR MSR挂接系统调用时,我发现这样做的唯一方法是使用以下命令将我的自定义LSTAR系统调用处理程序手动添加到影子页表中:MmCreateShadowMapping
。在Windows 10 1809更新之前,它一直运行良好。自从1809更新以来,PAGE
初始化后不久就丢弃了内核部分中的阴影映射代码页。我猜想Microsoft抓住了这种解决方法,并通过丢弃页面来处理它。如果没有引导内核,就无法解决此问题。
在考虑了可能的解决方案之后,我决定使用扩展功能启用寄存器(EFER) 进行钩子操作,以退出每个SYSCALL和后续的SYSRET指令并模拟其操作(您可以在Intel中找到EFER MSR的定义软件开发人员手册,第3A卷,第2.2.1 Extended Feature Enable Register
)部分。现在您可能在想,这怎么可能?但是,当您手中有被颠覆的处理器时,可能性几乎是无限的!
在MSR位图中设置适当的位时,可以控制和屏蔽SYSCALL Enable
EFER MSR(或SCE位)的值。参考“英特尔软件开发人员手册,卷2B ”下的“部分” 4.3 INSTRUCTIONS (M-U)
,我们可以清楚地看到SYSCALL指令的工作方式,并注意到我们可以利用EFER SCE位(AMD64体系结构程序员手册V3 r3.26具有与之相当的指令参考)。页419,有些人可能会更容易理解)。
从Intel SDM中获取SYSCALL指令的操作如下:
IF (CS.L ≠ 1 ) or (IA32_EFER.LMA ≠ 1) or (IA32_EFER.SCE ≠ 1) (* Not in 64-Bit Mode or SYSCALL/SYSRET not enabled in IA32_EFER *) THEN #UD; FI; RCX ← RIP; (* Will contain address of next instruction *) RIP ← IA32_LSTAR; R11 ← RFLAGS; RFLAGS ← RFLAGS AND NOT(IA32_FMASK); CS.Selector ← IA32_STAR[47:32] AND FFFCH (* Operating system provides CS; RPL forced to 0 *) (* Set rest of CS to a fixed value *) CS.Base ← 0; (* Flat segment *) CS.Limit ← FFFFFH; (* With 4-KByte granularity, implies a 4-GByte limit *) CS.Type ← 11; (* Execute/read code, accessed *) CS.S ← 1; CS.DPL ← 0; CS.P ← 1; CS.L ← 1; (* Entry is to 64-bit mode *) CS.D ← 0; (* Required if CS.L = 1 *) CS.G ← 1; (* 4-KByte granularity *) CPL ← 0; SS.Selector ← IA32_STAR[47:32] + 8; (* SS just above CS *) (* Set rest of SS to a fixed value *) SS.Base ← 0; (* Flat segment *) SS.Limit ← FFFFFH; (* With 4-KByte granularity, implies a 4-GByte limit *) SS.Type ← 3; (* Read/write data, accessed *) SS.S ← 1; SS.DPL ← 0; SS.P ← 1; SS.B ← 1; (* 32-bit stack segment *) SS.G ← 1; (* 4-KByte granularity *)
我们可以看到导致未定义操作码异常(#UD)的第一行条件包含对EFER SCE位的条件检查。知道如果清除了EFER SCE,我们可能会导致#UD异常,现在我们知道可以使用异常位图在每条SYSCALL指令上VM退出。
尽管每条SYSCALL指令在系统调用处理程序中都应该有一条后续的SYSRET指令,以便恢复执行回到上一个上下文。SYSRET的操作类似于SYSCALL指令,可以将其视为IRET指令的近亲。
再次从Intel SDM中取出,SYSRET指令操作如下:
IF (CS.L ≠ 1 ) or (IA32_EFER.LMA ≠ 1) or (IA32_EFER.SCE ≠ 1) (* Not in 64-Bit Mode or SYSCALL/SYSRET not enabled in IA32_EFER *) THEN #UD; FI; IF (CPL ≠ 0) OR (RCX is not canonical) THEN #GP(0); FI; IF (operand size is 64-bit) THEN (* Return to 64-Bit Mode *) RIP ← RCX; ELSE (* Return to Compatibility Mode *) RIP ← ECX; FI; RFLAGS ← (R11 & 3C7FD7H) | 2; (* Clear RF, VM, reserved bits; set bit 2 *) IF (operand size is 64-bit) THEN CS.Selector ← IA32_STAR[63:48]+16; ELSE CS.Selector ← IA32_STAR[63:48]; FI; CS.Selector ← CS.Selector OR 3; (* RPL forced to 3 *) (* Set rest of CS to a fixed value *) CS.Base ← 0; (* Flat segment *) CS.Limit ← FFFFFH; (* With 4-KByte granularity, implies a 4-GByte limit *) CS.Type ← 11; (* Execute/read code, accessed *) CS.S ← 1; CS.DPL ← 3; CS.P ← 1; IF (operand size is 64-bit) THEN (* Return to 64-Bit Mode *) CS.L ← 1; (* 64-bit code segment *) CS.D ← 0; (* Required if CS.L = 1 *) ELSE (* Return to Compatibility Mode *) CS.L ← 0; (* Compatibility mode *) CS.D ← 1; (* 32-bit code segment *) FI; CS.G ← 1; (* 4-KByte granularity *) CPL ← 3; SS.Selector ← (IA32_STAR[63:48]+8) OR 3; (* RPL forced to 3 *) (* Set rest of SS to a fixed value *) SS.Base ← 0; (* Flat segment *) SS.Limit ← FFFFFH; (* With 4-KByte granularity, implies a 4-GByte limit *) SS.Type ← 3; (* Read/write data, accessed *) SS.S ← 1; SS.DPL ← 3; SS.P ← 1; SS.B ← 1; (* 32-bit stack segment*) SS.G ← 1; (* 4-KByte granularity *)
我们可以看到导致#UD异常的第一行条件与SYSCALL指令相同。至此,我们知道开始导致VM退出并模拟系统调用是很好的做法,但让我们回顾一下我们必须做的所有事情:
- 启用VMX。
- 在VMCS中设置VM进入控件,以在VM条目上加载EFER MSR。
- 在VMCS中设置VM退出控件,以在VM退出时保存EFER MSR。
- 在VMCS中设置MSR位图,以在读取和写入EFER MSR时退出。
- 设置VMCS中的异常位图以退出#UD异常。
- 将EFER MSR读取VM出口上的SCE位置1。
- 清除(屏蔽)EFER MSR写入VM出口上的SCE位。
- 处理#UD指令以模拟SYSCALL或SYSRET指令。
下一个问题是检测#UD是由SYSCALL还是SYSRET指令引起的。为了简单起见,从RIP读取操作码足以确定导致#UD的指令。KVAS使事情稍微复杂化,因此,如果CR3 PCID指示用户模式目录表,我们需要以不同的方式进行处理。当然,还有比读取指令操作码更好的最佳方法(例如,钩住中断表本身,或者如果可以安全地假设没有其他事情会导致#UD,则使用切换或计数器在处理syscall或sysret之间进行切换)。
仿真SYSCALL和SYSRET指令就像按照手册中概述的指令操作一样容易。以下代码只是一个基本的仿真,为简化起见,我特意省去了兼容性和保护模式以及SYSRET #GP异常的处理:
// // SYSCALL instruction emulation routine // static BOOLEAN VmmpEmulateSYSCALL( IN PVIRTUAL_CPU VirtualCpu ) { X86_SEGMENT_REGISTER Cs, Ss; UINT64 MsrValue; // // Save the address of the instruction following SYSCALL into RCX and then // load RIP from MSR_LSTAR. // MsrValue = ReadMSR( MSR_LSTAR ); VirtualCpu->Context->Rcx = VirtualCpu->Context->Rip; VirtualCpu->Context->Rip = MsrValue; VmcsWrite( VMCS_GUEST_RIP, VirtualCpu->Context->Rip ); // // Save RFLAGS into R11 and then mask RFLAGS using MSR_FMASK. // MsrValue = ReadMSR( MSR_FMASK ); VirtualCpu->Context->R11 = VirtualCpu->Context->Rflags; VirtualCpu->Context->Rflags &= ~(MsrValue | X86_FLAGS_RF); VmcsWrite( VMCS_GUEST_RFLAGS, VirtualCpu->Context->Rflags ); // // Load the CS and SS selectors with values derived from bits 47:32 of MSR_STAR. // MsrValue = ReadMSR( MSR_STAR ); Cs.Selector = (UINT16)((MsrValue >> 32) & ~3); // STAR[47:32] & ~RPL3 Cs.Base = 0; // flat segment Cs.Limit = (UINT32)~0; // 4GB limit Cs.Attributes = 0xA9B; // L+DB+P+S+DPL0+Code VmcsWriteSegment( X86_REG_CS, &Cs ); Ss.Selector = (UINT16)(((MsrValue >> 32) & ~3) + 8); // STAR[47:32] + 8 Ss.Base = 0; // flat segment Ss.Limit = (UINT32)~0; // 4GB limit Ss.Attributes = 0xC93; // G+DB+P+S+DPL0+Data VmcsWriteSegment( X86_REG_SS, &Ss ); return TRUE; }
// // SYSRET instruction emulation routine // static BOOLEAN VmmpEmulateSYSRET( IN PVIRTUAL_CPU VirtualCpu ) { X86_SEGMENT_REGISTER Cs, Ss; UINT64 MsrValue; // // Load RIP from RCX. // VirtualCpu->Context->Rip = VirtualCpu->Context->Rcx; VmcsWrite( VMCS_GUEST_RIP, VirtualCpu->Context->Rip ); // // Load RFLAGS from R11. Clear RF, VM, reserved bits. // VirtualCpu->Context->Rflags = (VirtualCpu->Context->R11 & ~(X86_FLAGS_RF | X86_FLAGS_VM | X86_FLAGS_RESERVED_BITS)) | X86_FLAGS_FIXED; VmcsWrite( VMCS_GUEST_RFLAGS, VirtualCpu->Context->Rflags ); // // SYSRET loads the CS and SS selectors with values derived from bits 63:48 of MSR_STAR. // MsrValue = ReadMSR( MSR_STAR ); Cs.Selector = (UINT16)(((MsrValue >> 48) + 16) | 3); // (STAR[63:48]+16) | 3 (* RPL forced to 3 *) Cs.Base = 0; // Flat segment Cs.Limit = (UINT32)~0; // 4GB limit Cs.Attributes = 0xAFB; // L+DB+P+S+DPL3+Code VmcsWriteSegment( X86_REG_CS, &Cs ); Ss.Selector = (UINT16)(((MsrValue >> 48) + 8) | 3); // (STAR[63:48]+8) | 3 (* RPL forced to 3 *) Ss.Base = 0; // Flat segment Ss.Limit = (UINT32)~0; // 4GB limit Ss.Attributes = 0xCF3; // G+DB+P+S+DPL3+Data VmcsWriteSegment( X86_REG_SS, &Ss ); return TRUE; } 您可以简单地从#UD处理程序调用SYSCALL和SYSRET仿真例程,该例程还可以检测导致异常的指令。这是一个简单的示例,其中包含支持KVAS的代码:
#define IS_SYSRET_INSTRUCTION(Code) \ (*((PUINT8)(Code) + 0) == 0x48 && \ *((PUINT8)(Code) + 1) == 0x0F && \ *((PUINT8)(Code) + 2) == 0x07) #define IS_SYSCALL_INSTRUCTION(Code) \ (*((PUINT8)(Code) + 0) == 0x0F && \ *((PUINT8)(Code) + 1) == 0x05) static BOOLEAN VmmpHandleUD( IN PVIRTUAL_CPU VirtualCpu ) { UINTN GuestCr3; UINTN OriginalCr3; UINTN Rip = VirtualCpu->Context->Rip; // // Due to KVA Shadowing, we need to switch to a different directory table base // if the PCID indicates this is a user mode directory table base. // GuestCr3 = VmxGetGuestControlRegister( VirtualCpu, X86_CTRL_CR3 ); if ((GuestCr3 & PCID_MASK) != PCID_NONE) { OriginalCr3 = ReadCr3( ); WriteCr3( PsGetCurrentProcess( )->DirectoryTableBase ); if (IS_SYSRET_INSTRUCTION( Rip )) { WriteCr3( OriginalCr3 ); goto EmulateSYSRET; } if (IS_SYSCALL_INSTRUCTION( Rip )) { WriteCr3( OriginalCr3 ); goto EmulateSYSCALL; } WriteCr3( OriginalCr3 ); return FALSE; } else { if (IS_SYSRET_INSTRUCTION( Rip )) goto EmulateSYSRET; if (IS_SYSCALL_INSTRUCTION( Rip )) goto EmulateSYSCALL; return FALSE; } // // Emulate SYSRET instruction. // EmulateSYSRET: LOG_DEBUG( "SYSRET instruction => 0x%llX", Rip ); return VmmpEmulateSYSRET( VirtualCpu ); // // Emulate SYSCALL instruction. // EmulateSYSCALL: LOG_DEBUG( "SYSCALL instruction => 0x%llX", Rip ); return VmmpEmulateSYSCALL( VirtualCpu ); } 如果确定SYSCALL或SYSRET指令已导致#UD异常,则只需跳过将异常注入到Guest中,因为该异常是有意引起的,然后优雅地恢复到Guest。例:
case X86_TRAP_UD: // INVALID OPCODE FAULT LOG_DEBUG( "VMX => #UD Rip = 0x%llX", VirtualCpu->Context->Rip ); // // Handle the #UD, checking if this exception was intentional. // if (!VmmpHandleUD( VirtualCpu )) { // // If this #UD was found to be unintentional, inject a #UD interruption into the guest. // VmxInjectInterruption( VirtualCpu, InterruptVectorType, VMX_INTR_NO_ERR_CODE ); } // continued code flow then return back to guest....本文来源于Lonely Blog -全球网络安全资讯平台, 转载请注明出处: https://blog.wuhao13.xin/2777.html