绕过内存扫描器:恶意软件定制化规避YARA、PE-sieve等技术

本文详细介绍了如何通过加密数据、堆加密、代码混淆、执行重定向和返回地址欺骗等技术,规避YARA、PE-sieve、Moneta等内存扫描工具,并公开了专为Cobalt Strike设计的AceLdr工具。

规避内存扫描器

Kyle Avery //

引言

本文补充了我在DEF CON 30上的演讲——“规避内存扫描器:定制恶意软件以规避YARA、PE-sieve等”,其中包括公开发布的新工具AceLdr。演讲幻灯片可在会议网站上获取。

随着开源工具和商业安全产品在扫描Windows进程内存中恶意软件的能力不断提升,红队被迫改进其技术以持续规避它们。

通常,信标C2植入遵循一个常见范式:恶意软件执行指令后休眠一段时间。这个过程为检测和规避提供了一系列机会,本文旨在详细说明。

内存扫描器能力

开源内存扫描器具有不同的功能,可分为以下类别。

模式匹配

签名或模式匹配可能是内存扫描器和商业安全产品最知名的功能。YARA是这种技术的一个典型例子。YARA可以使用条件逻辑执行字符串和字节模式匹配。例如,考虑以下示例规则:

1
2
3
4
5
6
7
8
9
rule Example
{
  strings:
    $a = "This program cannot" xor
    $b = { 41 42 ( 43 | 44 ) ?? 46 }

  condition:
    $a or $b
}

在此规则中,目标必须包含以下之一才能匹配:

  • 字符串“This program cannot”或任何单字节XOR加密变体。
  • 字节41和42,后跟43或44,任意单个字节,以及46。

这个简单示例应能很好地展示YARA的可能性。从简单的字符串或字节模式到这些原语的相对复杂组合都可以定义。

由于YARA扫描目标进程分配的所有内存,许多项目基于YARA构建更高效的扫描器,具有特定目标。例如,BeaconEye仅扫描堆内存,以搜索在初始化时动态分配的Cobalt Strike配置结构。

商业安全产品如AV和EDR也已知使用YARA。具体来说,Carbon Black和CrowdStrike明确提到使用YARA,其他供应商可能也会使用。

快速Google搜索可以找到许多针对Cobalt Strike的YARA规则。例如,以下演示使用一组针对Cobalt Strike的规则扫描两个cmd.exe进程:一个良性,一个注入植入。

检测Cobalt Strike与YARA

内存属性

内存的属性,如权限和映射信息,也可用于识别潜在恶意代码。内存可读、可写或可执行,并映射为图像提交或私有提交数据。如果内存是通过从磁盘加载文件(如EXE或DLL)创建的,则为“图像提交”。如果进程通过API调用(如VirtualAlloc)动态分配内存,则为“私有提交”。

Moneta扫描内存页以查找可执行和私有提交内存。所有代码必须可执行,但Windows上的代码倾向于从磁盘加载。可执行私有内存合法地出现在JIT环境中,如.NET运行时或Web浏览器。此外,Moneta将检查所有线程的起始地址是否为私有提交内存地址。这个检查足够简单以规避,因为线程的起始地址在创建后不会更改。可以创建具有图像提交起始地址的新线程,挂起状态,修改以执行目标shellcode,然后恢复。

PE-sieve将根据用法扫描可执行、不可执行或不可访问的内存,以查找通常出现在shellcode中的模式。此外,PE-sieve将检查所有线程的返回地址是否为私有提交内存地址。

检测Cobalt Strike与Moneta和PE-sieve

堆栈跟踪

最后,更近期的内存扫描器引入了线程调用堆栈跟踪以识别潜在恶意代码。BeaconHunter和Hunt-Sleeping-Beacons等工具基于一个简单前提:识别任何等待原因为“DelayExecution”的线程。由于Cobalt Strike和许多其他植入使用Sleep API调用,此方法可以可靠地检测恶意软件植入。不幸的是,该技术通常伴随许多误报。

自AceLdr初始发布以来,Hunt-Sleeping-Beacons已更新为新方法以检测FOLIAGE(更多内容见下一节)。扫描器现在查找等待原因为“UserRequest”的线程,这些线程在其调用堆栈某处具有指向KiUserApcDispatcher的返回地址。这将在下面进一步详细说明。

堆栈跟踪的一个有趣变体可以在MalMemDetect中找到。此扫描器挂钩API调用,如RtlAllocateHeap,以在执行时检查返回地址。当Beacon调用这些API之一时,堆栈上的返回地址将指向位于私有提交内存中的植入shellcode。

