文章总结: 这篇文章介绍了一种名为DirtyVanity的新型代码注入技术,它利用Windows操作系统中的fork机制来绕过EDR系统。通过先向目标进程写入有效载荷,然后使用远程fork机制创建目标进程副本,并将派生进程的起始地址设置为有效载荷,从而执行恶意代码。这种方法绕过了传统EDR对分配-写入-执行链的监控,因为从EDR角度看,新派生的进程从未被写入过。文章提供了技术实现细节和代码示例,建议安全研究人员需要关注fork原语的监控。 综合评分: 91 文章分类: 漏洞分析,渗透测试,代码审计,安全工具,免杀
Dirty Vanity:代码注入和 EDR 绕过的新方法 | 深度本能
Ots安全
2025年12月19日 12:18 广东
威胁简报
恶意软件
漏洞攻击
Dirty Vanity 是一种新型代码注入技术,它利用了 Windows 操作系统中鲜为人知的机制——fork(分叉)。本文将深入探讨 fork,分析其合法用途,并展示如何通过注入恶意代码来操纵 fork,从而绕过 EDR(事件检测与响应)系统。
实施新的代码注入技术通常遵循简单的公式,这使得防御此类攻击变得容易。但有时,也会出现一些新的、不寻常的技术,常规协议无法对其进行缓解。例如:Dirty Vanity。
分叉背景
进程分叉是指从调用进程创建一个新进程的行为。“fork”这个名称源于UNIX系统中用于创建进程的系统调用——“fork”和“exec”。
Dirty Vanity 滥用了 Windows 中存在的合法 fork 机制。
Windows Fork
Windows 本身并不使用 fork 和 exec 来创建进程。然而,它通过其遗留的 POSIX 子系统(自 1993 年 Windows NT 首个版本起就已包含)支持这些操作,该子系统旨在支持基本的 UNIX 二进制文件执行。POSIX 子系统早已被取代(先是 Windows XP 中的Windows Services for UNIX (SFU),后来是现在的Windows Subsystem for Linux (WSL)),但其代码至今仍然影响着 Windows 系统。
下面介绍的是 psxdll.dll,这是一个 dll 文件,它是该子系统的核心部分,用于导出基本的 UNIX API:
图1:分叉起源
我们可以看到,这个 _fork 内部是通过调用 Ntdll 的 RtlCloneUserProcess 来实现的,该函数执行实际的 fork 操作。
在上面的例子中,我们看到了 Windows Fork 的起源,而以下机制至今仍在使用 Fork:
进程反射——一种进程分叉机制,其目标是对应持续提供服务的进程进行分析。WDI(Windows 诊断基础架构)正是利用进程反射来实现这一目标:
图 2:过程反思
进程快照——允许您捕获进程的部分或全部状态。它可以利用 Windows 内部的 POSIX 分支克隆功能,高效地捕获进程的虚拟地址内容。
恶意用例示例:
通过 fork 进行凭据转储 – 在凭据转储领域,许多防御措施都集中在 LSASS.exe 上,因为它存储了已登录用户的凭据。对于那些利用前面提到的 fork 机制来 fork LSASS 并访问受保护程度较低的 fork 分支内容的防御措施,存在一种 fork 绕过方法:
图 3:通过 Forking 进行凭证转储
总而言之,Windows 包含类似传统 UNIX 分支的功能,但它最初旨在支持这种分支方式,同时还提供了一种功能更强大的远程分支选项。利用 Windows 的这种远程分支能力,我们可以像上述恶意 LSASS 转储用例中那样,操纵防御机制。在 Dirty Vanity 的案例中,我们将演示如何进一步滥用此功能。
分叉 API
在介绍 Dirty Vanity 如何滥用远程 fork 之前,我们将先介绍可以调用 fork 的 Windows API。我们首先介绍支持 POSIX 基本 fork 的 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 来创建进程对象和初始线程,而 PspAllocateProcess 调用 MmInitializeProcessAddressSpace 时会使用一个标志来指定地址应该是目标进程的写时复制副本,而不是初始进程地址空间。
- 如果 RtlCreateProcessReflection 的调用者指定了 PVOID StartRoutine,则 RtlpProcessReflectionStartup 会在关闭之前将执行权转移到该 StartRoutine。如果提供了 PVOID StartContext 作为参数,它也会将其作为参数传递给 RtlpProcessReflectionStartup。
正如你可能已经猜到的那样,PVOID StartRoutine 在 Dirty Vanity 中扮演着关键角色。
大部分的 fork 操作都是在内核模式下完成的,其中最有趣的部分之一是它将目标进程的所有地址空间复制到 fork 的进程,包括动态分配和运行时修改,这就引出了 Dirty Vanity。
代码注入和端点检测与响应 (EDR)
让我们简要介绍一下传统注射的步骤。
为了使注入的代码在目标进程中运行,注入器将执行以下操作:
-
步骤 1:为 shellcode 分配空间以进行注入,或者找到一个代码洞穴来存放它。
-
步骤 2:使用各种写入原语将 shellcode 写入步骤 1 中创建的空间。
-
写入进程内存
-
NtMapViewOfSection
-
GlobalAddAtom
步骤 3:使用各种执行原语执行步骤 2 中编写的 shellcode。
- NtSetContextThread
- NtQueueApcThread
- IAT 钩子及钩子的调用
注入器可以选择任意 Allocate、Write 和 Execute 原语组合,调用它们,并创建注入。
由于注入原语的动态特性,大多数 EDR 会尝试通过钩住所有已知的注入原语来应对注入攻击。以下示例展示了这种方法,其中 Injector.exe 对 Explorer.exe 执行最简单的注入:
图 4:对 Explorer.exe 的简单注入
当 EDR 监控系统时,它会监控同一目标上的所有原始程序,并在 Explorer.exe 上捕获所有这三个程序:
- 分配 = VirtualAllocEx
- 将内容写入分配 = WriteProcessMemory
- 执行写入的内容 = 创建远程线程
当监控到最终执行原语时,EDR 将检测/阻止此注入尝试。
Dirty Vanity在行动
Dirty Vanity 利用之前描述的 Windows 远程 fork 机制,将其作为注入领域的一种新原语——Fork。其背后的概念很简单,包含以下步骤:
1.初始写入步骤:以任何你喜欢的方式将有效载荷分配并写入目标进程,例如:
- VirtualAllocEx 和 WriteProcessMemory
- NtCreateSection 和 NtMapViewOfSection
- 任何其他更可取的方式
2.Fork & Execute 步骤:对目标进程执行远程 fork,并将进程起始地址设置为有效载荷(有效载荷会被 fork 到同一位置),具体操作如下:
- RtlCreateProcessReflection(PVOID StartRoutine = 指向克隆的 shellcode)
- NtCreateProcess[Ex] + 对克隆的 shellcode 使用任何执行原语。
让我们将这些步骤应用到之前的例子上:
图 5:Dirty Vanity Flow
Injector.exe 正常启动,首先调用 VirtualAllocEx,然后通过 Explorer.exe 调用 WriteProcessMemory。监控此系统的 EDR 会将这些操作关联起来,并等待第三个执行原语,以将此操作标记为注入。
在 Dirty Vanity 中,预期的执行原语不会发生,而是恢复到远程 fork 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 Flow》的先决条件
要调用 Dirty Vanity,我们需要一个具有以下访问权限的目标进程句柄:
- RtlCreateProcessReflection 变体:PROCESS_VM_OPERATION | PROCESS_CREATE_THREAD | PROCESS_DUP_HANDLE
- NtCreateProcess[Ex] 变体:PROCESS_CREATE_PROCESS
为了完整实现,目标进程句柄应包含这些访问权限的组合,以及适合您选择的初始写入步骤的访问权限。
Dirty Vanity 通过 RtlCreateProcessReflection
本博客背后的研究重点是使用 RtlCreateProcessReflection 方法进行概念验证。
以下是使用该代码片段实现 Dirty Vanity 效果的示例:
unsignedchar shellcode[] = {0x40, 0x55, 0x57, ...};
size_t bytesWritten = 0;
// 使用适当的权限打开 fork 目标
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);
#define RTL_CLONE_PROCESS_FLAGS_INHERIT_HANDLES 0x00000002
HMODULE ntlib = LoadLibraryA("ntdll.dll");
Rtl_CreateProcessReflection RtlCreateProcessReflection = (Rtl_CreateProcessReflection)GetProcAddress(ntlib, "RtlCreateProcessReflection");
T_RTLP_PROCESS_REFLECTION_REFLECTION_INFORMATION info = { 0 };
// Fork target & Execute shellcode base within clone
NTSTATUS ret = RtlCreateProcessReflection(victimHandle, RTL_CLONE_PROCESS_FLAGS_INHERIT_HANDLES, baseAddress, NULL, NULL, &info);
最初尝试这个概念验证时,我们使用了一个基本的 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-SP RetAddr Call Site
00000000da`df9ffb10 00007ff8`8b7570c2 USER32!GetDpiForCurrentProcess+0x14
01000000da`df9ffb40 00007ff8`8b75703b USER32!ValidateDpiAwarenessContextEx+0x32
02000000da`df9ffb70 00007ff8`8b7bc2da USER32!SetThreadDpiAwarenessContext+0x4b
03000000da`df9ffba0 00007ff8`8b7bc0d8 USER32!MessageBoxTimeoutW+0x19a
04000000da`df9ffca0 00007ff8`8b7bbcee USER32!MessageBoxTimeoutA+0x108
05000000da`df9ffd00 000002d3`71bf0050 USER32!MessageBoxA+0x4e
06000000da`df9ffd40 00007ff8`8c210000 0x000002d3`71bf0050
shellcode 已成功 fork 并执行,但 USER32!MessageBoxA 的内部却无法从 fork 中运行。
简而言之,USER32!MessageBoxA 需要将 user32!gSharedInfo 结构映射到进程。
我们派生的进程缺少该功能,因为 user32!gSharedInfo 通过 ViewUnmap 设置显式映射到每个进程:
“ViewUnmap:视图将不会映射到子进程” – MSDN
这意味着 ViewUnmap 数据(例如 user32!gSharedInfo)对克隆进程的子进程是隐藏的。为了克服这一障碍,我们的概念验证方案采用的方法是使用完全独立的 NTDLL shellcode,因此它不依赖于任何其他组件。
我们使用https://github.com/rainerzufalldererste/windows_x64_shellcode_template作为模板创建了一个自定义的基于 ntdll 的 shellcode,其功能如下:
1.从 LDR 中检测 Ntdll API
2.使用 RtlInitUnicodeString、RtlAllocateHeap 和 RtlCreateProcessParametersEx 创建参数
3.调用 NtCreateUserProcess
- 进程:C:\Windows\System32\cmd.exe
- 命令行:/k msg * “Hello from Dirty Vanity”
完整源代码请见:https://github.com/deepinstinct/Dirty-Vanity
总结起来:
图 6:通过 Explorer 的 PID 调用 Dirty Vanity。
图 7:结果进程树,其中派生的 Explorer 子进程正在执行我们的 shellcode。
概括
为了检测代码注入,传统的EDR解决方案会监控并关联同一进程上执行的“分配/写入/执行”操作。Fork API引入了一种新的注入原语——Fork,这挑战了传统的检测方法。
Dirty Vanity 利用 fork 技术将所有 Allocate 和 Write 操作克隆到一个新进程中。从 EDR 的角度来看,这个新进程从未被写入过,因此最终执行时不会被标记为注入。
- 使用 RtlCreateProcessReflection 进行 Fork 和 Execute,这是本研究的重点。
- 调用 RtlCreateProcessReflection 或 NtCreateProcess[Ex] 之后执行普通的 Execute 原语,这仍然是一个未探索的路径。
Dirty Vanity 改变了我们看待注入防御的方式,因为 fork 改变了操作系统监控的规则,EDR 必须监控所有出现的 fork 原语,最终跟踪 fork 进程,并以与其父进程相同的知识来处理它们。
如需了解此案的更多详情以及研究过程,请查看 Deep Instinct 研究团队在 Black Hat 大会上的演讲:https://i.blackhat.com/EU-22/Thursday-Briefings/EU-22-Nissan-DirtyVanity.pdf
References
- 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/ talking about forking locally with RtlCloneUserProcess & NtCreateUserProcess
- https://gist.github.com/juntalis/4366916 & https://gist.github.com/Cr4sh/126d844c28a7fbfd25c6 RtlCloneUserProcess usage, and useful constants
- https://gist.github.com/GeneralTesler/68903f7eb00f047d32a4d6c55da5a05c Credential dump use case using RtlCreateProcessReflection. it took reflection code from the next link
- https://github.com/hasherezade/pe-sieve/blob/master/utils/process_reflection.cpp RtlCreateProcessReflection source code framework
- https://www.matteomalvica.com/blog/2019/12/02/win-defender-atp-cred-bypass/ PssCaptureSnapshot → NtCreateProcessEx
- Windows Internals 7th part 1 on 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
END
公众号内容都来自国外平台-所有文章可通过点击阅读原文到达原文地址或参考地址
排版 编辑 | Ots 小安
采集 翻译 | Ots Ai牛马
公众号 | AnQuan7 (Ots安全)
查看原文:《Dirty Vanity:代码注入和 EDR 绕过的新方法 | 深度本能》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论