Windows 32-bit Custom Shellcode Development Part 1
1. Introduction
Windows 32-bit shellcode development is a process of writing executable code typically in 32-bit Windows that will be injected into the memory of a running process and will perform a specific operation i.e spawn reverse shell or calculator. In this series, I will write about how to write a custom shellcode from scratch covering basics to advanced.
2. Purpose
Writing custom shellcode is a critical skill in security research, penetration testing, and exploit development. While pre-made shellcode (like msfvenom payloads) exists, there are compelling reasons to craft custom shellcode. Modern systems have multiple layers of defenses to prevent exploitation using standard payloads. Custom shellcode helps evade these defenses such as:-
- Antivirus (AV) and Endpoint Detection and Response (EDR):
- Pre-built shellcode is often signature-based and easily detected.
- Custom shellcode can be obfuscated or encoded to bypass such detection.
- Sometimes, scenario of security research or exploit development requires minimal length of shellcode.
3. What is a shellcode?
Shellcode is nothing but a hex representation of machine instructions, each byte separated by \x. You can find the shellcode instructions using msf-nasm_shell command as well in Linux. For example shellcode of xor eax, eax will \x31\xC0 .

Therefore, you see shellcode, generated by msfvenom is normally in hex format.
4. Background Concept
If you understand about Server Side Template Injection (SSTI) vulnerability, you would have an idea that how SSTI payload works, We first load the relevant class in server’s memory and then execute function of that loaded class and pass relevant argument to it to achieve our goal as shown in below screenshot, where final payload is able to read the /etc/passwd file content:

From above image, it is revealed that starting from ‘str’ class we went deep and kept loading the classes until ‘_io.FileIO’ class which has ability to read file, whose path is provided as argument and then read function to retrieve the file content.
Similar concept applies to Windows shellcode development, we need to load the relevant Dynamic Link Library (dll) which has defined functions and by using those functions we find other functions and execute them to achieve our goal i.e trigger reverse shell or execute some other command.
5. Setup Environment
- VirtualBox or VMWare
- Windows 10 32-bit in virtual environment
- Install Visual Studio Build Tools (C/C++), then microsoft command line compiler cl.exe will be installed
- Install WinDbg 32-bit version
6. Compile base program
After setting up development environment, we will write shellcode in assembly embeded inside a c program. So, open notepad and create a new file with .c extension having following code and save it.
#include <windows.h>
int main()
{
_asm
{
xor ecx, ecx //zero ecx
int 3 //breakpoint
}
return 0;
}
The __asm keyword invokes the inline assembler and can appear wherever a C or C++ statement is legal. It can’t appear by itself. It must be followed by an assembly instruction, a group of instructions enclosed in braces, or, at minimum, an empty pair of braces.
- Open the “Developer command prompt for Visual Studio” with Administrator rights.
- Compile the
cprogram with following command
cl.exe shellcode.c /Zi
-
It will compile the program with debugging symbols and a
.pdbfile will also be generated along with.exe. -
Now run windbg and click on File -> Open Executable -> Select
.exefile generated after compilation. -
Windbg will automatically stop by hitting breakpoint before executing the loaded executable. Just enter
g(go) command to run the application and it will again stop at our inserted breakpointint 3as shown in figure below:

