A blueprint for evading industry leading endpoint protection in 2022(译文)

s0urce 2022-05-12 10:47:00

大约两年前,我辞去了全职红队操作员的工作。然而,它仍然是一个心仪的专业领域。几周前,我重新拾起昨日的爱好:绕过/逃避端点保护解决方案。

在这篇文章中,我们将探讨一系列可用于绕过行业领先的企业端点保护解决方案的技术。由于这纯粹是为了(道德)红队队员等安全从业者教育之用,因此我决定不公开发布源代码。本文旨在让安全行业的广大读者能够全面了解相关技术,而不是深入研究每种技术的细节。相反,具体的技术细节,读者可以参阅参考资料部分列出的文章。

在模拟对抗过程中,“初始访问”阶段的一个关键挑战,就是绕过企业端点上的检测和响应能力 (EDR)。由于商业的c2框架向红队队员提供的是不可修改的shellcode和二进制文件,所以,安全解决方案供应商会提取这些代码的特征(或者成为签名),那么,为了植入这些代码,红队就必须对其特征(静态和行为特征)进行必要的混淆处理。

在这篇文章中,我将介绍以下技术,最终目标是执行恶意shellcode,也称为(shellcode)加载程序:

  1. Shellcode加密技术
  2. 降低熵值
  3. (本地)AV沙箱逃逸技术
  4. 导入表混淆技术
  5. 禁用Windows事件跟踪 (ETW)
  6. 规避常见的恶意API调用模式
  7. 使用直接系统调用并规避“系统调用标记”
  8. 删除ntdll.dll中的钩子
  9. 伪造线程调用堆栈
  10. beacon的内存加密
  11. 自定义反射型加载程序
  12. 利用柔性配置文件配置OpSec

1. Shellcode加密技术

让我们从一个基本且很重要的话题开始,shellcode静态混淆。在我的加载程序中,我利用了XOR或RC4加密算法,因为它们不仅易于实现,而且不会留下太多加密活动的痕迹。如果用AES加密来混淆shellcode的静态特征的话,会在二进制文件的导入地址表中留下加密痕迹,所以,很容易引起人们的怀疑。实际上,这个加载程序的早期版本中专门用于AES解密函数(如CryptDecrypt、CryptHashData、CryptDeriveKey等),很容易引起Windows Defender的注意。

1.png

从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解决方案通常会检查这些API。通常情况下,这些API包括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,并且无法绕过针对ntdll.dll中WINAPI的任何钩子,因此,这样做纯粹为了从IAT中删除可疑的函数。

5. 禁用Windows事件跟踪 (ETW)

