Probably the oldest code execution technique out there and pretty much straightforward. in this technique, the shellcode is stored inside the loader itself. we will combine this technique with some other evasion tricks in future posts.
this technique can be implemented with multiple APIs, for the sake of simplicity i will combine all these different implementations into a single C file in one VS project.
Why do we need various APIs to write the same code and do the same thing?
Because different security products tend to hook different API functions, knowing different ways to skin the cat will come in handy when facing different security solutions.
Execution Flow:
Allocate memory (heap or stack) for shellcode storage (read/write permissions)
Copy the shellcode into allocated memory
Change memory permission to RX (read/execute)
Create a local thread and pass the shellcode memory address as a parameter
We can do this in 3 steps by setting the initial memory permissions to RWX (read/write/execute) so that we don't have to flip the permissions later, but since RWX memory permission is not very common in benign applications, AV/EDR products will quickly flag our loader.
1. Memory Allocation
Windows offers a few APIs for memory allocation in both heap and stack. in classic shellcode execution, we can either use both Win32 API and CRT functions or rely completely in Win32 API.
VirtualAlloc
This is the first and most used API function for memory allocation, it is also the most hooked API by EDRs :)
VirtualAlloc takes the following parameters:
LPVOID VirtualAlloc(
[in, optional] LPVOID lpAddress, // starting address for memory allocation (set to 0, we don't care about this for now)
[in] SIZE_T dwSize, // size of memory to allocate (set to size of shellcode)
[in] DWORD flAllocationType, // type of allocation (usually set to MEM_COMMIT)
[in] DWORD flProtect // memory permissions (better set to PAGE_READWRITE or 0x04 and changed later)
);
To see other memory permissions offered by VirtualAlloc, check out the MSDN documentation:
GlobalAlloc is an older API for memory allocation, often used in Windows 16-bit applications. It allocates memory in the global heap.
GlobalAlloc takes the following parameters:
DECLSPEC_ALLOCATOR HGLOBAL GlobalAlloc(
[in] UINT uFlags, // memory allocation attributes (default is GMEM_MOVEABLE, returns a pointer)
[in] SIZE_T dwBytes // number of bytes to allocate (set to size of shellcode)
);
LocalAlloc is similar to GlobalAlloc but it allocates memory from the local heap. This function was often used in older Windows programs.
LocalAlloc takes the following parameters:
DECLSPEC_ALLOCATOR HLOCAL LocalAlloc(
[in] UINT uFlags, // allocation attribute (set to LMEM_ZEROINIT for initializes memory contents to zero.)
[in] SIZE_T uBytes // number of bytes to allocate (set to size of shellcode)
);
This API allocates memory from a heap (local or global). It can be useful for scenarios where you want to use a managed memory space or in scenarios where the shellcode is too large to store in stack. its also suitable for stagers that want to retrieve the shellcode from a remote location, for example a server over the Internet.
HeapAlloc takes the following parameters:
DECLSPEC_ALLOCATOR LPVOID HeapAlloc(
[in] HANDLE hHeap, // handle to local or global process heap : HANDLE hHeap = GetProcessHeap();
[in] DWORD dwFlags, // allocation option (set to HEAP_ZERO_MEMORY to zero out)
[in] SIZE_T dwBytes // number of bytes to allocate (set to size of shellcode)
);
After copying the shellcode, we have to change the memory permissions from RW to RX and make that region executable .
VirtualProtect
It changes the memory permission of a region in current process context.
VirtualProtect takes the following parameters:
BOOL VirtualProtect(
[in] LPVOID lpAddress, // start address of target memory region (set to the pointer to allocated memory )
[in] SIZE_T dwSize, // size of target memory region (size of shellcode)
[in] DWORD flNewProtect, // new memory permissions (set to AGE_EXECUTE_READ)
[out] PDWORD lpflOldProtect // pointer to old memory protection (a DWORD for holding the old permissions)
);
Last Step is to create a new thread and pass the executable memory region address to it.
CreateThread
Creates a thread in local process.
CreateThread takes the following parameters:
HANDLE CreateThread(
[in, optional] LPSECURITY_ATTRIBUTES lpThreadAttributes, // handle inheritence by child (set to 0 for now)
[in] SIZE_T dwStackSize, // initial stack size (set to 0 by deafult)
[in] LPTHREAD_START_ROUTINE lpStartAddress, // pointer to function to be executed (we are using the memory address pointer here)
[in, optional] __drv_aliasesMem LPVOID lpParameter, // pointer to function parameters to pass (we dont have any so set to 0)
[in] DWORD dwCreationFlags, // thread creation flag (set to 0 so that it the thread runs immediately after creation.)
[out, optional] LPDWORD lpThreadId // pointer that holds a handle to the thread
);