浅谈CobaltStrikeUDRL

前言

在阅读这篇文章之前, 我建议读者先掌握一些基础的逆向知识(PE结构、汇编等),其次是掌握反射Dll的加载原理,大家可以先看这两篇文章:反射Dll原理Shellcode原理,看完后阅读本次博客的内容可能会比较轻松。

本次博客的主要内容是针对CobaltStrike两篇官方文档的学习分享:UDRL简单开发UDRL混淆遮掩

内嵌式Loader

实现原理

CobaltStrike默认是采用传统的反射loader(stephenfewer),即“内嵌式“loader。Beacon的Dos头存放着调用ReflectiveLoader函数的调用地址,这样做的目的是,当Beacon被执行时,它会立即跳至ReflectiveLoader函数执行,函数执行完毕后返回DLL的入口函数地址

UDRL_14.png

为了更好理解CobaltStrike传统的反射Loader加载原理,我们需先取消掉Profile的所有配置(或者直接不加载Profile),随后将生成Beacon.bin文件放到Pe-bear中反汇编,通过查看其DOS头部分,可整理出其大致流程

  • 使用RIP寻址来获取beacon的基址,然后将beacon的基址存储在RDI寄存器

  • 调用导出的ReflectiveLoader函数(函数地址是0X188D4)

  • 调用已加载Beacon DLL的入口函数

QQ图片20231021193042

那么我们该如何确定这个0X188D4就是ReflectiveLoader函数的地址呢? 切换至导出表界面, 发现只有一个导出函数, 其RVA地址为194D4, 转换为FOA后即为188D4

image-20231021193713885

这里简单讲解下RVAFOA的方法:通过查看.text节的RawAddressVirtualAddress, 它们俩之间的差值为0xC00, RVA减去这个差值后的值即为FOA(194D4-0xC00=0X188D4)

image-20231021194009909

Aggressor脚本实现

从下述Aggressor脚本可发现,通过使用setup_reflective_loader函数来将自定义Loader替换掉Beacon中的默认Loader

image-20231027153912057

前置式Loader

实现原理

Double Pulsar是替代传统反射loader的另外一个项目,与传统反射loader不同的是,它没有将ReflectiveLoader函数编译到DLL中,而是放在DLL的前面,因此这也被称为“前置式loader”,这种方式最大的优点是能够反射加载任意PE文件,以下是“内嵌式loader”和“前置式loader”的对比图

image-20231026163819323

将生成的raw文件放入010 Editor中,可以发现前面部分为loader,后面部分为beacon

image-20231102200709345

源码分析

1.获取loader和beacon的地址

为了确定ReflectiveLoader的起始地址和结束地址,可使用关键字code_seg来指定哪些部分用于存储特定的功能, 然后通过字母值来对这些部分进行排序

例如下述代码所示,使用了#pragma code_seg(".text$a"),这样表示代码应该被放置在.text段的一个特定子段中,然后链接器会根据$后面的字符进行排序,也就是说,.text$a会在.text$b之前,这样可以确保函数或代码块在链接时按照预期的顺序出现

img

从上图可知,因为Loader是在Beacon前面,我们只需找到.text$a段就能定位到ReflectiveLoader函数的起始地址

image-20231026201315922

获取到ReflectiveLoader函数地址后,那么接下来就是获取Beacon的起始地址。通过字母值排序代码段可知,code_seg(".text$z")loader的末尾地址,这里使用LdrEnd()函数地址来表示loader末尾地址,那么,Beacon的起始地址 = Loader的末尾地址 + 1

image-20231026201600987
image-20231026202225728

2.获取系统函数地址

在CobaltStrike的UDRL模板中,使用CompileTimeHash函数替换了StephenFewer ReflectiveLoader所采用的静态哈希值,其使用constexpr关键字来定义函数,表示函数的返回值在编译时时已知的,这就意味着哈希值是在编译阶段生成的,而不是在程序运行时,而且可通过更改HASH_KEY的值可以帮助抵御简单的静态签名

image-20231026210004946

通过使用CompileTimeHash函数可以计算出指定模块名和函数名的哈希值

image-20231026210922037

然后在使用GetProcAddressByHash函数来获取指定函数的地址, 以此方便后续的API调用

image-20231026211907751

3.字符串声明方式

在C\C++中,通常字符串是保存在PE文件的.data节或.rdata节。假如我们要获取某个PE文件的shellcode,那么需在.text节中获取,然而字符串是存储在.data节中,这样一来字符串是无法被提取成shellcode的

