Patchguard: Detection of Hypervisor Based Introspection [P2]
No Errata For U!
如果您尚未阅读,请先阅读第一部分,其中概述了Patchguard使用的三个巧妙技巧。
KiErrata420Present
可以使用Hypervisor拦截LSTAR MSR以捕获读写操作。这是在大多数现代x86操作系统中挂钩系统调用的最常见和最高效的方法。然而,与我在线阅读的内容相反,如果不妥善处理,这可能会带来许多潜在的Hypervisor检测向量。通过在特权代码中使用一些巧妙技巧,我们可以可靠地确定LSTAR MSR上是否存在钩子,也就是说,如果Hypervisor中尚未实施适当的预防措施。从Windows 10 1903版本18362开始,微软添加了几种LSTAR钩子检测技术。
其中一种较简单的LSTAR钩子检测并未被赋予“errata”的迷因名称,也许它还不够好🙁——所以我们称它为KiErrata420Present(可能与微软内部称呼相差不远?)。
我在下面概述了检测过程:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
|
KiErrata420Present:
cli ; disable interrupts
mov r9d, 0C0000082h ;
mov ecx, r9d ;
rdmsr ; read LSTAR MSR value
shl rdx, 32 ;
or rax, rdx ; store LSTAR value in rax
lea rdx, [rdi+87Ah] ; store temp LSTAR value in rdx read from pg context
mov rbx, rax ; rbx = original LSTAR value
mov rax, rdx ; rax = temp LSTAR value
shr rdx, 32 ;
wrmsr ; write temporary LSTAR MSR value
mov r14d, 20000h ;
lea rax, [rdi+87Ch] ; rax = stub to execute syscall
mov rsi, 0A3A03F5891C8B4E8h ; rsi = constant to obfuscate pg context pointer
test [rdi+994h], r14d ; test if should store pg check data?
jnz short trigger_syscall ; if nz, skip tracing
mov r8, gs:KPCR.CurrentPrcb ;
lea rdx, [rdi+rsi] ;
mov rcx, [rdi+4C0h] ;
mov [rcx], rdx ;
mov rcx, [rdi+4C8h] ; store pg check related data
mov [rcx], r8 ;
mov rcx, [rdi+4D0h] ;
mov [rcx], r9 ;
mov rcx, [rdi+4D8h] ;
mov qword ptr [rcx], 112h ;
trigger_syscall:
call KeGuardDispatchICall ; dispatch call to syscall instruction stub
test [rdi+994h], r14d ; test if pg check should be traced?
jnz short restore_lstar ; if nz, skip tracing
mov rax, [rdi+4C0h] ;
mov [rax], rsi ;
mov rax, [rdi+4C8h] ;
mov [rax], r13 ; wipe pg check related data
mov rax, [rdi+4D0h] ;
mov [rax], r13 ;
mov rax, [rdi+4D8h] ;
mov [rax], r13 ;
restore_lstar:
mov rdx, rbx ; restore original LSTAR value
mov rax, rbx ;
shr rdx, 32 ;
mov ecx, 0C0000082h ;
wrmsr ; write original LSTAR MSR value
sti ; reenable interrupts
|
这个检查确实非常简单。它临时用自己临时的系统调用处理程序覆盖系统的LSTAR MSR值,并在之后恢复原始的LSTAR MSR值。我怎么确定这一点?让我们进一步深入探究。
首先,让我们找出写入LSTAR MSR的临时值是什么:
1
2
3
4
|
lea rdx, [rdi+87Ah] ; store temp LSTAR value in rdx read from pg context
mov rbx, rax ; rbx = original LSTAR value
mov rax, rdx ; rax = temp LSTAR value
shr rdx, 32 ;
|
如我们所见,写入的临时LSTAR值是RDI+0x87A处的地址。对patchguard回调稍有了解,我们知道RDI寄存器保存当前“patchguard上下文”的临时地址。利用这些知识,我们可以轻松确定在patchguard初始化例程中context+0x87A处写入的内容:
1
|
mov byte ptr [r14+87Ah], 0C3h ; store RET instruction
|
太好了,这是返回指令的操作码,非常有趣!
接下来,让我们找出KeGuardDispatchICall处的调用是什么。如您所知,KeGuardDispatchICall通过分支到RAX中给出的指令指针来工作。那么让我们检查一下RAX的来源:
1
|
lea rax, [rdi+87Ch] ; rax = stub to execute syscall
|
最后一步,确定在patchguard初始化例程中context+0x87C处写入的内容:
1
2
|
mov eax, 050Fh
mov [r14+87Ch], ax ; store SYSCALL instruction
|
我们看到的050Fh是什么意思?那就是SYSCALL指令的操作码!我想我们现在已经知道发生了什么。但让我们用一些伪代码简化一下:
1
2
3
4
5
6
|
_disable();
OriginalSyscall64 = __readmsr(MSR_LSTAR);
__writemsr(MSR_LSTAR, &PgContext->DummySyscallHandler); // C3 -> ret
KeGuardDispatchICall(&PgContext->Syscall); // 0F 05 -> syscall
__writemsr(MSR_LSTAR, OriginalSyscall64);
_enable();
|
简洁!它只是执行SYSCALL指令,然后立即从处理程序返回。
这对大多数利用LSTAR钩子的Hypervisor非常有效,并且更擅长烦扰那些尽力阻止客户机篡改LSTAR MSR的Hypervisor。在许多幼稚的LSTAR MSR钩子实现中,开发人员会完全禁止对LSTAR MSR的写入,这在这种情况下会导致故障,因为在这种情况下执行系统调用之前上下文未设置。Hyperbone的LSTAR MSR钩子就是这种实现的一个例子。
这对Hypervisor开发人员来说是一个令人沮丧的问题。然而,他们应该担心,因为对于Hypervisor开发人员来说,这个简单问题有一个相当简单的解决方案。解决方案是让客户机覆盖LSTAR MSR,并有效地影子化原始值。
那么这意味着客户机可以强制我们取消钩子???
是的,你说得对。然而,在这种情况下,我们可以在之后恢复我们的钩子,并且在几乎所有其他情况下也是如此,除非客户机自己创建了他们的系统调用钩子实现。不幸的是,在Windows中,patchguard有一个单独的检查来断言LSTAR MSR的值未被篡改。因此,实际上,没有客户机软件会在Windows上永久覆盖您宝贵的LSTAR MSR,除非他们禁用了patchguard,这完全是可能的,但也非常容易被捕获。此外,这些情况都可以在VMM中监控并根据需要规避。
对于这种情况,我们可以这样规避检测:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
|
VMM_EVENT_STATUS
HVAPI
VmmHandleMsrRead(
_In_ PVIRTUAL_CPU Vcpu
)
{
// ...
//
// Hide our LSTAR syscall hook handler address.
//
case MSR_LSTAR:
if (Vcpu->OriginalLSTAR) {
MsrValue = Vcpu->OriginalLSTAR;
} else {
MsrValue = __readmsr(MSR_LSTAR);
}
break;
// ...
}
VMM_EVENT_STATUS
HVAPI
VmmHandleMsrWrite(
_In_ PVIRTUAL_CPU Vcpu
)
{
// ...
//
// Let the guest overwrite our hook to avoid possible detection.
//
// If and only if the guest is writing the original LSTAR, we replace
// the MSR value with the hook LSTAR value.
//
// N.B. We do this to get around one of PatchGuard's syscall hook
// detections which works like this:
//
// _disable();
// OriginalSyscall64 = __readmsr(MSR_LSTAR);
// __writemsr(MSR_LSTAR, &PgCtx->PgSyscallDummy); // C3 -> ret
// KeGuardDispatchICall(&PgCtx->SyscallOpcode1); // 0F 05 -> syscall
// __writemsr(MSR_LSTAR, OriginalSyscall64);
// _enable();
//
case MSR_LSTAR:
if (MsrValue == Vcpu->OriginalLSTAR) {
MsrValue = Vcpu->HookLSTAR;
}
__writemsr(MSR_LSTAR, MsrValue);
break;
// ...
}
|
我们通过在读取和写入LSTAR MSR时影子化原始LSTAR值,完全解决了这个问题。
KiErrata1337Present
运用一些批判性思维,我利用从Patchguard派生的一些技巧,想出了自己相当离经叛道的LSTAR检测方法。我称这个为KiErrata1337Present,无耻地源自微软迷因“errata”命名方案,用于他们其他很酷的patchguard检查。
那些研究过现代64位系统调用处理程序在Linux和/或Windows中的人可能已经注意到它们以SWAPGS指令开始和(有时)结束。SWAPGS指令交换当前GS基址寄存器(IA32_GS_BASE)值与包含在MSR地址C0000102H(IA32_KERNEL_GS_BASE)中的内核GS基址寄存器值。
系统调用处理程序中紧接SWAPGS指令的指令是一个分段MOV指令。以下是KiSystemCall64的一瞥:
1
2
3
4
|
KiSystemCall64 proc near
swapgs ; swap GS base with IA32_KERNEL_GS_BASE
mov gs:KPCR.UserRsp, rsp ; store user mode stack in processor control region
mov rsp, gs:KPCR.Prcb.RspBase ; set the kernel stack from processor control region
|
很酷,知道这些细节后,我们知道我们可以搞乱GS基址以在系统调用处理程序内部引起页面错误(#PF)。等等,什么?
WTF 为什么你会想要故意页面错误???
你这么想是理智的。我们想要在系统调用处理程序内部错误的原因是为了读取系统调用处理程序的真实RIP。这是一个非常重要的细节!
好吧,让我们尝试故意生成一个页面错误(#PF):
1
2
3
4
5
6
7
8
9
10
11
|
KiErrata1337Present:
swapgs ; swapgs to emulate coming from user mode
mov ecx 0C0000102h ;
xor eax, eax ; set KERNEL_GS_BASE MSR to zero
xor edx, edx ;
wrmsr ;
syscall ; execute the syscall instruction to trigger fault
ret
|
砰!我们页面错误了——顺便说一句,这是件好事!
然而,一开始就有几个问题:Windows中的原始页面错误处理程序只会爆炸并错误检查,如果我们不恢复原始的GS基址和内核GS基址值,操作系统也会在下一个上下文切换中爆炸。所以我们需要临时挂钩中断描述符表(IDT),并备份GS基址,太简单了!
临时挂钩中断描述符表IDT的步骤如下:
- 禁用中断
- 保存原始IDT
- 加载我们的临时IDT
- 做你的事
- 恢复原始IDT
- 重新启用中断
以下是一些实现上述步骤的伪代码,包括我们需要的页面错误#PF异常钩子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
TempIdtr.Limit = sizeof(TempIdt) - 1;
TempIdtr.Base = (UINT64)&TempIdt[0];
for (IdtEntry in KPCR->IdtBase)
TempIdt[i] = IdtEntry; // Fill in temporary IDT
_disable(); // Disable interrupts
__sidt(&OriginalIdtr); // Backup original IDT
__lidt(&TempIdtr); // Load our temporary hook IDT
// Hook page fault handler.
TempIdt[PF] = PageFaultHookHandler;
// Trigger syscall that will purposely page fault!
KiErrata1337Present(); // This must be lean enough not to timeout watchdog!
__lidt(&OriginalIdtr); // Restore the original IDT.
_enable(); // Re-enable interrupts.
|
我们的页面错误处理程序目前什么都不做,只是从中断返回,我们测试时这样做。我们使用IRET指令来实现这一点。如果您还不熟悉IRET指令,请阅读有关它的内容,因为理解它对于之后我们实际创建检测非常重要!
这是我们无聊的页面错误钩子处理程序:
1
2
3
|
PageFaultHookHandler:
add rsp, 8 ; skip fault code on stack
iretq ; return from interrupt
|
现在我们设置了一个页面错误处理程序的钩子,让我们修复我们的KiErrata1337Present例程以备份和恢复原始的GS基址:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
KiErrata1337Present:
mov ecx, 0C0000101h ; read original GS_BASE MSR
rdmsr ;
push rdx ; backup original GS_BASE MSR
push rax ;
mov ecx, 0C0000102h ; read original KERNEL_GS_BASE MSR
rdmsr ;
push rdx ; backup original KERNEL_GS_BASE MSR
push rax ;
swapgs ; swapgs to emulate coming from user mode
xor eax, eax ;
xor edx, edx ; set KERNEL_GS_BASE MSR to zero
wrmsr ;
syscall ; execute syscall instruction which executes swapgs immediately
mov ecx, 0C0000102h ;
pop rax ;
pop rdx ; restore original KERNEL_GS_BASE MSR
wrmsr ;
mov ecx, 0C0000101h ;
pop rax ;
pop rdx ; restore original GS_BASE MSR
wrmsr ;
ret ; return back to caller
|
它工作了!现在来看多汁的检测部分!
就像我之前提到的,我们想要在系统调用处理程序中引起错误的全部原因是为了在错误时从机器陷阱帧读取RIP。那部分很容易。但是如果我们处于页面错误处理程序中,如何跳回我们的KiErrata1337Present例程?嗯,幸运的是,SYSCALL指令为我们保存了一个返回地址,这实际上是为了它的对应物SYSRET。当SYSCALL指令执行时,它会将下一条指令的地址存储在RCX寄存器中。
我们可以看到SYSCALL指令的操作如下:
1
2
3
4
5
|
RCX ← RIP; (* Will contain address of next instruction *)
RIP ← IA32_LSTAR;
R11 ← RFLAGS;
RFLAGS ← RFLAGS AND NOT(IA32_FMASK);
// .... memes
|
那么我们如何返回?简单,我们覆盖机器帧上的RIP地址。希望你现在理解IRET的工作原理,如果你还不熟悉的话,因为现在是我们利用它的操作来完成检测的时候。让我们聪明一点,使用XCHG指令一石二鸟:
1
2
3
4
|
PageFaultHookHandler:
add rsp, 8 ; skip fault code on stack
xchg qword [rsp], rcx ; xchg trap frame RIP with syscall return address in RCX
iretq ; return from interrupt
|
我们利用XCHG指令的优势,将RCX中的系统调用返回地址与陷阱帧中的RIP交换。这使我们能够有效地将真实的系统调用处理程序地址存储在RCX中,并且仍然分支到我们的SYSCALL指令之后的下一条指令。
差不多就是这样。那是一个疯狂的方法,不是吗?将所有内容整合在一起看起来像这样:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
// detect.c
VOID
DoTheThing(
VOID
)
{
KIDTENTRY64 TempIdt[19];
X64_DESCRIPTOR TempIdtr;
PVOID SyscallHandler;
TempIdtr.Limit = sizeof(TempIdt) - 1;
TempIdtr.Base = (UINT64)&TempIdt[0];
RtlCopyMemory(TempIdt, KeGetPcr()->IdtBase, TempIdtr.Limit + 1);
_disable(); // Disable interrupts
__sidt(&OriginalIdtr); // Backup original IDT
__lidt(&TempIdtr); // Load our temporary hook IDT
// Hook page fault handler.
TempIdt[X86_TRAP_PF].OffsetLow = (UINT16)(UINTN)PageFaultHookHandler;
TempIdt[X86_TRAP_PF].OffsetMiddle = (UINT16)((UINTN)PageFaultHookHandler >> 16);
TempIdt[X86_TRAP_PF].OffsetHigh = (UINT32)((UINTN)PageFaultHookHandler >> 32);
// Trigger syscall that will purposely page fault!
SyscallHandler = KiErrata1337Present();
__lidt(&OriginalIdtr); // Restore the original IDT.
_enable();
|