Let's suppose we’re conducting an offensive security exercise and need to bypass a security appliance--and need to call VirtualAlloc. We could do it in a less than optimal way, like this:
#include <windows.h>
int main() {
void* mem = VirtualAlloc(NULL, 0x1000, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
return 0;
}
Why is this suboptimal? If we were to load the compiled binary into a tool like PE-bear, or if a security appliance or EDR were monitoring Import Address Table (IAT) entries, the VirtualAlloc function call would be trivial to detect, since the IAT contains pointers to all statically imported functions, making them easy to inspect or hook.
Various security solutions and EDRs occasionally inspect these IAT functions by first scanning the binary and seeing what functions are exported. It then hooks those functions. But we can conceal this information (at least from a static point of view) and avoid such hooks.
A better idiom is to perform dynamic resolution of functions. I used this in a separate example on another blog post, "Bypassing Windows Defender." In that example, I also called the underlying NT function, NtAllocateVirtualMemory, rather than VirtualAlloc.
Instead of doing a raw VirtualAlloc call that tells the compiler to import the VirtualAlloc function during compile time, we can use a different programming idiom that allows us to dynamically resolve functions. This avoids detection based on the IAT, bypassing IAT hooks used by security appliances and EDRs. It also makes static analysis harder.
The programming idiom below allows us to dynamically resolve VirtualAlloc within kernel32.dll, rather than being resolved by the compiler and hardcoded into the IAT:
HMODULE hKernel32 = GetModuleHandleA("kernel32.dll");
FARPROC pVirtualAlloc = GetProcAddress(hKernel32, "VirtualAlloc");
In this way, we can use function pointers to dynamically resolve any functions we need from kernel32.dll:
#include <windows.h>
#include <stdio.h>
int main() {
// Get handle to kernel32.dll
HMODULE hKernel32 = GetModuleHandleA("kernel32.dll");
if (!hKernel32) {
printf("Failed to get handle to kernel32.dll\n");
return 1;
}
// Get pointer to VirtualAlloc
FARPROC pVirtualAlloc = GetProcAddress(hKernel32, "VirtualAlloc");
if (!pVirtualAlloc) {
printf("Failed to resolve VirtualAlloc\n");
return 1;
}
// Cast to correct function signature
LPVOID (*MyVirtualAlloc)(LPVOID, SIZE_T, DWORD, DWORD) =
(LPVOID (*)(LPVOID, SIZE_T, DWORD, DWORD))pVirtualAlloc;
// Call VirtualAlloc via function pointer
LPVOID mem = MyVirtualAlloc(NULL, 0x1000, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (!mem) {
printf("VirtualAlloc failed\n");
return 1;
}
printf("Memory allocated at: %p\n", mem);
return 0;
}
One could go even further by resolving functions without relying on GetProcAddress, such as by walking the Process Environment Block (PEB). That is, by manually parsing Windows structures to locate DLL and function addresses without calling standard APIs.
Static Import; No Bypass
Here, I'm using the C program from above. We can plainly see that the IAT includes a call to VirtualAlloc.
Dynamically Resolved IAT; IAT Hooking Bypassed!
This binary is from the C program I wrote for the "Bypassing Windows Defender" post, which uses dynamic IAT resolution.
Calls to VirtualAlloc--and various other dynamically resolved calls--don't show up during static analysis. Only the ones that were statically imported at compile time do.
Comments
Post a Comment