为了更加直观的展示上述所描述的观点,我们使用Compiler Explorer网站来查看代码的反汇编形式。

首先,我们使用了声明字符串的三种不同方式,网站中已经为我们使用不同颜色来标明对应的代码行了(黄、紫、红)

首先来看String1变量的反汇编(黄色部分),它将逐个字符存储在堆栈中,而不是先创建一个在.data.rodata节的全局或静态副本然后再复制过来

再来看下String2(紫色部分)变量,注意lea rcx, OFFSET FLAT:$SG2658,这条指令加载字符串"Hello"的地址到rcx寄存器,$SG2657是一个由编译器生成的标签,表示该字符串在.rodata节或类似的只读数据节中的位置,后面的String3(红色部分)变量亦是如此

总结来说,String1变量是可以被提取为shellcode的,因为它没有依赖.data节,而另外两个变量都依赖了.data

image-20231027104610806

但是使用String1这种声明方法来初始化字符串会很不方便,现在有另外一种可替代的方法,当使用constexpr关键字来初始化char数组时,生成的字符串和String1变量是几乎一样的,如下图所示

image-20231027113459880

为了方便使用,我们将其封装成两个宏,分别用于创建ASCII字符串和宽字符串

Aggressor脚本实现

以下是前置式loader的aggressor脚本:

image-20231027154240991

UDRL混淆

当UDRL应用到Beacon时,如下所示的stage块中定义的PE修改会被忽略掉,这是因为这些选项与反射加载器的操作紧密关联。例如,当beacon的某些内容以特定方式被加密了,那么我们的加载器需要知道如何去解密这些内容。接下来将讲解如何利用Aggressor脚本来实现对Beacon的混淆

自定义PE头

在Profile配置中,Stage块的某些选项允许用户修改明显的PE文件特征,比如magic_mz,它允许用户自定义4个字节的MZ头,但是当UDRL应用后此功能将不再支持,不过我们可以使用Aggressor脚本去实现此功能,甚至比magic_mz功能更加强大

首先在UDRL中,我们先自定义PE Header结构,在此结构中,我们仅存放PE头结构和节表结构等有效信息。

在aggressor脚本中,为了对接上述我们自定义的PE Header,我们可以使用pedump函数将原始Beacon的PE头信息映射至一个哈希变量pe_header_map中, 然后将其再打包成一个字节流并赋值给pe_header_data变量

为了替换Beacon的原始PE头,我们需先使用substr函数提取PE文件的SECTION部分,然后再将SECTION部分与新创建的pe_header_data进行合并

下图是原始Beacon和修改后Beacon的对比图,修改后的Beacon已经将大多数PE特征去除掉了

img

但是这样做会引发另外一个问题,即SECTION部分的起始地址发生了变化。例如,原始Beacon中.text节的PointerToRawData值为0x400,但是当我们移除它的PE头后,.text节的PointerToRawData值需改为0x0,这样我们的loader才能识别到SECTION部分

解决上述问题的最好方法就是修改RAW Beacon的基址,如果将Beacon的基址偏移减去0x400SizeOfHeaders),那么后续我们就可以继续使用原始的PointerToRawData

除了修改SECTION.PointerToRawData(FOA)之外,还需修改SECTION.VirtualAddress(RVA)。

例如,PE头在内存状态时的大小通常为0x1000,因此我们需将Beacon在加载状态时的基址减去0x1000

字符串替换

在Aggressor脚本,若要替换Beacon中的某些字符串,通常会用到strrep函数,但是此函数有个缺点,它可能会更改Beacon某些部分的大小,从而导致PE文件执行时出现崩溃。例如下述代码所示,这样操作会导致原始字符串的大小出现变化

为了解决这种情况,我们可自定义一个strrep_pad函数,在替换字符串之前先用NULL字节填补它(其实现原理与transform块中的strrep类似)

以下是strrep_pad函数的定义, 目的是替换一个字符串中的特定字节序列,并确保新的字节序列与原始字节序列具有相同的长度。如果新的字节序列较短,它会使用零字节(\x00)进行填充, 此函数有个前提是替换的字符串长度不能大于被替换的字符串长度

你也可以使用Aggressor的内置函数setup_transformation,来将Profile配置中transform块定义的规则应用到Payload上

混淆处理

1.异或遮掩

使用如下自定义函数mask_section可以对指定SECTION部分的内容进行异或遮掩。除此之外,Aggressor还提供了一个内置函数pe_mask_section函数用于遮掩指定SECTION部分

至于遮掩所用到的密钥,这里采用随机生成的可变长度密钥

