Dirty Vanity And Pool Party的分析和思考
任意门 发表于 江苏 二进制安全 1949浏览 · 2024-02-04 14:21

本文是用来学习 在 Black Hat 上出现过的两种绕过EDR两种手法;之所以把这两种手法放在一起讨论是因为其思路有异曲同工;都是与Window的进程的相关的利用技术来进行另类绕过。而且两个文章提有铺垫传统进程注入的手段其分三步:第一步 为shellcode 分配空间。第二步将shellcode 写入到创建的空间中;其中用到的函数比如有WriteProcessMemory,NtMapViewOfSection,GlobalAddAtom等。第三步使用漏洞执行原语执行第二步分配的shellcode。其实也就是分配写入执行;下述介绍的两种技术主要是通过代码的角度来理解其实现的过程最后测试成功注入。

Dirty Vanity

首先来说一下blackhat2022中提出的一种 bypass EDR的方法 利用了Windows一种鲜为人知的机制process forking。
Dirty Vanity使用Windows Fork进程规避EDR的进程注入:首先也是通过先写入将shellcode分配并且写入目标进程(通过VirtualAllocEx 和 WriteProcessMemory或者NtCreateSection 和 NtMapViewOfSection 等);其次就是Fork和执行 在目标进程上执行远程Fork;并将进程的起始地址设置为shellcode通过使用如下函数。

RtlCreateProcessReflection (PVOID StartRoutine = 指向克隆的shellcode)
NtCreateProcess[Ex] + 克隆的shellcode执行原语

从下图的流程可以看出首先是Injector.exe 通过VirtualAllocEx 在Explorer.exe申请并且分配一段内存空间;然后在通过WriteProcessMemory写人其中。但随便就使用了Dirty Vanity的核心部分;通过Fork将Explorer.exe创造一个副本;并且Fork的结果了包含Explorer.exe原地址空间的副本包括初始写入步骤的shellcode。然后将Fork进程的起始地址设置为shellcode最后完成指行。
而其主要是通过RtlCreateProcessReflection函数指向克隆的shellcode;然后再将进程起始地址设置为克隆的shellcode;最后Fork的Explorer.exe进程将会包含shellcode并执行它完成进程注入。

接下来看一下代码的具体实现逻辑;首先是拿到准备注入进程的句柄;这里要注意的是使用函数OpenProcess打开句柄的权限设置要设置PROCESS_VM_OPERATION PROCESS_VM_WRITE PROCESS_CREATE_THREAD这三个权限访问目标进程句柄才行。
OpenProcess(PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_CREATE_THREAD | PROCESS_DUP_HANDLE, TRUE, victimPid);


然后通过VirtualAllocEx函数为shellcode申请内存空间;在通过WriteProcessMemory函数将其写入到分配的内存空间;


最后通过先是获取 RtlCreateProcessReflection 函数指针;然后创建目标进程的Reflection镜像并将结果存储在info中。

最后执行的效果如下图所示成功的fork了一个explore.exe并且在其下方进程树有了我们的cmd.exe

Pool Party

blackhat2023中提出了Pool party其采用windows线程池的新进程注入技术;这里就直接阐述其核心原理首先我们要知道Windows用户模式下的线程池:所有Windows进程都有一个线程池而且线程池由结构体表示,这样就可以方便执行内存函数。 线程池由三个不同的工作队列组成,每个队列专用于不同类型的工作项下图就是定义的攻击面;下图可以看见TP_POOL Task Queue和Timer Queue都是在用户态下的;而I/O完成队列则是深处在内核态下这也为后续的利用埋下伏笔。

