Getting the Base Address of kernel32.dll

Finding the base address of kernel32.dll in on Windows for shellcode, exploit development or otherwise can be a challenging due to various constraints.

But, there are three main ways to accomplish this, which are discussed in the following sub-sections after an overview of how system DLLs work.

Kernel32.dll and System DLLs

Firstly, we should note that kernel32.dll, along with many other system DLLs, is not loaded each time for each process. Instead, there’s a single instance of kernel32.dll loaded into memory by the operating system, which is shared across all processes.

While the code sections of kernel32.dll (and other system DLLs) are mapped into the virtual address space of each process; these sections are backed by the same physical memory pages. This means that while each process has its own virtual address mapping for kernel32.dll, the actual code resides in a single set of physical pages shared among all processes.

Also while the code sections are shared, each process has its own separate data section for each DLL. This is necessary because different processes might have different data values even though they execute the same code.

And, as a security measure modern Windows systems use ASLR, which randomizes the base addresses of loaded modules (including kernel32.dll) across different processes. So,the virtual address of kernel32.dll may be different in each process, but it still maps back to the same physical memory.

Note, each process has an Import Address Table which contains the addresses of the functions it uses from DLLs like kernel32.dll. The IAT is filled with the correct addresses for these functions relative to the base address at which the DLL is mapped in the process’s address space. And, how functions can be accessed from a DLL.

Using Known Offsets

In some cases, particularly in older or unpatched systems, offsets from certain known addresses to the base of kernel32.dll might be predictable.

Though this is much less reliable on modern systems with security enhancements

Calculating the Address via the Windows API

In some scenarios calculating directly the base address of kernel32.dll via the Windows API might be appropriate.

Once it is known it can be used to work out the base address of functions, so they can be called via their base address in assembly (shellcode).

A shellcode example, where the known kernel base address is used to call known functions from the DLL, WinExec and ExitProcess, in assembly is shown below.

; KERNEL32 address in memory: 0x75910000
; ExitProcess address in memory is: 0x75938630
; WinExec address in memory is: 0x7596F560

section .data

section .bss

section .text
  global _start   ; must be declared for linker

_start:
  xor  ecx, ecx         ; zero out ecx
  push ecx              ; string terminator 0x00 for "calc.exe" string
  push 0x6578652e       ; exe. : 6578652e
  push 0x636c6163       ; clac : 636c6163

  mov  eax, esp         ; save pointer to "calc.exe" string in ebx

  ; UINT WinExec([in] LPCSTR lpCmdLine, [in] UINT   uCmdShow);
  inc  ecx              ; uCmdShow = 1
  push ecx              ; uCmdShow *ptr to stack in 2nd position - LIFO
  push eax              ; lpcmdLine *ptr to stack in 1st position
  mov  ebx, 0x7596f560  ; call WinExec() function addr in kernel32.dll
  call ebx

  ; void ExitProcess([in] UINT uExitCode);
  xor  eax, eax         ; zero out eax
  push eax              ; push NULL
  mov  eax, 0x75938630  ; call ExitProcess function addr in kernel32.dll
  jmp  eax              ; execute the ExitProcess function

While the corresponding C program to find these address is shown below.

#include <windows.h>
#include <stdio.h>

int main() {
  unsigned long Kernel32Addr;      // kernel32.dll address
  unsigned long ExitProcessAddr;   // ExitProcess address
  unsigned long WinExecAddr;       // WinExec address

  Kernel32Addr = GetModuleHandle("kernel32.dll");
  printf("KERNEL32 address in memory: 0x%08p\n", Kernel32Addr);

  ExitProcessAddr = GetProcAddress(Kernel32Addr, "ExitProcess");
  printf("ExitProcess address in memory is: 0x%08p\n", ExitProcessAddr);

  WinExecAddr = GetProcAddress(Kernel32Addr, "WinExec");
  printf("WinExec address in memory is: 0x%08p\n", WinExecAddr);

  getchar();
  return 0;
}

However, this is generally unreliable due to ASLR (Address Space Layout Randomization) and differences in Windows versions.

Walking the In-Memory Module List

The alternative is walking the linked list of loaded modules maintained by the system in each process.