许多EDR解决方案广泛利用了Windows事件追踪(ETW),特别是Microsoft Defender for Endpoint(以前被称为Microsoft ATP)。ETW允许对一个进程的功能和WINAPI调用进行广泛的检测和追踪。此外,ETW在内核中也有一些组件,主要是为系统调用和其他内核操作注册回调,但也包括一个用户态组件,它是ntdll.dll的一部分(详见https://binarly.io/posts/Design_issues_of_modern_EDRs_bypassing_ETW-based_solutions/index.html)。由于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上仍然有效,但这种方法“动静太大”。

6. 规避常见的恶意API调用模式

大多数行为检测最终都是检测恶意模式,比如在很短的时间范围内针对特定的WINAPI的顺序调用。第4节中简要提到的可疑的WINAPI调用通常用于执行shellcode,因此,它们都是重点监控对象。然而,这些调用有时也用于良性活动(因此,这些VirtualAlloc、WriteProcess、CreateThread模式通常需要与内存分配和写入约250KB的shellcode等行为综合考虑),因此,EDR解决方案的挑战是如何区分良性和恶意调用。Filip Olszak撰写了一篇很好的博文,介绍了如何利用延迟以及分配和写入的内存空间的大小来甄别良性的WINAPI调用行为。简而言之,他的方法考虑到了典型的shellcode加载程序的以下行为:

  • 与其分配一大块内存并直接将~250KB的implant shellcode写入该内存,不如分配小块但连续的内存,例如<64KB的内存,并将其标记为NO_ACCESS。然后,将shellcode按照相应的块大小写入这些内存页中。
  • 在上述的每一个操作之间引入延迟。这将增加执行shellcode所需的时间,但也会淡化连续执行模式。

这项技术需要注意的一点是,要确保在连续的内存页中找到一个可以容纳整个shellcode的内存位置。实际上,Filip的DripLoader就实现了这个概念。

我所构建的加载程序并不会将shellcode注入到另一个进程中,而是使用NtCreateThread在自己的进程空间中启动shellcode。一个未知的进程(我们的二进制文件的流行度很低)进入其他进程(通常是Windows本地进程)是一种可疑的活动,需要高度注意(推荐阅读https://www.cobaltstrike.com/blog/cobalt-strike-4-5-fork-run-youre-history/)。当我们在加载程序的进程空间中的线程中运行shellcode时,更容易被进程中的良性线程执行和内存操作的噪音所掩盖。然而,缺点是,只要有一个后渗透模块发生崩溃,就会殃及加载程序进程,从而使implant随之崩溃。不过,借助于维持权限技术以及运行稳定可靠的BOF,还是可以克服这个缺点的。

7. 使用直接系统调用并规避“系统调用标记”

装载程序可以利用直接系统调用来绕过EDR在ntdll.dll中设置的钩子。需要说明的是,这里不会深入讨论直接系统调用的工作原理,因为这超出了本文的讨论范围,感兴趣的读者就可以参考网络上的优秀文章(例如Outflank,详见https://outflank.nl/blog/2019/06/19/red-team-tactics-combining-direct-system-calls-and-srdi-to-bypass-av-edr/)。

简而言之,直接系统调用是直接调用等效内核系统调用的WINAPI。举例来说,我们不是调用ntdll.dll库中的VirtualAlloc函数,而是调用Windows内核中定义的、具有等效功能的内核函数NtAlocateVirtualMemory。这么做的好处是,能够绕过监控调用(在本例中)ntdll.dll库中定义的函数VirtualAlloc的所有EDR钩子。

为了直接调用一个系统调用,我们可以从ntdll.dll中获取想要调用的系统调用的syscall ID,然后,使用函数签名将函数的正确顺序和参数类型压入堆栈中,并调用syscall <id>指令。实际上,我们可以借助某些工具来自动完成上述操作,比如SysWhispers2SysWhisper3。从免杀的角度来看,直接调用系统调用有两个问题:

  • 二进制文件最终会用到系统调用指令,这很容易被静态检测到(又称“系统调用标记”,详见https://klezvirus.github.io/RedTeaming/AV_Evasion/NoSysWhisper/)。
  • 与通过等效ntdll.dll调用的系统调用的正常用法不同,系统调用的返回地址并不指向ntdll.dll。相反,它指向我们调用系统调用的代码,该代码驻留在ntdll.dll之外的内存区域。这是没有通过ntdll.dll调用系统调用的标志,表明这里很可能有猫腻。

为了克服这些问题,我们可以:

  • 实现彩蛋猎手机制。先用彩蛋(一些随机的、唯一的、可识别的模式)替换syscall指令,然后在运行时,再在内存中搜索这个彩蛋,并使用ReadProcessMemory和WriteProcessMemory等WINAPI调用将其替换为syscall指令。之后,我们可以正常使用直接系统调用了。这种技术已经由klezVirus实现。
  • 我们不从自己的代码中调用syscall指令,而是在ntdll.dll中搜索syscall指令,并在我们准备好调用系统调用的堆栈后跳转到该内存地址。这将导致RIP中的返回地址指向ntdll.dll内存区域。

实际上,SysWhisper3已经实现了这两种技术。

8. 删除ntdll.dll中的钩子

另一种绕过ntdll.dll中EDR钩子的好方法,就是用ntdll.dll的新副本覆盖默认加载(并被EDR钩住)的ntdll.dll。通常情况下,Windows进程加载的第一个DLL就是ntdll.dll库。而EDR解决方案需要确保他们的DLL随之加载,以便在我们的代码执行之前将所有钩子布置到已加载的ntdll.dll中。如果我们的代码之后在内存中加载一个新的ntdll.dll副本,这些EDR钩子将被覆盖。RefleXXion是一个C++库,它实现了https://www.mdsec.co.uk/2022/01/edr-parallel-asis-through-analysis/中介绍的各种技术。RelfeXXion使用直接系统调用NtOpenSection和NtMapViewOfSection来获得\KnownDlls\ntdll.dll(具有以前加载的DLL的注册表路径)中纯净的ntdll.dll的句柄。然后,它就会覆盖已加载的ntdll.dll的.TEXT节,从而“冲走”所有的EDR钩子。

我建议使用RefleXXion库,相关技巧可以参考上面第7节的介绍。

9. 伪造线程调用堆栈

接下来的两节,将为读者介绍两种技术,用于帮助内存中的shellcode规避检测。由于implant的信标行为的缘故,在大部分时间里,它们都处于休眠状态,等待来自其控制者的传入任务。在这段时间里,implant很容易被EDR的内存扫描技术所发现。这篇文章中描述的两种规避方法中的第一种,就是伪造线程调用栈。

当implant处于休眠状态时,它的线程返回地址会指向驻留在内存中的shellcode。通过检查可疑进程中线程的返回地址,我们的implant shellcode很容易被发现。为了避免这种情况,可以设法打破返回地址和shellcode之间的这种联系。我们可以通过钩住Sleep()函数来做到这一点。当这个钩子被(由implant/ beacon shellcode)调用时,我们用0x0覆盖返回地址并调用原来的Sleep()函数。当Sleep()返回时,我们把原来的返回地址放回原处,这样线程就会返回到正确的地址继续执行。实际上,Mariusz Banach已经在他的ThreadStackSpoofer项目中实现了这种技术。这个存储库不仅提供了关于该技术的技术细节,还概述了一些注意事项。

我们可以在下面两张截图中看到,伪造的线程调用堆栈的情况,其中非伪造的调用堆栈指向non-backed的内存位置,而伪造的线程调用堆栈指向我们挂钩的休眠(MySleep)函数,并“切掉”了调用堆栈的其他部分。

1.png

默认的beacon线程调用栈

1.png

伪造的beacon线程调用堆栈

10. beacon的内存加密

另一种绕过内存检测的方法,就是在休眠时对implant的可执行内存区域进行加密。使用与上节所述相同的休眠钩子,我们可以通过检查调用方地址(调用Sleep()的beacon代码,以及我们的MySleep()钩子)获得shellcode内存段。如果调用方的内存段的权限为MEM_PRIVATE和EXECUTABLE,并且长度与shellcode的大小相仿,那么,就可以用XOR函数对这个内存段进行加密,并调用Sleep()函数。然后,Sleep()函数返回时,会解密该内存段,并返回其地址。

另一种绕过内存检测的方法,是注册一个向量异常处理程序(Vectored Exception Handler,VEH),用于处理NO_ACCESS违规异常、解密内存段并将权限改为RX。然后在进入休眠之前,将内存段标记为NO_ACCESS,这样的话,当Sleep()函数返回时,它就会抛出一个内存访问违规异常。因为我们已经注册了相应的VEH,所以该异常将在该线程上下文中处理,并且可以在抛出异常的同一位置恢复。该VEH可以进行解密并将权限改回RX,这样implant就可以继续执行了。这种技术可以避免implant进入休眠时出现可检测的Sleep()钩子。

Mariusz Banach已经在ShellcodeFluctuation中实现了这种技术。

11. 自定义反射型加载程序

我们在这个加载程序中执行的beacon shellcode最终是一个需要在内存中执行的DLL。许多C2框架利用了Stephen Fewer的ReflectiveLoader。关于反射型DLL 加载程序的工作原理有很多书面解释,Stephen Fewer的代码也提供了很好的文档,但简而言之,反射型加载程序可以完成以下操作:

  1. 解析加载DLL所需kernel32.dll WINAPI的地址(例如VirtualAlloc, LoadLibraryA等);
  2. 将DLL及其相应的节写入内存中;
  3. 建立DLL导入表,以便DLL可以调用ntdll.dll和kernel32.dll WINAPI;
  4. 加载额外的库并解析其导入函数地址;
  5. 调用DLL的入口点。

Cobalt Strike目前支持以反射方式将DLL加载至内存的自定义,允许红队队员自定义beacon DLL的加载方式,并添加了相应的免杀技术。Bobby Cooke和Santiago P使用Cobalt Strike的UDRL建立了一个隐蔽的加载程序(BokuLoader),我自己的加载程序使用的就是它。BokuLoader实现了多种免杀技术:

  1. 限制对GetProcAddress()的调用(通常情况下,解析函数地址的WINAPI调用都会被EDR挂钩,就像我们在第4节做的那样)
  2. AMSI&ETW绕过
  3. 只使用直接系统调用
  4. 只使用RW或RX,而不使用RWX(EXECUTE_READWRITE)权限
  5. 从内存中删除beacon DLL的头文件

另外,确保取消这两个定义的注释,以通过HellsGate和HalosGate使用直接系统调用,从而顺利绕过ETW和AMSI(其实没有必要,因为我们已经禁用了ETW,也没有将加载程序注入另一个进程)。

12. 利用柔性配置文件配置OpSec

在柔性C2配置文件中,确保配置了以下选项,以限制使用具有RWX标记的内存(不仅会引起怀疑,并且很容易被检测出来),并在beacon启动后清除shellcode。

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(以及其他防御产品),我们在2022年4月中旬进行对这两款产品测试时,检出率为0——它们与SentinelOne一样,都是端点保护行业的领头羊。

1.png

CrowdStrike Falcon没有发出任何警报

1.png

Windows Defender(还有Microsoft Defender for Endpoint,但是这里没有截图)没有发出任何警报

当然,这只是完全入侵端点的第一步,这并不意味着EDR解决方案已经彻底没戏了:实际上这取决于红队人员接下来选择的后渗透活动/模块,某些implant仍然有可能被EDR解决方案捕获。一般来说,红队人员接下来要么运行BOF,要么通过implant的SOCKS代理功能为后利用工具搭建隧道。此外,还可以考虑把EDR钩子的补丁放回我们的Sleep()钩子中,以避免检测到解钩以及删除ETW/AMSI补丁的动作。

这是一场猫捉老鼠的游戏,而猫无疑正在变得越来越棒。

原文地址:https://vanmieghem.io/blueprint-for-evading-edr-in-2022/

评论

S

s0urce

这个人很懒,没有留下任何介绍

twitter weibo github wechat

随机分类

MongoDB安全 文章:3 篇
安全管理 文章:7 篇
漏洞分析 文章:212 篇
运维安全 文章:62 篇
Windows安全 文章:88 篇

扫码关注公众号

WeChat Offical Account QRCode

最新评论

Yukong

🐮皮

H

HHHeey

好的,谢谢师傅的解答

Article_kelp

a类中的变量secret_class_var = "secret"是在merge

H

HHHeey

secret_var = 1 def test(): pass

H

hgsmonkey

tql!!!

目录