Prologue

In the last blog we saw about the PE structure and its components. In order to master the art of dynamic shellcoding, we still have to understand few more concepts like PEB, TEB, Export Directory and Export Address Table. In simpler terms, PEB and TEB provide process-specific base addresses, while EAT enables dynamic function resolution, facilitating dynamic memory addressing in Windows programs. In this blog we will discuss about these in detail.

Process Environment Block (PEB)

Process Environment Block (PEB) is a data structure that is being used in Windows processes to maintain information across the whole process under user-mode memory. This block stores crucial information like process name and its ID, whether the process is being debugged or not, base address of the image, modules loaded into the memory, the command line used to invoke the process etc. The PEB is created by kernel but handles only user land data of the process in a fixed memory address througout the process.

The structure of PEB from winternl.h is given below,

For more on PEB, visit the official documentation

There are three important fields to note: BeingDebugged, Ldr and ProcessParameters.

1. BeingDebugged: Indicates whether the specified process is currently being debugged.

2. ProcessParameters: A pointer to an RTL_USER_PROCESS_PARAMETERS structure that contains process parameter information such as the command line.

C++
typedef struct _RTL_USER_PROCESS_PARAMETERS {
  BYTE           Reserved1[16];
  PVOID          Reserved2[10];
  UNICODE_STRING ImagePathName;
  UNICODE_STRING CommandLine;
} RTL_USER_PROCESS_PARAMETERS, *PRTL_USER_PROCESS_PARAMETERS;

ImagePathName consists the path of the image file of the process and CommandLine has the command line passed to execute the process.

3. Ldr: A pointer to a PPEB_LDR_DATA structure that contains information about the loaded modules for the process. This is a important structure from PEB, which can be used for many offensive purposes and evasion.

The structure of Ldr is,

C++
typedef struct _PEB_LDR_DATA {
  BYTE       Reserved1[8];
  PVOID      Reserved2[3];
  LIST_ENTRY InMemoryOrderModuleList;
} PEB_LDR_DATA, *PPEB_LDR_DATA;

The InMemoryOrderModuleList is the head of a doubly-linked list that contains the loaded modules for the process. Each item in the list is a pointer to an LDR_DATA_TABLE_ENTRY structure. By default the first link has the data of the parent process, following it there will be ntdll.dll and next to it kernel32.dll and next to it will be the kernelbase.dll.

The structure of the InMemoryOrderModuleList linked lists is,

C++
typedef struct _LIST_ENTRY {
   struct _LIST_ENTRY *Flink;
   struct _LIST_ENTRY *Blink;
} LIST_ENTRY, *PLIST_ENTRY, *RESTRICTED_POINTER PRLIST_ENTRY;

Since it is a linked list, each entry (ie. module information) is linked with each other in a loop. The address of each linked list has a pointer to the data structure of LDR_DATA_TABLE_ENTRY.

C++
typedef struct _LDR_DATA_TABLE_ENTRY {
    PVOID Reserved1[2];
    LIST_ENTRY InMemoryOrderLinks;
    PVOID Reserved2[2];
    PVOID DllBase;
    PVOID EntryPoint;
    PVOID Reserved3;
    UNICODE_STRING FullDllName;
    BYTE Reserved4[8];
    PVOID Reserved5[3];
    union {
        ULONG CheckSum;
        PVOID Reserved6;
    };
    ULONG TimeDateStamp;
} LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;

The DllBase will be the memory address of the PE module loaded into the process. For the art of creating dynamic shellcode, we will be requiring the base address of kernel32.dll from the PEB to extract the required function address from its Export Directory.

Thread Environment Block (TEB)

In certain instances, a process may encompass multiple threads, initially commencing with a primary thread and spawning additional threads as needed, all sharing the same virtual addresses. Each thread possesses its unique resources, encompassing exception handlers, local storage, and more. Consequently, similar to the PEB, each thread maintains its TEB within the process address space.

The structure of TEB from winternl.h is given below,

