自从类似于Linux的KPTI的KVA Shadowing(KVAS)(由Microsoft开发来缓解Meltdown漏洞)诞生以来,在潜在的恶意软件中挂载系统调用在Windows中变得越来越困难。在我更新我的利用syscall挂钩策略来协助进行控制流分析虚拟化工具集时,我在尝试添加对任何启用KVAS的Windows版本的支持遇到了麻烦。这是由于Windows将系统调用处理程序KiSystemCall64Shadow映射到内核影子页表。因此,在尝试使用LSTAR MSR挂接系统调用时,我发现唯一的方法是使用MmCreateShadowMapping将我的自定义LSTAR系统调用处理程序手动添加到影子页表中。在Windows 10 1809更新之前,这种方式能够很好地运行。然而自从1809更新以来,内核的PAGE段中的影子映射代码的页面在初始化后不久就被丢弃。我猜想Microsoft了解到了,并通过丢弃页面来对抗此方法。似乎如果不引导内核,就无法解决此问题。
在考虑了可能的解决方案之后,我决定使用EFER寄存器进行挂钩操作,来模拟每个SYSCALL和后续的SYSRET指令的操作(您可以在Intel软件开发人员手册,卷3A,第2.2.1节“EFER寄存器”中找到EFER MSR的定义 。 现在您可能在想,这怎么可能? 但是,当您手中有被开启硬件虚拟化(这个过程称之为”subvert”)的处理器时,可能性几乎是无限的!
在MSR位图中设置适当的位时,可以控制和屏蔽EFER MSR的对SYSCALL指令的启用位(或SCE位)。 参考英特尔软件开发人员手册,第2B卷,第4.3节 指令集(M-U)下的内容,我们可以清楚地看到SYSCALL指令的工作方式,以及我们可以利用EFER SCE位来达到我们的目的(AMD64体系结构程序员手册V3 r3.26具有 (第419页)上几乎等同的指令参考,这部分可能比较容易理解)。
从Intel 开发文档中获取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指令上来拦截这种VMExit事件。
尽管每条SYSCALL指令在系统调用处理程序中都应该有一条后续的SYSRET指令,以便恢复执行回到上一个上下文。 SYSRET的操作类似于SYSCALL指令,可以将其视为IRET指令的近亲。
再次查看Intel 开发文档,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指令相同。至此,我们可以开始利用#UD异常导致VMExit事件并模拟系统调用了.在这之前,让我们回顾一下我们必须做的所有事情:
- 启用VMX。
- 在VMCS中设置VM-entry controls字段,以在VM Entry时加载EFER MSR。
- 在VMCS中设置VM-exit controls字段,以在VM Exit时保存EFER MSR。
- 在VMCS中设置MSR位图来使读取和写入EFER MSR时产生VMExit事件。
- 设置VMCS中的异常位图来使发生#UD异常时产生VMExit事件。
- 将EFER MSR读取VM出口上的SCE位置1。
- 在写入EFER MSR产生的VMExit事件处理例程对SCE位置0。
- 处理产生#UD异常的指令来模拟SYSCALL或SYSRET指令。
下一个问题是检测#UD是由SYSCALL还是SYSRET指令引起的。为简单起见,从RIP读取操作码足以确定导致#UD的指令。 然而KVAS导致了稍微复杂一点.如果CR3 PCID位域指示用户模式页目录表基址,我们就需要以不同的方式进行处理。当然,还有比读取指令操作码更好的方法(例如,如果可以假定没有其他事情会导致#UD,那么挂钩中断表,或者使用触发器或计数器在处理syscall或sysret之间进行切换)。
模拟SYSCALL和SYSRET指令就像按照手册中概述的指令操作一样容易。以下代码只是一个基本的模拟,为简化起见,我特意省去了对compatibility和保护模式以及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....
那么我们如何才能有效地使用这种方法呢? 在SYSCALL模拟处理例程中,我们可以访问guest寄存器,这些寄存器包含系统调用索引以及根据x64 ABI相关联的参数,因此我们可以随心所欲地来使用这种方法!
发表评论
您还未登录,请先登录。
登录