Why C-Based Code Cannot Be Used as Shellcode
Using C to Build a calc.exe
Executable
|
|
Then extract the .text section using objcopy:
|
|
Disassemble with ndisasm
|
|
From the disassembly, we can observe:
-
NOP Padding – Instructions such as nop word [cs:rax+rax+0x0] are inserted by the compiler for alignment or anti-disassembly purposes.
-
Strong Dependencies – Frequent usage of rel instructions (e.g., mov rax, [rel 0x34a0]) and API calls via the Import Address Table (IAT), such as jmp [rel 0x72ac] for WinExec.
-
Indirect Jumps – Instructions like FF25 (e.g., jmp [rel 0x72ac]) rely on fixed addresses in the import table, indicating tight coupling with the PE loader.
-
PE Format Reliance – The code uses system APIs through the IAT and expects the Windows PE loader to handle relocation, section initialization, and runtime setup.
-
Static API References – API calls like WinExec are accessed through statically defined entries in the import table.
-
Excessive Padding and Alignment – Compiler-inserted padding like
0F1F4000 nop dword [rax+0x0]
and section alignment at addresses like 0x1800.
Using msfvenom
to Generate Shellcode for calc.exe
payload:
|
|
Disassemble the Shellcode
|
|
Compare MSF shellcode and C
Msfvenom | C | |
---|---|---|
Generator Tool | Metaploit’s msfvenom |
Extracted .text section from compiled PE |
Code Type | Pure position-independent code (PIC) | Compiler-generated, PE-dependent |
Dependency | No external dependencies, self-contained | Requires PE runtime, IAT, and relocation support |
Size | Compact (about 66 bytes) | Large (about 6KB) |
Feature | 1. Direct system calls, no standard library dependencies 2. Dynamically resolve WinExec address (via PEB traversal) 3. No redundant data, completely location independent |
1. Rely on PE Import Table (IAT) parsing API 2. Contains compiler-generated prologue/epilogue (such as stack frame adjustments) 3. Relocations and debugging information are present (even if the .text section is extracted) |
Execute Environmental | Can run from any memory address | Requires full PE loader support |
API call method | Dynamic resolution (gs:[0x60]→PEB→kernel32.dll) | Hard-coded address via IAT |
Code alignment | No padding bytes | Contains NOPs and alignment for structure |
Minimal, self-contained | Contains additional runtime/library code |
How to Improve C-Based Shellcode
To make your payload suitable for shellcode use, consider the following techniques:
-
PEB Traversal: Manually traverse the Process Environment Block (PEB) to locate loaded modules like kernel32.dll and resolve API addresses dynamically.
-
Dynamic API Resolution: Avoid using the Import Table; instead, resolve API addresses at runtime using hash-based or name-based lookup.
-
Avoid Compiler-Inserted Code: Use assembly or minimal C with inline assembly to bypass compiler-generated code (e.g., prologue/epilogue, SEH).
-
Position-Independent Code: Ensure your code can execute from any memory address without relying on relocations or fixed sections.
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?
fs:[0x30] is PEB
andfs:[0x18] is TEB
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
# 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:
|
|
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:
- DllBase: The base address of the loaded module (essential for finding exported functions)
- FullDllName/BaseDllName: The module’s path and filename (e.g., “C:\Windows\System32\kernel32.dll”)
- InXXXOrderLinks: Linked list entries for different load orders
- 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
Data Directory address : 00007fffc97770000 (kernel32.dll) + 0x90160 (data directory).
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:
- Validate the PE file signature (
e_magic = 0x5A4D
→ “MZ”). - 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
(not real base address)
ExitProcess RVA is 0009D4DE
(not real base address)
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.
|
|
Access PEB_LDR_DATA
Offset 0x18 within the PEB points to the Ldr field, which holds module loader information.
|
|
Get InMemoryOrderModuleList
This doubly-linked list at offset 0x20 of PEB_LDR_DATA contains entries for all loaded modules.
|
|
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
|
|
Get Base Address of kernel32.dll
The DllBase field at offset 0x20 of the _LDR_DATA_TABLE_ENTRY gives the base address.
|
|
Parse Export Table
We now locate the Export Directory inside the PE header of kernel32.dll.
|
|
Locate Function Name
We want to find the address of GetProcAddress.
|
|
Use GetProcAddress to get LoadLibraryA address
|
|
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.