剖析Cobalt Strike的DLL Stager
2021-05-08 15:20:31 Author: xz.aliyun.com(查看原文) 阅读量:272 收藏

NVISO最近监测到了针对其金融部门客户的作为目标的行动。根据员工关于可疑邮件的报告,该攻击在其开始阶段就被发现。虽然没有造成任何伤害,但我们通常会确定任何与之相关的指标,以确保对实施此行为者进行额外的监视。

被报告邮件是申请公司其中一个公开招聘的职位,并试图发送恶意文档。除了利用实际的工作Offer,还引起我们注意的是恶意文档中还存在execution-guardrails 。通过对该文档分析,发现了通过Component Object Model Hijacking(组件对象模型劫持)来维持Cobalt Strike Stager的意图

在我空闲的时间里,我很享受分析NVISO标记的野外样本,因此进一步解剖 Cobalt Strike DLL payload,这篇博客文章将介绍有效载荷的结构,设计选择,并重点介绍如何减少日志足迹和缩短Shellcode的时间窗口。

分析执行流

为了了解,恶意代码是如何工作的,我们必须去分析其从开始到结束的行为。在本节中,我们将介绍一下流程。

1.通过DllMain初始化执行

2.通过WriteBufferToPipe,将加密shellcode发送到命名管道

3.通过管道进行读取,通过PipeDecryptExec,解密shellcode并执行

如前所述,恶意文档的DLL的载荷意图伪装为COM in-process server。有了这些认识,我们可以着眼与DLL公开的一些已知的入口点。

从技术层面来说,恶意代码可以发生在该8个函数中任意一个,但恶意代码通常驻留在DllMain给定的函数中,除了TLS callbacks,它是最可能执行的函数。

DllMain:动态链接库(DLL)的可选入口点。当系统启动或终止进程或线程时,它将使用进程的第一个线程为每个已加载的DLL调用入口点函数。当使用LoadLibraryFreeLibrary函数加载或卸载DLL时,系统也会调用DLL的入口点函数。

docs.microsoft.com/zh-CN/windows/win32/dlls/dllmain

DllMain 入口点

从下面的捕获结果可以看到,该DllMain函数只是通过创建一个新线程来简单执行另一个函数。该线程函数我们命名为DllMainThread,它不需要提供任何参数即可执行。

分析DllMainThread函数发现其实它是对我们将发现的恶意载荷的解密和执行函数的一个附加的包装.(被保护函数在捕获中被称为DecryptBufferAndExec)

进一步深入,我们可以看到恶意逻辑的开始。具有Cobalt Strike经验的分析师会立马意识到这个众所周知的MSSE-%d-server特征。

上面代码中发生了几件事:

1.该示例开始通过GetTickCount获取tick计数,然后将其除以0x26AA。尽管获取的计数通常是时间的度量,但下一个操作仅仅将其作为随机数来使用

2.然后该示例继续调用在sprintf函数周围的包装器。它的作用是格式化字符串为PipeName的缓冲区。如可以观察到,格式化字符串将是\\.\pipe\MSSE-%d-server其中%d为前面的除法计算的结果。(例如:\\.\pipe\MSSE-1234-server)。这个管道格式是有据可查的Cobalt Strike 威胁指标。

3.通过在全局变量定义管道的名称,恶意代码将创建一个新线程以运行WriteBufferToPipeThread.此函数使我们接下来即将分析的。

4.最后,在新的线程运行时,代码跳转到PipeDecryptExec例程。

到目前为止,我们有了线性的从DllMain入口点到DecryptBufferAndExec函数的执行过程,我们可以绘制如下流程:

如我们所见,两个线程现在将同时运行。让我们集中于其中写内容到管道 (WriteBufferToPipeThread) 的线程,其次是与之对应PipeDecryptExec的内容。

WriteBufferToPipe线程

写入生成的管道的线程是在DecryptBufferAndExec没有任何其他参数的情况下启动的。通过进入该函数,我们可以观察到它只是一个WriteBufferToPipe前的简单的包装器,此外传递如下从全局Payload变量(。(由pPayload指针指向))恢复的参数。

1.shellcode的大小,存储在offset=0x4处

2.指向包含加密shellcode缓冲区的指针,该缓冲区存储在offset=0x14处

WriteBufferToPipe函数中,我们可以注意到代码是通过创建新管道开始的。管道的名称是从PipeName全局变量中恢复的,如果您还记得的话,该全局变量先前是由sprintf函数填充的。

代码创建了单个实例,通过调用CreateNamedPipeA导出管道((PIPE_ACCESS_OUTBOUND)),然后通过调用ConnectNamedPipe将其连接到该实例。

如果连接成功,WriteBufferToPipe函数只要有shellcode的字节需要写入到管道就继续循环调用WriteFile 来实现写入。

值得注意的一个重要细节是,一旦shellcoode写到了管道中,先前打开管道的句柄就通过CloseHandle来关闭。这表明管道的唯一目的就是为了传输加密的shellcode。

