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.
- Access the TEB – the
FS
segment register points to the TEB. Specifically,FS:[0x30]
will give us the address of the PEB. - Access the PEB – from the TEB, we can access the PEB. The PEB structure contains a field called
Ldr
(offset0x0C
in the PEB structure), which is a pointer to the PEB_LDR_DATA structure. - 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.
- 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). - 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;
}