绕过用户模式EDR Hook原理及思路( 三 )


  • ETW事件
  • 内核回调
  • 过滤驱动程序
如果我们执行手动系统调用,而在调用的内核函数经过以上任何一种情况时,EDR可以利用机会检查我们线程的调用堆栈 。通过展开调用堆栈并检查返回地址 , EDR可以看到导致此系统调用的整个函数调用链 。
如果我们执行对kernel32!VirtualAlloc()的正常调用 , 调用堆栈可能如下所示:
绕过用户模式EDR Hook原理及思路

文章插图
图片
在这种情况下,对VirtualAlloc()的调用是由ManualSyscall!mAIn+0x53启动的 。按照调用的顺序,调用堆栈的相关部分如下:
  • ManualSyscall!main+0x53
  • KERNELBASE!VirtualAlloc+0x48
  • ntdll!NtAllocateVirtualMemory+0x14
  • nt!KiSystemServiceCopyEnd+0x25
这告诉我们(或者EDR)可执行文件(ManualSyscall.exe)调用了VirtualAlloc(),这个函数调用了NtAllocateVirtualMemory(),然后执行了一个系统调用以切换到内核模式 。
现在让我们看看进行直接系统调用时的调用堆栈:
绕过用户模式EDR Hook原理及思路

文章插图
图片
调用堆栈的相关部分按顺序如下:
  • ManualSyscall!direct_syscall+0xa
  • nt!KiSystemServiceCopyEnd+0x25
在这里,很明显内核转换是由ManualSyscall.exe内部的代码触发的 , 而不是ntdll 。但是,这有什么问题吗?
嗯,在像linux这样的系统上 , 应用程序直接发起系统调用是完全正常的 。但请记住我提到过Windows版本之间系统调用号会发生变化吗?结果,编写依赖于直接系统调用的Windows软件是非常不切实际的 。由于ntdll已经为您实现了每个系统调用,几乎没有理由进行手动系统调用 。除非你正在编写绕过EDR钩子的恶意软件 。你是在写用于绕过EDR钩子的恶意软件吗?
由于直接系统调用是恶意活动的强有力指标,更复杂的EDR系统将记录源自于ntdll之外的系统调用的检测情况 。说实话,你仍然可以在很多时候逃脱检测 , 但这有什么乐趣呢?
4.间接系统调用 
大多数EDR在Nt函数的开头写入它们的钩子 , 覆盖SSN但保留系统调用指令不变 。这使我们能够利用ntdll已经提供的系统调用指令,而不是引入我们自己的 。我们只需自己设置r10和eax寄存器,然后跳转到被挂钩的ntdll函数内的系统调用指令(位于EDR钩子之后) 。
绕过用户模式EDR Hook原理及思路

文章插图
图片
注意:我们并不严格需要test或jnz指令,它们只是为了向后兼容 。一些古老的CPU不支持syscall指令,而是使用int 0x2e 。test指令检查系统调用是否启用,如果没有启用,则回退到软中断 。如果我们希望支持这些系统,我们可以自己执行检查,然后根据需要跳转到int 0x2e指令(也位于Nt函数内) 。
就像直接系统调用一样,我们仍然需要系统调用号放入eax寄存器,但我们可以使用在直接系统调用部分详细介绍的所有相同技术 。
通过这种方式设置系统调用将给我们一个类似以下的调用堆栈:
绕过用户模式EDR Hook原理及思路

文章插图
图片
正如你所看到的 , 调用堆栈现在看起来好像是来自ntdll!NtAllocateVirtualMemory()而不是我们的可执行文件,因为从技术上讲确实是这样的 。
我们可能会遇到的一个问题是,如果EDR钩子或覆盖了Nt调用中的syscall指令的部分 。我从未见过这种情况发生 , 但理论上可能会发生 。在这种情况下,我们可以跳转到另一个未被挂钩的Nt函数内的syscall指令 。这仍然可以绕过仅验证调用名称是否来自ntdll的EDR,但对于检查内核函数是否与来自ntdll的函数相匹配的这种检查通常都会失败 。
更大的问题是 , 如果EDR检查的不仅仅是第一个返回地址 。不仅仅是系统调用的来源,还有执行系统调用的函数是谁调用的 。如果我们正在从位于动态分配内存中的某个shellcode进行间接系统调用,那么EDR将会察觉到 。来自于有效PE节(exe或DLL内存)之外的调用是相当可疑的 。
此外,由于函数被EDR挂钩,EDR的钩子预期会出现在调用堆栈中 。实际上,我并不确定哪些EDR,如果有的话,会检查这一点 。但是,正如你在这里看到的 , 从调用堆栈中很明显我们绕过了EDR的钩子 。


推荐阅读