C++
typedef struct _TEB {
  PVOID Reserved1[12];
  PPEB  ProcessEnvironmentBlock;
  PVOID Reserved2[399];
  BYTE  Reserved3[1952];
  PVOID TlsSlots[64];
  BYTE  Reserved4[8];
  PVOID Reserved5[26];
  PVOID ReservedForOle;
  PVOID Reserved6[4];
  PVOID TlsExpansionSlots;
} TEB, *PTEB;

The TEB inherits the required information for the threads from the parent process using ProcessEnvironmentBlock. TlsSlots represents the initial slots for Thread Local Storage in the Thread Environment Block, allowing threads to store thread-specific data. TlsExpansionSlots is a mechanism for dynamically expanding the available TLS slots if the initial allocation is insufficient, ensuring flexibility for programs with a high number of threads or extensive TLS requirements.

Debugging TEB and PEB

Each process can have single or many number of threads depending the image logic. Now, when we tried to access the PEB, we cannot reach it directly, because it is not directly referenced within registers. We have to go through from TEB to access PEB due to its inheriting nature. And in Windows x64 environment TEB can be accessed via gs register of the thread. In x86 bit environment, the TEB will be available in fs register.

We will use the same program which we used for our last blog,

ProgramOnDebugger

Now lets enumerate our TEB in WinDbg using !teb to get its address,

TEB-1

C
0:000> !teb
TEB at 0000006acb524000
    ExceptionList:        0000000000000000
    StackBase:            0000006acb700000
    StackLimit:           0000006acb6fd000
    SubSystemTib:         0000000000000000
    FiberData:            0000000000001e00
    ArbitraryUserPointer: 0000000000000000
    Self:                 0000006acb524000
    EnvironmentPointer:   0000000000000000
    ClientId:             00000000000037e8 . 0000000000005490
    RpcHandle:            0000000000000000
    Tls Storage:          00000171af364720
    PEB Address:          0000006acb523000
    LastErrorValue:       0
    LastStatusValue:      0
    Count Owned Locks:    0
    HardErrorMode:        0

Now we have got the address of TEB as 0x0000006acb524000

Lets view the TEB in its data structure using dt 0x0000006acb524000 ntdll!_TEB,

PEB-From-TEB

Since TEB inherits PEB, we can query TEB to get the address of the PEB block with the offfset of 0x60. Now we know that the PEB of this process is at 0x0000006acb523000.

This is the correct approach to find the PEB from the TEB of a thread. But in WinDbg, we can simply get the details of PEB using !peb command.

PEB-Details

We can see that the PEB gives the details about the process being debugged, base address of the image and loaded modules into the process. That is why we need to query PEB and handle it for offensive purposes. This is where the magic for querying dynamic information about a process is stored.

Now let's list the PEB using its address obtained from TEB using dx -r1 ((ntdll!_PEB *)0x6acb523000),

To query more on the loaded modules and its base address we need to get inside the Ldr of the PEB.

LDR-From-PEB

Now lets dig into the Ldr which is present at the offset of 0x018 from PEB and check the linked list of InMemoryOrderModuleList which holds the details of modules loaded into the memory of the process.

In the InLoadOrderModuleList, modules (executable files or dynamic link libraries) are arranged by their loading sequence, the InMemoryOrderModuleList, modules are organized based on their memory storage order within the process and the InInitializationOrderModuleList, modules are sorted according to the order in which they are initialized within the Process Environment Block.

Checking the linked list from InMemoryOrderModuleList from the offset of 0x020 from Ldr.

LDR-Linked-List

Lets check the details of each linked list entry using the _LDR_DATA_TABLE_ENTRY structure. The actual address of the structure will be 16 bytes / 0x10 before the _LIST_ENTRY address.

Now we have reached to a linked list entry where it contains the information of kernel32.dll module loaded into the process. We can get its base address from this entry with the offset of 0x30 from the entry. We can also see that _LIST_ENTRY is a part of _LDR_DATA_TABLE_ENTRY.

Kernel32-DLL-Entry

We have resolved the base address of the kernel32.dll module as 0x0x00007ffa11640000.

