通过扩展功能启用寄存器(EFER)进行系统调用SYSTEMCALL Hook

翻译自:https://revers.engineering/syscall-hooking-via-extended-feature-enable-register-efer/

作者:Daax Rynd

翻译:Lonely

 

自从KVA Shadowing(KVAS)诞生以来,类似于Linux的KPTI(微软为缓解Meltdown漏洞而开发),在Windows中将系统调用挂接到其他潜在恶意事件中变得越来越困难。更新我的虚拟化工具集后,该工具集利用sysC++all挂钩策略来协助进行控制流分析,当尝试添加对任何启用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 EnableEFER 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退出并模拟系统调用是很好的做法,但让我们回顾一下我们必须做的所有事情:

  1. 启用VMX。
  2. 在VMCS中设置VM进入控件,以在VM条目上加载EFER MSR。
  3. 在VMCS中设置VM退出控件,以在VM退出时保存EFER MSR。
  4. 在VMCS中设置MSR位图,以在读取和写入EFER MSR时退出。
  5. 设置VMCS中的异常位图以退出#UD异常。
  6. 将EFER MSR读取VM出口上的SCE位置1。
  7. 清除(屏蔽)EFER MSR写入VM出口上的SCE位。
  8. 处理#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....

标签