MDSec 提供了一个商业命令和控制框架,可以避免隐蔽的活动被检测到,不过这已经不是什么秘密了。考虑到这一点,我们一直在研发可以检测到它们的方法。有些人会认为,建立一个难以捉摸的信标的最佳方法是,不仅要了解你的对手发现你的方式,还要尝试找到他们将来可以检测到你的新方法。
在这项研究中,我们将介绍一些寻找信标的有效策略,这些策略由我们为执行这些策略而开发的 BeaconHunter 工具提供支持,并且我们打算在适当的时候将其开源。
虽然有各种不同的方法来检测在网络中运行的信标,但我们将较少地关注特定的开发后功能的功能,而更多地关注识别驻留或加载到内存中的信标的一般方法。
信标在运行和加载时的行为可以为防御者带来检测机会。许多商业框架是封闭源代码的,因此某些行为无法轻易更改,从而允许防御者创建与这些行为一致的签名。
一个很好的例子是信标如何加载自身及其依赖项,让我们看看与图像加载相关的行为如何为防御者提供检测机会。
为了让信标提供丰富的利用后框架,它通常严重依赖操作系统原生的库,允许开发人员通过避免静态地绑定许多依赖项,使信标的大小尽可能小。
通过分析大量的信标框架,我们已经注意到,它们中的许多会在加载时加载核心信标所需的所有依赖项,而不是在使用时。在某些情况下,这将导致在终端上发生的一系列事件,防御者可以轻松地对其进行签名。
例如,通过加载 winhttp.dll 和 wininet.dll,可以看到信标利用本地 Windows HTTP 库作为出口信标,当加载到通常不会执行 HTTP 交互的进程时,这些可能会突出显示异常。此外,一些信标还会加载使用更少的库,如credui.dll、dbghelp.dll或samcli.dll。
使用这些DLL加载序列,就可以使用EQL规则来构建签名,以检测信标何时执行。
例如,使用类似于以下的 EQL 规则,可以在短时间内检测或搜索所有加载 credui.dll 和 winhttp.dlls 的进程:
此类检测当然可以通过设计为模块化或负载依赖于使用或具有延迟负载的信标来避免。
在许多情况下,信标可能会保留在内存中,以避免磁盘检测。信标通常是由加载器注入内存的,加载器将创建一个新的线程或劫持一个现有线程,在其中运行信标。
信标的典型加载过程可能如下所示:
一旦信标在内存中运行,通过对进程的分析,我们通常可以利用许多指标来检测信标,让我们看一些内存检测的方法。
检测已知恶意软件的内存信标的最简单但最有效的策略之一可能是通过签名检测。虽然许多反病毒引擎和 EDR 实施自己的内存扫描例程,但防御者可以使用 Yara 规则轻松实现全面的内存扫描。
一个可以与yara64.exe命令行工具一起使用的简单的Yara规则可能是这样的,它将匹配检测内存中列出的三个字符串中的任何一个:
为嵌入在信标中的字符串/数据或来自 .text 部分的代码创建 Yara 规则在概念上可以用作检测已知内存恶意软件的有效技术。
Elastic 过去在如何利用它来检测内存中的 Cobalt Strike 方面做了一些出色的工作,建议阅读这些技术如何在实践中应用。
为了逃避这种内存扫描,信标可以使用多种技术来混淆它们在内存中的足迹,包括替换已知字符串,例如可以使用 Cobalt Strikes strrep 可塑性配置文件选项或使用混淆和休眠策略,例如我们在 Nighthawk 中使用的一种,用于在休眠时保护信标的所有字符串、数据和代码。
为了规避控制或更改进程的运行方式,信标或操作员可以将挂钩应用于内存中的某些函数。这些挂钩可以留下隐藏的痕迹,从而为防御者提供揭示隐藏信标的机会。让我们来看看这种行为的一些具体示例。
修补诸如 AMSI 之类的安全控制或削弱通过 Windows 事件跟踪获得的检测数据在攻击性社区中已经不是什么秘密了,事实上我们过去曾在介绍过这些策略。
这些补丁通常通过修改内存来应用,让我们看两个来自 Sliver C2 库的例子:
https://github.com/sliverarmory/injectEtwBypass
https://github.com/sliverarmory/injectAmsiBypass
正如我们在上面的屏幕截图中看到的,这两个示例都会导致信标将补丁应用到 ntdll!etwEventWrite 或 amsi.dll!AmsiOpenSession 函数。
考虑到这一点,低噪声检测的机会就出现了,只需搜索应用这些补丁的进程,以及其他常用的补丁函数,如AmsiScanBuffer或sleeppex,由Cobalt Strike的线程堆栈欺骗功能应用于这些函数。
如上所述,信标应用补丁来处理内存的情况并不少见,这可以为防御者创造检测机会。然而,一旦执行了开发后操作,如果信标删除了这些补丁,检测的准确率可能会降低。例如,植入程序可以在内存中执行 .NET 程序集之前应用 AMSI 补丁,然后在执行后将补丁恢复为其原始操作码。这种方法比简单地将未修复漏洞留在内存中要明智一些。
然而,为了避免重复,Windows将把公共dll返回到运行进程共享的物理内存中。如果信标或操作员执行对这些dll应用补丁的操作,则会发生写入时复制操作,从而使该页面成为该进程的私有页面。使用 QueryWorkingSetEx API,我们能够查询有关进程特定虚拟地址的页面的信息。在返回的 PSAPI_WORKING_SET_EX_INFORMATION 结构中是一个 PSAPI_WORKING_SET_EX_BLOCK 联合,它指示查询地址处页面的属性。在这个联合中,我们能够通过共享位的返回值来确定页面上是否发生了写入复制操作。此技术被诸如 Moneta 之类的内存扫描仪使用,并且在检测内存中的补丁时非常有效,即使原始补丁值已恢复。
然而,在大规模应用这种技术时存在一些误报的风险,因为 EDR 和防病毒软件应用它们自己的内存挂钩并不少见,这意味着它们可以使我们从查找中获得的一些价值无效用于写操作时的复制。然而,为了降低误报的风险,我们可以通过解析修改页面上的导出,并对 EDR通常不挂钩的函数(如EtwEventWrite或AmsiScanBuffer)应用更大的权重,从而对其应用更多智能。
如上所述,一旦信标在内存中运行,它通常会存在于一个或多个线程中,具体取决于信标是同步的还是异步的。与这些线程相关的异常可以提供信标活动的高信号指标,特别是当与其他指标结合或相互结合时。一些常见的可疑线程相关指标包括:
未映射的内存:源自虚拟内存且不受 DLL 支持的线程是注入线程的经典标识。这些线程可以通过寻找具有 MemoryType 为 MEM_IMAGE 和 MemoryState 为 MEM_COMMIT 的内存区域的线程来轻松检测到。或者,这些线程通常由 EDR 检查通过线程创建 API 的内核回调来检测。有许多工具可以查找这个标识。
延迟状态:大部分时间信标将处于休眠状态,然后醒来恢复其任务。为了实现这种休眠行为,通常使用诸如sleeppex这样的windows API调用,这将使线程处于等待状态,并将导致线程调用堆栈包含对 KernelBase.dll!SleepEx 和 ntdll.dll!NtDelayExecution 的调用。当与其他指标结合时,例如module stomping的迹象(稍后讨论)或调用堆栈中对虚拟内存的调用,那么这可能会提供一些信标行为。
绕过可疑线程检测的尝试最近变得流行起来,一些概念证明和商业实现正在已发布。
这些实现通常通过截断线程的调用堆栈(例如通过将帧的返回地址设置为空)或通过复制现有线程的上下文来工作。考虑到这一点,我们可以寻找更多指标来添加到我们的指标中:
可疑起始地址:截断线程调用堆栈的副作用之一是起始地址并非源自预期位置。也就是说,线程通常源自 ntdll!RtlUserThreadStart 和 kernel32!BaseThreadInitThunk,或者在 CLR 线程的情况下源自 ntdll!RtlGetAppContainerNamedObjectPath。寻找不遵循此模式的线程可用作进一步分析的可疑指标。此外,如果线程的 NtQueryInformationThread(ThreadQuerySetWin32StartAddress) 的返回值与最后一帧的返回地址之间存在不匹配,则线程的起始地址也可以被认为是可疑的,这意味着有潜在的截断。
初始帧之间的距离:如上所述,调用堆栈的初始起始地址通常源自用于执行线程初始化和创建的一组地址。注意到这些初始堆栈帧之间的距离相对一致,并且通常在第一帧和第二帧之间是静态的(例如 ntdll!RtlUserThreadStart 和 kernel32!BaseThreadInitThunk)。在调用堆栈被截断的情况下,这些帧之间的距离几乎肯定是可变的。
复制上下文:如上所述,除了截断线程的调用堆栈外,欺骗调用堆栈的一种方法是复制合法线程的上下文。这种技术可以有效且实施起来相对简单。首先,在线程的线程信息块中,有许多指向有关线程的各种信息的指针,这些跨线程的副本,例如具有相同堆栈基数(堆栈底部)和堆栈限制(堆栈上限)的多个线程,是线程上下文已被复制的良好指标。
一般来说,信标将从虚拟内存中运行,或者如果Module Stomping,则从 DLL 支持的区域内运行。
为了使信标恢复并执行其任务,信标所在的页面需要对其应用执行权限。
例如,我们可以在下面的截图中看到,0x22f96c5000的内存没有一个DLL支持,它被标记为“Private:Commit”(即 VirtualAlloc 导致的虚拟内存),并设置了 RX 页面权限:
这些指标是一个强烈的信号,表明有信标在该地区执行。
接下来的挑战便是如何避免这一指标,答案很简单,如果你的信标是从虚拟内存运行的,则它不能,在某些时候信标需要执行。折衷方案实际上是仅在信标执行任务时维护可执行权限,并在信标休眠时利用策略删除可执行权限。因此,避免诸如 SOCKS 代理之类的交易有助于最大限度地减少该指标的暴露:
一些植入程序采用了根据信标状态调整页面保护的策略,以及几个开源实现,例如 @Ilove2pwn_ 的 Foliage、@c5pider 的 Ekko、@mariuszbit 的 ShellcodeFluctuation 和 Josh Lospinoso 的 Gargoyle。
这些策略通常会利用某种形式的事件驱动执行来休眠和唤醒信标,使用ROP小工具重新执行来调用VirtualProtect,并将信标的页面重置为可执行权限。由MDSec的Peter Winter-Smith发现并被Ekko使用的基于计时器的技术,最初是由MDSec的Nighthawk c2逆向工程而来。简而言之,这种技术的工作原理是使用CreateTimerQueueTimer将多个计时器排队,然后当事件触发器返回到之前定义的Context记录时,使用NtContinue执行并调用VirtualProtect来重新启用执行位。
要更详细地理解这种技术,可以在这里找到@Ilove2pwn_的原始文章。
Module Stomping提供了一种将信标隐藏在内存中的替代方法,从而避免了与未映射内存中的信标相关的一些常见指标。为了实现这一点,需要加载一个不太可能被进程使用的合法DLL,信标通过模块自我复制,然后创建一个由 stomped 代码支持的线程:
Cobalt Strike 自 3.11 版本以来就提供了此功能,并且可以使用“set module_x64 / module_x86”可扩展配置选项使用。
虽然这种技术可以提供许多 OpSec 优势,但它确实留下了几个我们可以可靠检测的指标:
1.检测这种攻击的最简单的技术可能是比较内存中的模块内容和磁盘上存在的模块内容。代码部分的任何变化几乎肯定会暗示一些可疑的行为。这个进程当然是相对密集的,因为它需要从磁盘加载进程中的所有模块,并与运行中的内存进行比较。
2.如上所述,对进程内模块存储器的修改将导致发生写入操作时的复制。考虑到这一点,用于检测内存挂钩的相同逻辑也可以应用于检测Module Stomping,因为在内存中覆盖 DLL 将导致生成 DLL 的副本并清除共享位。
3.有几种方法可用于执行Module Stomping,其中一些涉及利用现有的 Windows API,例如 LoadLibrary,而其他更复杂的实现可能使用自定义加载器将 DLL 映射到内存中。一些技术具有与它们相关的已知指标,在 PEB 中留下可用于寻找该技术的常驻痕迹是高度可信的。稍后将更详细地讨论这方面的示例。
Cobalt Strike 是最受欢迎的命令和控制框架之一,受到防御者和攻击者的青睐。在这篇文章中,我们将讨论防御者如何使用第一篇文章中介绍的技术,在不同配置和跨网络中检测 Cobalt Strike 。所有分析均在 Cobalt Strike 4.6.1 上进行。、
Cobalt Strike 信标具有高度扩展性,因此某些指标可能会根据所选的扩展性配置文件选项而有所不同。
过去,在内存中寻找 Cobalt Strike 签名对于防御者来说是卓有成效的,之前 Elastic 提供了全面的记录。然而,从那时起,HelpSystems做了很多工作,Cobalt Strike 4.4 引入了休眠策略。
Cobalt Strike 为其混淆和休眠策略提供了以下可能的配置选项:
没有休眠掩码:信标、它的字符串和代码将在内存中保持明文,并且可以通过内存扫描轻松识别。
在可扩展配置文件中启用 sleep_mask:当在 malleable 配置文件中将 sleep_mask 设置为 true 时,beacon 将使用内置的混淆和休眠策略来屏蔽内存中使用 xor 来混淆字符串和数据的信标。正如 Elastic 在前面提到的帖子中所详述的,这当然可以通过定位代码部分来签名。
使用用户定义的休眠掩码:用户定义的休眠掩码向用户公开信标的混淆和休眠功能,允许他们滚动自己的实现。在这样做的同时,它还为用户提供了许多指向信标使用的任何堆记录的指针,以便用户可以对它们进行加密。利用用户定义的休眠掩码确实有一些折衷,特别是为了混淆 .text 部分,配置必须将“userwx”选项设置为 true。也就是说,如果要对其进行混淆,信标将始终存在于 RWX 内存中。如果 userwx 选项设置为 false,则信标将从 RX 内存中运行,但 .text 部分不会被混淆,因此可以进行签名。由操作员自行决定选择他们觉得最舒服的指标。
例如,当使用将 userwx 选项设置为 false 的 Sleep Mask Kit 时,可以使用以下 Yara 规则检测 Cobalt Strike:
对注入的信标运行此Yara规则将显示检测到签名:
启用 userwx 会将页面权限设置为 EXECUTE_READWRITE ,但这意味着信标现在正在混淆其 .text 部分:
Cobalt Strike 信标通常在具有 RX 或 RWX 页面权限的页面上运行,具体取决于可扩展配置文件的“userwx”配置选项的值,并且没有Module Stomping,将由未映射的内存支持。
这是一个明确的指标,使得在内存中发现信标相对来说很简单:
为了避免JIT程序集,可以扫描这些内存区域,搜索具有PAGE_EXECUTE_READWRITE或PAGE_EXECUTE_READ页面权限和MEM_COMMIT标志的页面。当与其他指标一起使用时,这对识别信标活动可能是有价值的。当使用-p标志时,我们将此签入到BeaconHunter中:
注入内存时,Cobalt Strike 会占用一个线程,信标是同步的。默认情况下,信标运行所在的线程是高度可疑的,并且有许多与之相关的指标。
在 Process Hacker 中检查 Cobalt Strike 信标的线程可能看起来像这样:
仅在上面的屏幕截图中,我们就可以看到一些使线程看起来非常可疑的指标:
首先,线程通常有一个 0x0 的起始地址。总体而言,这有点不规则,尽管从扫描合法进程来看,它确实有时会在某些进程(如 chrome.exe)中发生。
深入了解线程的调用堆栈,我们还注意到对 KernelBase!SleepEx 和 ntdll.dll!NtDelayExecution 的调用。这些调用是信标处于睡眠状态的标志,用于在信标处于睡眠状态时延迟线程的执行。
在调用 KernelBase.dll!SleepEx 之前,我们可以在 0x1b8ef69fcc7 的跟踪中看到调用,堆栈遍历尚未解析此地址的符号,因此几乎可以肯定它是虚拟内存。由虚拟内存支持的线程非常可疑,可能需要进一步分析。
综上所述,防御者能够高度自信地确定恶意活动来自线程。由于这些指标,BeaconHunter 会排除这些可疑线程:
还应该注意的是,Cobalt Strike 在 21 年 6 月将堆栈欺骗引入了到了工件中。但是,调用堆栈欺骗仅适用于通过工件工具包生成的 exe/dll 工件,而不是通过注入线程中的 shellcode 注入的信标。因此,它们不太可能有效地掩盖内存中的信标。
分析表明,fiber的使用很少见,因此可以通过分析 ntdll.dll!RtlUserFiberStart 的起始地址的调用堆栈来轻松找到这些fiber,当与其他指标结合使用时,可以为寻找 Cobalt 工件提供一个好的开端:
Cobalt Strike 支持使用“set module_x64”和“set module_x86”可扩展选项的Module Stomping。配置这些选项会导致信标由磁盘上的模块支持,提供一些操作安全方面的优势,而不是从虚拟内存中操作。
然而,Cobalt Strike Module Stomping技术的实施确实在流程的 PEB 中留下了一些攻击指标。除了将内存中的模块与磁盘上的副本进行比较或删除共享位之外,实际的实施技术还允许防御者创建高度准确的Module Stomping检测。
鉴于 Cobalt Strike 用于Module Stomping的方法,它首先使用对 LoadLibraryExA(moduleName, NULL, DONT_RESOLVE_DLL_REFERENCES) 的调用来加载牺牲 DLL:
这指示加载器不执行 DLL 入口点,并避免处理 DLL 的导入表以加载依赖项。
但是,这种方法的副作用是 PEB 中的 LDR_DATA_TABLE_ENTRY 结构中的某些属性被保留在罕见的配置中,具体而言,EntryPoint 属性将设置为 NULL 并且 ImageDLL 位为 false。
遍历PEB并解析该结构可以为配置中这些属性的组合提供一个高置信度指标:
Cobalt Strike C2 服务器基于 NanoHttpd,这是一个轻量级 Java HTTP 服务器,并进行了少量更改以使其与 Cobalt Strike 用例保持一致。FoxIT 之前已经完成了在野外识别 Cobalt Strike 团队服务器的工作,然而,多余的空白字符的痕迹早已被修补。当时,FoxIT的研究表明,Cobalt Strike C2服务器可能比NanoHttpd服务器多得多。
对 Cobalt Strike C2 服务器的粗略分析揭示了许多进一步的 C2 指纹识别方法。
第一个涉及到 Range HTTP 标头的使用,其中发送一个带有无效整数的请求将导致服务器不返回任何响应:
可以看到,发生了未处理的异常:
仔细查看C2服务器源代码(src/main/java/cloudstrike/WebServer.java),我们可以很快找到原因,服务器尝试将字符串转换为整数,但该值无效且未进行异常处理:
用于对 Cobalt Strike C2 服务器进行指纹识别的第二种技术也存在于 Range HTTP 标头中。提供服务器无法满足的范围(例如 1-0),将导致一个“range Not Satisfiable”错误。虽然这个错误也会出现在 NanoHTTPd 服务器中,但我们可以通过错误的 Server 标头暗示它是 Cobalt Strike,即 IIS、Apache 和 Nginx 等服务器不返回此错误:
进一步分析C2服务器的源代码,我们可以很容易地识别其他可能导致指纹的响应,例如 src/main/java/cloudstrike/NanoHTTPD.java 中的以下内容:
正如我们在上面看到的,当在 URI 中提交了无效的 URL 编码字节时,C2 服务器将返回一个可指纹识别的响应。
使用这个逻辑,我们可以构建一个 Nuclei 模板来扫描团队服务器:
匹配示例如下:
本文我们介绍了防御者可以识别来自野外 Cobalt Strike 信标恶意活动的一些技术,包括内存中、线程内和整个网络中的指标。
我们在本文中介绍了对 C2 框架执行威胁追踪的通用方法以及针对 Cobalt Strike 的实际示例。下一篇文章中,我们将分析由 Dark Vortex 开发的命令和控制框架 Brute Ratel。
参考及来源:https://www.mdsec.co.uk/2022/07/part-1-how-i-met-your-beacon-overview/
https://www.mdsec.co.uk/2022/07/part-2-how-i-met-your-beacon-cobalt-strike/