转载:https://vanmieghem.io/blueprint-for-evading-edr-in-2022/
Shellcode 加密
减少熵
逃离(本地)反病毒沙箱
导入表混淆
禁用 Windows 事件跟踪 (ETW)
规避常见的恶意 API 调用模式
直接系统调用和规避“系统调用标记”
拆除挂钩ntdll.dll
欺骗线程调用堆栈
信标的内存加密
自定义反射加载器
可扩展配置文件中的 OpSec 配置
让我们从一个基本但重要的话题开始,静态 shellcode 混淆。在我的加载程序中,我利用了 XOR 或 RC4 加密算法,因为它易于实现并且不会留下大量加载程序执行的加密活动的外部指标。用于混淆 shellcode 静态签名的 AES 加密会在二进制文件的导入地址表中留下痕迹,这增加了怀疑。在此加载程序的早期版本中,我已经让 Windows Defender 专门触发了 AES 解密函数(例如CryptDecrypt
、等)。CryptHashData
CryptDeriveKey
许多 AV/EDR 解决方案在评估未知二进制时考虑二进制熵。由于我们正在加密 shellcode,我们的二进制文件的熵相当高,这清楚地表明二进制文件中的代码部分被混淆了。
有几种方法可以减少二进制的熵,两种简单的方法是:
将低熵资源添加到二进制文件中,例如(低熵)图像。
添加字符串,比如英文字典或者一些"strings C:\Program Files\Google\Chrome\Application\100.0.4896.88\chrome.dll"
输出。
一个更优雅的解决方案是设计和实现一种算法,将 shellcode 混淆(编码/加密)成英文单词(低熵)。那将用一块石头杀死两只鸟。
许多 EDR 解决方案将在本地沙箱中运行二进制文件几秒钟以检查其行为。为了避免影响最终用户体验,他们无法在几秒钟内检查二进制文件(我过去曾看到 Avast 最多需要 30 秒,但那是个例外)。我们可以通过延迟执行我们的 shellcode 来滥用这个限制。简单地计算一个大素数是我个人的最爱。您可以更进一步,确定性地计算一个质数,并将该数字用作加密 shellcode 的(一部分)密钥。
您希望避免可疑的 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
然后创建一个指向该地址的函数指针:
typedef BOOL (WINAPI * pVirtualProtect)(LPVOID lpAddress, SIZE_T dwSize, DWORD flNewProtect, PDWORD lpflOldProtect);
pVirtualProtect fnVirtualProtect;
unsigned char sVirtualProtect[] = { 'V','i','r','t','u','a','l','P','r','o','t','e','c','t', 0x0 };
unsigned char sKernel32[] = { 'k','e','r','n','e','l','3','2','.','d','l','l', 0x0 };
fnVirtualProtect = (pVirtualProtect) GetProcAddress(GetModuleHandle((LPCSTR) sKernel32), (LPCSTR)sVirtualProtect);
// call VirtualProtect
fnVirtualProtect(address, dwSize, PAGE_READWRITE, &oldProt);
使用字符数组混淆字符串会将字符串分割成更小的部分,使它们更难从二进制文件中提取。
调用仍将是一个ntdll.dll
WINAPI,并且不会绕过 WINAPI 中的任何钩子ntdll.dll
,但纯粹是为了从 IAT 中删除可疑函数。
许多 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
) 的指令。
void disableETW(void) {
// return 0
unsigned char patch[] = { 0x48, 0x33, 0xc0, 0xc3}; // xor rax, rax; ret
ULONG oldprotect = 0;
size_t size = sizeof(patch);
HANDLE hCurrentProc = GetCurrentProcess();
unsigned char sEtwEventWrite[] = { 'E','t','w','E','v','e','n','t','W','r','i','t','e', 0x0 };
void *pEventWrite = GetProcAddress(GetModuleHandle((LPCSTR) sNtdll), (LPCSTR) sEtwEventWrite);
NtProtectVirtualMemory(hCurrentProc, &pEventWrite, (PSIZE_T) &size, PAGE_READWRITE, &oldprotect);
memcpy(pEventWrite, patch, size / sizeof(patch[0]));
NtProtectVirtualMemory(hCurrentProc, &pEventWrite, (PSIZE_T) &size, oldprotect, &oldprotect);
FlushInstructionCache(hCurrentProc, pEventWrite, size);
}
我发现上述方法仍然适用于两个经过测试的 EDR,但这是一个嘈杂的 ETW 补丁。
大多数行为检测最终都是基于检测恶意模式。其中一种模式是特定 WINAPI 调用在短时间内的顺序。第 4 节中简要提到的可疑 WINAPI 调用通常用于执行 shellcode,因此受到严格监控。但是,这些调用也用于良性活动(VirtualAlloc
, WriteProcess
,CreateThread
模式与内存分配和写入约 250KB 的 shellcode 相结合),因此 EDR 解决方案的挑战是区分良性和恶意调用。一篇很棒的博客文章,利用延迟和较小的分配和写入内存块来融入良性 WINAPI 调用行为。简而言之,他的方法调整了典型 shellcode 加载器的以下行为:
与其分配一大块内存并直接将 ~250KB 植入 shellcode 写入该内存,不如分配小的连续块,例如 <64KB 内存并将它们标记为NO_ACCESS
. 然后以类似的块大小将 shellcode 写入分配的内存页面。
在上述每个操作之间引入延迟。这将增加执行 shellcode 所需的时间,但也会使连续执行模式变得不那么突出。
这种技术的一个问题是确保您找到一个可以在连续内存页面中容纳整个 shellcode 的内存位置。Filip 的DripLoader实现了这个概念。
我构建的加载器不会将 shellcode 注入另一个进程,而是使用NtCreateThread
. 未知进程(我们的二进制文件实际上流行率很低)进入其他进程(通常是 Windows 原生进程)是突出的可疑活动(推荐阅读“Fork&Run – 你是历史”)。当我们在加载器进程空间的线程中运行 shellcode 时,更容易混入进程中良性线程执行和内存操作的噪音。然而,不利的一面是任何崩溃的开发后模块也会导致加载程序的进程崩溃,从而导致植入程序崩溃。持久性技术以及运行稳定可靠的BOF可以帮助克服这一缺点。
ntdll.dll
加载程序利用直接系统调用来绕过EDR放入的任何挂钩。
简而言之,直接系统调用是直接对内核系统调用等效的 WINAPI 调用。我们不调用它,而是调用它在 Windows 内核中定义的ntdll.dll
VirtualAlloc
内核等效项。NtAlocateVirtualMemory
这很棒,因为我们绕过了任何用于监控VirtualAlloc
对ntdll.dll
.
为了直接调用系统调用,我们获取要调用的系统调用的系统调用 ID ntdll.dll
,使用函数签名将函数参数的正确顺序和类型推送到堆栈,然后调用syscall <id>
指令。有几个工具可以为我们安排这一切,SysWhispers2和SysWhisper3就是两个很好的例子。从规避的角度来看,调用直接系统调用有两个问题:
您的二进制文件最终得到了syscall
易于静态检测的指令又名“系统调用的标记”
与通过其ntdll.dll
等效调用的系统调用的良性使用不同,系统调用的返回地址不指向ntdll.dll
. 相反,它指向我们调用系统调用的代码,它驻留在ntdll.dll
. 这是未通过调用的系统调用的指标ntdll.dll
,这是可疑的。
为了克服这些问题,我们可以做到以下几点:
实施猎蛋机制。用(一些随机的唯一可识别模式)替换syscall
指令,egg
并在运行时在内存中搜索它并用使用和WINAPI 调用的指令egg
替换它。之后,我们就可以正常使用直接系统调用了。该技术已由klezVirus实施。syscall
ReadProcessMemory
WriteProcessMemory
我们不是从我们自己的代码中调用指令,而是在我们准备好堆栈以调用系统调用后syscall
搜索syscall
指令并跳转到该内存地址。ntdll.dll
这将导致 RIP 中的返回地址指向ntdll.dll
内存区域。
这两种技术都是SysWhisper3的一部分。
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 节中描述的相同的技巧。
接下来的两节介绍了两种技术,可以规避检测内存中的 shellcode。由于植入物的信标行为,大部分时间植入物都处于睡眠状态,等待其操作员的传入任务。在此期间,植入物容易受到来自 EDR 的内存扫描技术的攻击。本文中描述的两种规避方法中的第一种是欺骗线程调用堆栈。
当植入物处于休眠状态时,它的线程返回地址指向我们驻留在内存中的 shellcode。通过检查可疑进程中线程的返回地址,可以很容易地识别出我们的植入 shellcode。为了避免这种情况,想打破返回地址和shellcode之间的这种联系。Sleep()
我们可以通过挂钩函数来做到这一点。当该钩子被调用时(通过植入/信标shellcode),我们用覆盖返回地址0x0
并调用原始Sleep()
函数。返回时Sleep()
,我们将原始返回地址放回原处,以便线程返回到正确的地址以继续执行。Mariusz Banach在他的ThreadStackSpoofer中实现了这种技术项目。这个 repo 提供了有关该技术的更多详细信息,并概述了一些注意事项。
我们可以在下面的两个屏幕截图中观察到欺骗线程调用堆栈的结果,其中非欺骗调用堆栈指向非支持的内存位置,而欺骗的线程调用堆栈指向我们挂钩的 Sleep( MySleep
) 函数并“切断”调用堆栈的其余部分。
内存检测的另一个规避方法是在休眠时加密植入程序的可执行内存区域。使用与上一节中描述的相同的睡眠挂钩,我们可以通过检查调用者地址(调用的信标代码Sleep()
以及我们的MySleep()
挂钩)来获取 shellcode 内存段。如果调用者内存区域的大小MEM_PRIVATE
与EXECUTABLE
我们的 shellcode 大致相同,那么内存段将使用 XOR 函数加密Sleep()
并被调用。然后Sleep()
返回,它解密内存段并返回给它。
另一种技术是注册一个向量异常处理程序 (VEH),它处理NO_ACCESS
违规异常、解密内存段并将权限更改为RX
. 然后就在休眠之前,将内存段标记为NO_ACCESS
,这样在Sleep()
返回时会抛出内存访问冲突异常。因为我们注册了一个 VEH,所以异常是在该线程上下文中处理的,并且可以在引发异常的完全相同的位置恢复。VEH 可以简单地解密并将权限更改回 RX,并且植入程序可以继续执行。这种技术可以防止Sleep()
植入物在睡眠时出现可检测的钩子。
Mariusz Banach也在ShellcodeFluctuation中实现了这种技术。
我们在这个加载器中执行的信标 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
or RX
,不使用RWX
( EXECUTE_READWRITE
) 权限
从内存中删除信标 DLL 标头
确保取消注释这两个定义以利用通过HellsGate 和 HalosGate的直接系统调用并绕过 ETW 和 AMSI(不是真正必要的,因为我们已经禁用 ETW 并且没有将加载程序注入另一个进程)。
在您的 Malleable C2 配置文件中,确保配置了以下选项,这些选项限制了RWX
标记内存(可疑且易于检测)的使用,并在信标启动后清理了 shellcode。
set startrwx "false";
set userwx "false";
set cleanup "true";
set stomppe "true";
set obfuscate "true";
set sleep_mask "true";
set smartinject "true";
推荐阅读
查看更多精彩内容,还请关注橘猫学安全:
每日坚持学习与分享,觉得文章对你有帮助可在底部给点个“再看”