This can be achieved by accessing the Thread Environment Block (TEB), which can be reached via the FS segment register in x86 architectures.

The TEB has a pointer to the Process Environment Block (PEB). In turn, this has a structure called Ldr which contains a linked list of LDR_DATA_TABLE_ENTRY structures, each representing a loaded module.

By iterating through this list, we can find the entry corresponding to kernel32.dll and retrieve its base address.

To do this in assembly we need to follow these steps.

  1. Access the TEB – the FS segment register points to the TEB. Specifically, FS:[0x30] will give us the address of the PEB.
  2. Access the PEB – from the TEB, we can access the PEB. The PEB structure contains a field called Ldr (offset 0x0C in the PEB structure), which is a pointer to the PEB_LDR_DATA structure.
  3. Walk the Module List – the PEB_LDR_DATA structure contains the InMemoryOrderModuleList, which is a linked list of LDR_DATA_TABLE_ENTRY structures representing each loaded module.
  4. Identify kernel32.dll – as we iterate through the InMemoryOrderModuleList, we need to compare the name of each module with “kernel32.dll” to identify the correct module or print them out for analysis (below).
  5. Get the Base Address – once we’ve identified the LDR_DATA_TABLE_ENTRY for kernel32.dll, its base address is available in that structure.

A C program to identify the first five entries is show below (a loop can also be used and commented out).

#include <stdio.h>
#include <windows.h>

typedef struct _UNICODE_STRING {
    USHORT Length;
    USHORT MaximumLength;
    PWSTR Buffer;
} UNICODE_STRING, *PUNICODE_STRING;

typedef struct _LDR_DATA_TABLE_ENTRY {
    LIST_ENTRY InLoadOrderLinks;
    LIST_ENTRY InMemoryOrderLinks;
    LIST_ENTRY InInitializationOrderLinks;
    PVOID DllBase;
    PVOID EntryPoint;
    ULONG SizeOfImage;
    UNICODE_STRING FullDllName;
    UNICODE_STRING BaseDllName;
    // ... other members ...
} LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;


void printModuleInfo(LDR_DATA_TABLE_ENTRY* entry) {
    if (entry != NULL) {
        UNICODE_STRING moduleName = entry->BaseDllName;
        wprintf(L"Module Name: %.*s, Base Address: %p\n", moduleName.Length / 2, moduleName.Buffer, entry->DllBase);
    }
}

int main() {
    void* pebAddress = NULL;
    void* ldrAddress = NULL;
    LIST_ENTRY* inMemoryOrderModuleList = NULL;
    LDR_DATA_TABLE_ENTRY* ldrDataTableEntry = NULL;

    // Get the address of the PEB
    asm ("movl %%fs:0x30, %0" : "=r" (pebAddress));

    // Get the address of the PEB->Ldr
    asm ("movl %1, %%eax;"
         "movl 0xC(%%eax), %%eax;"
         "movl %%eax, %0;"
         : "=r" (ldrAddress)
         : "r" (pebAddress)
         : "%eax"
    );

    // Get the InMemoryOrderModuleList
    asm ("movl %1, %%eax;"
         "movl 0x14(%%eax), %%eax;"  // Offset for InMemoryOrderModuleList
       "movl %%eax, %0;"
         : "=r" (inMemoryOrderModuleList)
         : "r" (ldrAddress)
         : "%eax"
    );

    // Print the first entry
    ldrDataTableEntry = CONTAINING_RECORD(inMemoryOrderModuleList, LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks);
    printf("First Entry in InMemoryOrderModuleList: %p\n", ldrDataTableEntry);
    printModuleInfo(ldrDataTableEntry);

    // Print the second entry
    ldrDataTableEntry = CONTAINING_RECORD(inMemoryOrderModuleList->Flink, LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks);
    printf("Second Entry in InMemoryOrderModuleList: %p\n", ldrDataTableEntry);
    printModuleInfo(ldrDataTableEntry);

    // Print the third entry
    ldrDataTableEntry = CONTAINING_RECORD(inMemoryOrderModuleList->Flink->Flink, LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks);
    printf("Third Entry in InMemoryOrderModuleList: %p\n", ldrDataTableEntry);
    printModuleInfo(ldrDataTableEntry);

    // Print the fourth entry
    ldrDataTableEntry = CONTAINING_RECORD(inMemoryOrderModuleList->Flink->Flink->Flink, LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks);
    printf("Fourth Entry in InMemoryOrderModuleList: %p\n", ldrDataTableEntry);
    printModuleInfo(ldrDataTableEntry);

    // Print the fifth entry
    ldrDataTableEntry = CONTAINING_RECORD(inMemoryOrderModuleList->Flink->Flink->Flink->Flink, LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks);
    printf("Fifth Entry in InMemoryOrderModuleList: %p\n", ldrDataTableEntry);
    printModuleInfo(ldrDataTableEntry);


    // Iterate through the InMemoryOrderModuleList
    //for (ldrDataTableEntry = CONTAINING_RECORD(inMemoryOrderModuleList->Flink, LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks);
    //     &ldrDataTableEntry->InMemoryOrderLinks != inMemoryOrderModuleList;
    //     ldrDataTableEntry = CONTAINING_RECORD(ldrDataTableEntry->InMemoryOrderLinks.Flink, LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks)) {
    //    printModuleInfo(ldrDataTableEntry);
    //}

    return 0;
}