为了确保loader可以检索到这些密钥,我们需将这些密钥的长度值放在PE_HEADER_DATA结构中(自定义PE头),然后在PE_HEADER_DATA后面创建一个缓冲区用于存放密钥

img

在UDRL中,我们创建了一个KEY_INFO结构来存储KEY的长度和地址,然后将其集合到XOR_KEYS结构中,表示每个节对应的密钥信息

下述代码为loader检索每个节密钥的过程:

2.压缩数据

CobaltStrike为我们在Sleep语言重写了LZNT1压缩算法,并集合在lznt1.cnalznt1_compress函数中,我们可以在Aggressor脚本中调用此函数来对遮掩后的Beacon进行压缩

在UDRL中,我们使用RtlDecompressBuffer函数对数据进行解压缩,其函数原型如下所示

由上述函数原型可知,函数的调用需要压缩数据和存放解压缩数据的大小作为参数,此处需注意的是,存放解压缩数据的空间最好大点,从而防止缓冲区溢出报错,因此aggressor脚本中,我们使用原始Beacon的大小来作为存放解压缩数据的大小

由于PE_HEADER_DATA结构已经被压缩了,我们需要在Aggressor脚本中创建一个UDRL_HEADER_DATA来存放压缩数据和解压缩数据的大小值

img

以下是在UDRL中创建的UDRL_HEADER_DATA结构

3.RC4加密

Aggressor脚本在此处选择了简单的RC4加密算法,使用随机的生成的rc4密钥并放置于UDRL_HEADER_DATA的后面,在UDRL_HEADER_DATA结构里存放RC4密钥的长度

img

当loader要检索RC4密钥时,可以在bufferBaseAddress的基础上加上UDRL_HEADER_DATA结构的大小

4.BASE64编码

在前面的部分中,我们严重地混淆了Beacon,这也使得它的熵值增加,从而容易被检测程序判定为恶意文件,因此我们可以通过使用Base64编码来减少它的熵值(因为Base64只有64个字符字母, 能够减少随机性)

Aggressor提供了一个内置函数base64_encode来进行Base64编码,虽然编码后会增加内容的长度,但是经过测试,混淆/压缩/rc4加密/base64编码后的Beacon和原始Beacon的大小相差不大

压缩、加密和编码后修改后的制品的高级概述。

UDRL处理混淆流程

在UDRL中分配两块内存区域,一块作为解密缓冲区(Temporary), 只拥有可读可写权限;另外一块作为加载缓冲区(LoaderImageMemory),拥有可读可写可执行权限,用作后续Beacon的执行

以下是UDRL处理Beacon的完整流程图,可以总结为4个步骤:

  • 1:首先对Beacon进行base64解码,然后将解码后的数据存放至loaderImageMemory

  • 2~3:对LoadedImageMemory的数据进行rc4解密,随后再进行解压缩,数据处理完后放到TemporaryMemory

  • 4:最后一步就是常见的反射加载流程,例如将Beacon的PE头和Section复制到新内存、解析导入、处理重定位等等

解码/解密/解压缩工作流程。

UDRL检测

CobaltStrike为我们提供了一个udrl.py脚本,用于检测我们自定义的反射loader是否能够正常加载Beacon,这样做的好处是不用启动Teamserver

udrl.py支持两种检测模式,分别是prepend-udrlstomp-udrl,脚本的执行格式如下所示:

例如我要检测自定义的前置式loader能否正常使用,那么第一个参数需填写为prepend-udrl,如下图所示则表示loader能够正常运行,并返回了loader的大小以及加载Beacon的起始地址

image-20231101011012178

若要检测内嵌式loader是否可正常运行,需将第一个参数改为stomp-udrl

image-20231101014454106

思考和总结

1.对loader的处理

当我们加载UDRL混淆的Aggressor脚本后,将生成的Raw文件上传至VT上,没有出现任何报毒,当然,要实现RAW在VT上全零不靠UDRL也行,这只是其中的一个思路

image-20231102203720729

但是如果你一直使用CobaltStrike提供的混淆反射loader,报毒也是迟早的事情,解决方法也很简单,这里推荐一个混淆二进制文件的项目:Shoggoth,我们只需对反射loader的bin文件进行混淆处理,以下是Shoggoth处理loader前后的对比图

image-20231102211819813

2.更多层的加密?

在CobaltStrike官方给出的UDRL混淆项目中,其实只用到了四层混淆(xor加密、压缩、rc4加密和base64编码),我们或许可以在其基础上再增添几层加密,虽然意义不是很大^^

最后更新于