要利用完成如下步骤:分别是攻击Work Factories-TP_WORK;File obiects (TP_IO);ALPC port objects (TP_ALPC);Job obiects (TP_JOB);Waitable objects-(TP_WAIT);TP_TIMER。
例如上述TP_WORK是插入到TP_POOL的任务队列中去;队列通知到I/O完成队列也就是TP_IO中去;
任何排队到l/O完成队列的TP_DIRECTnotification都会被执行;可以通过对象操作完成来排队;可以通过NtSetloCompletion系统调用直接排队。而TP_TIMER(TP_POOL Timer Queue)是设置队列定时器过期。
更多细节还是从提供的代码来观看;打开Pool Party会发现其文件下总共有八个主要逻辑文件分别是HandleHijacker.cpp,PoolParty.cpp,ThreadPool.cpp,main.cpp,Misc.cpp,Native.cpp,WinApi.cpp,WorkerFactory.cpp
首先来看一下WorkerFactory.cpp文件
这段代码包含两个函数:w_NtQueryInformationWorkerFactory 和 w_NtSetInformationWorkerFactory,它们是对 NtQueryInformationWorkerFactory 和 NtSetInformationWorkerFactory 函数的封装。其参数:hWorkerFactory:对工作工厂的句柄。
WorkerFactoryInformationClass:指定要查询的信息类型的信息类。WorkerFactoryInformation:接收所请求信息的缓冲区的指针。WorkerFactoryInformationLength:由 WorkerFactoryInformation 指向的缓冲区的大小。ReturnLength:指向接收写入到缓冲区的信息大小的变量的指针。
函数 w_NtSetInformationWorkerFactory参数:hWorkerFactory:对工作工厂的句柄。WorkerFactoryInformationClass:指定要设置的信息类型的信息类。WorkerFactoryInformation:指向包含要设置的信息的缓冲区的指针。WorkerFactoryInformationLength:由 WorkerFactoryInformation 指向的缓冲区的大小。


WinApi.cpp文件中这其中有两个关键的函数w_WriteFile 函数向文件写入数据如果操作是异步的(使用 overlapped 参数),并且返回 ERROR_IO_PENDING,则不会引发异常。
w_CreateJobObject函数创建或打开作业对象,用于作业对象句柄的自动关闭。
剩下就是通过w_SetInformationJobObject函数设置作业对象的信息;w_AssignProcessToJobObject函数将进程分配给作业对象。


在往下看一下Native.cpp文件此文件代码包含了一系列与 Windows Native API 相关的函数;比如代码中的w_ZwAssociateWaitCompletionPacket函数其关联等待完成包与 I/O 完成端口,用于异步 I/O 操作的完成通知。再到w_NtAlpcCreatePort函数用于创建 ALPC 端口,用于本地过程调用。w_ZwSetIoCompletion函数为设置 I/O 完成状态,用于 I/O 操作完成通知。w_NtSetTimer2函数设置定时器。

#include "Native.hpp"

void w_ZwAssociateWaitCompletionPacket(
    HANDLE WaitCopmletionPacketHandle,
    HANDLE IoCompletionHandle,
    HANDLE TargetObjectHandle,
    PVOID KeyContext,
    PVOID ApcContext,
    NTSTATUS IoStatus,
    ULONG_PTR IoStatusInformation,
    PBOOLEAN AlreadySignaled
) 
{
    NT_SUCCESS_OR_RAISE(
        "ZwAssociateWaitCompletionPacket",
        ZwAssociateWaitCompletionPacket(
            WaitCopmletionPacketHandle,
            IoCompletionHandle,
            TargetObjectHandle,
            KeyContext,
            ApcContext,
            IoStatus,
            IoStatusInformation,
            AlreadySignaled)
    );
}

void w_ZwSetInformationFile(
    HANDLE hFile,
    PIO_STATUS_BLOCK IoStatusBlock,
    PVOID FileInformation,
    ULONG Length,
    ULONG FileInformationClass
)
{
    NT_SUCCESS_OR_RAISE(
        "ZwSetInformationFile",
        ZwSetInformationFile(
            hFile,
            IoStatusBlock,
            FileInformation,
            Length,
            FileInformationClass)
    );
}

