通过EFER寄存器实现基于VT的syscall挂钩

 

自从类似于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事件并模拟系统调用了.在这之前,让我们回顾一下我们必须做的所有事情:

  1. 启用VMX。
  2. 在VMCS中设置VM-entry controls字段,以在VM Entry时加载EFER MSR。
  3. 在VMCS中设置VM-exit controls字段,以在VM Exit时保存EFER MSR。
  4. 在VMCS中设置MSR位图来使读取和写入EFER MSR时产生VMExit事件。
  5. 设置VMCS中的异常位图来使发生#UD异常时产生VMExit事件。
  6. 将EFER MSR读取VM出口上的SCE位置1。
  7. 在写入EFER MSR产生的VMExit事件处理例程对SCE位置0。
  8. 处理产生#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相关联的参数,因此我们可以随心所欲地来使用这种方法!

(完)