检测Cobalt Strike与MalMemDetect

上述讨论的工具具有超出本文范围的能力。如果您有兴趣了解更多,我建议查看每个扫描器的代码。

绕过内存扫描器

开发者可以利用其C2植入的休眠期实施保护措施,混淆恶意软件以减少扫描器检测的可能性。植入休眠时间越长,被所述保护措施规避的扫描器发现的可能性越低。

本文上下文中的绕过不会产生误报。它不旨在混淆分析师或与现有结果混合。真正的绕过导致内存扫描器在植入注入前后零结果。

加密数据

加密数据的第一个技术通常是单字节XOR。单字节XOR方便易实现,不需要API调用,且运行相对快速。不幸的是,YARA和PE-sieve等工具意识到这一点,并找到了轻松检测此加密方法的方法。

替代解决方案可能实现多字节XOR、AES或RC4的函数。然而,在以下部分中将明显看出,这也不是可行选项。为了完全规避像Moneta这样搜索任何可执行私有内存的扫描器,用于加密数据的代码必须位于图像提交内存中。

您可以使用Windows API执行AES加密,但这需要多个API调用的组合来加密和解密数据。Mimikatz中暗示了此问题的优秀解决方案。作者实现了SystemFunction032:一个可以从advapi32.dll解析的系统函数,用于执行RC4加密和解密。此API调用接受两个参数,包含目标内存和密钥,允许我们动态生成密钥并加密数据,而无需在私有提交内存中执行代码。技术上,SystemFunction032用于加密,SystemFunction033用于解密。然而,RC4密码是双向的,因此您可以使用任一API进行加密或解密。

堆加密

既然我们已经确定了加密数据的方法,我们必须决定哪些数据应该加密。本文开头引用了BeaconEye,一个扫描动态分配内存以查找Cobalt Strike配置数据结构的工具。

堆加密可能最好以以下两种方式之一执行:

  • 跟踪Beacon创建的堆条目
  • 利用辅助堆进行Beacon的分配

Cobalt Strike的官方Sleep Mask Kit提供了用于加密的内存地址列表。他们的解决方案很干净,但需要使用Sleep Mask Kit,如下一节所述,这阻止我们绕过一些扫描器。

去年,我发布了TitanLdr的一个分支,它在Beacon加载之前创建一个新堆。GetProcessHeap API在植入的IAT中被挂钩,以强制它在解析进程堆以分配内存时解析该堆。这允许我们加密辅助堆上的所有条目,因为只有植入应该使用它。以下演示使用此分支绕过BeaconEye。

规避BeaconEye

混淆可执行代码

持续规避像Moneta和PE-sieve这样的工具需要加密和内存权限控制的组合,以规避模式匹配和属性扫描。

可执行掩码存根

Sleep Mask Kit或Shellcode Fluctuation中使用的可执行存根可以在静止时加密植入代码并使其不可执行。然而,两个示例都要求至少一个可执行区域保持不变。使用“掩码存根”技术的扫描器将始终至少有一个检测点,并且可以创建YARA规则来检测存根本身。

返回导向编程(ROP)

Gargoyle PoC影响了本节讨论的其他技术的创建。作者使用异步过程调用排队并执行一系列ROP小工具,这些工具在启动代码不可执行时运行。

Gargoyle仅适用于32位Windows,且PoC仅执行消息框。今年早些时候,Waldo-irc发布了YouMayPasser:Gargoyle的64位实现,准备与Cobalt Strike一起使用。

使用上下文重定向执行

Gargoyle和YouMayPasser实现了将植入代码更改为不可执行的目标,但它们遭受与许多ROP漏洞利用相同的问题:不同版本的Windows需要修改小工具偏移。有方法解决此问题,但它们可能引入显著复杂性。

受Gargoyle启发,Austin Hudson发布了FOLIAGE:传统ROP的替代方案,它使用NtContinue API调用来在休眠期间控制执行。NtContinue通常用于错误处理以恢复线程的执行上下文。它接受一个新上下文作为单个参数,并修改当前线程以使用此上下文。上下文结构指定CPU寄存器的值,包括指令指针,因此它可以重定向执行到指定地址。FOLIAGE排队一系列APC,这些APC执行NtContinue以重复切换上下文。在混淆植入的链中,为以下每个步骤使用新的上下文结构:

  • 等待新事件以防止线程退出
  • 更改植入内存为不可执行
  • 指示KsecDD驱动程序加密植入内存
  • 保存原始线程的上下文
  • 将原始线程的上下文设置为伪造上下文(更多内容见后)
  • 使用NtDelayExecution休眠指定时间
  • 指示KsecDD驱动程序解密植入内存
  • 恢复原始线程上下文
  • 更改植入内存为可执行
  • 退出新线程