HANDLE w_NtAlpcCreatePort(POBJECT_ATTRIBUTES ObjectAttributes, PALPC_PORT_ATTRIBUTES PortAttributes) {
    HANDLE hAlpc;
    NT_SUCCESS_OR_RAISE(
        "NtAlpcCreatePort",
        NtAlpcCreatePort(&hAlpc, ObjectAttributes, PortAttributes)
    );
    return hAlpc;
}

void w_NtAlpcSetInformation(HANDLE hAlpc, ULONG PortInformationClass, PVOID PortInformation, ULONG Length) 
{
    NT_SUCCESS_OR_RAISE(
        "NtAlpcSetInformation", 
         NtAlpcSetInformation(hAlpc, PortInformationClass, PortInformation, Length)
    );
}


HANDLE w_NtAlpcConnectPort(
    PUNICODE_STRING PortName,
    POBJECT_ATTRIBUTES ObjectAttributes,
    PALPC_PORT_ATTRIBUTES PortAttributes,
    DWORD ConnectionFlags,
    PSID RequiredServerSid,
    PPORT_MESSAGE ConnectionMessage,
    PSIZE_T ConnectMessageSize,
    PALPC_MESSAGE_ATTRIBUTES OutMessageAttributes,
    PALPC_MESSAGE_ATTRIBUTES InMessageAttributes,
    PLARGE_INTEGER Timeout
) 
{
    HANDLE hAlpc;
    NT_SUCCESS_OR_RAISE(
        "NtAlpcConnectPort",
        NtAlpcConnectPort(
            &hAlpc,
            PortName,
            ObjectAttributes,
            PortAttributes,
            ConnectionFlags,
            RequiredServerSid,
            ConnectionMessage,
            ConnectMessageSize,
            OutMessageAttributes,
            InMessageAttributes,
            Timeout)
    );

    return hAlpc;
}

BOOLEAN w_RtlAdjustPrivilege(ULONG Privilege, BOOLEAN Enable, BOOLEAN CurrentThread)
{
    BOOLEAN Enabled = NULL;
    NT_SUCCESS_OR_RAISE(
        "RtlAdjustPrivilege", 
        RtlAdjustPrivilege(
            Privilege, 
            Enable,
            CurrentThread,
            &Enabled)
    );
    return Enabled;
}

void w_ZwSetIoCompletion(HANDLE IoCompletionHandle, PVOID KeyContext, PVOID ApcContext, NTSTATUS IoStatus, ULONG_PTR IoStatusInformation)
{
    NT_SUCCESS_OR_RAISE(
        "ZwSetIoCompletion",
        ZwSetIoCompletion(
            IoCompletionHandle,
            KeyContext,
            ApcContext,
            IoStatus,
            IoStatusInformation)
    );
}

void w_NtSetTimer2(HANDLE TimerHandle, PLARGE_INTEGER DueTime, PLARGE_INTEGER Period, PT2_SET_PARAMETERS Parameters) 
{
    NT_SUCCESS_OR_RAISE(
        "NtSetTimer2",
        NtSetTimer2(
            TimerHandle,
            DueTime,
            Period,
            Parameters)
    );

}

Misc.cpp 中主要实现了可以用于捕获和处理 Windows API 调用中的错误信息。其中,w_FormatMessageA 函数封装了 FormatMessageA 的调用,提供了更方便的接口来获取错误信息。


ThreadPool.cpp中其文件中主要定义了一些用于创建线程池相关对象的函数;像w_CreateThreadpoolWork函数创建线程池工作项,并返回PFULL_TP_WORK类型的指针。通过w_TpAllocAlpcCompletion函数分配 ALPC 完成例程并返回 PFULL_TP_ALPC 类型的指针。通过 TpAllocJobNotification 分配作业通知等

HandleHijacker.cpp中主要就是有一个HijackProcessHandle函数此函数用于劫持指定类型的进程句柄。
并且通过调用 NtQueryInformationProcess 获取目标进程的句柄信息,然后遍历每个句柄,复制满足条件的对象句柄。还使用了 HijackProcessHandle 函数来劫持工作工厂(TpWorkerFactory)类型的句柄; I/O 完成(IoCompletion)类型的句柄;还有IR 定时器(IRTimer)类型的句柄。


