前言
在hypervisor中,特别是安全软件的hv,接管syscall无非三种办法,MSR HOOK,EPT/NPT HOOK,EFER HOOK。本文来炒旧饭,对这些方法进行总结以及来说一下怎么检测这些方法。
注意,这个是科普文.相关技术其实已经快十几年了.不喜勿喷
系统syscall过程
大部分人对这块不是很理解(真不是syscall免杀),所以我们需要先复习一下syscall的过程.介绍一下系统是怎么从r3切换到r0的:
在经过一系列复杂的包装后,到ntdll.dll的调用会发出syscall指令,我们以ntvirtualalloc为视角,我们简单看一下这个指令在CPU里面会干什么:
首先在ntdll进行syscall后:
会进入CPU的微码执行过程(amd手册的,intel手册的懒得找了):
让我们说一下, 对于初学者来说,我们只需要知道
-
rcx被设置为了rip
RCX := RIP; (* Will contain address of next instruction *)
-
并且 rip 已经等于一个msr寄存器的值,这个是 msr_lstar(非常重要)
RIP := IA32_LSTAR;
剩下的就交给系统了。你可能会看到,学免杀时候,syscall的是需要一个index的, 那么这里面syscall的sysnumber呢?ida里面截图的是mov eax, 74h去哪里了。这是因为,syscall是CPU指令,哪些字段用什么,是系统 规定的。如windows用eax, linux用rax
这也是我们接下来说的:
windows在初始化的时候会给msr_lstar寄存器设置一个地址,用于接受syscall的ip:
这样r3发syscall指令其实就走到了系统的 KiSystemCall64这个函数里面
对于初学者来说,只需要知道KiSystemCall64在干了参数数量识别(通过用eax的值 比对ssdt/sssdt表)
大于4个参数用栈复制
外就不需要其他的了
msr hoook
这个是非常经典的经典款方法,起因是在win7 x64后的PG的出现, 再也不能和x32一样hook KiFastCallEntry了, 因为PageGuard的出现, 会定时检查msr_lstar,读一次,看看是不是被换了.
但是由于有hypervisor了,hv能接管寄存器访问.所以我们可以:
接管msr_lstar寄存器的访问,如AMD是通过设置msr_bitmap字段进行接管
设置amd的 msr权限映射表(permissions map) 大小是2个PAGE_SIZE
在amd手册里面MSR Intercepts有提到,如果要拦截中断
关系如下:
000h–7FFh 0000_0000h–0000_1FFFh
800h–FFFh C000_0000h–C000_1FFFh
1000h–17FFh C001_0000h–C001_1FFFh
1800h–1FFFh Reserved
比如IA32_MSR_EFER的msr值是C0000080h
那么就在
800h - FFFh 之间
要控制IA32_MSR_EFER 计算出在bitmap里面的偏移,然后置位为1
(IA32_MSR_EFER - C0000000h) 2(控制位) + 800h 8(CHAR_BIT)
在msr_lstar访问的时候给假的地址(原来的地址)
void vt_shadow_msr(ULARGE_INTEGER* fake_msr_value, bool is_read, uintptr_t origin_msr, _guest_status* guest_context) {
if (is_read) {
if (fake_msr_value->QuadPart == NULL) {
fake_msr_value->QuadPart = origin_msr;
}
guest_context->guest_context->Rax = fake_msr_value->LowPart;
guest_context->guest_context->Rdx = fake_msr_value->HighPart;
}
else {
fake_msr_value->LowPart = guest_context->guest_context->Rax & MAXUINT32;
fake_msr_value->HighPart = guest_context->guest_context->Rdx & MAXUINT32;
}
}
....
msr exit handler:
if (msr_id == ia32_lstar) {
vt_shadow_msr(&guest_status->fake_lstar_msr_value, access_type, g_orig_system_call, guest_status);
}
这样,所有的call都会进我们的fake_kisystemcall了,剩下的就是手写模拟一下kisystemcall64:
最后把系统的SSDT表换成自己的就行:
这样就能挂钩任意API并且监控任意API了
微软如何检测的
在微软新版本系统中
翻开微软的内核,有如下函数
KiErrata361Present
KiErrataSkx55Present
KiErrata704Present
…
这些函数有如下特点:
名字莫名其妙
调用者也莫名其妙,比如KiErrata361Present在一个IDA分析都要半个小时的函数里面调用.
大部分都是微软检测虚拟机的函数
KiErrata704Present:
他长这样:
asm_pg_KiErrata704Present proc
mov ecx, 0C0000084h
rdmsr
push rdx
push rax
and eax, 0FFFFFEFFh
wrmsr
pushfq
or qword ptr[rsp], 100h
popfq
syscall ; Low latency system call
mov r10, rcx
mov ecx, 0C0000084h
pop rax
pop rdx
wrmsr
mov rax, r10
ret
asm_pg_KiErrata704Present endp
让我们简单的说一下,这个函数首先保存FMASK的值,然后设置MSR值,以便SYSCALL操作不会修改TF.然后修改TF位.让下一步指令单步执行(#DB异常),DB异常后果就是如果你hook了lstar,断点就打在了你的kisystemcall64上了.你的位置会被直接暴露.然后就是蓝屏(蓝屏代码在系统的DB异常处理函数里)
KiErrata361Present:
asm_pg_KiErrata361Present proc
mov ax,ss
pushfq
or qword ptr[rsp],100h
popfq
mov ss,ax
db 0f1h ;icebp
pushfq
and qword ptr[rsp],0FFFFFEFFh
popfq
ret
asm_pg_KiErrata361Present endp
这个方法借鉴了一个drew的方法
https://howtohypervise.blogspot.com/2019/01/a-common-missight-in-most-hypervisors.html
这个原理的核心是”mov ss,XXX”导致的异常会推迟.按道理来说如果没有vmexit发生则会在vmexit指令那边执行延迟异常.但是如果有vmexit而且你的垃圾玩具机没有处理的话则会在vmexit指令的下面一个指令执行延迟异常.导致看起来有一个指令被跳过一样
关于这个异常简单说明一下,icebp这个指令是intel的一个奇葩,他在intel是会走DB异常的,而且会要求CPU优先处理他的异常(因为他的type是ia32_prisw_exception,比普通的DB异常高一级).但是如果他跟mov ss结合在一起呢?
看我的CVE-2018-8897学习笔记:
https://key08.com/index.php/2021/03/13/968.html
mov ss会产生一个异常,但是这个异常会在下一个指令执行的时候再抛出.因此 他首先mov ss了,这样下一个指令才会执行异常, 到icebp的时候, mov ss造成的异常就应该被抛出了。但是同时icebp也会造成一个异常,而且异常优先级哎比mov ss的高,你说巧不巧.这样CPU就会处理icebp的异常而不处理mov ss的异常,但是mov ss的异常是必须要求dr6的single_instruction bit位(也就叫做DR6.BS位)为1的,要不然就炸。很遗憾的是icebp的异常他不会设置这个bs位,导致直接炸虚拟机.很多时候虚拟机在2004跑起来了结果一阵子就虚拟机guest机异常了就是这个毛病
缺点
在启动kvashadow后,win10 1809之前也不是没办法,可以用MMCreateShadowMapping映射自己的kisystemcall64到shadowmapping里面,但是之后,没了!也就是说,在之后这个方法会跟KVA-Shadow不兼容,所以要么关掉它-这意味着你的用户会再次受到熔断漏洞的问题
扩展: EFER HOOK
让我们加点料继续说一下一些有趣的事情.再次回顾上面的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;
是的,当CPU的efer.sec被清空的时候,就会抛#UD异常
而HV是可以接管UD的,所以就能利用efer让syscall抛异常,再在异常里面接管它.
EFER是MSR中的一员,它的地址是0xC0000080,它是x64提体系的基石。当EFER.LME=1时,表示要开起IA-32e模式。可是,此时并不代表已经进入了IA-32e模式,因为开启IA-32e模式后必须要开启分页内存管理机制,也就是说,当EFER.LME=1且CR0.PG=1时,处理器会将EFER.LMA置为1,当其成功置为1以后,才表示IA-32e模式处于激活状态。
处理器在上电开始运行时或复位后是处于实地址模式,CR0控制寄存器的PE标志用来控制处理器处于实地址模式还是保护模式。标志寄存器(EFLAGS)的VM标志用来控制处理器是在虚拟8086模式还是普通保护模式下,EFER寄存器的LME用来启用IA-32e模式
所以,我们可以先初始化的时候接管异常:
vcpu->stack->guest_vmcb.control.InterceptException |= (1UL <<
amd64_invalid_opcode);
然后修改guest的efer,去掉sec
vcpu->stack->guest_vmcb.state_save.Efer = function::_huoji_readmsr(amd64_efer) & ~amd64_efer_sec;
这样在异常里面模拟一次syscall就行了,不过这部分代码丢了,就截图一下网上的吧:
这块最开始的来源是:;
https://revers.engineering/syscall-hooking-via-extended-feature-enable-register-efer/
问题
实际用性能堪忧.因为系统每时每刻都在进行syscall,每次syscall CPU都要抛异常,速度已经满到离谱的了
原文始发于微信公众号(冲鸭安全):从核晶入手浅谈一下syscall这块的攻防对抗
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论