如何绕过EDR的内存保护机制

Track-SSG   ·   发表于 2019-01-22 11:01:16   ·   漏洞文章
译文声明

本文是翻译文章,文章原作者fsx30,文章来源:medium.com                                
原文地址:https://medium.com/@fsx30/bypass-edrs-memory-protection-introduction-to-hooking-2efb21acffd6                            


译文仅供参考,具体内容表达以及含义原文为准

 

一、前言

在最近一次内部渗透过程中,我遇到了某款EDR(端点检测与响应)产品。这款产品可以保护lsass的内存空间,导致我无法使用Mmimikatz来导出明文凭据。

ProcDump工具也无法导出lsass内存,如上图所示。

 

二、误入歧途

之前我也是一名恶意软件开发者,因此知道可以使用某些方法,通过驱动绕过这种检测和保护策略。我首先想到的是Obregistercallback,这是许多反病毒软件经常使用的一个函数。由于许多反病毒产品在winapi hook方面处理得不是特别好,因此微软推出了这个回调函数。然而在MSDN页面的底部,大家可以注意到这一句话:“该函数可以在Windows Vista with Service Pack 1 (SP1)、Windows Server 2008以及更高版本上使用”。这里我面对的是Windows Server 2003系统。因此,我们无法使用这个函数来完成该任务。

经过数小时奋战后,我在csrss.exe上使用了一些“黑科技”,尝试通过csrss.exe继承lsass.exe的句柄,最终我成功获得了lsass.exe的一个PROCESS_ALL_ACCESS句柄。我使用的具体方法是滥用csrss.exe来生成一个子进程,然后继承lsass的已有的句柄。

然而,正当我以为大功告成时,却发现事情没有那么简单。这款EDR产品会阻止我们将shellcode注入csrss,也无法通过RtlCreateUserThread创建线程。然而由于某些原因,虽然代码无法以子进程方式执行并继承句柄,但仍然可以通过某种方式获得lsass.exePROCESS_ALL_ACCESS句柄。

这究竟是为什么?

别着急,我们可以尝试先使用一句简单的代码来获得lsass.exe的句柄:

HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, lsasspid);

结果是我竟然能成功获得具备完全控制权的lsass.exe句柄,EDR并没有限制这个行为。此时我才意识到,一开始我的研究方向就是错的,EDR并不禁止我们获取句柄,但获得句柄后的下一步操作会受到严格控制。

 

三、回到正轨

既然我们能获得完整权限的lsass.exe句柄,现在我们可以继续前进,寻找下一个问题。如果我们立即使用该句柄来调用MiniDumpWriteDump(),则会操作失败。

让我们进一步分析这个警告:“Violation: LsassRead”(“违规:LsassRead”)。我并没有读取任何数据,为什么会出现这个提示?我只是想转储进程而已。然而,如果想转储远程进程,MiniDumpWriteDump()中必须要调用某些WINAPI(如ReadProcessMemory,即RPM)。这里我们可以来看一下ReactOS中的MiniDumpWriteDump源码。

如上图所示,(2)处的dump_exception_info()以及其他许多函数都需要依赖(3)处的RPM函数,而(1)处的MiniDumpWriteDump会引用到这些函数,这可能就是我们问题的根源。现在我们的经验就可以派上用场,我们需要知道Windows系统的内部原理,也要知道系统如何处理WINAPI。这里我们以ReadProcessMemory为例来说明。

ReadProcessMemory只是一个封装函数,其中会执行各种健全性检查(如检查是否存在nullptr),这就是RPM的功能。然而,RPM还会调用NtReadVirtualMemory,后者在执行syscall之前会先设置寄存器。syscall指令会通知CPU进入内核模式,然后调用同样名为NtReadVirtualMemory的另一个函数,该函数会实际执行ReadProcessMemory所需完成的操作。

— — — — — -Userland — — — —- — — — | — — — Kernel Land — — — —

RPM — > NtReadVirtualMemory --> SYSCALL->NtReadVirtualMemory

Kernel32 — — -ntdll — — — — — — — — — - — — — — — ntoskrnl

了解这一点后,我们现在必须澄清这款EDR产品如何检测并阻止我们调用RPM/NtReadVirtualMemory。答案非常简单:就是hook。大家可以参考我之前的一篇<a href=”https://medium.com/@fsx30/vectored-exception-handling-hooking-via-forced-exception-f888754549c6″>文章了解关于hook更多信息。简而言之,我们可以利用hook,将代码插入任何函数中,也能获取函数参数以及返回值。我非常确定这款EDR使用了我之前提到的某种hook技术。

