Continue paste post: Shellcode - Why Cannot Use C Write Shellcode
Improvement:
- PEB Traversal
- Dynamic API Resolution
PEB Traversal
What is the PEB?
The Process Environment Block (PEB) is a data structure that stores process-wide information. It is a member of the Thread Environment Block (TEB) and contains information about the process, including a linked list of all loaded modules.
How to Get the PEB Pointer?
Module enumeration typically starts from the PEB, so retrieving its pointer is the first step. The PEB pointer resides within the TEB, and the TEB pointer is located at fs:[0x18] on x86 or gs:[0x30] on x64 systems.
Common Methods to Access TEB/PEB:
- Debugger View (e.g., x64dbg, WinDbg) : View the fs/gs register — it points to the TEB. Then follow the offset to get the PEB.
Disassemble or Examine Memory Layouts : Use TEB struct layout to locate the PEB.
Accessing via Segment Register
TEB and TIB Layout
# ref: https://www.vergiliusproject.com/kernels/x64/windows-11/24h2/_NT_TIB32
struct _NT_TIB32 {
ULONG ExceptionList; // 0x00
ULONG StackBase; // 0x04
ULONG StackLimit; // 0x08
ULONG SubSystemTib; // 0x0C
union {
ULONG FiberData; // 0x10
ULONG Version;
};
ULONG ArbitraryUserPointer; // 0x14
ULONG Self; // 0x18 <- fs:[0x18] = TEB pointer
};
# ref: https://www.vergiliusproject.com/kernels/x64/windows-11/24h2/_TEB32
struct _TEB32 {
struct _NT_TIB32 NtTib; // 0x00
ULONG EnvironmentPointer; // 0x1C
struct _CLIENT_ID32 ClientId;// 0x20
ULONG ActiveRpcHandle; // 0x28
ULONG ThreadLocalStoragePointer; // 0x2C
ULONG ProcessEnvironmentBlock; // 0x30 <- fs:[0x30] = PEB pointer
...
};
TIB struct is included in TEB as the first member, so the first address of TIB is the first address of TEB.
Dynamic API Resolution
Retrieving Kernel32.dll Base Address
Using PowerShell:
Get-Process -Id 1300 | ForEach-Object {
$_.Modules | Where-Object { $_.ModuleName -like "kernel32.dll" } |
Select-Object ModuleName, @{Name="BaseAddress"; Expression={$_.BaseAddress.ToString("X")}}, FileName
}
Using x64dbg:
Using WinDbg (Follow Pointer Chain):
TEB->PEB->Ldr->InMemoryOrderLoadList->currentProgram->ntdll->kernel32.BaseDll
Export Table Traversal Flow
When resolving functions like WinExec dynamically, the process is:
- Access Kernel32 Base Address
-
Parse PE Headers
- Locate IMAGE_DOS_HEADER at DllBase
- Use
e_lfanew
to find IMAGE_NT_HEADERS - Locate the Export Directory via Optional Header → DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]
-
Resolve API
- Parse Export Directory
- Match target name in Name Table
- Retrieve its Ordinal
- Use Ordinal to index Function Address Table
- Resolve absolute address = DllBase + Function RVA
WinDbg/Memory Viewer Visuals
1. Displaying the Thread Environment Block (TEB) Structure
Locating the PEB: The TEB contains a pointer to the PEB (PEB* ProcessEnvironmentBlock), which is essential for finding loaded modules (e.g., kernel32.dll).
2. Displaying the Process Environment Block (PEB) Structure
Process Environment Block (PEB) is a fundamental Windows structure containing process-wide data such as:
• Loaded Modules (DLLs) via PEB_LDR_DATA* Ldr
• Command-line Arguments
• Heap Information
• Binary Image Base Address
3. Analyzing the PEB_LDR_DATA Structure for Module Enumeration
Displays the PEB_LDR_DATA structure, a critical part of the Process Environment Block (PEB) that manages loaded modules (DLLs/EXEs) in a process. This structure contains three doubly linked lists used to track modules in different load orders:
a. InLoadOrderModuleList – Lists modules in the order they were loaded.
b. InMemoryOrderModuleList – Lists modules in the order they appear in memory.
c. InInitializationOrderModuleList – Lists modules in the order they were initialized.
4. Analyzing the _LDR_DATA_TABLE_ENTRY Structure
The _LDR_DATA_TABLE_ENTRY
structure contains critical information about each loaded module (DLL/EXE) in a process.
Key Fields Explained:
a. DllBase: The base address of the loaded module (essential for finding exported functions)
b. FullDllName/BaseDllName: The module's path and filename (e.g., "C:\Windows\System32\kernel32.dll")
c. InXXXOrderLinks: Linked list entries for different load orders
d. SizeOfImage: The module's size in memory
5. Examining the _IMAGE_DATA_DIRECTORY Structure
The _IMAGE_DATA_DIRECTORY
structure is a fundamental part of the Portable Executable (PE) file format in Windows. It describes a data directory entry in the PE header, which points to critical sections of a binary (EXE/DLL), such as:
- Export Table (for API functions)
- Import Table (for dependencies)
- Base Relocation Table (for ASLR)
- Debug Information
-
TLS (Thread Local Storage) Data
6. Analyzing PE Headers with _IMAGE_DOS_HEADER
The _IMAGE_DOS_HEADER structure is the starting point of every PE (Portable Executable) file (EXE/DLL). When examining it at a module's base address (e.g., kernel32.dll), you can:
a. Validate the PE file signature (e_magic = 0x5A4D
→ "MZ").
b. Locate the NT headers via e_lfanew
(offset to _IMAGE_NT_HEADERS).
7. Analyzing 64-bit PE NT Headers (_IMAGE_NT_HEADERS64)
The _IMAGE_NT_HEADERS64
structure defines the core metadata of a 64-bit PE file, including:
-
PE Signature (
"PE\0\0"
) -
File Header (
_IMAGE_FILE_HEADER
) -
Optional Header (
_IMAGE_OPTIONAL_HEADER64
) — Contains critical data directories (exports, imports, relocations).
8. Analyzing 64-bit PE Optional Header (_IMAGE_OPTIONAL_HEADER64)
The _IMAGE_OPTIONAL_HEADER64
structure contains critical metadata for 64-bit PE files, including:
-
Image base address (
AddressOfEntryPoint
,ImageBase
) -
Section alignment/sizes (
SectionAlignment
,FileAlignment
) - Data directories (exports, imports, relocations, TLS, etc.)
This is where you find the RVA (Relative Virtual Address) of key structures like the Export Directory
9. Dumping PE Data Directories
Get the RVA (Relative Virtual Address) and size of 16 PE data directories
10. Analyze Export Directory
11. List All function
PE Parsing Reference
Offset | Description |
---|---|
0x3c into the file | RVA of PE signature |
0x78 bytes after PE signature | RVA of Export Table |
0x14 into the Export Table | Number of functions exported by a module |
0x1c into the Export Table | RVA of Address Table - addresses of exported functions |
0x20 into the Export Table | RVA of Name Pointer Table - addresses of exported function names |
0x24 into the Export Table | RVA of Ordinal Table - function order number as listed in the table |
Get AddressOfNames RVA
- 00091afc
12. Dump AddressOfNames
Compare the first 8 bytes to see if it is "GetProcA"
...
TOO MUCH!!
Let's change to tool named PEView
13. Export Table - Number of Exported Functions
In this case, RVA contains 643 functions that module kernel32.dll export.
WinExec RVA is 000A48D3
ExitProcess RVA is 0009D4DE
Direct get process base address
GetProcAddress
WinExec
ExitProcess
Note: Some functions are forwarded/thunked; symbols may refer to implementation aliases like ExitProcessImplementation.
Manually Resolving API Addresses from PEB (x64 Shellcode Style)
Overview
This above explains how to manually resolve function addresses such as WinExec
, LoadLibraryA
, and ExitProcess
from within a shellcode or assembly stub. We walk through parsing the PEB (Process Environment Block), traversing the loader's module list, parsing the PE headers, and locating the desired API in the Export Address Table of kernel32.dll.
Process Flow
- Obtain kernel32.dll Base Address from the PEB
- Parse PE Structure starting from the base address:
- DOS Header → NT Header (via offset 0x3C)
- NT Header → Optional Header → Data Directory → Export Directory
- Parse Export Directory to:
- Locate the function name (WinExec, etc.) via the Name Pointer Table
- Resolve its ordinal
- Use the ordinal to get the RVA from the AddressOfFunctions table
- Add RVA to the DLL base address → function's actual memory address
Get PEB (Process Environment Block) Address (x64):
In x64 Windows, the PEB is accessible via the GS segment register. GS:[0x60] points directly to the PEB structure.
xor rcx, rcx ; RCX = 0
mov rax, [gs:rcx + 0x60]
Access PEB_LDR_DATA
Offset 0x18 within the PEB points to the Ldr field, which holds module loader information.
mov rax, [rax + 0x18] ; PEB -> Ldr (PEB_LDR_DATA)
Get InMemoryOrderModuleList
This doubly-linked list at offset 0x20 of PEB_LDR_DATA contains entries for all loaded modules.
mov rsi, [rax + 0x20] ; Ldr -> InMemoryOrderModuleList.Flink
Traverse Module List
Each node in the list is a _LDR_DATA_TABLE_ENTRY. Modules are typically ordered as:
- Executable itself
- ntdll.dll
- kernel32.dll
lodsq ; Load first module (ntdll.dll) into RAX, increment RSI
xchg rax, rsi ; Swap RAX and RSI (now RSI points to next module)
lodsq ; Load second module (kernel32.dll) into RAX
Get Base Address of kernel32.dll
The DllBase field at offset 0x20 of the _LDR_DATA_TABLE_ENTRY gives the base address.
mov rbx, [rax + 0x20] ; LDR_DATA_TABLE_ENTRY + 0x20 = DllBase (kernel32.dll 的 base address)
Parse Export Table
We now locate the Export Directory inside the PE header of kernel32.dll.
; Search Export Table
xor r8, r8 ; R8 = 0
mov r8d, [rbx + 0x3c] ; Get PE header offset (e_lfanew at offset 0x3C)
mov rdx, r8 ; Copy PE header offset to RDX
add rdx, rbx ; Calculate PE header address (base + offset)
mov r8d, [rdx + 0x88] ; Get Export Table RVA (offset 0x88 in PE header)
add r8, rbx. ; Calculate Export Table address (base + RVA)
Locate Function Name
We want to find the address of GetProcAddress.
; Search Name Pointer Table, prepare to find function name
xor rsi, rsi ; RSI = 0
mov esi, [r8 + 0x20] ; AddressOfNames (offset 0x20 in Export Table)
add rsi, rbx ; Calculate Name Pointer Table address (base + RVA)
xor rcx, rcx ; Zero out RCX (will be used as counter)
mov r9, 0x41636f7250746547 ; Prepare first 8 bytes of "GetProcA" for comparison (little-endian)
; Find GetProcAddress in function name
Get_Function:
inc rcx ; Increment function counter
xor rax, rax ; RAX = 0
mov eax, [rsi + rcx * 4] ; Get RVA of function name (array of 4-byte RVAs)
add rax, rbx ; Calculate function name address (base + RVA)
cmp QWORD [rax], r9 ; Compare first 8 bytes is "GetProcA"
jnz Get_Function ; If not matched, continue searching
xor rsi, rsi ; RSI = 0
mov esi, [r8 + 0x24] ; AddressOfNameOrdinals (offset 0x24 in Export Table)
add rsi, rbx ; Calculate Ordinal Table address (base + RVA)
mov cx, [rsi + rcx * 2] ; Get function ordinal (array of 2-byte values)
xor rsi, rsi ; RSI = 0
mov esi, [r8 + 0x1c] ; AddressOfFunctions (offset 0x1C in Export Table)
add rsi, rbx ; Calculate Address Table address (base + RVA)
xor rdx, rdx ; RDX = 0
mov edx, [rsi + rcx * 4] ; Get function RVA (array of 4-byte RVAs)
add rdx, rbx ; Calculate function address (base + RVA)
mov rdi, rdx ; Store GetProcAddress address in RDI
Use GetProcAddress to get LoadLibraryA address
; Use GetProcAddress to get LoadLibraryA address
getprocaddr:
mov rcx, 0x41797261 ; Prepare second part of "LoadLibraryA" string ("aryA")
push rcx ; Push to stack
mov rcx, 0x7262694c64616f4c. ; Prepare first part of "LoadLibraryA" string ("LoadLibr")
push rcx ; Push to stack
mov rdx, rsp ; RDX points to "LoadLibraryA" string on stack
mov rcx, rbx ; RCX contains kernel32.dll base address
sub rsp, 0x30 ; Allocate shadow space (32 bytes) + align stack
call rdi ; Call GetProcAddress(kernel32.dll, "LoadLibraryA")
add rsp, 0x30 ; Restore stack pointer
add rsp, 0x10 ; Clean up string from stack (16 bytes)
mov rsi, rax ; Store LoadLibraryA address in RSI
Continue: Resolve WinExec, ExitProcess, etc.
Repeating the steps: locate name in Export Table → get ordinal → get RVA → resolve to VA.
Here's the link to the full code — thanks to nevernever69 for providing the ultimate code to the call calc.exe shellcode.