Introduction
When developing custom shellcode for Windows x86, locating the Kernel32.dll base address is the first step. This module provides access to essential Windows APIs that we'll need, including:
LoadLibraryA
- For loading additional DLL modulesGetModuleHandleA
- For retrieving base addresses of loaded DLLsGetProcAddress
- For resolving function addresses
Kernel32.dll
Kernel32 is a DLL (dynamic link library) that contains various functions and resources required for the proper functioning of the Windows kernel. This also holds various functions that we need for our custom shellcode.
Understanding the Process Environment Block (PEB)
To locate Kernel32.dll, we'll leverage the Process Environment Block (PEB) structure. The PEB contains crucial information about the current process, including loaded modules. We can access it through the Thread Environment Block (TEB), which is referenced via the FS
segment register.
FS
register contains the pointer to the TEB
→ Offset 0x30 we find the data structure of TEB
PEB Method
- Dump the
TEB
structure and find on +0x030
a pointer to thePEB
structure
dt nt!_TEB @$teb
- Use _PEB value next to it
dt nt!_PEB 0x00bdf000
-
We want to get the LDR
dt _PEB_LDR_DATA 0x77311c60
dt _LIST_ENTRY (0x77311c60 + 0x1c)
dt _LDR_DATA_TABLE_ENTRY ( 0x00f41cd8 - 0x10)
We do - 0x10 to get the beginning
Setting it up in Assembly
Step 1. Function Prologue and Stack Setup
int3 ; Debug breakpoint. (Remove for production)
mov ebp, esp ; Save current stack pointer (simulate function call prologue)
sub esp, 60h ; Reserve 0x60 bytes on the stack to protect local data
int3
is a breakpoint used during debugging.mov ebp, esp
establishes a new stack frame, much like entering a function.sub esp, 60h
allocates local space to ensure that the stack won’t be clobbered by later operations.
Step 2. Accessing the PEB
xor ecx, ecx ; Clear ECX, setting it to 0.
mov esi, fs:[ecx+30h] ; Load the address of the PEB into ESI.
xor ecx, ecx
clears ECX.mov esi, fs:[0x30]
retrieves the pointer to the Process Environment Block (PEB) from the FS segment. In Windows, the PEB is located at FS:[0x30].
Step 3. Navigating the Loader Data
mov esi, [esi+0Ch] ; ESI now points to PEB->Ldr (loader data)
mov esi, [esi+1Ch] ; ESI now points to InInitializationOrderModuleList
mov esi, [esi+0Ch]
follows the PEB pointer to the loader data structure (_PEB_LDR_DATA
), which contains information about all loaded modules.mov esi, [esi+1Ch]
steps into the InInitializationOrder list—a linked list representing the order in which modules were initialized.
Step 4. Walking the Module List
The code now enters a loop that examines each module in the InInitializationOrderModuleList
:
next_module:
mov ebx, [esi+8h] ; Load the module's base address into EBX.
mov edi, [esi+20h] ; Load the pointer to the module’s Unicode name into EDI.
mov esi, [esi] ; Advance to the next module in the list.
cmp [edi+12*2], cx ; Compare the WORD at offset 24 (12 characters * 2 bytes each) to 0.
jne next_module ; If not equal, this module isn’t kernel32.dll; try the next one.
ret
Let’s break this down:
-
mov ebx, [esi+8h]
:Retrieves the module’s base address from the current list entry. This is important because once the correct module is found, you’ll need its base address to resolve functions.
-
mov edi, [esi+20h]
:Loads a pointer to the module’s name, stored as a Unicode string (each character is 2 bytes).
-
mov esi, [esi]
:Advances the pointer to the next module entry by following the linked list’s “flink” (forward link).
-
cmp [edi+12*2], cx
:Here’s the clever trick. The code checks the 13th Unicode character of the module name (offset
12*2
equals 24 bytes).- Why? Because the string "kernel32.dll" is exactly 12 characters long.
- If the 13th character (i.e., at position 24) is a NULL terminator, it means the module name is exactly 12 characters long—our heuristic for identifying kernel32.dll.
-
jne next_module
:If the comparison fails (the 13th character isn’t NULL), the module is not kernel32.dll, and the code jumps back to process the next module.
-
ret
:When the comparison succeeds, the function returns. At this point, EBX holds the base address of kernel32.dll.
Full Assembly code
;-----------------------------------------------------------
; Shellcode to Locate kernel32.dll Base Address
;
; This code accesses the Process Environment Block (PEB) via
; the FS segment, navigates the PEB's Loader Data, and iterates
; through the InInitializationOrderModuleList. It checks each
; module's Unicode name to see if the 13th character (at offset
; 24 bytes, since each Unicode char is 2 bytes) is a NULL.
; When that condition is met, the module is assumed to be
; kernel32.dll, and its base address (already loaded into EBX)
; is available for further use.
;
; Note: The "int3" instruction is a debug breakpoint and should be
; removed for production use.
;-----------------------------------------------------------
global _start
section .text
_start:
int3 ; Debug breakpoint. Remove for production.
mov ebp, esp ; Establish a stack frame.
sub esp, 0x60 ; Reserve 0x60 bytes on the stack.
;-------------------------
; find_kernel32:
;-------------------------
find_kernel32:
xor ecx, ecx ; Clear ECX (ECX = 0).
mov esi, fs:[ecx+0x30] ; Load the address of the PEB into ESI.
; (PEB is pointed to by fs:[0x30])
mov esi, [esi+0x0C] ; ESI = PEB->Ldr (pointer to loader data).
mov esi, [esi+0x1C] ; ESI = PEB->Ldr.InInitializationOrderModuleList.
;-------------------------
; next_module:
;-------------------------
next_module:
mov ebx, [esi+0x8] ; EBX = Current module's base address.
mov edi, [esi+0x20] ; EDI = Pointer to the module's Unicode name.
mov esi, [esi] ; Move to the next entry in the module list.
; Compare the WORD at offset 24 (i.e. the 13th Unicode character)
; with 0. (12 characters * 2 bytes per character = 24 bytes)
cmp word [edi+24], 0
jne next_module ; Not a match? Check the next module.
ret ; kernel32.dll found; its base address is now in EBX.