原文链接
原文链接:https://vanmieghem.io/blueprint-for-evading-edr-in-2022/
大约两年前,我辞去了全职红队操作员的工作。然而,它仍然是一个非常贴近我心的专业领域。几周前,我正在寻找一个新的副项目,并决定恢复我的一个旧的红队爱好:绕过/逃避端点保护解决方案。
在这篇文章中,我想列出一系列可用于绕过行业领先的企业端点保护解决方案的技术。这纯粹是为了(道德)红队队员等的教育目的,所以我决定不公开发布源代码。这篇文章的目的是让安全行业的广大受众都能接触到,而不是深入研究每种技术的细节。相反,我会参考其他比我更深入的文章。
在对手模拟中,“初始访问”阶段的一个关键挑战是绕过企业端点上的检测和响应能力 (EDR)。商业命令和控制框架向红队操作员提供不可修改的 shellcode 和二进制文件,这些文件由端点保护行业大量签名,为了执行该植入,该 shellcode 的签名(静态和行为)需要被混淆。
在这篇文章中,我将介绍以下技术,最终目标是执行恶意 shellcode,也称为(shellcode)加载程序:
- Shellcode 加密
- 减少熵
- 逃离(本地)反病毒沙箱
- 导入表混淆
- 禁用 Windows 事件跟踪 (ETW)
- 规避常见的恶意 API 调用模式
- 直接系统调用和规避“系统调用标记”
- 拆除挂钩
ntdll.dll
- 欺骗线程调用堆栈
- 信标的内存加密
- 自定义反射加载器
- 可扩展配置文件中的 OpSec 配置
1、Shellcode加密
让我们从一个基本但重要的话题开始,静态 shellcode 混淆。在我的加载程序中,我利用了 XOR 或 RC4 加密算法,因为它易于实现并且不会留下大量加载程序执行的加密活动的外部指标。用于混淆 shellcode 静态签名的 AES 加密会在二进制文件的导入地址表中留下痕迹,这增加了怀疑。在此加载程序的早期版本中,我已经让 Windows Defender 专门触发了 AES 解密函数(例如CryptDecrypt
、等)。CryptHashData
CryptDeriveKey
dumpbin /imports 的输出,这是二进制文件中仅使用 AES 解密函数的简单赠品。
2. 减少熵
许多 AV/EDR 解决方案在评估未知二进制时考虑二进制熵。由于我们正在加密 shellcode,我们的二进制文件的熵相当高,这清楚地表明二进制文件中的代码部分被混淆了。
有几种方法可以减少二进制的熵,两种简单的方法是:
- 将低熵资源添加到二进制文件中,例如(低熵)图像。
- 添加字符串,比如英文字典或者一些
"strings C:\Program Files\Google\Chrome\Application\100.0.4896.88\chrome.dll"
输出。
一个更优雅的解决方案是设计和实现一种算法,将 shellcode 混淆(编码/加密)成英文单词(低熵)。那将用一块石头杀死两只鸟。
3. 逃离(本地)AV 沙箱
许多 EDR 解决方案将在本地沙箱中运行二进制文件几秒钟以检查其行为。为避免影响最终用户体验,他们无法承受超过几秒钟的检查二进制文件(我曾见过 Avast 过去最多需要 30 秒,但那是个例外)。我们可以通过延迟执行我们的 shellcode 来滥用这个限制。简单地计算一个大素数是我个人的最爱。您可以更进一步,确定性地计算一个质数,并将该数字用作加密 shellcode 的(一部分)密钥。
4.导入表混淆
您希望避免可疑的 Windows API (WINAPI) 出现在我们的 IAT(导入地址表)中。此表包含您的二进制文件从其他系统库导入的所有 Windows API 的概述。可以在此处找到可疑 API 列表(因此通常由 EDR 解决方案检查) 。通常,这些是VirtualAlloc
, VirtualProtect
, WriteProcessMemory
,CreateRemoteThread
等SetThreadContext
。运行dumpbin /exports <binary.exe>
将列出所有导入。在大多数情况下,我们将使用直接系统调用来绕过可疑 WINAPI 调用的两个 EDR 挂钩(请参阅第 7 节),但对于不太可疑的 API 调用,此方法工作得很好。
我们添加 WINAPI 调用的函数签名,获取 WINAPI 的地址,ntdll.dll
然后创建一个指向该地址的函数指针:
1 2 3 4 5 6 7 8 9 10 |
<strong>typedef</strong> <strong>BOOL</strong> (WINAPI <strong>*</strong> pVirtualProtect)(LPVOID lpAddress, SIZE_T dwSize, DWORD flNewProtect, PDWORD lpflOldProtect); pVirtualProtect fnVirtualProtect; <strong>unsigned</strong> <strong>char</strong> sVirtualProtect[] <strong>=</strong> { 'V','i','r','t','u','a','l','P','r','o','t','e','c','t', 0x0 }; <strong>unsigned</strong> <strong>char</strong> sKernel32[] <strong>=</strong> { 'k','e','r','n','e','l','3','2','.','d','l','l', 0x0 }; fnVirtualProtect <strong>=</strong> (pVirtualProtect) GetProcAddress(GetModuleHandle((LPCSTR) sKernel32), (LPCSTR)sVirtualProtect); <em>// call VirtualProtect</em> fnVirtualProtect(address, dwSize, PAGE_READWRITE, <strong>&</strong>oldProt); |
使用字符数组混淆字符串会将字符串分割成更小的部分,使它们更难以从二进制文件中提取。
该调用仍将针对ntdll.dll
WINAPI,并且不会绕过 WINAPI 中的任何挂钩ntdll.dll
,但纯粹是为了从 IAT 中删除可疑函数。
5. 禁用 Windows 事件跟踪 (ETW)
许多 EDR 解决方案广泛利用 Windows 事件跟踪 (ETW),特别是 Microsoft Defender for Endpoint(以前称为 Microsoft ATP)。ETW 允许对进程的功能和 WINAPI 调用进行广泛的检测和跟踪。ETW 在内核中有组件,主要用于为系统调用和其他内核操作注册回调,但也包含一个用户态组件,它是ntdll.dll
(ETW 深度潜水和攻击向量)的一部分。因为ntdll.dll
是一个 DLL 加载到我们的二进制进程中,所以我们可以完全控制这个 DLL 和 ETW 功能。用户空间中的ETW有很多不同的绕过方式,但最常见的是修补函数 EtwEventWrite
调用它来写入/记录 ETW 事件。我们在 中获取它的地址ntdll.dll
,并将它的第一条指令替换为返回 0 ( SUCCESS
) 的指令。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
<strong>void</strong> <strong>disableETW</strong>(<strong>void</strong>) { <em>// return 0</em> <strong>unsigned</strong> <strong>char</strong> patch[] <strong>=</strong> { 0x48, 0x33, 0xc0, 0xc3}; <em>// xor rax, rax; ret</em> ULONG oldprotect <strong>=</strong> 0; <strong>size_t</strong> size <strong>=</strong> <strong>sizeof</strong>(patch); HANDLE hCurrentProc <strong>=</strong> GetCurrentProcess(); <strong>unsigned</strong> <strong>char</strong> sEtwEventWrite[] <strong>=</strong> { 'E','t','w','E','v','e','n','t','W','r','i','t','e', 0x0 }; <strong>void</strong> <strong>*</strong>pEventWrite <strong>=</strong> GetProcAddress(GetModuleHandle((LPCSTR) sNtdll), (LPCSTR) sEtwEventWrite); NtProtectVirtualMemory(hCurrentProc, <strong>&</strong>pEventWrite, (PSIZE_T) <strong>&</strong>size, PAGE_READWRITE, <strong>&</strong>oldprotect); memcpy(pEventWrite, patch, size <strong>/</strong> <strong>sizeof</strong>(patch[0])); NtProtectVirtualMemory(hCurrentProc, <strong>&</strong>pEventWrite, (PSIZE_T) <strong>&</strong>size, oldprotect, <strong>&</strong>oldprotect); FlushInstructionCache(hCurrentProc, pEventWrite, size); } |
我发现上述方法仍然适用于两个经过测试的 EDR,但这是一个嘈杂的 ETW 补丁。
6. 规避常见的恶意 API 调用模式
大多数行为检测最终都是基于检测恶意模式。其中一种模式是特定 WINAPI 调用在短时间内的顺序。第 4 节中简要提到的可疑 WINAPI 调用通常用于执行 shellcode,因此受到严格监控。然而,这些调用也用于良性活动(VirtualAlloc
, WriteProcess
,CreateThread
模式与内存分配和写入约 250KB 的 shellcode 相结合),因此 EDR 解决方案的挑战是区分良性和恶意调用。Filip Olszak 写了一篇很棒的博客文章,利用延迟和较小的分配和写入内存块来融入良性 WINAPI 调用行为。简而言之,他的方法调整了典型 shellcode 加载器的以下行为:
- 与其分配一大块内存并直接将 ~250KB 植入 shellcode 写入该内存,不如分配小的连续块,例如 <64KB 内存并将它们标记为
NO_ACCESS
. 然后以类似的块大小将 shellcode 写入分配的内存页面。 - 在上述每个操作之间引入延迟。这将增加执行 shellcode 所需的时间,但也会使连续执行模式变得不那么突出。
这种技术的一个问题是确保您找到一个可以在连续内存页面中容纳整个 shellcode 的内存位置。Filip 的DripLoader实现了这个概念。
我构建的加载器不会将 shellcode 注入另一个进程,而是使用NtCreateThread
. 未知进程(我们的二进制文件实际上流行率很低)进入其他进程(通常是 Windows 原生进程)是突出的可疑活动(推荐阅读“Fork&Run – 你是历史”)。当我们在加载器进程空间的线程中运行 shellcode 时,更容易混入进程中良性线程执行和内存操作的噪音。然而,不利的一面是,任何崩溃的开发后模块也会导致加载程序的进程崩溃,从而导致植入程序崩溃。持久性技术以及运行稳定可靠的BOF可以帮助克服这一缺点。
7. 直接系统调用和回避“系统调用标记”
ntdll.dll
加载程序利用直接系统调用来绕过EDR放入的任何挂钩。我想避免过多详细介绍直接系统调用的工作原理,因为这不是这篇文章的目的,并且已经写了很多关于它的精彩文章(例如Outflank)。
简而言之,直接系统调用是直接对内核系统调用等效的 WINAPI 调用。我们不调用它,而是调用它在 Windows 内核中定义的ntdll.dll
VirtualAlloc
内核等效项。NtAlocateVirtualMemory
这很棒,因为我们绕过了任何用于监控VirtualAlloc
对ntdll.dll
.
为了直接调用系统调用,我们获取要调用的系统调用的系统调用 ID ntdll.dll
,使用函数签名将函数参数的正确顺序和类型推送到堆栈,然后调用syscall <id>
指令。有几个工具可以为我们安排这一切,SysWhispers2和SysWhisper3就是两个很好的例子。从规避的角度来看,调用直接系统调用有两个问题:
- 您的二进制文件最终得到了
syscall
易于静态检测的指令(也称为“系统调用的标记”,更多内容请参见“ SysWhispers 已死,SysWhispers 万岁! ”)。 - 与通过其
ntdll.dll
等效调用的系统调用的良性使用不同,系统调用的返回地址不指向ntdll.dll
. 相反,它指向我们调用系统调用的代码,它驻留在ntdll.dll
. 这是未通过调用的系统调用的指标ntdll.dll
,这是可疑的。
为了克服这些问题,我们可以做到以下几点:
- 实施猎蛋机制。用(一些随机的唯一可识别模式)替换
syscall
指令,egg
并在运行时在内存中搜索它并用使用和WINAPI 调用的指令egg
替换它。之后,我们就可以正常使用直接系统调用了。该技术已由klezVirus实施。syscall
ReadProcessMemory
WriteProcessMemory
- 我们不是从我们自己的代码中调用指令,而是在我们准备好堆栈以调用系统调用后
syscall
搜索syscall
指令并跳转到该内存地址。ntdll.dll
这将导致 RIP 中的返回地址指向ntdll.dll
内存区域。
这两种技术都是SysWhisper3的一部分。
8. 拆除挂钩ntdll.dll
另一个逃避 EDR 挂钩的好方法ntdll.dll
是ntdll.dll
用来自ntdll.dll
. ntdll.dll
是任何 Windows 进程加载的第一个 DLL。EDR 解决方案确保它们的 DLL 在不久之后加载,这ntdll.dll
在我们自己的代码执行之前将所有钩子放置在加载中。如果我们的代码之后在内存中加载一个新副本ntdll.dll
,这些 EDR 挂钩将被覆盖。RefleXXion是一个 C++ 库,它实现了MDSec对该技术所做的研究。RelfeXXion 使用直接系统调用NtOpenSection
并NtMapViewOfSection
获得一个清理的ntdll.dll
句柄\KnownDlls\ntdll.dll
(具有先前加载的 DLL 的注册表路径)。然后它会覆盖已.TEXT
加载的部分ntdll.dll
,这会清除 EDR 挂钩。
我建议使用调整 RefleXXion 库来使用与上面第 7 节中描述的相同的技巧。
9. 欺骗线程调用栈
接下来的两节介绍了两种技术,可以规避检测内存中的 shellcode。由于植入物的信标行为,大部分时间植入物都处于睡眠状态,等待其操作员的传入任务。在此期间,植入物容易受到来自 EDR 的内存扫描技术的攻击。本文中描述的两种规避方法中的第一种是欺骗线程调用堆栈。
当植入物处于休眠状态时,它的线程返回地址指向我们驻留在内存中的 shellcode。通过检查可疑进程中线程的返回地址,可以很容易地识别出我们的植入 shellcode。为了避免这种情况,想打破返回地址和shellcode之间的这种联系。Sleep()
我们可以通过挂钩函数来做到这一点。当该钩子被调用时(通过植入/信标shellcode),我们用覆盖返回地址0x0
并调用原始Sleep()
函数。返回时Sleep()
,我们将原始返回地址放回原处,以便线程返回正确的地址以继续执行。Mariusz Banach在他的ThreadStackSpoofer中实现了这种技术项目。这个 repo 提供了有关该技术的更多详细信息,并概述了一些注意事项。
我们可以在下面的两个屏幕截图中观察到欺骗线程调用堆栈的结果,其中非欺骗调用堆栈指向非支持的内存位置,而欺骗的线程调用堆栈指向我们挂钩的 Sleep( MySleep
) 函数并“切断”调用堆栈的其余部分。
默认信标线程调用堆栈。
欺骗信标线程调用堆栈。
10.信标内存加密
内存检测的另一个规避方法是在休眠时加密植入程序的可执行内存区域。使用与上一节中描述的相同的睡眠挂钩,我们可以通过检查调用者地址(调用的信标代码Sleep()
以及我们的MySleep()
挂钩)来获取 shellcode 内存段。如果调用者内存区域的大小MEM_PRIVATE
与EXECUTABLE
我们的 shellcode 大致相同,那么内存段将使用 XOR 函数加密Sleep()
并被调用。然后Sleep()
返回,它解密内存段并返回给它。
另一种技术是注册一个向量异常处理程序 (VEH),它处理NO_ACCESS
违规异常、解密内存段并将权限更改为RX
. 然后就在休眠之前,将内存段标记为NO_ACCESS
,这样在Sleep()
返回时会抛出内存访问冲突异常。因为我们注册了一个 VEH,所以异常是在该线程上下文中处理的,并且可以在引发异常的完全相同的位置恢复。VEH 可以简单地解密并将权限更改回 RX,并且植入程序可以继续执行。这种技术可以防止Sleep()
植入物在睡眠时出现可检测的钩子。
Mariusz Banach也在ShellcodeFluctuation中实现了这种技术。
11.自定义反射加载器
我们在这个加载器中执行的信标 shellcode 最终是一个需要在内存中执行的 DLL。许多 C2 框架利用 Stephen Fewer 的ReflectiveLoader。关于反射 DLL 加载器的工作原理有很多书面解释,Stephen Fewer 的代码也有很好的文档记录,但简而言之,反射加载器执行以下操作:
kernel32.dll
将地址解析为加载 DLL 所需的必要WINAPI(例如VirtualAlloc
,LoadLibraryA
等)- 将 DLL 及其部分写入内存
- 建立 DLL 导入表,以便 DLL 可以调用
ntdll.dll
和kernel32.dll
WINAPI - 加载任何其他库并解析它们各自的导入函数地址
- 调用 DLL 入口点
Cobalt Strike 增加了对在内存中反射加载 DLL 的自定义方式的支持,允许红队操作员自定义加载信标 DLL 的方式并添加规避技术。Bobby Cooke 和 Santiago P使用我在装载机中使用的 Cobalt Strike 的 UDRL构建了一个隐形装载机 ( BokuLoader )。BokuLoader 实现了几种规避技术:
- 限制调用
GetProcAddress()
(通常是 EDR 挂钩的 WINAPI 调用来解析函数地址,就像我们在第 4 节中所做的那样) - AMSI & ETW 绕过
- 仅使用直接系统调用
- 仅使用
RW
orRX
,不使用RWX
(EXECUTE_READWRITE
) 权限 - 从内存中删除信标 DLL 标头
确保取消注释这两个定义以利用通过HellsGate 和 HalosGate的直接系统调用并绕过 ETW 和 AMSI(不是真正必要的,因为我们已经禁用 ETW 并且没有将加载程序注入另一个进程)。
12. Malleable 配置文件中的 OpSec 配置
在您的 Malleable C2 配置文件中,确保配置了以下选项,这些选项限制了RWX
标记内存(可疑且易于检测)的使用,并在信标启动后清理了 shellcode。
1 2 3 4 5 6 7 8 |
set startrwx "false"; set userwx "false"; set cleanup "true"; set stomppe "true"; set obfuscate "true"; set sleep_mask "true"; set smartinject "true"; |
结论
结合这些技术,您可以绕过(以及其他)Microsoft Defender for Endpoint 和 CrowdStrike Falcon,检测次数为 0(2022 年 4 月中旬测试),它们与 SentinelOne 一起引领端点保护行业。
CrowdStrike Falcon 有 0 个警报。
Windows Defender(以及 Microsoft Defender for Endpoint,未截屏)带有 0 个警报。
当然,这只是完全破坏端点的第一步,这并不意味着 EDR 解决方案的“游戏结束”。根据红队操作员接下来选择的开发后活动/模块,植入物仍然可以“游戏结束”。一般来说,要么运行 BOF,要么通过植入物的 SOCKS 代理功能隧道化 post-ex 工具。还要考虑将 EDR 钩子补丁放回我们的Sleep()
钩子中以避免检测到脱钩,以及删除 ETW/AMSI 补丁。
这是一场猫捉老鼠的游戏,猫无疑会越来越好。