0X00前言
在最近一次攻防演练的过程当中,笔者在接管"打点"移交的目标后,遇到了一款不太出名(默默无闻)的EDR产品。但是毕竟是EDR,这款安全产品也成功的阻止了笔者企图利用ProcDump访问Lsass内存,使用二开后的Mimikatz转储明文凭证的操作。由于这款安全产品的拦截,笔者也在心里嘀咕,搞定不了大厂的EDR产品,还搞不过你这个三流水平的EDR嘛,话不多说开干!
0X01继承句柄绕过EDR
在花费了半天时间后,如下图所示,我成功了。作为一名渗透测试工程师,笔者知道许多EDR产品利用系统驱动程序,可以Hook安全函数来达到检测和阻止网络攻击的目的。笔者首先想到Obregistercallback函数。那么Obregistercallback是什么呢?Obregistercallback是一个函数或者方法的名称,一般被用来注册回调函数。Microsoft创建该函数的原因可能是许多安全产品会通过执行WinAPI挂钩来重组恶意软件Rootkit。于是,笔者突然想到Hoang Bui博客提到的一种绕过方案(参考链接),即利用csrss.exe继承Lsass.exe,通过PROCESS_ALL_ACCESS成功获得了Lsass.exe 的句柄后,不替换原有的挂钩函数,而是利用在系统的其他部分创建一个新函数,通过修改Ntdll的导入地址表(IAT),我们将函数的指针替换为指向创建的新函数的指针。
补充:在Windows编程中,PROCESS_ALL_ACCESS是一个权限常量,它提供对进程对象的所有可能的访问权限。这意味着使用这个权限标志来打开一个进程时,你将获得对该进程几乎所有操作的权限,包括读写内存、读取信息、修改权限、挂起和恢复线程等。
据笔者判断,遇见的这款EDR系统很可能就是采用了前述技术中的一种或几种来实现特定的监控功能。通常情况下,EDR产品依赖于在系统服务中运行的服务和驱动程序,这些组件多数在内核模式下执行。借助内核模式的权限,驱动程序有能力在RPM调用链的不同层级中实施挂钩。如果系统允许任何驱动程序在没有限制的情况下进行挂钩,那么这将在Windows操作环境中构成严重的安全风险。基于这种考虑,微软设计了内核补丁防护机制(KPP或称Patch Guard),该机制对内核进行全面监视,一旦发现非授权的更改,就会立即触发蓝屏(BSOD)。这一机制涵盖了包括ntoskrnl在内的核心部分,此部分承载了WinAPI在内核级别的实现逻辑。了解到这一点,我们可以肯定的是,EDR系统并未也不可能在ntoskrnl的相关部分进行挂钩任何内核级别的函数,这就意味着用户模式下的RPM和NtReadVirtualMemory调用仍然是潜在的操作点。
0X02Hook的起源
Hoang Bui曾经在博客上探讨过EDR(终端检测与响应)产品监测和阻止RPM/NtReadVirtualMemory调用的机制,并指出"Hook"这一关键技术。Hook(挂钩)是什么?答:挂钩是用于通过拦截软件组件之间传递的函数调用或消息或事件来改变或增强操作系统、应用程序或其他软件组件的行为(百科解释)。也可以换一句话说:挂钩是允许在某个函数执行过程中插入自定义代码,以便操作函数的参数和结果的方法。常见的Hook方法如下所示:
- 绕过挂钩
- 虚拟方法表 (VMT) 挂钩
- 导入/导出地址表 (IAT/EAT) 挂钩
- VEH挂钩
随着检测需求的增加,逃避这些检测所需的方法数量也在增加。对于每种方法,都有优点和缺点。让我们看一下一些优点和缺点。
绕过挂钩
优点:实施简单,速度非常快,并且普遍适用
缺点:.text 部分需要字节修补。内存中的图像与磁盘上的图像不匹配。检出率高
虚拟方法表 (VMT) 挂钩
优点:速度快,实现简单,不需要修补 .text 部分,并且没有本机 API 来检测此方法
缺点:仅适用于虚拟表的功能,仍然需要修改内存(最有可能在.data内部)
导入/导出地址表 (IAT/EAT) 挂钩
与VMT挂钩非常相似 — 但它仅适用于导入/导出表的功能。
VEH 挂钩
优点:不需要修改内存并且是隐秘的。
缺点:实施起来很复杂,而且非常非常慢。
接下来,我们来学习挂钩函数需要采取的步骤,总结如下(附带HHook代码作为参考):
- 找到想要hook的函数的地址
- 注册一个向量异常句柄,将EIP/RIP更改为您自己的函数
- 使用VirtualProtect将PAGE_GUARD修饰符添加到目标函数的地址页
- 等待目标函数被调用,触发PAGE_GUARD_VIOLATION异常
- VEH捕获异常,并将流程重定向到新的函数
#include "LeoSpecial.h" #include <time.h> void(WINAPI *o_Sleep)(DWORD dwMilliseconds) = Sleep; void hk_sleep(DWORD dwMilliseconds) { printf("[+] Hooked! Removing %d milliseconds worth of sleep!\n", dwMilliseconds); return; } int main() { //Hook codes! LeoHook Leo; if (!Leo.Hook((uintptr_t)Sleep, (uintptr_t)hk_sleep)) printf("[-] Failed to hook...\n"); double time_spent = 0.0; clock_t begin = clock(); //Hooked Sleep(100000); clock_t end = clock(); time_spent += (double)(end - begin) / CLOCKS_PER_SEC; printf("[+] Time elpased is %f seconds\n", time_spent); //Unhook if (!Leo.Unhook()) printf("[-] Failed to unhook...\n"); time_spent = 0.0; begin = clock(); Sleep(1000); end = clock(); time_spent += (double)(end - begin) / CLOCKS_PER_SEC; printf("[+] Time elpased is %f seconds\n", time_spent); std::cin.get(); return 0; }
0X03异常句柄
异常句柄是什么?答:在Windows(和 Linux)上就异常处理是一段代码,它提供了处理异常的单一机制;但在Linux中异常处理的数量非常简单且有限;在Windows中却会有无数的异常处理试图避免进程崩溃,并且还允许用户注册自己的向量异常处理程序。
正如笔者之前提到的,在Windows中,用户可以使用WinAPI的AddVectoredExceptionHandler注册自定义 VEH。笔者感兴趣的是 ContextRecord,它使我们能够访问调试寄存器、浮点寄存器、段寄存器、通用寄存器以及控制寄存器。这使得我们可以直接修改EIP/RIP等控制寄存器来实现执行流程的修改。笔者建议通过如下两种使用方法可靠地引发异常,该异常可以被系统的VEH可靠地捕获。
第一种方法:使用标志“PAGE_GUARD”标记页面,这将在访问时触发称为STATUS_GUARD_PAGE_VIOLATION 的异常。如果程序尝试访问保护页内的地址,系统会引发 STATUS_GUARD_PAGE_VIOLATION (0x80000001) 异常。系统还会清除 PAGE_GUARD 修饰符,删除内存页的保护页状态。这实际上意味着,每次捕获 PAGE_GUARD_VIOLATION 时,都需要重新应用PAGE_GUARD修饰符来保留钩子。
第二种方法:使用标志 NO_ACCESS 标记内存页,这将在访问时触发称为 STATUS_ACCESS_VIOLATION 的异常。引入STATUS_SINGLE_STEP异常。从名称上看,并不清楚它是什么类型的例外,如果想知道为什么其中没有VIOLATION一词,我会在下文解释。 STATUS_SINGLE_STEP实际上不是违规,而是一种检测跟踪陷阱的机制或其他单指令机制,表示一条指令已执行。这可以通过将ContextRecord的EFlags设置为|= 0x100来实现。
内存页是可以物理分配的最小内存量,通常为4096字节。当我们使用VirtualProtect在页面上设置保护标志时,我们不能选择性地选择将其应用到页面的哪一部分,而是完全应用到整个页面。如果我们的函数完全对齐在页面的开头并且也是该页面内唯一的数据,那就没问题了。然而,大多数时候,一个函数与多个其他函数/数据共享它们所在的页面,并且通常不从页面的基地址开始。因此,当我们想要在要挂钩的函数上触发异常时,无论我们位于页面的哪个位置,异常都会不加区别地触发。这就是STATUS_SINGLE_STEP发挥作用的地方。使用按位或运算符和0x100设置Eflags,我们可以在页面中一次执行1条指令,直到到达我们想要挂钩的确切地址,然后我们将执行EIP/RIP修改并实现我们需要的挂钩,如下图所示(图片链接)。
为了直观说明,设定笔者的计划挂钩的目标地址为绿色地址,这也是笔者目标函数的起始处。修改后的文本如下:可以通过STATUS_SINGLE_STEP来逐一实施指令,直至抵达目标地址,并且对EIP/RIP进行更改。如果当前的函数不是计划挂钩的函数,而且函数偶然在同一页上,那么STATUS_SINGLE_STEP将会继续行进,直到返回调用该函数的位置,此后,地址将不再处于那一页上,这样就永远无法抵达绿色地址,从而有效地防止挂钩错误的函数。以上是实施PAGE_GUARD/向量异常处理挂钩所需要的全部知识!
0X04总结
以上介绍了在渗透测试的过程当中,遇见不清楚目标EDR厂商的情况下,利用csrss.exe继承Lsass.exe句柄,通过修改Ntdll的导入地址表(IAT)逃避函数挂钩的方法。顺便科普了一下如何利用Hook,以及利用Hook的要点,希望大家能够有所收获!
参考资料:
https://www.cnblogs.com/xwj-pandababy/articles/3464604.html
https://medium.com/@fsx30/bypass-edrs-memory-protection-introduction-to-hooking-2efb21acffd6
https://www.anquanke.com/post/id/215178