一、前言
在最近一次内部渗透过程中,我遇到了某款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.exe
的PROCESS_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
(这是另一个话题)。顺着这条路走,我们最终会进入ntdll
的NtReadVirtualMemory
。然而,如果我们观察NtReadVirtualMemory
,并且知道detour hook的基本样式,我们就知道这里存在一些异常。该函数的前5个字节已经被修改,其余字节保持原样。我们可以观察该函数附近的其他类似函数来发现这一点。其他函数的格式非常相似,如下所示:
0x4C, 0x8B, 0xD1, // mov r10, rcx; NtReadVirtualMemory
0xB8, 0x3c, 0x00, 0x00, 0x00, // eax, 3ch — 即syscall的编号
0x0F, 0x05, // syscall
0xC3 // 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位系统(未经测试),大家可以访问此处下载源代码。