PoolParty.cpp负责将shellcode注入到目标进程;它获取目标进程句柄;劫持特定类型句柄(工作工厂、I/O完成、定时器等)的方法,分配目标进程内存,将Shellcode写入已分配的内存,最后执行Shellcode。、
通过RemoteTpWorkInsertion、RemoteTpWaitInsertion、RemoteTpIoInsertion、RemoteTpAlpcInsertion、RemoteTpJobInsertion、RemoteTpDirectInsertion等这些类继承自PoolParty
都覆盖了HijackHandles方法以劫持适当的句柄,并覆盖了SetupExecution方法以执行特定注入。
还使用了Boost Logging记录有关注入过程各个步骤的信息。


最后到main函数中也就是main.cpp这里也介绍了利用步骤操作了如下过程
(WorkerFactoryStartRoutineOverwrite)覆盖目标进程工厂的启动例程
(RemoteTpWorkInsertion)将TP_WORK工作项插入目标进程的线程池
(RemoteTpWaitInsertion)将TP_WAIT工作项插入目标进程的线程池
(RemoteTpIoInsertion)将TP_IO工作项插入目标进程的线程池
(RemoteTpAlpcInsertion)将TP_ALPC工作项插入目标进程的线程池
(RemoteTpJobInsertion)将TP_JOB工作项插入目标进程的线程池
(RemoteTpDirectInsertion)将TP_DIRECT工作项插入目标进程的线程池
(RemoteTpTimerInsertion)将TP_TIMER工作项插入目标进程的线程池

int main(int argc, char** argv)
{
    InitLogging();

    try 
    {
        const auto CmdArgs = ParseArgs(argc, argv);

        if (CmdArgs.bDebugPrivilege)
        {
            w_RtlAdjustPrivilege(SeDebugPrivilege, TRUE, FALSE);
            BOOST_LOG_TRIVIAL(info) << "Retrieved SeDebugPrivilege successfully";
        }

        const auto Injector = PoolPartyFactory(CmdArgs.VariantId, CmdArgs.TargetPid);
        Injector->Inject();
    }
    catch (const std::exception& ex) 
    {
        BOOST_LOG_TRIVIAL(error) << ex.what();
        return 0;
    }

    return 1;
}

最后调用 InitLogging() 初始化日志。解析命令行参数并检查调试权限创建 PoolParty 派生类的实例,并调用 Inject方法执行注入。最后执行效果如下成功将shellcode注入到explore.exe进程中去完成利用。

思考

上述两种手法一种是通过windows进程的fork机制一种是通过进程的进程池的结构特性来完成的注入;那是否能将这两种手法相互结合;也就是将进程池的目标换成windows进程fork的对象而且还能通过父进程的特殊性对注入的子进程进行有效的规避。
但是上述代码中有一些关键函数的直接调用在有的edr中可能会被挂钩检测到,那么是否可以通过间接调用的方式将其代码进行组合起来比如通过如下的代码进行一些代码的替换从而规避检测;

DWORD SetThreadContextThread(LPVOID param) {
    NtSetContextThread(NULL, NULL);
    return 0;
}

SetUnhandledExceptionFilter(BreakpointHandler);
HANDLE new_thread = CreateThread(NULL, NULL, SetThreadContextThread, NULL, CREATE_SUSPENDED, NULL);
SetSyscallBreakpoints((LPVOID)NtSetContextThread, new_thread);
ResumeThread(new_thread);

甚至还可以将上述部分代码中的api更改为间接调用的方式去进行修补还可以修改寄存器的值等比如设置硬件断点进行bypassEDR
可参考https://malwaretech.com/2023/12/silly-edr-bypasses-and-where-to-find-them.html
通过不同的方式进行结合说不定会有其他更多的玩法。

0 条评论
某人
表情
可输入 255
目录