通过查看FOLIAGE中sleep.c的第217-512行,可以进一步检查此过程。

几个月前,C5pider声称已逆向MDSec NightHawk以创建Ekko:FOLIAGE的替代方案,它使用CreateTimerQueueTimer而不是NtQueueApcThread来排队调用NtContinue。

以下演示使用FOLIAGE绕过Moneta和PE-sieve。

规避Moneta和PE-sieve

NtContinue不是唯一使用上下文结构强制更改执行的API调用。它方便地只需要一个参数,但也有可行的替代方案。

避免休眠

像BeaconHunter和Hunt-Sleeping-Beacons这样的工具会警报等待原因为“DelayExecution”的线程。使用不设置此等待原因的替代延迟执行方法可以轻松规避此检测。WaitForSingleObject是一个符合此要求的API,并设置等待原因为“UserRequest”。以下演示将Sleep API调用替换为WaitForSingleObject以绕过这些工具。

规避Hunt-Sleeping-Beacons

返回地址欺骗

欺骗返回地址涉及修改调用堆栈返回地址,使其不指向私有提交内存。本节可以分为两种不同的技术:静止时和执行时返回地址欺骗。

静止时欺骗

“静止时”指的是植入在休眠期间。到目前为止讨论的大多数技术也关注此时。商业安全产品似乎不在静止时扫描线程调用堆栈,但像PE-sieve这样的开源扫描器在扫描时会检查返回地址。

使用ThreadStackSpoofer等技术部分规避此检测。此PoC通过用零覆盖返回地址来隐藏它,有效截断堆栈。然后,根据堆栈的状态,此技术可能将参数泄漏到堆栈上。这些参数可能类似于内存地址,为检查返回地址的扫描器创建指示器。

FOLIAGE中演示了更稳定的技术。作者使用NtSetContextThread用制造上下文覆盖原始线程的上下文,设置所需的返回地址。NtSetContextThread的使用相对罕见,可能是一个检测点。作者在发布时未观察到开源扫描器或商业安全产品对此行为发出警报。

执行时欺骗

线程调用堆栈可能被捕获的另一个时间是“执行时”。这在MalMemDetect中最清楚地演示,如上所述。当我们进行挂钩API调用以规避此类工具时,我们的返回地址必须指向图像提交内存。

x64返回地址欺骗PoC很好地实现了这一点。在API调用之前,从加载的DLL存储ROP小工具作为返回地址,该小工具跳转到存根以恢复继续执行所需的上下文。

规避MalMemDetect

自AceLdr发布以来,Hunt-Sleeping-Beacons已更新以检测FOLIAGE。扫描器现在将检查所有等待原因为“UserRequest”的线程,这些线程在其调用堆栈某处具有指向KiUserApcDispatcher的返回地址。使用FOLIAGE的公共实现无法轻松绕过此点,因为它需要在执行时对休眠链中的API调用进行调用堆栈欺骗。由于FOLIAGE混淆了用于返回地址欺骗的shellcode,它不能被APC线程调用来欺骗返回地址。

AceLdr

作为此研究的一部分,我发布了一个称为AceLdr的先前讨论技术的实现。此工具是Cobalt Strike的用户定义反射加载器(UDRL),在发布时具有以下功能:

  • 绕过每个引用的扫描器
  • 易于使用 – 导入单个CNA脚本
  • 使用SystemFunction032加密
  • 使用辅助堆动态内存加密
  • 使用FOLIAGE代码混淆和加密
  • 使用WaitForSingleObject延迟执行
  • 对InternetConnectA、NtWaitForSingleObject和RtlAllocateHeap执行时返回地址欺骗

Black Hills Information Security在公开发布前大约一年使用了此工具。以下是AceLdr绕过多个内存扫描器的演示。

使用AceLdr规避内存扫描器

结束语

虽然AceLdr明确为Cobalt Strike制作,但本文演示的技术可以轻松移植到许多其他项目。这里介绍的每种方法都绕过了现有扫描器。然而,这并不保证它们将规避未来实现,正如我们已经看到Hunt-Sleeping-Beacons那样。

内存扫描器和商业安全产品不相同,但它们共享许多特征。例如,规避开源扫描器并不保证安全产品规避。此外,安全产品规避通常不需要完全的内存扫描器绕过,因为系统资源和开发成本限制了供应商。

致谢

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计