使用Cobalt Strike和Gargoyle绕过杀软的内存扫描——todo,待实践,包括一些链接文章
导语:本文将主要介绍我在Cobalt Strike的Beacon有效载荷和gargoyle内存扫描规避技术绕过杀软的内存扫描方面的研究。
本文将主要介绍我在Cobalt Strike的Beacon有效载荷和gargoyle内存扫描规避技术绕过杀软的内存扫描方面的研究。并且会提供一个使用gargoyle在计时器上执行Cobalt Strike Beacon有效载荷的概念验证(PoC)。这个PoC背后的假设是使用内存扫描技术来对抗终端检测与响应解决方案(EDR),这些扫描技术以规则的时间间隔发生,并且不会在非可执行内存上发出警报(也可能是因为我们触发的警报淹没在了大量密集的警报中)。通过“跳入”内存和“跳出”内存的方式,实现了避免在扫描器运行时将有效载荷驻留在内存中,然后在安全扫描完成后将其再次重新放入内存中的攻击目标。
这篇文章的内容假设读者熟悉 gargoyle 内存扫描规避技术以及Matt Graeber 提出的使用 C 语言编写优化的 Windows shellcode 的技术。
简介
现在的很多企业越来越多地选择采用复杂的终端检测与响应解决方案(EDR),专门用于在整个企业中大规模检测高级恶意软件。常见的这种解决方案包括Carbon Black,Crowdstrike’s Falcon,ENDGAME,Cyber Reason,Countercept,Cylance和FireEye HX。[1] 我们在进行有针对性的攻击模拟时所面临的挑战之一就是我们需要经常在运行了某种类型的EDR解决方案的主机上获得立足点。因此,在渗透测试中,至关重要的是,我们能够绕过任何先进的检测功能以保持攻击行为的隐藏状态。
许多EDR解决方案都具有强大的功能,可以有效地检测受感染主机上的可疑行为,例如:
1.内存扫描技术,例如寻找反射加载的DLL,注入线程[2]和 inline/IAT/EAT HOOK[3]
2..实时系统跟踪,例如进程执行,文件写入和注册表活动
3.命令行记录和分析
4.网络追踪
5.常见的跨进程访问技术,例如监视CreateRemoteThread,WriteProcessMemory和VirtualAllocEx
有许多恶意软件系列和常见的攻击框架都利用了典型的代码注入技术,例如使用反射加载DLL和线程注入,这种攻击技术可以通过在整个企业中大规模使用内存扫描和异常检测技术进行检测(请参阅 https://www.countercept.com/our-thinking/advanced-attack-detection/ 和 https://www.endgame.com/blog/technical-blog/hunting-memory ,了解有关此类技术的更多信息)。
因此,许多攻击者为了隐藏痕迹而对他们的工具进行了很多更改,他们特别关注绕过内存扫描的技术。例如,Raphael Mudge的Cobalt Strike 为“内存中的威胁仿真” 引入了许多 新功能,如下:
1.支持反射加载的DLL修改内存权限(而不是仅将内存页设置为RWX)
2.清除反射加载的DLL的初始内存分配
3.模块化 ‘stomping’ 实现绕过注入线程的安全扫描程序,以便Beacon可以从DLL的合法TEXT节运行
此外,其他安全研究人员已经研究了通过“代码洞穴”(code caves)或使用SetThreadContext等技术绕过注入线程的安全扫描程序。尤其要说的是,@xpn发表的关于‘Evading Get-InjectedThread ‘ 的优秀博文。
这些绕过方法基本上都是专注于让已经隐藏在内存中的有效载荷更难以被杀软检测到。然而,另一种可能的方法是针对内存扫描检测技术本身的固有缺陷。例如,内存扫描可能是性能密集型和误报严重的一种检测手法,这意味着这种检测技术在数千个终端上进行扩展时,效果不佳。因此,许多供应商将专注于监控特定的进程(例如通常会有针对性的对常见的Windows进程进行监控),可疑的可执行内存区域(即RWX内存页)和固定时间间隔的扫描(在更密集的扫描技术中,每十五分钟到一天一次的扫描间隔可能会有所不同。在这里可以将内存与磁盘进行比较)。[4]
因此,如果攻击者可以在非可执行内存区域中隐藏有效载荷并且在没有进行内存扫描的情况下以特定间隔触发有效载荷的执行,那么就可以绕过针对内存扫描的检测技术。例如,gargoyle内存扫描规避技术使用Windows计时器和特殊构造的代码以及rop代码来避免杀软检测到任何可疑的可执行内存。然而,除了能弹个消息框之外,gargoyle PoC似乎做不了什么 —— 那么我们可以用它来执行Cobalt Strike 的Beacon有效载荷吗?
gargoyle
Gargoyle是一种用于绕过Josh Lospinoso内存扫描器的PoC技术。它能够使攻击者以特定的时间间隔“唤醒”之前的活动,并将有效载荷隐藏在非可执行内存中,然后将自身标记为可执行内存来执行某些操作。要实现这个 gargoyle 需要执行以下步骤:
1.创建一个Windows Waitable Timer 对象,该对象在指定的时间间隔后执行对用户定义函数(通过’pfnCompletionRoutine’参数提供)的回调。SetWaitableTimer 函数还使得用户能够提供一个“lpArgToCompletionRoutine”参数,这个参数可以通过堆栈传递到指定的回调函数。
2.在这种情况下,’pfnCompletionRoutine’参数指向位于mshtml.dll中的特制ROP代码(’pop *; pop esp; ret’),’lpArgToCompletionRoutine’参数是指向攻击者控制的堆栈的指针。
3.Gargoyle 执行任意代码,然后将其自身设置为不可执行的内存(通过VirtualProtectEx)并返回执行到’WaitForSingleObjectEx’,这个函数会一直等待直到触发计时器。
4.当达到了计时器的执行时间时,Waitable Timer对象将通过异步过程调用(APC)执行特制的ROP代码。
5.ROP代码会通过’lpArgToCompletionRoutine’参数提供的值弹出到esp中并切换堆栈。
6.特殊堆栈包含了一个指向VirtualProtectEx的指针(和参数),这会导致对VirtualProtectEx的返回调用,然后将有效载荷区域标记为可执行的内存。
7.最后Gargoyle会返回到有效载荷的开头部分并再次开始执行。
方法
通过gargoyle PoC在计时器上执行有效载荷的方法需要以下两个关键步骤:
1.开发一种技术来检索和暂存有效载荷,跟踪其内存中的配置文件(即原始分配的内存地址,反射加载的分配内存以及任何后续启动的线程),然后在指定的时间段之后从内存中取消映射。
2.以上面描述的方式实现这种技术,就可以将其集成到现有的gargoyle PoC中。由于gargoyle PoC使用的有效载荷是用汇编语言编写的(请参阅主git存储库中的‘setup.nasm’),所以,我们采用的方法是先用C语言编写代码然后将其编译为位置无关代码(PIC),然后替换掉当前代码中弹消息框的gargoyle有效载荷。之所以这么做是因为第一步生成的代码的复杂性和重复(和独立)测试代码的需要以及实现用高级语言而不是汇编语言编写所有内容的想法。
第一步:暂存/删除Beacon有效载荷
下面的技术是实现该方法的第一步:
1.将Beacon有效载荷以“READ_ONLY”属性写入内存。这种方法避免了通过网络持续不断的检索有效载荷。
2.在只读内存中找到“隐藏”的Beacon有效载荷,为其分配新内存,然后将其复制到新内存中,并在反射DLL的开头创建一个新线程。作为反射加载过程的一部分,随后分配另一个“RWX”内存区域。
3.保留对原始分配的引用。
4.通过内存扫描找到反射加载的内存分配。
5.通过线程扫描查找由Beacon启动的可疑线程(Windows内核将在原始内存分配时记录此线程的起始地址)。
6.用指定的时间周期执行Sleep。
7.终止属于Beacon的线程。
8.从内存中取消原始分配内存和反射加载分配内存的映射。
这种技术有许多局限性。首先,内存或线程扫描效率不高。但是,由于我们没有控制Beacon用于反射加载过程的bootstrapping shellcode或导出的反射加载器函数 ,因此我们无法轻易获得从VirtualAlloc的附加调用返回的指针。所以,我们并不知道beacon在编译时将自身加载到了内存中,也就不能轻易获得其主线程的句柄。
其次,TerminateThread WINAPI的调用存在着风险,一般来说要尽可能的避免这种风险。微软建议只有在调用者确切知道目标线程正在做什么时才可以使用TerminateThread,在这种情况下我们是不知道的。在此研究期间,我了解了请求线程退出的不同方法,但发现只有TerminateThread才能工作而不会导致崩溃。然而,虽然这适用于此PoC,但尚未经过全面的测试,可能会导致无法预料的问题。更好的方法可能是连接到beacon的命名管道并发送exit命令指示它正常终止。
第二步:与 Gargoyle PoC 的集成
下一步是在计时器上插入我们的代码用于暂存/移除Beacon并作为gargoyle的主要有效载荷。这需要完成以下两个阶段:
1.使用 Matt Graeber 提出的技术用C语言编写优化的 Windows shellcode创建在第一阶段所描述的位置无关代码(PIC)版本,下文中将称其为“Metalgear”。位置无关的有效载荷包含与上述技术相同的逻辑,但它是通过遍历已加载模块的链接列表并将每个函数与预先计算的散列进行比较来解析Windows API函数。应该强调的是,这种做法是低效的,因为它需要在每次执行代码时解析函数指针,而gargoyle允许我们通过 ‘SetupConfiguration’结构 传递函数指针 。[5] 但是,出于初始化 PoC 的目的(并且如“方法”章节部分所述),单独编写和测试 Metalgear PIC 更有效,更简单。
2.将 Metalgear 插入到用于接收 ROP 或 堆栈Trampoline 的 gargoyle 代码(‘setup.nasm’)中并替换弹消息框的原始有效载荷。在构建过程中,gargoyle 将’setup.nasm’编译为shellcode,然后将编译的有效载荷(’setup.pic’)写入内存,并作为 PoC 的一部分。因此,此文件包括设置 WaitableTimer 对象的所有逻辑、弹出消息框并设置WaitForSingleObject和VirtualProtectEx的尾调用(函数式编程术语)。因此,通过使用 IDA 或 WinDbg ,我们就可以分割已编译的’setup.pic’有效载荷,删除弹出消息框的默认调用并替换为我们构造的 Metalgear PIC代码。
这个阶段有两个非常重要的点。首先,由于我们基本上将不同字节位的shellcode混合在了一起,所以我们需要确保Metalgear在完成运行后可以恢复完全相同的执行状态。这是必需要做的,这样它就不会破坏特制的堆栈并导致gargoyle的尾部调用失败。当尝试通过代码洞穴(code cave)(例如通过’SuspendThread / GetThreadContext / threadContext.Eip -> 代码洞穴’机制)在远程进程中插入线程时也要注意同类问题。[6]
因此,我们可以采用类似的解决方案并使用PUSHAD/PUSHFD指令将寄存器的标志和eflags PUSH到堆栈中,从而在对 Metalgear PIC执行“上下文切换”之前保存寄存器的状态。一旦 Metalgear 完成执行(并确保已正确清除堆栈),我们就可以使用 POPAD/POPFD 指令将 gargoyle 恢复到其原始执行状态。下面的伪代码块演示了这种方法,并显示了修改后的包含 Metalgear 有效载荷的“setup.nasm”:
; Replace the return address on our trampoline reset_trampoline: mov ecx, [ebx+ Configuration.VirtualProtectEx] mov [ebx+ Configuration.trampoline], ecx ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;; Arbitrary code goes here. Note that the ;;;; default stack is pretty small (65k). ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; Execute Metalgear pushad pushfd Metalgear PIC popfd popad ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;Time to setup tail calls to go down ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
其次,在这种情况下编译的Metalgear PIC是一个单独的代码块,由一个函数进行处理,该函数用于将WINAPI函数的哈希解析为函数指针(’GetProcAddressWithHash‘)。但是,在默认情况下,Metalgear PIC一旦完成执行就会崩溃,除非它被引导到gargoyle的尾部调用。可以通过两种方式避免这种崩溃,可以通过在执行完成后向gargoyle的尾部调用中添加一个 go/relative jmp(通过内联asm),也可以通过内联任何函数调用来实现。
演示
以下屏幕截图展示了 Metalgear PoC 的实际应用。下图所示的是 gargoyle 正在受害者主机上运行,但目前正处于 “Sleep” 状态。下图中高亮的部分显示了 gargoyle 有效载荷存储在内存地址是0x00BE0000 的内存中:
我们可以使用SysInternals工具包的VMMap来检查程序内存的状态。下面的屏幕截图显示了存储有效载荷的内存区域(0x00BE0000)被标记为不可执行:
此外,在进程中没有运行可疑的注入线程:
当达到计时器的时间周期时,APC将被执行,并且gargoyle将自己标记为可执行内存。然后它将继续运行Metalgear,之后将Beacon注入内存,并为我们提供一个shell:
下面的屏幕截图显示了在0x00BE0000处的原始有效载荷的内存页权限被更改为RWX:
此外,我们现在可以识别到一个注入的线程和两个可疑的RWX内存区域(0x02FE0000和0x03090000),它们都属于注入的Beacon有效载荷(256K和268K区域):
可以使用下面的进程浏览器观察注入的线程(可通过起始地址’0x0’识别):
如果此时触发了内存安全扫描,则可能会标记一个可疑线程和两个反射加载的DLL(分别对应于原始分配的内存区域以及由反射加载进程随后将有效载荷“复制”到的另一个可疑的内存区域)。应该强调的是,这里使用的是Cobalt Strike的默认配置,而没有使用最近新出现的任何内存中规避技术。
在指定的时间之后,Metalgear将继续终止属于Beacon的线程并取消映射RWX内存区域,然后返回到gargoyle尾部并将其自身设置为只读。然后它将等待下一次触发定时器并重新启动 Beacon。之后这个过程会无限期地重复执行。
下面的视频演示了在实际场景中使用的技术:
该视频以 gargoyle 处于“睡眠”状态开始。然后演示了我们的 gargoyle 有效载荷当前是不可执行的,并且没有可疑线程或内存指示符。此时,唯一的内存驻留物是与 gargoyle 相关联的非可执行区域和不可执行的且“隐藏”的 Beacon 有效载荷。在这个 PoC 中,没有对隐藏的 Beacon 有效载荷应用混淆或加密技术,不过添加这些东西也是非常简单的。
当计时器被触发时,gargoyle 将自己设置为可执行的内存并运行 Metalgear,后者继续将 beacon 注入内存,为我们提供shell。我们现在可以识别到两个 RWX 区域和一个与 Beacon 对应的可疑线程。(对于这个演示,Beacon 在终止之前的活动时间为一分钟。这纯粹是为了演示技术,计时器可以被配置为任意时间周期,例如可以设置为15分钟的活动时间,30分钟的睡眠时间)。
一分钟的活动时间结束后,Metalgear 会结束 beacon 并将自己置于不可执行的状态。任何可疑的特征(包括注入的线程和RWX内存)现在均已消失。在下一次触发定时器时,Metalgear 将在暂存 Beacon 之前会再次隐藏在只读内存中。
限制
由于本文提出的技术的实验性质,因此,本文中给出的 PoC 受到许多限制:
1.由于 Beacon 在每次迭代时都会被终止,因此必须每次重新创建新的会话,这将使得在实际的渗透测试中难以使用。
2.在 Metalgear 中使用线程和内存扫描不是最佳的解决方案。
3.一旦我们运行后,我们就没有考虑过内存配置文件。因此,许多使用可疑特征进行实时跟踪的EDR 解决方案,例如通过异常线程创建和动态内存分配的手段,仍然可以发现 Beacon 有效载荷。
4.由于 Beacon 的设计并没有考虑本文所描述的这种情况,因此终止/暂停会影响漏洞利用阶段的工作。
理想情况下,在每次调用时,我们都希望模拟Beacon的‘check-in’,因为它可以获取新的命令,执行完命令并立即返回到休眠状态。在此设置中,我们的内存配置文件仅仅反映了网络上Beacon的延迟。因此,另一种可能的方法是暂停与Beacon相关联的线程,并将与反射加载的Beacon有效载荷相关联的内存权限改变为READ_ONLY。但是,这意味着线程将始终存在,并且需要使用另一种技术(例如使用ROP代码或SetThreadContext)来隐藏此线程的起始位置,使其看起来好像属于一个合法映射的DLL。
参考
[1] Hexacorn维护了最新的EDR解决方案及其各自的功能列表(http://www.hexacorn.com/blog/2016/08/06/endpoint-detection-and-response-edr-solutions-sheet/)
[2] Jared Atkinson实现的Get-InjectedThread:https://gist.github.com/jaredcatkinson/23905d34537ce4b5b1818c3e6405c1d2
[3] 有关此类技术的更多信息,请参阅Countercept的“内存分析”白皮书:https://www.countercept.com/our-thinking/memory-analysis-whitepaper/
[4] 此外,许多EDR解决方案使用可疑特征进行实时跟踪,例如本地/远程线程创建和动态内存分配,但是在本博文中对这类技术的关注较少。
[5] 此外,用于Metalgear的大多数API调用都位于kernel32.dll中,后者在每个进程中用相同的地址加载到了内存中,因此也可能使用硬编码的RVA。
[6] 有关此类线程劫持技术的更多信息,请参阅Nick Cano的代码:https://github.com/GameHackingBook/GameHackingCode/blob/master/Chapter7_CodeInjection/main-codeInjection.cpp