一旦`WriteBufferToPipe `函数执行完毕,线程终止。总体而言,执行流程非常简单,可以如下绘制:

PipeDecryptExec 流程

作为快速恢复部分, 该PipeDecryptExec流程在创建WriteBufferToPipe线程后被立即执行。执行的一个任务是分配一个内存区域,接收要通过命名管道传输的shellcode。为此,将存储在全局Payload变量偏移0x4处的shellcode大小作为参数来执行malloc调用。

一旦缓冲区分配完成,代码将休眠1024毫秒(0x400),并将缓冲区位置和大小作为参数调用FillBufferFromPipe。如果该函数调用失败则返回FALSE (0),那么代码会再次循环至该Sleep调用,并再次尝试操作,直到操作成功为止。这些调用和循环是必需的,因为多线程示例必须等待至shellcode写入到了管道中。

一旦将shellcode写入到了分配的缓冲区中,PipeDecryptExec最终会通过XorDecodeAndCreateThread来解密并执行shellcode。

要将加密的shellcode从管道传输到分配的缓冲区中, FillBufferFromPipe通过CreateFileA以只读的方式(GENERIC_READ))打开管道.就像创建管道一样,从全局PipeName变量中获取名称。如果访问管道失败,则函数将继续返回FALSE (0),而导致上述Sleep并重试的循环。

一旦管道以只读模式打开,FillBufferFromPipe函数接着会继续复制shellcode直到分配缓冲区使用ReadFile填满为止。缓冲区填满后,CloseHandle关闭命名管道的句柄,并且FillBufferFromPipe函数返回TRUE (1).

一旦FillBufferFromPipe成功完成,命名管道已经完成了它的任务和加密的shellcode已经从一个存储区域移动到另一个。

回到调用者PipeDecryptExec函数中,一旦FillBufferFromPipe函数调用返回TRUEXorDecodeAndCreateThread将使用以下参数进行调用:

  1. 包含复制的shellcode的缓冲区。
  2. shellcode的长度,存储在全局Payload变量的offset=0x4处。
  3. 对称XOR解密密钥,存储在全局Payload变量的offset=0x8处。

调用后,该XorDecodeAndCreateThread函数首先使用VirtualAlloc分配另一个内存区域。分配的区域具有读/写权限(PAGE_READWRITE),但不能执行。通过不同时具有可写和可执行权限,这个示例可能是尝试躲避一些只寻找PAGE_EXECUTE_READWRITE区域的安全解决方案。

一旦这个区域被分配,函数就会在Shellcode缓冲区上循环并使用简单的xor操作将每个字节解密

分配到新的内存区域中。

解密完成后,GetModuleHandleAndGetProcAddressToArg函数被调用。它的作用放置指向两个有用的函数指针到内存:GetModuleHandleA and GetProcAddress。这些函数能够允许shellcode

解析其他过程,而不必依赖于它们的导入。在存储这些指针之前,该GetModuleHandleAndGetProcAddressToArg函数首先确保特定值不是FALSE0)。令人惊讶的是,存储在全局变量(此处称为zero)中的该值始终为FALSE,因此指针从未被存储。

回到调用者函数,XorDecodeAndCreateThread使用VirtualProtect更改shellcode的存储区域为可执行权限(PAGE_EXECUTE_READ),最终创建一个新线程。该线程从JumpToParameter函数开始,该函数充当shellcode的简单包装,shellcode作为参数提供。

从这里开始,执行先前的加密Cobalt Strike shellcode stager ,解析WinINet 过程,下载最终的信标然后执行它。我们不会在这篇文章中介绍shellcode的分析,因为它应该用一篇专属的文章来分析。

尽管最后一个流程包含了更多的分支和逻辑,但总体流程图仍然非常简单。

内存流分析

在上述分析中,最令人惊讶的是存在一个众所周知的命名管道。通过在管道出口处解密shellcode或进行进程间通信,可以将管道用作防御逃避机制。

但在我们的案例中,它只是充当memcpy将加密的Shellcode从DLL移至另一个缓冲区的作用。

那么为什么要使用这种开销呢? 正如另以为同事指出,答案在于Artifact Kit,这是Cobalt Strike的依赖项:

Cobalt Strike uses the Artifact Kit to generate its executables and DLLs. The Artifact Kit is a source code framework to build executables and DLLs that evade some anti-virus products. […] One of the techniques [see: src-common/bypass-pipe.c in the Artifact Kit] generates executables and DLLs that serve shellcode to themselves over a named pipe. If an anti-virus sandbox does not emulate named pipes, it will not find the known bad shellcode.

cobaltstrike.com/help-artifact-kit

正如我们在上图中所看到的,在malloc缓冲区加密shellcode的stageing为了躲避检测会产生大量开销。XorDecodeAndCreateThread直接从初始加密的Shellcode中读取则可以避免这些操作。如下图所示,避免使用命名管道将进一步消除对循环Sleep调用的需求,因为数据将随时可用。

看来我们找到了一种减少得到shellcode时间的方法。但是流行的防病毒解决方案是否被命名管道所欺骗?