然而,大家应该知道大多数EDR产品会使用服务(特别是运行在内核模式中的驱动)。具备内核模式的访问权限后,驱动就可以在RPM的各级调用栈中执行hook操作。然而,如果任何驱动能够轻易hook任何级别的函数,这样也会在Windows环境中留下一个巨大的安全漏洞。因此,微软提出了一个解决方案来避免这种篡改行为,也就是所谓的Kernel Patch Protection(KPP或者Patch Guard)。KPP基本上会扫描内核中的每个级别,如果检测到篡改操作,则会触发BSOD(蓝屏)。这种机制也会覆盖负责WINAPI内核级逻辑的ntoskrnl。了解这些知识后,我们可以肯定EDR并不会hook调用栈中任何内核级的函数,因此可以把重点放在用户模式下的RPM以及NtReadVirtualMemory

 

四、Hook分析

为了澄清这些函数在我们应用程序内存中的具体位置,我们只需使用printf,以函数名为参数,通过%p格式化输出该信息即可,如下所示:

然而,与RPM不同的是,NtReadVirtualMemory并不是ntdll中的导出函数,因此我们无法以正常方式引用该函数。我们必须指定函数的原型,并将ntdll.lib引入我们的工程中,如下图所示:

一切准备就绪后,我们可以运行应用程序,观察输出结果:

现在,我们已经能够获取RPM以及NtReadVirtualMemory的具体地址,然后我会使用我最喜欢的逆向分析工具(即Cheat Engine)来读取相应内存,分析这部分结构。

ReadProcessMemory

NtReadVirtualMemory

RPM函数看上去一切正常,该函数会设置某些栈和寄存器值,然后在Kernelbase中调用ReadProcessMemory(这是另一个话题)。顺着这条路走,我们最终会进入ntdllNtReadVirtualMemory。然而,如果我们观察NtReadVirtualMemory,并且知道detour hook的基本样式,我们就知道这里存在一些异常。该函数的前5个字节已经被修改,其余字节保持原样。我们可以观察该函数附近的其他类似函数来发现这一点。其他函数的格式非常相似,如下所示:

0x4C, 0x8B, 0xD1, // mov r10, rcx; NtReadVirtualMemory0xB8, 0x3c, 0x00, 0x00, 0x00, // eax, 3ch —  即syscall的编号0x0F, 0x05, // syscall0xC3 // retn

这里不同的地方在于syscall的调用号(用来标识内核模式下需要调用哪个WINAPI函数)。然而,对于NtReadVirtualMemory,第一条指令实际上是一条JMP指令,会跳转到内存中的另一个地址。我们可以跟进这个地址:

跟进后我们并没有停留在ntdll模块内,而是位于CyMemdef64.dll模块中,现在我们终于找到目标了。

EDR在原始的NtReadVirtualMemory函数内设置了一条跳转指令,将代码执行流重定向到自己的模块中,然后检查是否存在恶意行为。如果没通过检查,则Nt*函数就会返回一个错误代码,永远不会进入内核模式,导致执行失败。

 

五、绕过限制

现在我们已经澄清EDR如何检测并阻止我们调用WINAPI,但我们如何绕过这个限制?这里可以采用两种解决方案:

重新打补丁

我们知道NtReadVirtualMemory函数的原始形态,因此我们可以使用正确的指令覆盖jmp指令。这样就可以避免我们的调用被CyMemDef64.dll拦截,最终进入EDR无法控制的内核模式。

Ntdll IAT Hook

我们也能够创建自己的函数,具体功能与重新打补丁类似,但我们并没有覆盖被hook的函数,而是在其他地方重新创建该函数。然后,我们可以遍历ntdll的导入地址表(IAT),获取NtReadVirtualMemory对应的指针,将其指向我们新创建的fixed_NtReadVirtualMemory函数。这种方法的优点在于,如果EDR决定检查hook机制是否正常,就会发现hook机制并没有被修改。目标函数永远不会被调用,而ntdll的IAT已经指向其他位置。

 

六、处理结果

我选择使用第一种方法,这种方法比较简单,能让我快速完成任务。然而,第二种方法也不难,我准备在随后几天实现该方法。

经过处理后,我们的AndrewSpecial.exe程序再也不会被拦截,如下图所示:

 

七、总结

本文介绍的方法适用于这款EDR,然而,我们也可以逆向分析其他EDR产品,根据这些产品的限制机制开发通用的绕过方式,这一点并不难,因为这些产品毕竟不能hook所有目标函数(这里再次感谢KPP)。

此外,这种方法同样适用于64位(已在所有Windows版本上测试过)和32位系统(未经测试),大家可以访问此处下载源代码。

本文翻译自 medium.com,原文链接


打赏我,让我更有动力~

0 条回复   |  直到 2019-1-22 | 1797 次浏览
登录后才可发表内容
返回顶部 投诉反馈

© 2016 - 2024 掌控者 All Rights Reserved.