ProxyAlloc: evading NtAllocateVirtualMemory detection ft. Elastic Defend & Binary Ninja
In this article, we will explore a method for in-process shellcode execution evasion. This method is specifically designed to avoid the detection of NtAllocateVirtualMemory calls from unsigned DLLs.
Preface
Not long ago, one of my standard in-process shellcode execution methods for the Red Team engagements I have worked on looked similar to this:
This method has variations, such as using additional NtProtectVirtualMemory
calls to avoid allocating memory with RWX
protections. Most of them should look familiar to you and usually take the following form:
PAGE_NOACCESS -> PAGE_READWRITE -> PAGE_EXECUTE_READ PAGE_READWRITE -> PAGE_EXECUTE_READ PAGE_READWRITE -> PAGE_EXECUTE
This is a well-known technique, but it is not often detected in a corporate environment where business processes prevail over security considerations (and usually, rightly so).
I was, however, surprised, when I tried to launch my implant in a new lab that has Elastic stack configured, with Elastic Defend as an agent and the most aggressive detection methods turned on.
Detection
Right when the implant was launched, I observed the following:
When I looked into the specifics of that detection, it became obvious that NtAllocateVirtualMemory
/ NtProtectVirtualMemory
calls are being monitored:
After thinking about it for some time and related evasion discussions with @zimnyaa (I suggest checking his blog at tishina.in), an idea came to my mind.
Let us review the call stack when the NtAllocateVirtualMemory
call happens:
Essentially, the call stack relevant to our objective at this point can be translated to the following:
unsigned_binary -> signed_ntdll_ZwAllocateVirtualMemory
What if we could place some signed module in between, to pretend that we are not directly calling NtAllocateVirtualMemory
? It appears that we can.
Discovery
Multiple Microsoft-signed DLLs are present at C:\Windows\System32\*
.
I decided to utilize Binary Ninja with its awesome Python API to scan every signed DLL there for functions that might serve as a wrapper for NtAllocateVirtualMemory
.
The high-level overview of the search algorithm for any DLL is as follows:
Check if
NtAllocateVirtualMemory
is imported by our target.If imported, check all its call sites for two separate special cases below.
Mark the location if
Protect
andRegionSize
arguments can be supplied through the caller function's parameters.Mark the location if
RWX
memory of more than64KB
is allocated.
The script is as follows and could be improved to account for more valid cases:
After running the script, we can observe multiple findings:
For example, both of those functions are essentially wrappers around NtAllocateVirtualMemory
:
Curious readers might ask, if we consider calling them instead of NtAllocateVirtualMemory
, how is this different from calling kernel32.VirtualAlloc
?
Two main differences that are relevant for us:
VirtualAlloc
is monitored by security solutions even more thanNtAllocateVirtualMemory
.It is an exported API function, while both functions above are internal for
verifier.dll
.
Other than that, yes, it is very similar to VirtualAlloc
, which itself is a wrapper around NtAllocateVirtualMemory
:
The technique itself: ProxyAlloc
I decided to call this method ProxyAlloc, as we are proxying our actual call to NtAllocateVirtualMemory
through any Microsoft-signed DLL that has an internal wrapper around it.
The code for this technique is as follows (example with verifier.AVrfpNtAllocateVirtualMemory
):
It also could be improved, for example, by using pattern scanning instead of plain offsets.
The call stack observed after using this method is:
Which roughly equals to the following scheme, as expected:
unsigned_binary -> signed_dll_offset -> signed_ntdll_ZwAllocateVirtualMemory
Final Test
After testing this with an actual loader and modified Havoc (Demon agent shellcode) as our C2 of choice, Elastic Defend did not generate any alerts.
Last updated