- Windbg will also show the corresponding instruction in another section of actual
ccode.
7. Process of writing custom shellcode
- Find
kernelbase.dllbase addressAfter having kernelbase.dll base address, we will be able to enumerate in its memory space to find for exported functions.
- Find Export Table
Export table contains information like Address of function etc about all functions exported by dll.
- Find
GetProcAddressfunction exported by kernelbase.dll
As microsoft says, GetProcAddress retrieves the address of an exported function (also known as a procedure) or variable from the specified dynamic-link library (DLL).
- Find
LoadLibraryAfunction usingGetProcAddressfunction
So that we can load further dll i.e user32.dll to load functions of our interest which are not included in kernelbase.dll
-
Load
user32.dllin memory usingLoadLibraryA -
Locate
7.1 Find kernelbase.dll base address
In Windows, certain DLL like ntdll.dll, kernel32.dll, kernelbase.dll etc. are almost always loaded into the address space of a process due to their essential role in system operations and process initialization.
- We need to check the elements of Thread Environment Block (TEB) structure as microsoft says:
The Thread Environment Block (TEB) structure describes the state of a thread. It stores information about a single thread within a process. Each thread in a process has its own TEB, and this structure contains data that the thread needs to perform its tasks. So, our intention is the find the Process Environment Block (PEB) structure inside TEB. We can do this by using following command in windbg:
0:000> dt _TEB @$teb
shellcode!_TEB
+0x000 NtTib : _NT_TIB
+0x01c EnvironmentPointer : (null)
+0x020 ClientId : _CLIENT_ID
+0x028 ActiveRpcHandle : (null)
+0x02c ThreadLocalStoragePointer : 0x00582e90 Void
+0x030 ProcessEnvironmentBlock : 0x00273000 _PEB
+0x034 LastErrorValue : 0
+0x038 CountOfOwnedCriticalSections : 0
+0x03c CsrClientThread : (null)
+0x040 Win32ThreadInfo : (null)
Reference: https://learn.microsoft.com/en-us/windows/win32/api/winternl/ns-winternl-teb
At offset of 0x30 we can see Process Environment Block (_PEB) structure is located at address 0x00273000. So, we can write following assembly code to reach _PEB
int 3 // insert breakpoint
xor ecx, ecx // zero ecx
mov eax, fs:[ecx+0x30] // eax = PEB
7.1.1 Process Environment Block (PEB)
The _PEB (Process Environment Block) is a critical data structure in Windows operating systems. It provides a wealth of information about the process in which it resides, including its modules, environment variables, heap, and other runtime data. It is typically used internally by the operating system but is also of interest to developers, reverse engineers, and exploit developers.
typedef struct _PEB {
BYTE Reserved1[2];
BYTE BeingDebugged;
BYTE Reserved2[1];
PVOID Reserved3[2];
PPEB_LDR_DATA Ldr;
PRTL_USER_PROCESS_PARAMETERS ProcessParameters;
PVOID Reserved4[3];
PVOID AtlThunkSListPtr;
PVOID Reserved5;
ULONG Reserved6;
PVOID Reserved7;
ULONG Reserved8;
ULONG AtlThunkSListPtr32;
PVOID Reserved9[45];
BYTE Reserved10[96];
PPS_POST_PROCESS_INIT_ROUTINE PostProcessInitRoutine;
BYTE Reserved11[128];
PVOID Reserved12[1];
ULONG SessionId;
} PEB, *PPEB;
We are primarily interested in Ldr (A pointer to a PEB_LDR_DATA structure that contains information about the loaded modules for the process) element.
Reserved1[2] means an array of 2 elements and the type of this array is BYTE, so 2 * 1 = 2 BYTES means first member will occupy 2 BYTES and PVOID represents pointer which is of 4 BYTES and Reserved[2] means 2 elements of type pointer means 4 * 2 = 8 BYTES, So as per this calculation we can reach at
Ldrmember at an offset of 0xC
BYTE Reserved1[2] = 1 * 2 = 2
BYTE BeingDebugged = 1 * 1 = 1
BYTE Reserved2[1] = 1 * 1 = 1
PVOID Reserved3[2] = 4 * 2 = 8
Total = 2 + 1 + 1 + 8 = 12 BYTES (0xC in hex)
Let’s enumerate _PEB using command dt _PEB <address_of_PEB>
0:000> dt _PEB 0x00273000
shellcode!_PEB
+0x000 InheritedAddressSpace : 0 ''
+0x001 ReadImageFileExecOptions : 0 ''
+0x002 BeingDebugged : 0x1 ''
+0x003 BitField : 0x4 ''
+0x003 ImageUsesLargePages : 0y0
+0x003 IsProtectedProcess : 0y0
+0x003 IsImageDynamicallyRelocated : 0y1
+0x003 SkipPatchingUser32Forwarders : 0y0
+0x003 IsPackagedProcess : 0y0
+0x003 IsAppContainer : 0y0
+0x003 IsProtectedProcessLight : 0y0
+0x003 IsLongPathAwareProcess : 0y0
+0x004 Mutant : 0xffffffff Void
+0x008 ImageBaseAddress : 0x00010000 Void
+0x00c Ldr : 0x773e1c60 _PEB_LDR_DATA
+0x010 ProcessParameters : 0x00581c60 _RTL_USER_PROCESS_PARAMETERS
Here Ldr at an offset of 0xcis a pointer, pointing to _PEB_LDR_DATA structure.
So, we can write following assembly code to reach _PEB_LDR_DATA
int 3 // insert breakpoint
xor ecx, ecx // zero ecx
mov eax, fs:[ecx+0x30] // eax = PEB
mov eax, [eax+0x0c] // eax = _PEB_LDR_DATA
Did you notice difference between _PEB layout and view in windbg? I leave it on you to research its answer.
7.1.2 _PEB_LDR_DATA structure
The _PEB_LDR_DATA structure is part of the Process Environment Block (PEB), which provides critical information about the process and its modules in a Windows system. This structure specifically manages information about the loaded modules (DLLs) in a process.
Here is a typical layout of the _PEB_LDR_DATA structure:
typedef struct _PEB_LDR_DATA {
ULONG Length; // Size of the structure
BOOLEAN Initialized; // Flag indicating if the structure is initialized
HANDLE SsHandle; // Reserved (not used)
LIST_ENTRY InLoadOrderModuleList; // List of modules in load order
LIST_ENTRY InMemoryOrderModuleList; // List of modules in memory order
LIST_ENTRY InInitializationOrderModuleList; // List of modules in initialization order
} PEB_LDR_DATA, *PPEB_LDR_DATA;
Let’s validate it using windbg dt command as below:
0:000> ? poi(@$peb+0xc) Evaluate expression: 2000559200 = 773e1c60 0:000> dt _PEB_LDR_DATA 0x773e1c60 shellcode!_PEB_LDR_DATA +0x000 Length : 0x30 +0x004 Initialized : 0x1 '' +0x008 SsHandle : (null) +0x00c InLoadOrderModuleList : _LIST_ENTRY [ 0x5824e0 - 0x582c90 ] +0x014 InMemoryOrderModuleList : _LIST_ENTRY [ 0x5824e8 - 0x582c98 ] +0x01c InInitializationOrderModuleList : _LIST_ENTRY [ 0x5823e8 - 0x5828d0 ] +0x024 EntryInProgress : (null) +0x028 ShutdownInProgress : 0 '' +0x02c ShutdownThreadId : (null)
Members of _PEB_LDR_DATA structure that are of our interest are as follows:-
-
InLoadOrderModuleList: A doubly linked list containing entries for all loaded modules (DLLs) in the order they were loaded. Each entry is of type
_LDR_DATA_TABLE_ENTRY -
InMemoryOrderModuleList: A doubly linked list of modules ordered by their memory address. Useful for operations involving address ranges or base addresses of DLLs.
-
InInitializationOrderModuleList: A doubly linked list of modules in the order they were initialized. Ensures proper initialization sequencing for dependent modules.
We will deep dive into InMemoryOrderModuleList. Let’s display InMemoryOrderModuleList of type _LIST_ENTRY as shown below:
0:000> ? poi(poi(@$peb+0xc)+0x14)
Evaluate expression: 5776616 = 005824e8
0:000> dt _LIST_ENTRY 005824e8
shellcode!_LIST_ENTRY
[ 0x5823e0 - 0x773e1c74 ]
+0x000 Flink : 0x005823e0 _LIST_ENTRY [ 0x5828c8 - 0x5824e8 ]
+0x004 Blink : 0x773e1c74 _LIST_ENTRY [ 0x5824e8 - 0x582c98 ]
LIST_ENTRY structure is a double link list and contains two elements in each node Flink (Forward Link - 4 Bytes) and Blink (Backward Link - 4 Bytes). So each node is of 8 Bytes in size.
Note: Important thing to note here is that _LDR_DATA_TABLE_ENTRY is itself a member of _PEB_LDR_DATA structure at offset 0x8 from InMemoryOrderLinks. When we displayed _LDR_DATA_TABLE_ENTRY from 0x8 in backward it is shown as below:
0:000> dt _LDR_DATA_TABLE_ENTRY 0x5824e8-0x8 ntdll!_LDR_DATA_TABLE_ENTRY +0x000 InLoadOrderLinks : _LIST_ENTRY [ 0x5823d8 - 0x773e1c6c ] +0x008 InMemoryOrderLinks : _LIST_ENTRY [ 0x5823e0 - 0x773e1c74 ] +0x010 InInitializationOrderLinks : _LIST_ENTRY [ 0x0 - 0x0 ] +0x018 DllBase : 0x00010000 Void +0x01c EntryPoint : 0x00011140 Void +0x020 SizeOfImage : 0x7b000 +0x024 FullDllName : _UNICODE_STRING "C:\Users\unknown\Desktop\custom-shellcode\shellcode.exe" +0x02c BaseDllName : _UNICODE_STRING "shellcode.exe"
Excellent! We are able to see the dll base address at an offset of 0x18 in DllBase property and dll name at an offset of 0x2c in BaseDllName property in first node of double linked list. In the same way, we can move forward to check the dll info in second node as shown below:
0:000> dt _LDR_DATA_TABLE_ENTRY 0x4a23e0-0x8 ntdll!_LDR_DATA_TABLE_ENTRY +0x000 InLoadOrderLinks : _LIST_ENTRY [ 0x4a28c0 - 0x4a24e0 ] +0x008 InMemoryOrderLinks : _LIST_ENTRY [ 0x4a28c8 - 0x4a24e8 ] +0x010 InInitializationOrderLinks : _LIST_ENTRY [ 0x4a2ca0 - 0x77321c7c ] +0x018 DllBase : 0x77200000 Void +0x01c EntryPoint : (null) +0x020 SizeOfImage : 0x19f000 +0x024 FullDllName : _UNICODE_STRING "C:\Windows\SYSTEM32\ntdll.dll" +0x02c BaseDllName : _UNICODE_STRING "ntdll.dll"
Let’s check the third node.
0:000> dt _LDR_DATA_TABLE_ENTRY 0x4a28c8-0x8 ntdll!_LDR_DATA_TABLE_ENTRY +0x000 InLoadOrderLinks : _LIST_ENTRY [ 0x4a2c90 - 0x4a23d8 ] +0x008 InMemoryOrderLinks : _LIST_ENTRY [ 0x4a2c98 - 0x4a23e0 ] +0x010 InInitializationOrderLinks : _LIST_ENTRY [ 0x4a2b00 - 0x4a2ca0 ] +0x018 DllBase : 0x76070000 Void +0x01c EntryPoint : 0x7608d890 Void +0x020 SizeOfImage : 0x9d000 +0x024 FullDllName : _UNICODE_STRING "C:\Windows\System32\KERNEL32.DLL" +0x02c BaseDllName : _UNICODE_STRING "KERNEL32.DLL"
Let’s check the fourth node.
0:000> dt _LDR_DATA_TABLE_ENTRY 0x4a2c98-0x8 ntdll!_LDR_DATA_TABLE_ENTRY +0x000 InLoadOrderLinks : _LIST_ENTRY [ 0x4a2af0 - 0x4a28c0 ] +0x008 InMemoryOrderLinks : _LIST_ENTRY [ 0x4a2af8 - 0x4a28c8 ] +0x010 InInitializationOrderLinks : _LIST_ENTRY [ 0x4a28d0 - 0x4a23e8 ] +0x018 DllBase : 0x75130000 Void +0x01c EntryPoint : 0x7522c410 Void +0x020 SizeOfImage : 0x23e000 +0x024 FullDllName : _UNICODE_STRING "C:\Windows\System32\KERNELBASE.dll" +0x02c BaseDllName : _UNICODE_STRING "KERNELBASE.dll"
So, we can write following assembly code to reach the node having kernelbase.dll info.
int 3 // insert breakpoint
xor ecx, ecx // zero ecx
mov eax, fs:[ecx+0x30] // eax = PEB
mov eax, [eax+0x0c] // eax = _PEB_LDR_DATA
mov eax, [eax+0x14] // eax = shellcode.exe or first node
mov eax, [eax] // eax = ntdll.dll
mov eax, [eax] // eax = kernel32.dll
mov eax, [eax] // eax = kernelbase.dll
mov ebx, [eax+0x10] // ebx = DllBase
Add the code inside _asm block, recompile and load the binary in windbg and by using t command analyse the value in ebx register at the end as shown below:
