本文的目的旨在帮助安全团队开展安全测试,发现安全问题,切勿将本文中提到的技术用作攻击行为,请切实遵守国家法律法规。
重要声明: 本文档中的信息和工具仅用于授权的安全测试和研究目的。未经授权使用这些工具进行攻击或数据提取是非法的,并可能导致严重的法律后果。使用本文档中的任何内容时,请确保您遵守所有适用的法律法规,并获得适当的授权。
Dirty Vanity是一种新的代码注入技术,它利用了Windows操作系统中一种鲜为人知的机制——分叉(forking)。在本文中,我们将深入探讨分叉机制,探究其合法用途,并展示如何通过注入恶意代码来利用它蒙蔽端点检测与响应(EDR)系统。
实现一种新的代码注入技术通常遵循一个简单的公式,这使得防御此类攻击具有可管理性。偶尔会出现一种新的独特技术,无法通过常规协议进行缓解。Dirty Vanity就是一个典型例子。
分叉背景
分叉进程是指从调用进程创建一个新进程的行为。“fork”(分叉)这个名字源于UNIX系统中创建进程的系统调用——“fork”和“exec”。
Dirty Vanity是对Windows中合法分叉机制的滥用。
Windows中的分叉
Windows本身在创建进程时并不使用fork和exec。然而,它的遗留POSIX子系统(自1993年Windows NT第一版就已包含)支持这两个调用,该子系统旨在支持基本的UNIX二进制文件执行。POSIX子系统早已被替代(先是在Windows XP中被Windows Services for UNIX (SFU) 替代,后来又被当前的Windows Subsystem for Linux (WSL) 取代),但其代码至今仍对Windows产生影响。
下面是对psxdll.dll的查看,它是这个子系统的核心部分,导出了基本的UNIX API:
图1:分叉的起源
正如我们所见,这个_fork
在内部通过调用Ntdll的RtlCloneUserProcess来实现实际的分叉。
在上面的例子中,我们看到了Windows分叉的起源,并且以下机制至今仍在使用分叉:
- 进程反射(Process Reflection)
一种分叉机制,其目的是允许对持续提供服务的进程进行分析。Windows诊断基础结构(WDI)正是利用进程反射来实现这一点:
- 进程快照(Process Snapshotting)
使你能够部分或全部捕获进程状态。它可以利用Windows内部的POSIX分叉克隆功能高效地捕获进程的虚拟地址内容。 - 恶意用例示例:通过分叉转储凭证(Credential Dumping via forking)
在凭证转储领域,许多防御措施都集中在存储已登录用户凭证的LSASS.exe上。有一种针对这些防御措施的分叉绕过方法,它利用前面提到的一种分叉机制对LSASS进行分叉,并访问保护较弱的分叉内容:
总之,Windows包含一种分叉能力,类似于它最初旨在支持的传统UNIX分叉,但它揭示了一种不同且更强大的远程分叉选项。有了Windows 中的这种远程分叉可能性,我们可以像在上述恶意转储LSASS的用例中那样操纵防御机制。在Dirty Vanity的案例中,我们将展示它如何被进一步滥用。
分叉API
在介绍Dirty Vanity如何滥用远程分叉之前,我们将介绍可以调用分叉的Windows API。我们从支持POSIX基础分叉的API开始:
RtlCloneUserProcess(
ULONG ProcessFlags,
PSECURITY_DESCRIPTOR ProcessSecurityDescriptor,
PSECURITY_DESCRIPTOR ThreadSecurityDescriptor,
HANDLE DebugPort,
PRTL_USER_PROCESS_INFORMATION ProcessInformation
);
RtlCloneUserProcess本质上是NtCreateUserProcess的一个包装器,调用了相同的功能。
NtCreateUserProcess(
PHANDLE ProcessHandle,
PHANDLE ThreadHandle,
ACCESS_MASK ProcessDesiredAccess,
ACCESS_MASK ThreadDesiredAccess,
POBJECT_ATTRIBUTES ProcessObjectAttributes,
POBJECT_ATTRIBUTES ThreadObjectAttributes,
ULONG ProcessFlags,
ULONG ThreadFlags,
PVOID ProcessParameters,
PPS_CREATE_INFO CreateInfo,
PPS_ATTRIBUTE_LIST AttributeList
);
NtCreateUserProcess是一个系统调用。它通过在PPS_ATTRIBUTE_LIST的AttributeList参数中设置PS_ATTRIBUTE_PARENT_PROCESS来公开进程分叉,如下所示:
NTSTATUS NtForkUserProcess()
{
HANDLE hProcess = nullptr, hThread = nullptr;
OBJECT_ATTRIBUTES poa = { sizeof(poa) };
OBJECT_ATTRIBUTES toa = { sizeof(toa) };
PS_CREATE_INFO createInfo = {sizeof(createInfo)};
createInfo.State = PsCreateInitialState;
// 在属性列表中添加父进程句柄
PPS_ATTRIBUTE_LIST attributeList;
PPS_ATTRIBUTE attribute;
UCHAR attributeListBuffer[FIELD_OFFSET(PS_ATTRIBUTE_LIST, Attributes) + sizeof(PS_ATTRIBUTE) * 1];
memset(attributeListBuffer, 0, sizeof(attributeListBuffer));
attributeList = reinterpret_cast<PPS_ATTRIBUTE_LIST>(attributeListBuffer);
attributeList->TotalLength = FIELD_OFFSET(PS_ATTRIBUTE_LIST, Attributes) + sizeof(PS_ATTRIBUTE) * 1;
attribute = &attributeList->Attributes[0];
attribute->Attribute = PS_ATTRIBUTE_PARENT_PROCESS;
attribute->Size = sizeof(HANDLE);
attribute->ValuePtr = GetCurrentProcess();
NtCreateUserProcessFunc const NtCreateUserProcess = reinterpret_cast<NtCreateUserProcessFunc>(GetProcAddress(LoadLibraryA("ntdll.dll"), "NtCreateUserProcess"));
NTSTATUS res = NtCreateUserProcess(&hProcess, &hThread, 0, 0, nullptr, nullptr, PROCESS_CREATE_FLAGS_INHERIT_FROM_PARENT | PROCESS_CREATE_FLAGS_INHERIT_HANDLES, THREAD_CREATE_FLAGS_CREATE_SUSPENDED, nullptr, &createInfo, attributeList);
auto pid = GetProcessId(hProcess);
return res;
}
正如我们所总结的,Windows中更强大的分叉变体是远程分叉,然而,如果我们尝试在这个例子中将attribute->ValuePtr = GetCurrentProcess();
替换为不同的句柄:attribute->ValuePtr = someOtherHandle;
,我们将会失败,并得到错误代码STATUS_INVALID_PARAMETER==0xC000000D
,这意味着这个API无法进行远程分叉。
远程分叉
现在我们将探索进程反射和进程快照背后的API,因为这些是在Windows中提供远程分叉的机制。
进程快照通过调用Kernel32!PssCaptureSnapshot来触发,如果我们深入调用链,会看到Kernel32! PssCaptureSnapshot调用ntdll!PssNtCaptureSnapshot,进而调用ntdll! NtCreateProcessEx。
让我们看看NtCreateProcessEx及其旧版本NtCreateProcess:
NtCreateProcessEx(
PHANDLE ProcessHandle,
ACCESS_MASK DesiredAccess,
POBJECT_ATTRIBUTES ObjectAttributes,
HANDLE ParentProcess,
ULONG Flags,
HANDLE SectionHandle,
HANDLE DebugPort,
HANDLE ExceptionPort,
BOOLEAN InJob
);
NtCreateProcess(
PHANDLE ProcessHandle,
ACCESS_MASK DesiredAccess,
POBJECT_ATTRIBUTES ObjectAttributes,
HANDLE ParentProcess,
BOOLEAN InheritObjectTable,
HANDLE SectionHandle,
HANDLE DebugPort,
HANDLE ExceptionPort
);
NtCreateProcess[Ex]是两个旧版的进程创建系统调用,它们提供了另一种访问分叉机制的途径。与较新的 NtCreateUserProcess不同,使用它们时,通过将HANDLE ParentProcess参数设置为目标进程句柄,就可以对远程进程进行分叉。
进程反射通过调用RtlCreateProcessReflection来触发:
RtlCreateProcessReflection(
HANDLE ProcessHandle,
ULONG Flags,
PVOID StartRoutine,
PVOID StartContext,
HANDLE EventHandle,
T_RTLP_PROCESS_REFLECTION_REFLECTION_INFORMATION* ReflectionInformation
);
RtlCreateProcessReflection将对由HANDLE ProcessHandle表示的进程进行分叉。
它执行以下操作:
-
创建一个共享内存段。
-
用参数填充共享内存段。
-
将共享内存段映射到当前进程和目标进程中。
-
通过调用RtlpCreateUserThreadEx在目标进程中创建一个线程。该线程被定向到在ntdll的RtlpProcessReflectionStartup函数中开始执行。
-
创建的线程调用RtlCloneUserProcess,并传递它从与发起进程共享的内存映射中获得的参数。如前所述,RtlCloneUserProcess包装了NtCreateUserProcess,将当前进程分叉到新的目标进程。
-
在内核模式下,NtCreateUserProcess在创建新进程时执行的大部分代码路径是相同的,不同之处在于它调用 PspAllocateProcess来创建进程对象和初始线程时,会使用一个标志调用 MmInitializeProcessAddressSpace,该标志指定地址应该是目标进程的写时复制副本,而不是初始进程地址空间。
-
如果RtlCreateProcessReflection的调用者指定了PVOID StartRoutine,RtlpProcessReflectionStartup将在结束前将执行转移到该例程。如果提供了PVOID StartContext,它也会作为参数提供。
正如你可能已经猜到的,PVOID StartRoutine在Dirty Vanity中起着关键作用。
大部分分叉的繁重工作是在内核模式下完成的,最有趣的部分之一是它会将目标进程的所有地址空间复制到分叉进程中,包括动态分配和运行时修改,这就引出了Dirty Vanity。
Dirty Vanity设置
代码注入和端点检测与响应(EDR)
让我们简要介绍一下传统注入的步骤。
为了使注入的代码在目标进程中启动并运行,注入器将执行以下操作:
- 步骤1
:为要注入的shellcode分配空间,或者为其找到一个代码洞穴。 - 步骤2
:使用各种写入原语将shellcode写入步骤1中创建的空间。 -
WriteProcessMemory -
NtMapViewOfSection -
GlobalAddAtom - 步骤3
:使用各种执行原语执行步骤2中写入的shellcode。 -
NtSetContextThread -
NtQueueApcThread -
IAT Hook(导入地址表挂钩)并调用挂钩
注入器可以选择任何分配、写入和执行原语的组合,调用它们来创建一次注入。
由于注入原语的动态性质,大多数EDR会尝试通过挂钩它们所知道的所有原语来处理注入。以下是一个示例,Injector.exe正在对Explorer.exe执行最简单的注入:
图4:对Explorer.exe的简单注入
当EDR监控系统时,它会监控同一目标上的所有原语,并捕获对Explorer.exe的所有这三个操作:
-
分配 = VirtualAllocEx -
向分配的空间写入内容 = WriteProcessMemory -
执行写入的内容 = CreateRemoteThread
当监控到最终的执行原语时,EDR将检测到/阻止这次注入尝试。
Dirty Vanity的工作原理
Dirty Vanity利用前面描述的Windows远程分叉机制作为注入领域的一种新原语——Fork(分叉)。其背后的概念很简单,它由以下步骤组成:
- 初始写入步骤
以任何你喜欢的方式在目标进程中分配并写入你的有效载荷,例如: -
VirtualAllocEx & WriteProcessMemory -
NtCreateSection & NtMapViewOfSection -
任何其他你喜欢的方式 - 分叉与执行步骤
对目标进程执行远程分叉,并将进程起始地址设置为有效载荷(有效载荷会被分叉到相同的位置),使用: -
RtlCreateProcessReflection(PVOID StartRoutine = 指向克隆的shellcode) -
NtCreateProcess[Ex] + 对克隆的shellcode的任何执行原语
让我们将这些步骤应用到我们之前的示例中:
图5:Dirty Vanity流程
Injector.exe通常会先使用VirtualAllocEx,然后对Explorer.exe使用WriteProcessMemory。监控此系统的EDR会关联这些操作,并等待第三个执行原语将此操作标记为注入。
在Dirty Vanity中,预期的执行原语不会发生,而是我们转向一个远程分叉API。
现在Explorer.exe被分叉成它自己的一个副本,分叉后的结果进程包含Explorer.exe地址空间的一个副本,包括在初始写入步骤中加载到相同地址且具有相同内存保护的有效载荷。
通过将分叉进程的起始地址设置为我们的有效载荷,它将被执行。这可以通过以下方式完成:
-
RtlCreateProcessReflection(PVOID StartRoutine = 指向克隆的shellcode) -
NtCreateProcess[Ex] + 对克隆的shellcode的后续执行原语
在这些步骤完成后,我们分叉的Explorer.exe包含我们的有效载荷并执行它。
Dirty Vanity背后的新颖之处在于分叉所造成的分离:虽然在目标进程上正常完成分配和写入阶段,但它们不会被捕获,因为实际的执行阶段(从EDR的角度来看,这是确定为注入的关键)是在分叉的目标进程上执行的。
从EDR的角度来看,新分叉的Explorer.exe从未被写入过,并且对它的执行与写入尝试没有关联。
由于这种独特的执行方式,Dirty Vanity能够绕过常见的EDR检测方法。
运行Dirty Vanity的先决条件
为了调用Dirty Vanity,我们需要一个具有以下访问权限的目标进程句柄:
-
RtlCreateProcessReflection变体:PROCESS_VM_OPERATION | PROCESS_CREATE_THREAD | PROCESS_DUP_HANDLE -
NtCreateProcess[Ex]变体:PROCESS_CREATE_PROCESS
对于完整的实现,目标进程句柄应该包含这些访问权限的组合,以及适合你选择的初始写入步骤的权限。
通过RtlCreateProcessReflection实现的Dirty Vanity
本文背后的研究重点是使用RtlCreateProcessReflection方法的一个概念验证(POC)。
以下是使用它执行Dirty Vanity的代码片段:
unsigned char shellcode[] = {0x40, 0x55, 0x57, ...};
size_t bytesWritten = 0;
// 以适当的权限打开分叉目标
HANDLE victimHandle = OpenProcess(PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_CREATE_THREAD | PROCESS_DUP_HANDLE, TRUE, victimPid);
// 在目标进程中分配shellcode大小的空间
DWORD_PTR shellcodeSize = sizeof(shellcode);
LPVOID baseAddress = VirtualAllocEx(victimHandle, nullptr, shellcodeSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
// 写入shellcode
BOOL status = WriteProcessMemory(victimHandle, baseAddress, shellcode, shellcodeSize, &bytesWritten);
HMODULE ntlib = LoadLibraryA("ntdll.dll");
Rtl_CreateProcessReflection RtlCreateProcessReflection = (Rtl_CreateProcessReflection)GetProcAddress(ntlib, "RtlCreateProcessReflection");
T_RTLP_PROCESS_REFLECTION_REFLECTION_INFORMATION info = { 0 };
// 分叉目标并在克隆中执行shellcode
NTSTATUS ret = RtlCreateProcessReflection(victimHandle, RTL_CLONE_PROCESS_FLAGS_INHERIT_HANDLES, baseAddress, NULL, NULL, &info);
当首次尝试这个POC时,我们使用了一个基本的MessageBoxA shellcode,结果导致了访问冲突异常:
1:002> g
(6738.da4): Access violation - code c0000005 (first chance)
First-chance exceptions are reported before any exception handling.
This exception may be expected and handled.
USER32!GetDpiForCurrentProcess+0x14:
00007ff8`8b75719c 0fb798661b0000 movzx ebx,word ptr [rax+1B66h] ds:000002d3`6ef92ba6=????
1:002> k
# Child-SPRetAddrCall Site
00 000000da`df9ffb10 00007ff8`8b7570c2 USER32!GetDpiForCurrentProcess+0x14
01 000000da`df9ffb40 00007ff8`8b75703b USER32!ValidateDpiAwarenessContextEx+0x32
02 000000da`df9ffb70 00007ff8`8b7bc2da USER32!SetThreadDpiAwarenessContext+0x4b
03 000000da`df9ffba0 00007ff8`8b7bc0d8 USER32!MessageBoxTimeoutW+0x19a
04 000000da`df9ffca0 00007ff8`8b7bbcee USER32!MessageBoxTimeoutA+0x108
05 000000da`df9ffd00 000002d3`71bf0050 USER32!MessageBoxA+0x4e
06 000000da`df9ffd40 00007ff8`8c210000 0x000002d3`71bf0050shellcode
简而言之,USER32!MessageBoxA
需要将user32!gSharedInfo
结构映射到进程中。
我们的分叉进程缺少这个结构,因为user32!gSharedInfo
是通过ViewUnmap
设置明确映射到每个进程的。根据微软官方文档(MSDN)的解释,“ViewUnmap:该视图不会被映射到子进程中”。
这意味着,像user32!gSharedInfo
这样的ViewUnmap
数据对克隆出的子进程是隐藏的。为了克服这个障碍,我们的概念验证采用的方法是使用仅依赖NTDLL
的shellcode,这种shellcode是完全独立的,因此不依赖于这些数据段。
我们使用了https://github.com/rainerzufalldererste/windows_x64_shellcode_template作为模板,创建了一个基于ntdll
的自定义shellcode,它执行以下操作:
-
从加载程序数据结构(LDR)中检测 Ntdll
API。 -
使用 RtlInitUnicodeString
、RtlAllocateHeap
和RtlCreateProcessParametersEx
创建参数。 -
调用 NtCreateUserProcess
。 -
进程: C:WindowsSystem32cmd.exe
-
命令行: /k msg * “Hello from Dirty Vanity”
完整的源代码请访问:https://github.com/deepinstinct/Dirty-Vanity
以下是操作过程汇总:
图6:针对Explorer进程ID调用Dirty Vanity
图7:结果进程树,分叉的Explorer子进程正在执行我们的shellcode。
总结
为了检测代码注入,端点检测与响应(EDR)解决方案传统上会监控并关联在同一进程上执行的“分配/写入/执行”操作。分叉API引入了一种新的注入原语——“分叉”,对传统的检测方法构成了挑战。
Dirty Vanity利用分叉将任何分配和写入操作克隆到一个新进程中。从EDR的角度来看,这个新进程从未被写入过,因此在最终执行时不会被标记为已注入。其执行方式包括:
-
本研究重点关注的使用 RtlCreateProcessReflection
进行的“分叉并执行”。 -
在调用 RtlCreateProcessReflection
之后使用普通执行原语,或者使用NtCreateProcess[Ex]
,后者仍是一条未被深入探索的路径 。
Dirty Vanity改变了我们对注入防御的看法,因为分叉改变了操作系统监控的规则。EDR必须做出应对,监控所有涉及分叉的原语,最终追踪分叉进程,并像对待其父进程一样了解它们。
有关此案例的更多详细信息以及研究过程的更多内容,请查看Deep Instinct研究团队在黑帽大会(Black Hat)上的演讲:https://i.blackhat.com/EU-22/Thursday-Briefings/EU-22-Nissan-DirtyVanity.pdf。
参考文献
-
https://github.com/deepinstinct/Dirty-Vanity -
https://i.blackhat.com/EU-22/Thursday-Briefings/EU-22-Nissan-DirtyVanity.pdf -
https://billdemirkapi.me/abusing-windows-implementation-of-fork-for-stealthy-memory-operations/ 讨论了使用 RtlCloneUserProcess
和NtCreateUserProcess
进行本地分叉的情况 -
https://gist.github.com/juntalis/4366916 和 https://gist.github.com/Cr4sh/126d844c28a7fbfd25c6 关于 RtlCloneUserProcess
的用法及有用常量 -
https://gist.github.com/GeneralTesler/68903f7eb00f047d32a4d6c55da5a05c 使用 RtlCreateProcessReflection
进行凭证转储的用例,其反射代码来源于下一个链接 -
https://github.com/hasherezade/pe-sieve/blob/master/utils/process_reflection.cpp RtlCreateProcessReflection
的源代码框架 -
https://www.matteomalvica.com/blog/2019/12/02/win-defender-atp-cred-bypass/ 关于 PssCaptureSnapshot → NtCreateProcessEx
的内容 -
《Windows Internals 7th part 1》中关于 RtlCreateProcessReflection
的内容 -
https://paper.bobylive.com/Meeting_Papers/BlackHat/USA-2011/BH_US_11_Mandt_win32k_Slides.pdf -
https://www.youtube.com/watch?v=EkGDSqpfzgg -
https://github.com/rainerzufalldererste/windows_x64_shellcode_template
原文始发于微信公众号(网空安全手札):Dirty Vanity:一种新的代码注入与绕过EDR的方法
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论