Writing Custom Shellcode in Assembly for Windows x86 (Finding Kernel32.dll)

February 10, 2025

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 modules
  • GetModuleHandleA - For retrieving base addresses of loaded DLLs
  • GetProcAddress - 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

  1. Dump the TEB structure and find on +0x030 a pointer to the PEB structure
dt nt!_TEB @$teb

image1.png

  1. Use _PEB value next to it
dt nt!_PEB 0x00bdf000 
  1. We want to get the LDR

    image2.png

dt _PEB_LDR_DATA 0x77311c60  

image3.png

dt _LIST_ENTRY (0x77311c60 + 0x1c)

image4.png

dt _LDR_DATA_TABLE_ENTRY ( 0x00f41cd8 - 0x10)

We do - 0x10 to get the beginning

image5.png

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.

image6.png