今年,Cobalt Strike宣称在新版本中改进了睡眠时的内存保护,能够加密包括Sleep Mask在内的所有内存空间[1],进一步减少了睡眠期间在内存中的特征暴露,提高了其在内存中的对抗性,简单地通过内存Pattern进行内存扫描与检测的适用性大幅降低。随后,Elastic发布了8.8版本,添加了基于内核调用堆栈的检测功能,针对内存威胁如内核线程启动、调用堆栈异常、模块踩踏等威胁推出了针对性的检测方式。Elastic通过有别于传统内存扫描的方式实现威胁检测,本文将围绕调用堆栈上的攻防博弈展开叙述。
调用堆栈是一个强大的数据源,如果获取的数据可靠,则能够清晰反映行为的执行过程。基于栈回溯信息,能够还原相关API的调用序列,进而推测出其对应的行为。基于调用栈的检测方式从粒度更加精细的API调用情况对恶意行为进行识别。最新的Elastic Agent已支持基于ETW实现对相关API的监控,并获取其执行时的调用栈信息:
可以从收集到的日志中看出,Elastic支持对栈回溯信息中出现的可疑调用进行告警。以下是一个Elastic的开源规则:
可见,Elastic在内存防护上已经深入到了API调用级别。综合调用堆栈与可疑行为日志信息,Elastic能够更加精准地还原威胁行为的具体细节,对威胁行为进行研判与告警。Elastic基于调用栈实现了对一系列传统方式难以检测的威胁行为的检测告警[2],以上面展示的恶意宏为例:
传统的检测方式中若仅有进程、DLL加载等粗粒度的信息,只能通过Office相关进程对vbe.dll的加载与文件本身来自网络,具有Web标记以及其可疑子进程等信息来推断该文件的潜在恶意行为。但这无法说明文件中确实存在风险行为,更像是在以“宁可错杀一千,不可放过一个”的查杀方案在最大程度上保障终端的安全。但结合了调用链信息的检测可以从可疑行为的调用中获取更为具体的信息,如xlAutoOpen表明宏代码会在文档打开时自动执行,shell32!ShellExecute和kernel32.dll!WinExec则对命令执行相关API做了针对性监控:
基于具体的调用信息无论是进行规则编写还是检测告警,都能够使得最终的防护效果得到提升。
基于调用堆栈的行为检测方式是对传统内存检测的有力补充,在终端检测上实现了更加细粒度的日志搜集,提高了告警准确度,也大大增加了检测绕过的难度。攻击者即便对载荷内存做了睡眠期间的加密保护,以不暴露任何特征,但检测方从调用堆栈上仍能分析出潜在的恶意行为。
调用堆栈无疑是检测潜在威胁、获取关键信息的重要遥测数据来源,对减少误报,提高告警精准性有着不可或缺的作用。但基于调用堆栈的告警往往建立在“栈是可信的”这一基础上,若获取到的栈信息本身就不可靠,即从根源上瓦解了栈回溯构建起的防御工事。调用堆栈截断与调用栈欺骗即为两种最为常见的对抗方式。
如下图所示,攻击者通过篡改调用栈,提前截断回溯检测,使得无法通过调用栈回溯到BaseThreadInitThunk和RtlThreadStart等常见的线程启动API,以阻止获取到线程地址等信息进一步的回溯分析。
截断的调用栈会使得终端检测收集的信息出现缺失,无法获得完整的API调用序列,从而无法对恶意行为作出准确的判断和告警。但提前截断的调用栈同样是一个较为明显的特征,在检测对抗中需要更进一步的优化。
调用栈欺骗在近年来被广泛应用到C2工具中以实现检测逃逸。其原理是通过构造虚假调用栈帧或利用特殊函数的代理执行来实现对基于栈回溯获取可疑地址的检测方式的对抗。
1)虚假栈帧构造
对于调用栈截断的方式,由于调用栈被异常截断,因而也是潜在的检测点之一。且篡改的调用栈由攻击者自行构造,并非真实的调用栈,故在回溯检查时可能会被标记以进行进一步的检测。在调用栈截断的基础上发展出了通过构造虚假的调用栈,且调用栈中的地址均来自真实的调用地址的堆栈欺骗方式[3]。如下图所示,虚假栈帧的构成中,函数地址为真实的MessageBoxA,且参数的构造也遵循合法函数传递:
最终的调用栈呈现如下:
整个调用栈中不再出现来自未知模块的调用,且基于调用栈回溯也无法得到真实的调用信息,自然也无法基于此得到准确的行为判定。相比起简单粗暴的调用栈截断,构造虚假栈帧对堆栈进行填充使得能够将栈一直展开到栈底,且其中的所有地址都有模块信息,大大降低了可疑度。但由于伪造栈帧的函数选取不慎,还是有潜在的暴露风险,例如选取了NtAllocateVirtualMemory、NtAllocateVirtualMemory、SetDefaultCommConfigW等不会返回的函数,若调用栈上出现了类似的函数则说明该进程可能正在掩饰恶意行为。
2)利用特殊函数代理执行
根据BRC4武器商的披露,EDR等终端安全设备会通过挂钩或ETW对LoadLibrary乃至LdrLoadDll做监控,当检测到上述函数的返回地址指向可疑的RX属性的shellcode区域即判定该区域是可疑的有效负载。为了规避这种检测逻辑,需要对LoadLibrary的调用进行掩藏。
BRC4团队提出了利用回调函数中断调用链跟踪的方案[4],且利用了TpAllocWork的回调执行以避免回调与调用者处于同一个线程中。利用TpAllocWork调用自定义函数,并把目标模块的模块路径作为参数传入自定义函数。自定义函数通过汇编代码自行布置参数,然后通过jmp跳转到LoadLibraryA的函数指针上,完成调用。最终LoadLibraryA的调用栈如下:
可以看到,攻击者成功将LoadLibraryA伪造成一个来自已知模块的调用,整个调用栈上不存在任何来自未知模块的地址。通过TpAllocWork的回调执行,结合jmp指令对目标函数的函数指针调用,使得栈上不出现来自调用方的返回地址,从而掩盖栈上的调用链信息。
Cobalt Strike的开发团队也有相应的解决方案[5],他们介绍了一个调用栈伪造的开源项目CallStackMasker[6],能够支持硬编码的调用栈伪造以及动态获取处于指定线程状态下的其他合法线程调用栈实现伪造两种方式构造虚假的调用关系。下面左图为未经伪造的调用栈,右图为伪造后的结果:
可以看到,经过伪造后的调用栈隐去了调用者的相关信息,已无法从中回溯到可疑的信息,对基于栈回溯的安全检测方式造成了不小的干扰。且由于该项目支持利用系统上的其他线程的合法调用栈进行伪装,其检出难度大大增加。
由Elastic的开源规则可以看出,基于栈回溯获取相关调用来实现威胁检测的方式中,检测规则规定了行为具体调用的API,通过具体的API调用链来甄别恶意行为。在去年Nighthawk的更新中就对一系列敏感API调用进行了等效替换,以此寻找替代API以实现绕过终端防护对敏感API的挂钩。例如常见的创建线程操作,其调用序列如下:
通过对NtCreateThreadEx的调用链探究,存在以下调用关系:
可见,除了CreateRemoteThreadEx、RtlCreateUserThread这两个常见的线程创建方式之外,还可以利用RtlCreateProcessReflection来实现相同的功能。攻击者可以利用类似的方式来挖掘等价的替代API调用,实现对API监控以及基于调用链的行为判断的绕过。
由近期以Elastic代表的终端检测与Cobalt Strike代表的C2武器的演变可以看到,目前终端对抗已越来越向底层发展,逐渐由粗粒度的行为链的检测与对抗发展成API层面的调用链检测与对抗。但基于调用链进行行为研判与告警的方案的实现前提是能够获取到真实的调用栈,如果调用栈经过伪造或替代了敏感API调用,则大大削减了数据来源的可靠性,最终的研判与告警效果也大打折扣。终端行为的监控与防护需要获取更为可靠的监控信息,做出更准确的判断以达成更好的防护效果。
[1] https://www.cobaltstrike.com/blog/cobalt-strike-and-yara-can-i-have-your-signature
[2] https://www.elastic.co/security-labs/peeling-back-the-curtain-with-call-stacks
[3] https://github.com/klezVirus/SilentMoonwalk/tree/masterhttps://github.com/klezVirus/SilentMoonwalk/tree/master
[4] https://0xdarkvortex.dev/proxying-dll-loads-for-hiding-etwti-stack-tracing/
[5] https://www.cobaltstrike.com/blog/behind-the-mask-spoofing-call-stacks-dynamically-with-timers
[6] https://github.com/Cobalt-Strike/CallStackMasker/tree/main