修补执行流程

为了检验该推测, 让我们改进恶意执行流程。对于初学者,我们可以跳过与管道毋庸的调用,而直接在DllMainThread函数调用PipeDecryptExec,从而绕过管道的创建和编写。汇编级的修补方式执行过程超出了本文的讨论范围,这里我们仅感兴趣于流程的抽象。

PipeDecryptExec功能还需要打补丁以跳过malloc分配\读取管道,并确保它能够提供XorDecodeAndCreateThread需要的DLL的加密shellcode而不是现在不存在的重复区域。

修补我们的执行流程后,如果安全解决方案将这些未使用的指令作为检测基础,则我们可以将其都清空。

应用补丁后, 们最终得到了一条线性且较短,直到执行Shellcode的路径。下图专注于此修补路径,不包括下面的分支WriteBufferToPipeThread.

由于我们也弄清楚了shellcode如何进行加密(我们拥有xor密钥)一样,我们修改了两个示例以修改C2的地址,因为它可用于识别我们的目标客户。

为确保shellcode不依赖任何绕过的调用,我们启动了一个快速的Python HTTPS服务器,并确保经过编辑的domain解析为127.0.0.1。然后,我们可以通过rundll32.exe调用原始DLL和修补的DLL,并观察shellcode如何去试图检索Cobalt Strike Beacon,以证明我们的补丁程序没有影响到shellcode。我们调用的导出的StartW 函数是在Sleep调用周围的简单包装。

防病毒审查

那么,命名管道实际上是否可以用作防御逃避机制?尽管有有效的方法来衡量补丁程序的影响(例如:比较多个沙盒解决方案),但VirusTotal确实提供了快速的初步评估。因此,我们向VirusTotal提交了以下版本重新编辑C2的样本:

  • wpdshext.dll.custom.vir 这是经过编辑的Cobalt Strike DLL。
  • wpdshext.dll.custom.patched.vir 这是我们未命名的补丁和编辑过的Cobalt Strike DLL。

由于原始的Cobalt Strike包含可识别的特征(命名管道),因此我们希望修补后的版本具有较低的检测率,即使Artifact Kit不这样认为。

正如我们预期的那样,Cobalt Strike所利用的命名管道开销实际上是作为检测基础。从以上截图中可以看出,原始版本(左)仅获得17次检测,而修补版本(右)获得的共16次检测少了一个。在给出的解决方案中,我们注意到ESET和Sophos未能检测到无管道版本,而ZoneAlarm无法识别原始版本。

一个值得注意的观察结果是,一个适配流程处于中间过程的补丁,但未对未使用的代码清0,结果发现它是检测到最多的版本,总共有20个匹配。出现更高检测率是因为此修补程序允许不认识管道的防病毒提供商也仍然可以使用与管道相关的操作签名去定位shellcode。

尽管这些测试针对的是默认的Cobalt Strike缺乏命名管道的行为。但有人可能会争辩,定制的命名管道模式将具有最好的效果。尽管我们在最初的测试中没有想到这种变体,但我们在次日提交了一个版本(改变了管道名称为NVISO-RULES-%d而不是MSSE-%d-server),然后获得了18个检测结果。作为比较,我们另外两个样本的检测率在一夜之间增加到30+。但是,我们必须考虑这18个检测结果受初始shellcode的影响。

结论

事实证明,逆向恶意的Cobalt Strike DLL比预期的要有趣。总体而言,我们注意到存在嘈杂的操作,这些操作的使用不是功能要求,甚至可以充当检测基础。为了证实我们的假设,我们修补了执行流程,并观察了简化版本如何以较低的检测率(几乎未更改)到达C2服务器。

因此,这个分析为什么很重要?

蓝队

首先,最重要的是,这个载荷的分析突出显示了常见的Cobalt Strike DLL特征,使我们可以进一步微调检测规则。虽然这个Stager是第一个被分析的DLL,但我们确实寻找了其他Cobalt Strike格式,比如默认信标和可以用的可延展的C2,包括动态链接库和可移植的可执行文件。出乎意料的是,所有格式都共享了这个常见的文档记录的MSSE-%d-server的管道名称,并且对开源检测规则的快速搜索显示了它被寻找的东西很少。

红队

除了对NVISO的防御行动有所帮助外,这项研究还使我们的进攻团队在选择使用定制交付机制方面感到更加安慰。更重要的是,遵循我们记录的设计选择。在针对成熟环境的操作中使用命名管道更有可能引发危险信号,并且到目前为止,至少在不更改生成方式的情况下,似乎仍无法提供任何可逃避的优势。

对于下一个针对我们客户的参与者: 我期待着修改您的样本并测试更改后的管道名称的有效性。

译者注: 修改管道名称还是蛮有用的...

本文未翻译文章,原文链接:https://blog.nviso.eu/2021/04/26/anatomy-of-cobalt-strike-dll-stagers/


文章来源: http://xz.aliyun.com/t/9512
如有侵权请联系:admin#unsafe.sh