A less complex example which accesses the second module entry and prints out each of the various hex addresses concerned is shown below.

#include <stdio.h>
#include <windows.h>


typedef struct _UNICODE_STRING {
    USHORT Length;
    USHORT MaximumLength;
    PWSTR Buffer;
} UNICODE_STRING, *PUNICODE_STRING;

typedef struct _LDR_DATA_TABLE_ENTRY {
    LIST_ENTRY InLoadOrderLinks;
    LIST_ENTRY InMemoryOrderLinks;
    LIST_ENTRY InInitializationOrderLinks;
    PVOID DllBase;
    PVOID EntryPoint;
    ULONG SizeOfImage;
    UNICODE_STRING FullDllName;
    UNICODE_STRING BaseDllName;
    ULONG Flags;
    USHORT LoadCount;
    USHORT TlsIndex;
    LIST_ENTRY HashLinks;
    ULONG TimeDateStamp;
    PVOID EntryPointActivationContext;
    PVOID PatchInformation;
    LIST_ENTRY ForwarderLinks;
    LIST_ENTRY ServiceTagLinks;
    LIST_ENTRY StaticLinks;
    PVOID ContextInformation;
    ULONG64 OriginalBase;
    LARGE_INTEGER LoadTime;
} LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;

int main() {
    void* pebAddress = NULL;
    void* ldrAddress = NULL;
    LIST_ENTRY* inMemoryOrderModuleList = NULL;
    LDR_DATA_TABLE_ENTRY* ldrDataTableEntry = NULL;
    UNICODE_STRING* moduleName = NULL;

    // Get the address of the PEB
    asm ("movl %%fs:0x30, %0" : "=r" (pebAddress));

    // Get the address of the PEB->Ldr
    asm ("movl %1, %%eax;"
         "movl 0xC(%%eax), %%eax;"
         "movl %%eax, %0;"
         : "=r" (ldrAddress)
         : "r" (pebAddress)
         : "%eax"
    );

    // Get the first entry in InMemoryOrderModuleList
    asm ("movl %1, %%eax;"
         "movl 0x14(%%eax), %%eax;"  // Offset for InMemoryOrderModuleList
         "movl %%eax, %0;"
         : "=r" (inMemoryOrderModuleList)
         : "r" (ldrAddress)
         : "%eax"
    );

    // Cast the second entry in InMemoryOrderModuleList to LDR_DATA_TABLE_ENTRY
    ldrDataTableEntry = CONTAINING_RECORD(inMemoryOrderModuleList->Flink, LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks);

    // Get the BaseDllName
    moduleName = &(ldrDataTableEntry->BaseDllName);

    printf("PEB Address: %p\n", pebAddress);
    printf("Ldr Address: %p\n", ldrAddress);
    printf("InMemoryOrderModuleList: %p\n", inMemoryOrderModuleList);
    printf("Second Entry in InMemoryOrderModuleList: %p\n", ldrDataTableEntry);
    wprintf(L"Module Name: %.*s\n", moduleName->Length / 2, moduleName->Buffer);

    return 0;
}

Similar Posts