Lets check the DOS signature of the base address to confirm it,

Kernel32-DLL-DOSByte

Indeed it a PE and we can start parsing its internals to reach the Export Address Table to get the functions stored inside this DLL.

Relative Virtual Address

A Relative Virtual Address, or RVA for short, is the numerical disparity between two Virtual Addresses (VA), denoting the greater of the two. A Virtual Address is the original memory location, while the Relative Virtual Address (RVA) signifies the address relative to the ImageBase. In this context, ImageBase refers to the initial memory location where the executable file is loaded.

C++
  Virtual Address = ImageBase + RVA

Parsing the PE Structure

To confirm our kernel32.dll address, we can simply query the WinDbg to fetch it.

C
0:000> lm m kernel32
Browse full module list
start             end                 module name
00007ffa`11640000 00007ffa`116fd000   KERNEL32   (pdb symbols)          C:\ProgramData\Dbg\sym\kernel32.pdb\B07C97792B439ABC0DF83499536C7AE53\kernel32.pdb

In the last blog, we have discussed about the PE structure and its components. Using this base address we are going to reach the e_lfanew, which is at the offset of 0x3c from the base address.

e_lfanew

The e_lfanew is at the RVA of 0n240 in decimal and 0xf0 in hexadecimal from the base location of the kernel32.dll. Now we know the location of e_lfanew, lets parse it so that we can reach the PE signature.

C
0:000> dd 0x00007ffa`11640000+0xf0
00007ffa`116400f0  00004550 00078664 9ec9da27 00000000
00007ffa`11640100  00000000 202200f0 140e020b 0007d600
00007ffa`11640110  0003a600 00000000 000173e0 00001000
00007ffa`11640120  11640000 00007ffa 00001000 00000200
00007ffa`11640130  0000000a 0000000a 0000000a 00000000
00007ffa`11640140  000bd000 00000400 000c0e27 41600003
00007ffa`11640150  00040000 00000000 00001000 00000000
00007ffa`11640160  00100000 00000000 00001000 00000000
0:000> db 0x00007ffa`11640000+0xf0
00007ffa`116400f0  50 45 00 00 64 86 07 00-27 da c9 9e 00 00 00 00  PE..d...'.......
00007ffa`11640100  00 00 00 00 f0 00 22 20-0b 02 0e 14 00 d6 07 00  ......" ........
00007ffa`11640110  00 a6 03 00 00 00 00 00-e0 73 01 00 00 10 00 00  .........s......
00007ffa`11640120  00 00 64 11 fa 7f 00 00-00 10 00 00 00 02 00 00  ..d.............
00007ffa`11640130  0a 00 00 00 0a 00 00 00-0a 00 00 00 00 00 00 00  ................
00007ffa`11640140  00 d0 0b 00 00 04 00 00-27 0e 0c 00 03 00 60 41  ........'.....`A
00007ffa`11640150  00 00 04 00 00 00 00 00-00 10 00 00 00 00 00 00  ................
00007ffa`11640160  00 00 10 00 00 00 00 00-00 10 00 00 00 00 00 00  ................

Reaching-PE-Signature

We can confirm that we have reached into the PE header. Now let's access it in the _IMAGE_NT_HEADERS64 format.

