要点
在 Elastic,我们每年会举办两次 ON 周活动,工程师们会分成“黑客马拉松”团队,解决由团队投票选出的技术难题。本文介绍了另一次 Elastic ON 周的成果,我们在其中深入研究了 Elastic Endpoint 勒索软件防护的创新应用。我们的研究使用了自 7.14 版本以来部署的现有勒索软件金丝雀防护功能,以生成被识别为勒索软件的进程的内存快照(即记录进程信息的数据集合)。通过分析这些快照,我们的研究展示了我们如何恢复取证过程的关键信息,甚至是加密密钥,从而实现完全解密。
从 Elastic Defend 8.11 版本开始,添加了此进程内存快照机制,使 DFIR 团队能够在 Elastic Endpoint 的安全安装文件夹中(默认情况下,$ElasticInstallPath\Endpoint\cache\RansomwareDumps
)找到由我们的勒索软件防护标记的勒索软件的内存转储。
简介
在 2024 年,我们无需解释勒索软件是什么,它已成为一个价值数十亿美元的产业,也无需解释即使预算无限的公司也难以控制或阻止它。这些攻击者已经成熟且高效,通常超越了取证和恶意软件分析等安全功能。
当前防护状态
值得庆幸的是,多年来,AV/EDR 在检测和预防勒索软件方面变得越来越好。在最常见的现有缓解措施中,我们发现
- 通过签名进行的静态和动态检测:通常在不同级别执行(通过文件或 ELF/PE 部分级别的哈希)和文件活动(对具有高熵变化的文件进行写访问),其优点是易于快速实施,但也可能产生误报
- 逆向工程:逆向二进制文件可以暴露干扰执行的新方法,因为恶意软件作者会实施操作系统级别的故障保护(例如,通过互斥体对象)和/或网络故障保护(如 WANNACRY)
- 恢复备份:这些备份并不总是经过彻底测试,即使它们工作正常,也存在上次备份与感染时刻之间的数据丢失风险
- 卷影副本:与恢复备份有些类似,勒索软件通常会主动定位并尝试在加密系统上的文件之前销毁它们
- 高熵和快速文件更改:这种方法纯粹是实验性的,旨在检测文件内容中的剧烈变化作为加密的指标,但是,这也非常容易产生误报 (FP)
- 最后的密码学弱点:这是迄今为止最复杂的缓解措施,因为它需要逆向工程和密码学知识,但也需要运气,因为攻击者希望作者推出自己的加密 API(有关一些示例,请参阅 Elastic 的 Mark Mager 2019 年 DEFCON 演讲);只要根据文档正确实施,这种方法就不能对抗现代操作系统原生加密 API。
勒索软件(通常)如何工作,以及为什么这很重要
我们必须了解我们正在保护什么以及它如何在内部运行,这样才能有效。这种多样性强调可能永远不会有一种通用的解决方案来对抗所有勒索软件变体。了解这种多样性也强调了我们技术的重要性,该技术提供了关于勒索软件的重要见解。
从高层次来看,勒索软件执行的操作顺序通常总结如下
- 交付:这可以通过多种方式完成,从社会工程到 0-day/1-day 漏洞利用。此方法还可以依赖弱密码来远程感染目标。
- C2 通信:一旦执行开始,勒索软件可能会与 C2 通信,以交换配置并共享有关受害者的信息。此步骤还可以为 C2 留有终止开关,以防止进一步感染
- 加密:建立加密上下文后,该进程会递归地浏览文件系统,查找具有特定扩展名的文件并对其进行加密。
- 勒索:在与 C2 共享解密密钥后,勒索软件将发布勒索通知,并(通常非常明显地)通知受感染的用户其操作以及获取解密密钥的方式。此时,所有允许恢复的加密上下文可能已经丢失
- 传播:如果可能,勒索软件可能会尝试自动感染更多系统。
但是,从较低层次来看,勒索软件的运作方式却非常独特:例如,专注于交付步骤,臭名昭著的WANNACRY 勒索软件通过 Windows 操作系统中的一个漏洞传播,该漏洞被称为 EternalBlue;而 LOCKBIT 变体倾向于使用网络钓鱼电子邮件、漏洞利用工具包或利用受损的远程桌面协议 (RDP) 凭据进行感染。
在本次研究中,我们最感兴趣的是第三步,因为它通常是检测和预防最有效的地方,例如使用我们的金丝雀防护。
了解 Elastic Endpoint 中的金丝雀文件功能
Elastic Endpoint 勒索软件防护起源于 Elastic 7.14,它使用金丝雀文件,目的是通过(过度)写入一些特定文件来尝试诱捕勒索软件。这提供了一个高度可信的指标,表明罪魁祸首进程正在尝试加密所有文件。
金丝雀文件看起来和任何其他文件完全一样——它可以具有有效内容(DOCX、PDF 等),隐藏或标记为系统文件以避免用户篡改。但是,勒索软件不能“指纹”并避开金丝雀文件。所有这些因素都导致了针对勒索软件访问的可靠指标。
尽管金丝雀文件在提供勒索软件指标方面非常成功,但在 Windows 系统上,很难确定在检测(以及如果需要,终止)发生之前没有加密任何文件。这不是产品缺陷,这是由于 MiniFilter 在 Windows 上的工作方式的结构所致。因此,即使攻击被阻止,某些文件也可能已被加密。更糟糕的是,如果进程被终止,则完全失去检索原始内容的可能性。
这就是我们 ON 周研究的开始……
扩展我们的金丝雀防护以生成进程快照
基本原理
这项初步研究的理念如下
- 在内核级别,检测尝试写入具有特定名称的文件(我们的金丝雀)的访问
- 从用户空间,生成尝试写入操作的罪魁祸首进程的进程转储,并向驱动程序发送信号以继续按设计执行。
- 分析进程转储
由于 ON 周仅限一周,这是我们开发原型最初的时间框架。
实现
在内核空间
按照文档完善的 MiniFilter API 文档,开发一个 MiniFilter 驱动程序来监视对具有特定名称的文件的写入访问相对容易。
- 声明包含我们要安装的回调的筛选器表,一个用于在调用
NtWriteFile()
时进行写入访问,另一个用于尝试写入映射的节时进行写入访问
- 创建并注册筛选器,包括要监视的文件名模式并开始筛选
图片 2:声明要为 MiniFilter 驱动程序检查的文件名模式 一旦我们的筛选器注册到筛选器管理器,当触发特定的系统调用时,写入访问将通过我们驱动程序的回调:当进程尝试将缓冲区写入文件时,通过 NtWriteFile
,或者当进程尝试创建具有写入访问的文件支持映射的节时(SECTION_MAP_WRITE
),通过 NtCreateSection()
。
正如我们所看到的,这两种操作都会导致调用进程被暂停(调用我们的函数 SuspendProcessById
),允许用户空间进程对其内存进行快照。以下视频总结了所有这些步骤
在用户空间
生成内存转储是一种强大的机制,它深深植根于 Windows 中,并且是其错误报告机制(或 WER)的重要组成部分。通过简单而明确的 API 调用,如 MiniDumpWriteDump
,任何用户或程序都可以转储(如果权限允许)目标进程的完整内存布局和内容,以及更多信息,具体取决于调用期间传递的标志,例如
- 句柄信息
- 线程信息
- 卸载的模块详细信息等
可以在 此处查阅可用类型的完整参考列表。
我们决定使用为调试软件设计的内存转储来扩展我们勒索软件保护功能的现有金丝雀文件功能。当检测到勒索软件时,我们会在进程终止之前生成完整的内存转储。使用内存转储对抗恶意软件具有巨大的优势,包括
- 揭示进程内存布局,当打包模糊了内存区域时,这一点尤其有用
- 披露进程运行时的所有内存内容,包括未擦除的内存区域,因为 Windows 出于性能原因不会立即擦除内存
- 提供稳定且安全的方式通过模拟来试验恶意软件
很快,我们就有了稳定可靠的方法来检测金丝雀写入访问,并生成触发它们的勒索软件的完整内存转储。由于时间限制,我们选择了两个流行的家族来测试我们项目的分析阶段:NOTPETYA 和 WANNACRY。
原型代码可以在 此处找到,不适用于生产环境。请自行承担风险,使用非生产系统进行实验。
真实示例
从进程运行时恢复密钥:NOTPETYA 的案例
为什么选择 NOTPETYA?这是一个很好的第一个候选者,因为它使用一个随机会话密钥加密所有文件。它还使用了强大的密码术
- RSA-1024 用于主机级非对称加密密钥
- 用于加密文件的唯一 AES-128 CBC 密钥
使用上述驱动程序和代理,我们可以轻松地在受控环境中运行 NOTPETYA(SHA1 027cc450ef5f8c5f653329641ec1fed91f694e0d229928963b30f6b0d7d3a745
),并在可预测的运行时位置获得进程小型转储。
我们当前的设计导致驱动程序同步捕获写入,因此我们在分析转储文件时确切地知道进程运行时的位置。但是,我们仍然需要一些逆向工程来了解会话密钥是如何生成的。
事实证明,对这个 NOTPETYA DLL 进行逆向工程非常简单,这帮助我们快速行动。
- 在进行一些初步检查后,DLL 尝试遍历所有可能的驱动器盘符,并且对于每个匹配项(即,该字母(例如
C:\
)存在),将创建一个0x20
线程上下文以继续进行加密
- 每个线程都使用 Microsoft CryptoAPI 初始化自己的加密上下文;我们注意到使用了 AES-CBC 128 位
- 递归地加密文件(最大递归级别为 15),删除赎金消息并销毁加密上下文
- 文件加密本身是使用文件支持的映射来覆盖具有特定目标的扩展名的文件来执行的
这为我们提供了一个非常基本的基于堆栈的上下文结构
c
struct _THREAD_CONTEXT { /* sizeof=0x20, align=0x4, mappedto_50) */
/* 00000000 */ WORD lpswzRootPathName[4];
/* 00000008 */ HANDLE hProvider;
/* 0000000C */ PVOID field_C;
/* 00000010 */ LPVOID pBase64Data;
/* 00000014 */ HCRYPTPROV hKey;
/* 00000018 */ DWORD field_18;
/* 0000001C */ HANDLE hFile;
};
有了这些知识,我们可以在转储中进行进一步探索。由于我们知道写入访问是使用 kernel32!CreateFileMapping
进行的,这意味着调用了 ntdll!NtCreateSection
,我们可以隔离触发对金丝雀文件进行系统调用的活动线程
dx @$curprocess.Threads.Where( t => t.Stack.Frames.First().ToDisplayString().Contains("NtCreateSection") )
如前所述,我们已经隔离了上下文会话,并且知道它位于堆栈中。从基指针到会话上下文,我们可以从上下文结构成员 _THREAD_CONTEXT.hKey
(偏移量 0x14)检索加密上下文。
0:007:x86> dx @$curthread.Stack.Frames[3].Attributes.FrameOffset + 0x10
@$curthread.Stack.Frames[3].Attributes.FrameOffset + 0x10 : 0x518d210
0:007:x86> dps poi(0x518d210) l6
004859a0 003a0043
004859a4 0000005c
004859a8 00538418
004859ac 00000000
004859b0 04060550
004859b4 0048fc48 <<< hKey
0:007:x86> dps 0048fc48
0048fc48 74a850c0 rsaenh!CPGenKey
0048fc4c 74a9ad90 rsaenh!CPDeriveKey
0048fc50 74a886c0 rsaenh!CPDestroyKey
0048fc54 74a9c770 rsaenh!CPSetKeyParam
0048fc58 74a898c0 rsaenh!CPGetKeyParam
0048fc5c 74a84c40 rsaenh!CPExportKey
0048fc60 74a86290 rsaenh!CPImportKey
0048fc64 74a99880 rsaenh!CPEncrypt
0048fc68 74a8a500 rsaenh!CPDecrypt
0048fc6c 74a9b5c0 rsaenh!CPDuplicateKey
0048fc70 00538418
0048fc74 e3155764 <<< hCryptKey
0048fc78 22222222
[...]
Microsoft 没有公开提供加密上下文结构,但已对其进行了 逆向工程
struct HCRYPTKEY
{
void* CPGenKey;
void* CPDeriveKey;
void* CPDestroyKey;
void* CPSetKeyParam;
void* CPGetKeyParam;
void* CPExportKey;
void* CPImportKey;
void* CPEncrypt;
void* CPDecrypt;
void* CPDuplicateKey;
HCRYPTPROV hCryptProv;
magic_s *magic; // XOR-ed
};
struct magic_s
{
key_data_s *key_data;
};
struct key_data_s
{
void *unknown; // XOR-ed pointer
uint32_t alg;
uint32_t flags;
uint32_t key_size;
void* key_bytes;
};
从这个上下文中,我们可以提取和解码 AES 结构的位置,因为已知该密钥对于 32 位进程为 0xE35A172C
0:007:x86> ? e3155764^ 0xE35A172C
Evaluate expression: 5193800 = 004f4048
0:007:x86> dps poi(004f4048 ) l5
0053cdd0 e3152844 // /* +0 */ unknown
0053cdd4 0000660e // /* +4 */ alg
0053cdd8 00000001 // /* +8 */ flags
0053cddc 00000010 // /* +c */ key_size
0053cde0 0053ce70 // /* +10 */ key_bytes
从转储中,我们还知道密钥的类型 (AES-CBC)、内存位置 (0x053ce70
) 和大小 (0x10
)。会话密钥可以成功检索!
这不仅允许完全解密此进程的所有加密文件,而且精明的观察者会注意到,所有 这些步骤都可以自动化,从而使我们仅使用生成的内存转储即可创建解密器!
要完整查看此过程,您可以观看 演示并查看 GitHub 上的 代码。
我们甚至可以创建适用于所有感染同一变体的机器的解密脚本。尽管 WinDbg 是首选工具,但所有这些步骤都可以完全自动化,从而使这种方法非常可扩展。
从进程运行时预测加密密钥:WANNACRY 的案例
WANNACRY 是我们认为适合此实验的另一个勒索软件家族,因为它众所周知,最重要的是对于这项研究而言,它使用了更复杂的文件加密逻辑
深入研究 Windows (伪)随机数生成
为了加密文件,WANNACRY 使用 Windows 的加密库,并通过高级 API 函数 advapi32!CryptGenRandom
为每个文件生成一个随机 AES 密钥。每个密钥都与相应的文件关联,然后进行 RSA 加密并提交给其 C2。按照设计,我们用于对抗 NOTPETYA 的方法在这里不起作用。WANNACRY 为我们提供了一个不同的挑战,再次证明拥有完整的内存转储可以提供其他宝贵的资源。
随机数生成通常不像大多数人想象的那么随机。生成真正的随机数既具有挑战性又成本高昂,而这一挑战是任何加密算法的核心。
Windows(与其他操作系统类似)以伪随机方式生成随机数。这意味着随机数生成器使用加密函数(例如,XorShift 或 Mersenne-Twister)导出初始状态(称为种子)。使用 PRNG 的逻辑后果之一是,知道随机生成器在 T 时刻的状态使我们能够精确地知道 T+1、T+2 等时刻的所有随机值。请注意,这不是一个弱点,因为随机性是一项高度复杂且性能成本高的操作;这种方法是一个很好的折衷。
我们将利用此属性来击败 WANNACRY。知道 WANNACRY 会反复调用 CryptGenRandom 来为每个文件生成 AES 加密,如果我们有办法通过模拟小型转储文件来严格了解这些值,那么我们也将知道可能的 AES 密钥。这看起来很有希望,但也可能隐藏着一些障碍。
退一步来说,CryptGenRandom 究竟是什么?它有什么作用? MSDN 告诉我们,这个(已弃用的)函数使用加密服务提供程序(HCRYPTPROV)来填充缓冲区,使其包含随机内容。通过在 Windows 11 x64 上使用 WinDbg 设置断点到 CryptGenRandom,我们可以深入了解其内部运作机制。我们可以轻松地遍历高级 API,并观察到 advapi32!CryptGenRandom
是 cryptsp!CryptGenRandom
的一个包装器,而 cryptsp!CryptGenRandom
反过来又会引导我们到 rsaenh.dll
中的 CPGenRandom
函数。
0:000> g
Breakpoint 9 hit
CRYPTSP!CryptGenRandom+0x29:
00007ffc`990c1699 488b8be0000000 mov rcx,qword ptr [rbx+0E0h] ds:000001e1`38ade010=e35a16cde1cff7d0
0:000> dps @rbx
000001e1`38addf30 00007ffc`987956d0 rsaenh!CPAcquireContext
000001e1`38addf38 00007ffc`987951e0 rsaenh!CPReleaseContext
000001e1`38addf40 00007ffc`98791140 rsaenh!CPGenKey
000001e1`38addf48 00007ffc`987a8f80 rsaenh!CPDeriveKey
000001e1`38addf50 00007ffc`987948a0 rsaenh!CPDestroyKey
000001e1`38addf58 00007ffc`987aaac0 rsaenh!CPSetKeyParam
[...]
0:000> t
CRYPTSP!CryptGenRandom+0x3c:
00007ffc`990c16ac ff1506c50000 call qword ptr [CRYPTSP!_guard_dispatch_icall_fptr (00007ffc`990cdbb8)] ds:00007ffc`990cdbb8={CRYPTSP!guard_dispatch_icall_nop (00007ffc`990c4d30)}
0:000> r rax, rcx,rdx ,r8
rax=00007ffc987954d0 rcx=e35a16cde1cff7d0 rdx=0000000000000010 r8=00000065859bfe70
0:000> .printf "%y\n", @rax
rsaenh!CPGenRandom (00007ffc`987954d0)
当 CRYPTSP!CryptGenRandom
被调用时,RCX 寄存器保存着指向编码加密提供程序的指针,该指针与魔术常量 0xE35A172CD96214A0
进行异或运算(还记得我们之前使用的 0xE35A172C
魔术常量吗?这是它的 64 位版本)。在 IDA 中查看 rsaenh!CPGenRandom
可以清楚地看到,加密提供程序句柄仅用作检查以确定传递给函数的上下文的正确有效性,而对随机数生成没有实际意义。
整个随机数生成逻辑被转移到函数 cryptbase!SystemFunction036
,它只接受两个参数:接收随机数据的缓冲区及其长度。这真是个好消息,因为随机数生成没有攻击者可以在运行时利用的外部因素来使生成更加复杂。深入研究之后,我们意识到 cryptbase!SystemFunction036
本身只不过是 bcryptprimitives!ProcessPrng
的一个轻量级包装器,而从函数名来看,它似乎符合我们的预期。
bcryptprimitives
DLL 是加密下一代 API (CNG) 的一部分,非常复杂。完全逆向工程超出本次研究范围,因此我们只关注我们感兴趣的部分。首先,我们观察到,一旦加载到进程中,该库就会初始化进程种子 - 要么来自 rdrand
指令,要么来自对 IumKernelState
trustlet 的 VTL1 调用,该调用在明确命名的 InitUmRootRngState
函数中进行。然后,它会填充一个随机数生成器状态表,并在 ntdll!_KUSER_SHARED_DATA::RNGSeedVersion
中更新 RNG 种子版本状态。
当调用 ProcessPrng
时,下一个伪随机数的生成由特定于 CPU 的状态决定。确切地说,当前线程运行所在的处理器编号用作索引来加载和生成下一个数字。我们稍后会详细解释,但这在未来将具有挑战性。使用此状态信息,通过调用 AesRNGState_generate
生成下一个数字,并将结果存储在参数中给定的缓冲区内。
这对我们想要完成的任务来说是一个不可忽视的问题。在支持多处理器的 Windows 上(所有现代 PC 都是如此),很难始终如一地知道线程正在哪个处理器上运行,这使得生成预测变得不可能。但是,Windows 提供了一些方法来影响调度程序,如下所示。
通过用户模式模拟内存转储来预测伪随机数
请记住,为了击败 WANNACRY,我们需要能够直接从内存转储中执行函数 cryptbase!SystemFunction036
。我们可以通过使用模拟器(如 QEMU 或 Bochs)来实现这一点,方法是映射执行上下文(填充内存布局、恢复 TEB/PEB 等),这些上下文是从勒索软件的内存转储中收集的,我们按照以下步骤进行操作
- 解析用户模式转储以提取并映射所有内存布局;对于这一步,我们使用了 udmp-parser 库的 Python 绑定
- 在模拟器中完全重建一个可工作的内存布局,为此使用了 bochscpu 及其 Python 绑定
- 通过查找函数
cryptbase!SystemFunction036
并模拟运行时,重建有效的线程上下文
但是,我们仍然无法预测调用 cryptbase!SystemFunction036
的线程将在哪个 CPU 上运行,因此无法准确预测该函数返回的后续值。在单核机器上,这不是问题,因为我们的 PRNG 状态表将只包含一个条目,并且这种方法经过测试可以完美地开箱即用。但是,它在多核系统上会失败,因为只有第一次调用 cryptbase!SystemFunction036
才会返回正确的随机值。
为了在多核机器上进行准确的模拟,我们需要知道下一个调用 cryptbase!SystemFunction036
的线程在运行时将在哪个处理器上被调用,这几乎是不可能的。测试了两种可能的方法
- 从转储中,我们了解了整个 PRNG 状态表。因此,我们可以使模拟脚本挂钩函数
ntdll!RtlGetCurrentProcessorNumberEx
,并使用它来确定随机表中的索引,然后让它生成特定核心的所有值。这种方法被证明是成功的,但是非常繁琐,尤其是在大规模应用时,因为自动化会产生指数级的可能性来检索正确生成的序列。 - 另一种选择发生在金丝雀检测期间。一旦金丝雀确认它是勒索软件,我们就可以强制罪魁祸首进程的 CPU 亲和性仅为一个 CPU,我们可以自由选择其索引。这可以从内核或用户模式完成,只要使用
PROCESS_SET_INFORMATION
访问权限打开目标进程即可。此处理器索引将确定在AesStateTable
数组中采取的条目,这样做可以让我们通过模拟可靠地预测 PNRG 的所有未来值。
要全面了解 WANNACRY 进程,您可以观看演示。我们还在 GitHub 上提供了可供审查的代码。
对这两种技术的测试表明,可以使用我们拥有的迷你转储来预测 PRNG 的未来值。这将对像 WANNACRY 这样的勒索软件非常有帮助,后者使用 Windows PRNG 为每个加密文件生成唯一的 AES 密钥。
将这项研究纳入 Elastic Endpoint
Elastic 的 ON Week 是一个不受约束地进行实验的地方,并且经常为现有的 Elastic 解决方案带来巨大的改进。
进程快照生成已添加到 Elastic Security 的 8.11 版本中。启用保护后,如果检测到勒索软件,端点将在恢复执行之前生成完整的内存进程转储,这可能会导致勒索软件进程终止。我们希望这个简单的添加可以通过提供对勒索软件尝试执行的操作的更好洞察力来进一步帮助 DFIR 团队。
最近的新闻表明,如果公开发布进程内存转储,可能会泄露大量有价值的私人信息。因此,必须强调的是,即使启用了该功能,也不会将任何内存转储提交给 Elastic。转储文件由端点在本地生成(并压缩),生成的文件存储在 Elastic 的安全安装文件夹中(默认情况下,$ElasticInstallPath\Endpoint\cache\RansomwareDumps
)。这样,攻击者就无法轻易篡改转储文件,但取证和事件响应团队可以轻松访问这些文件,以帮助他们进行恢复过程。
让我们演示一下在新的 Elastic 8.11 中针对 NOTPETYA 的此功能是如何工作的:观看演示
结束语
这结束了我们 ON Week 的研究,并取得了相当积极的成果。我们是否提出了针对所有勒索软件的万无一失的解决方案?没有,而且这种解决方案可能永远不会存在。正如我们在引言中强调的那样,勒索软件存在如此多的类型和变种,因此似乎不可能有一种适用于所有情况的解决方案。
然而,这项研究发现,这种方法在 FP 风险、系统要求和潜在结果之间提供了很好的权衡。如果金丝雀功能将其标记为勒索软件,则快照进程内存的风险很小。在误报的情况下,计算机只会最终在受保护的位置保留一个转储文件(并且 ZIP 压缩将大大减少磁盘上的占用空间)。
虽然这不是完美的勒索软件解决方案,但提供勒索软件的内存转储可以促进取证工作,并有可能使团队恢复甚至预测会话加密密钥。完整的内存转储是调试和取证的绝佳帮手,因为它们提供了运行时事件的详尽视图。多亏了模拟,我们可以自信地回溯导致妥协的一些步骤,并有望修复它。