C
0:000> dt nt!_IMAGE_NT_HEADERS64 00007ffa`11640000+0xf0
ntdll!_IMAGE_NT_HEADERS64
   +0x000 Signature        : 0x4550
   +0x004 FileHeader       : _IMAGE_FILE_HEADER
   +0x018 OptionalHeader   : _IMAGE_OPTIONAL_HEADER64

NT-Headers

Now lets get into the File Header and Optional Header,

Optional Header is present at the offset of 0x18 from the PE Signature

File-Optional-Headers

Let's query the DataDirectory from Optional Headers which is present at the offset of 0x70 to find the Export Directory RVA. In the last blog, we have seen that the Export Directory is the first directory in the list structure.

DataDirectories

We can also use !dh 0x00007ffa11640000 to dump the information of the PE header,

We can directly get the Export Directory RVA using the above command,

C
   99080 [    DF58] address [size] of Export Directory

Now we have found the RVA of Export Directory, lets query it now from its strucutre as show below.

C++
public struct IMAGE_EXPORT_DIRECTORY{
        public UInt32 Characteristics;
        public UInt32 TimeDateStamp;
        public UInt16 MajorVersion;
        public UInt16 MinorVersion;
        public UInt32 Name;
        public UInt32 Base;
        public UInt32 NumberOfFunctions;
        public UInt32 NumberOfNames;
        public UInt32 AddressOfFunctions;
        public UInt32 AddressOfNames;
        public UInt32 AddressOfNameOrdinals;
}

The values stored inside the IMAGE_EXPORT_DIRECTORY structure of Export Directory RVA is,

C
0:000> dd 0x00007ffa11640000+0x99080
00007ffa`116d9080  00000000 9ec9da27 00000000 0009d07c
00007ffa`116d9090  00000001 00000662 00000662 000990a8
00007ffa`116d90a0  0009aa30 0009c3b8 0009d0a1 0009d0d7
00007ffa`116d90b0  00020390 0001ba10 00059230 000128f0
00007ffa`116d90c0  00025950 00025960 0009d15d 0003bdd0
00007ffa`116d90d0  00059370 000593d0 00022580 0001e5d0
00007ffa`116d90e0  00039710 00020bb0 00039730 00037b30
00007ffa`116d90f0  0009d296 0009d2d6 00007200 000255a0
0:000> db 0x00007ffa11640000+0x99080
00007ffa`116d9080  00 00 00 00 27 da c9 9e-00 00 00 00 7c d0 09 00  ....'.......|...
00007ffa`116d9090  01 00 00 00 62 06 00 00-62 06 00 00 a8 90 09 00  ....b...b.......
00007ffa`116d90a0  30 aa 09 00 b8 c3 09 00-a1 d0 09 00 d7 d0 09 00  0...............
00007ffa`116d90b0  90 03 02 00 10 ba 01 00-30 92 05 00 f0 28 01 00  ........0....(..
00007ffa`116d90c0  50 59 02 00 60 59 02 00-5d d1 09 00 d0 bd 03 00  PY..`Y..].......
00007ffa`116d90d0  70 93 05 00 d0 93 05 00-80 25 02 00 d0 e5 01 00  p........%......
00007ffa`116d90e0  10 97 03 00 b0 0b 02 00-30 97 03 00 30 7b 03 00  ........0...0{..
00007ffa`116d90f0  96 d2 09 00 d6 d2 09 00-00 72 00 00 a0 55 02 00  .........r...U..

ExportDirectory

By mapping it to its structure, we get the required addresses.

C++
public struct IMAGE_EXPORT_DIRECTORY{
        public UInt32 Characteristics;       // 00000000
        public UInt32 TimeDateStamp;         // 9ec9da27
        public UInt16 MajorVersion;          // 0000
        public UInt16 MinorVersion;          // 0000
        public UInt32 Name;                  // 0009d07c
        public UInt32 Base;                  // 00000001
        public UInt32 NumberOfFunctions;     // 00000662
        public UInt32 NumberOfNames;         // 00000662
        public UInt32 AddressOfFunctions;    // 000990a8 
        public UInt32 AddressOfNames;        // 0009aa30
        public UInt32 AddressOfNameOrdinals; // 0009c3b8
}

Export Directory

Export Directory consists of three important pointers. AddressOfFunctions also known as Export Address Table (EAT) which contains the information of all functions/symbols from the target module. AddressOfNames or Name Pointer Table which contains the names of the functions in the module. AddressOfNameOrdinals or Ordinal Table contains the ordinal value, also known as the index of the function in the Export Address Table.

ExportDirectoryPEBear

This is how it will be when analyzed in PE Bear for kernel32.dll.

The Export Address Table will be placed at an offset of 0x1c from the Export Directory, Name Pointer Table will be at the offset of 0x20 and the Ordinal Table will be at the offset of 0x24.

And the NumberOfFunctions is present with the offset of 0x14. We will be requiring this value to initialize our counter to search for the target function within the Name Pointer Table. If it matches then we use the Ordinal Table with reference to the target function to find its RVA from the Export Address Table. An ordinal is just the identifier for a function within a DLL, but these values are not static among different versions of DLL. That is why we start searching from the Name Pointer Table with refernce of Ordinal Table.

From the Name Pointer Table RVA, we can see the function names are being stored in the pointers,

NamePointers

Lets analyze the Export Directory of kernel32.dll using PEview.

NamePointerTable

Assume that we are looking for WinExec function from the DLL. The value/index for the function in the Name Pointer table will be the same value as the counter, while iterating in the table with incremental value of 4 bytes.

OrdinalTable

To get the Ordinal Number from the Ordinal Table, we just have to search with the index of (Counter Value * 2) from virtual address of Ordinal Table. Ironically, for x64 bit environment the Ordinal Number will be (Counter Value - 1).

EAT

It becomes easier now to get the RVA of the target function with a simple logic. Each pointer storing the RVA of the functions differ by 4 bytes from the virtual address of EAT. To find the exact RVA value, we just have to look with the index of (Ordinal Number * 4) from the EAT.

RVA Of Target Function

In order to locate the virtual address of a target function, we need to locate the RVA of the target function within the Export Address Table. This logic requires a few iterations and conditional checks. I have made some graphical representation of it, for easier understanding.

Workflow

  1. Load the TEB from the gs register.
  2. Access the PEB from the TEB with an offset of 0x60.
  3. From the PEB access the Ldr with an offset of 0x18.
  4. Querying the InMemoryOrderModuleList from Ldr with an offset of 0x20.
  5. Get the address of the parent process list entry from the linked list with 0x00 as offset.
  6. Iterating to the next link and fetching the list entry of ntdll.dll with 0x00 as offset.
  7. Iterating to the next link and fetching the list entry of kernel32.dll with 0x00 as offset. Upto here these modules position will be universally similar in the same order.
  8. From the list entry of kernel32.dll get the base address from the offset of 0x30.
  9. From the base address of kernel32.dll find the RVA of the PE header/PE signature with an offset of 0x3c.
  10. Calculating the address of PE header (Base address of kernel32.dll + RVA of PE header).
  11. Finding the RVA of Export Directory from the address of PE header with the offset of 0x18+0x70. The first data directory itself will be the Export Directory.
  12. Calculating the address of Export Directory (Base address of kernel32.dll + RVA of Export Directory).
  13. Fetching the number of exported functions from the address of Export Directory with an offset of 0x14.
  14. Finding the RVA of Export Address Table which is present with an offset of 0x1c from the Export Directory.
  15. Calculating the address of Export Address Table (Base address of kernel32.dll + RVA of Export Address Table).
  16. Finding the RVA of Name Pointer Table which is present with an offset of 0x20 from the Export Directory.
  17. Calculating the address of Name Pointer Table (Base address of kernel32.dll + RVA of Name Pointer Table).
  18. Finding the RVA of Ordinal Table which is present with an offset of 0x24 from the Export Directory.
  19. Calculating the address of Ordinal Table (Base address of kernel32.dll + RVA of Ordinal Table).
  20. Looping through the Name Pointer Table to find the target function address name in it with incrementing counter value up to the number of functions present in the Export Directory.
  21. Store the counter value into a variable when the target function name gets matched.
  22. Find the Ordinal Number of the target function from the Ordinal Table with the calculated address with respect to the counter value (Address of Ordinal Table + (2 * Counter Value)). Each ordinal entry is 2 bytes.
  23. Find the RVA of the target function from the Address Table with the calculated address specific to the ordinal number (Address of Address Table + (4 * Ordinal Number)). Each entry in address table is 4 bytes.
  24. Virtual address of the target function should be calculated (Base address of kernel32.dll + RVA of target function).

We will get into this calculation in depth to calculate our shellcode in the next blog post.

References

Windows Shellcode

Location Export Directory Table