This article focuses on how to locate the Export Directory Table from the PE file structure. This is the second part of a blog series that focuses on how to create a custom Win32 reverse shellcode.
The first part can be found at the following link
According to Microsoft Docs,
The export symbol information begins with the export directory table, which describes the remainder of the export symbol information. The export directory table contains address information that is used to resolve imports to the entry points within this image. Following, the export address table contains the address of exported entry points and exported data and absolutes. An ordinal number is used as an index into the export address table. If the address specified is not within the export section (as defined by the address and length that are indicated in the optional header), the field is an export RVA, which is an actual address in code or data. Otherwise, the field is a forwarder RVA, which names a symbol in another DLL.
Search for the Export Directory Table
From the previous post [pt .1], we have accomplished to locate the kernelbase.dll
base address. Now that we have the kernelbase.dll
address, we need to parse the PE file structure to find the offset of the export directory table. It is worth to mention here that we will not proceed further in details about the PE file structure, but the following screenshot can provide useful information about the format of the PE file structure.
The image above has been taken from the following blog post which explains the PE format in more detail.
Before moving further, we need to locate the exact offset of the e_lfanew
. The e_lfanew
field is a 4-byte offset into the file where the PE file header is located. It is necessary to use this offset to locate the PE header in the file. In WinDbg we run the dt -n _IMAGE_DOS_HEADER
as follows
0:000> dt -n _IMAGE_DOS_HEADER
ntdll!_IMAGE_DOS_HEADER
+0x000 e_magic : Uint2B
+0x002 e_cblp : Uint2B
+0x004 e_cp : Uint2B
+0x006 e_crlc : Uint2B
+0x008 e_cparhdr : Uint2B
+0x00a e_minalloc : Uint2B
+0x00c e_maxalloc : Uint2B
+0x00e e_ss : Uint2B
+0x010 e_sp : Uint2B
+0x012 e_csum : Uint2B
+0x014 e_ip : Uint2B
+0x016 e_cs : Uint2B
+0x018 e_lfarlc : Uint2B
+0x01a e_ovno : Uint2B
+0x01c e_res : [4] Uint2B
+0x024 e_oemid : Uint2B
+0x026 e_oeminfo : Uint2B
+0x028 e_res2 : [10] Uint2B
+0x03c e_lfanew : Int4B
As we see above, the e_lfanew
exists at offset 0x03c
. According to [ this ] blog ,the MS-DOS header occupies the first 64 bytes of the PE file. A structure representing its content is described below
typedef struct _IMAGE_DOS_HEADER { USHORT e_magic; USHORT e_cblp; USHORT e_cp; USHORT e_crlc; USHORT e_cparhdr; USHORT e_minalloc; USHORT e_maxalloc; USHORT e_ss; USHORT e_sp; USHORT e_csum; USHORT e_ip; USHORT e_cs; USHORT e_lfarlc; USHORT e_ovno; USHORT e_res[4]; USHORT e_oemid; USHORT e_oeminfo; USHORT e_res2[10]; LONG e_lfanew; } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
Now that we know the location of the PE header, we will move further to locate the offset of the Export Directory Table from the _IMAGE_EXPORT_DIRECTORY
structure. First we will locate the base address of the kernelbase.dll
module
0:000> lm
start end module name
000a0000 000bf000 testasm C (private pdb symbols) C:\symbols\testasm\testasm.pdb
712c0000 71434000 ucrtbased (deferred)
71440000 7145b000 VCRUNTIME140D (deferred)
760a0000 76190000 KERNEL32 (deferred)
76190000 763a3000 KERNELBASE (pdb symbols) c:\symbols\wkernelbase.pdb\4FB470EF91F049226E7209E0E1ADD6791\wkernelbase.pdb
77630000 777d2000 ntdll (pdb symbols) c:\symbols\wntdll.pdb\DBC8C8F74C0E3696E951B77F0BB8569F1\wntdll.pdb
At this point we will use the base address of kernelbase.dll
module marked in red above in order to find the offset of the Export Directory Table for the specific DLL
0:000> !dh 76190000 -f
File Type: DLL
FILE HEADER VALUES
14C machine (i386)
6 number of sections
AE908B72 time date stamp
0 file pointer to symbol table
0 number of symbols
E0 size of optional header
2102 characteristics
Executable
32 bit word machine
DLL
OPTIONAL HEADER VALUES
10B magic #
14.20 linker version
1D5E00 size of code
39000 size of initialized data
0 size of uninitialized data
114030 address of entry point
1000 base of code
----- new -----
76190000 image base
1000 section alignment
200 file alignment
3 subsystem (Windows CUI)
10.00 operating system version
10.00 image version
10.00 subsystem version
213000 size of image
400 size of headers
2237E3 checksum
00040000 size of stack reserve
00001000 size of stack commit
00100000 size of heap reserve
00001000 size of heap commit
4140 DLL characteristics
Dynamic base
NX compatible
Guard
1C8030 [ EDC0] address [size] of Export Directory
1DBAF4 [ 3C] address [size] of Import Directory
1E2000 [ 548] address [size] of Resource Directory
0 [ 0] address [size] of Exception Directory
20CE00 [ 6BF0] address [size] of Security Directory
1E3000 [ 2F998] address [size] of Base Relocation Directory
81860 [ 70] address [size] of Debug Directory
0 [ 0] address [size] of Description Directory
0 [ 0] address [size] of Special Directory
0 [ 0] address [size] of Thread Storage Directory
11C0 [ AC] address [size] of Load Configuration Directory
0 [ 0] address [size] of Bound Import Directory
1DB000 [ AE8] address [size] of Import Address Table Directory
1C6020 [ 480] address [size] of Delay Import Directory
0 [ 0] address [size] of COR20 Header Directory
0 [ 0] address [size] of Reserved Directory
As we see above, and if we go down the structure, we will see ( highlighted in red above ), that the Export Directory exists at offset 1C8030
from the kernelbase.dll
base address. So, according to this information, we are able to locate all the values of the arguments passed to the IMAGE_EXPORT_DIRECTORY
as seen below highlighted in red.
0:000> dd 76190000+1C8030 76358030 00000000 ae908b72 00000000 001cca86 76358040 00000001 0000076b 0000076b 001c8058 76358050 001c9e04 001cbbb0 00183ee0 001216a0 76358060 00114e40 001ccaeb 00128350 0011ec40 76358070 001dbaf0 00128010 001ac010 00127770 76358080 001ac0b0 001ac160 001ac1d0 001ac290 76358090 001ccc2b 001ccc61 001779e0 00124f40 763580a0 00123720 000fd490 001ac350 001ac3a0
And all the above information can be mapped using the IMAGE_EXPORT_DIRECTORY
structure as seen below
public struct IMAGE_EXPORT_DIRECTORY { public UInt32 Characteristics; // 00000000 public UInt32 TimeDateStamp; // ae908b72 public UInt16 MajorVersion; // 0000 public UInt16 MinorVersion; // 0000 public UInt32 Name; // 001cca86 public UInt32 Base; // 00000001 public UInt32 NumberOfFunctions; // 0000076b public UInt32 NumberOfNames; // 0000076b public UInt32 AddressOfFunctions; // 001c8058 public UInt32 AddressOfNames; // 001c9e04 public UInt32 AddressOfNameOrdinals; // 001cbbb0 }
At this point we are most interested at the following structure fields, AddressOfFunctions
, AddressOfNames
and AddressOfNameOrdinals
MOV EDX,DWORD PTR DS:[EBX+3C] ; EDX = DOS->e_lfanew ADD EDX,EBX ; EDX = PE Header MOV EDX,DWORD PTR DS:[EDX+78] ; EDX = Offset export table ADD EDX,EBX ; EDX = Export table MOV ESI,DWORD PTR DS:[EDX+20] ; ESI = Offset names table ADD ESI,EBX ; ESI = Names table XOR ECX,ECX ; EXC = 0
At the first line we will move the pointer to e_lfanew
to the edx
register at offset 0x3C
, because the size of the MS-DOS header is 0x40
bytes and the last 4 bytes are the e_lfanew
pointer. At the second line we add the value in edx
to the base address, because the pointer is relative to the base address.
At the third line, the offset 0x78
of the PE header holds the DataDirectory
for the exports. We know this because the size of all PE headers (Signature
, FileHeader
and OptionalHeader
) before the DataDirectory
is exactly 0x78
bytes and the export is the first entry in the DataDirectory
table. At the fourth line, we add this value to the edx
register and after that we should be placed on the export table of the kernelbase.dll
.
At the fifth line, in the IMAGE_EXPORT_DIRECTORY
structure, at the offset 0x20
, we can find the pointer to the AddressOfNames
, and from there we can get the exported function names. This is required because we try to find the function by its name even if it is possible using some other methods. Furthermore, we will save the pointer in the esi
register and set ecx
register to 0.
We are now located at the AddressOfNames
, an array of pointers ( relative to the image base address, which is the address where kernelbase.dll
is loaded into memory ). So each 4 bytes will represent a pointer to a function name. Moreover, we can find the function name, and the function name ordinal ( the number
of the GetProcAddress
function ) as shown below:
GetFunction: INC ECX ; increment counter LODSD ; Get name offset ADD EAX,EBX ; Get function name CMP dword [EAX], 0x50746547 ; "PteG" JNZ SHORT GetFunction ; jump to GetFunction label if not "GetP" CMP dword [EAX + 0x4], 0x41636F72 ; "Acor" JNZ SHORT GetFunction ; jump to GetFunction label if not "rocA" CMP dword [EAX + 0x8],0x65726464 ; "erdd" JNZ SHORT GetFunction ; jump to GetFunction label if not "ddre"
First we will increment ecx
register, which is the counter of the functions and the function ordinal number. Next we will use the esi
register as the pointer to the first function name. The lodsd
instruction will load in eax
the offset to the function name and then the value in eax
will be added to the ebx
( kernelbase
base address ) in order to find the correct pointer. Note that the lodsd
instruction will also increment the esi
register value by 4. This helps us because we do not have to increment it manually, we just need to call again lodsd
in order to get the next pointer which points to the next module.
Furthermore, the correct pointer to the exported function name has been loaded in the eax
register. Now we need to check if the exported function name is the GetProcAddress
. For that reason we compare the exported function name with 0x50746547
. This value is actually 50 74 65 47
in hex, which in little endian means "PteG" in ascii char format. So, we compare if the first 4 bytes of the current function name are "GetP". If they are not, jnz
instruction will jump again at our label GetFunction
and then will continue with the next function name. If it is, we will also check the next 4 bytes, which must be "Acor" and the next 4 bytes "erdd" until to be sure we do not find other function that starts with "GetP".
At this point we have only found the ordinal of the GetProcAddress
function. We will use the ordinal in order to find the actual address of the GetProcAddress
function:
MOV ESI,DWORD PTR DS:[EDX+24] ; ESI = Offset ordinals ADD ESI,EBX ; ESI = Ordinals table MOV CX,WORD PTR DS:[ESI+ECX*2] ; CX = Number of function DEC ECX ; Decrement the ordinal MOV ESI,DWORD PTR DS:[EDX+1C] ; ESI = Offset address table ADD ESI,EBX ; ESI = Address table MOV EDX,DWORD PTR DS:[ESI+ECX*4] ; EDX = Pointer(offset) ADD EDX,EBX ; EDX = GetProcAddress
At the first line above, in edx
we have a pointer to the IMAGE_EXPORT_DIRECTORY
structure. At offset 0x24
of the structure we can find the AddressOfNameOrdinals
offset. In second line, we add this offset to ebx
register which is the image base of the kernelbase.dll
so we get a valid pointer to the name ordinals table.
At the third line, the esi
register contains the pointer to the name ordinals array. This array contains two bytes. We have the name ordinal byte (index) of GetProcAddress
function in ecx
register, so this way we can get the function address ordinal (index). This will help us to get the function address. In fourth line we have to decrement the ordinal byte because the name ordinals starts from zero (0).
At the fifth line, at the offset 0x1c
we can find the AddressOfFunctions
, the pointer to the function pointer array. At the sixth line we just add the image base of kernelbase.dll
. Then we will be placed at the beginning of the array.
At seventh line, we have the correct index for the AddressOfFunctions
array in ecx
. There we have found the GetProcAddress
function pointer (relative to the image base) at the ecx
location. Furthermore, we use ecx * 4
because each pointer has 4 bytes and esi
points to the beginning of the array. In eighth line, we add the image base, so in the edx
register we will have the pointer to the GetProcAddress
function.
At this point we can see the full implementation we did so far from the previous blog posts until now
#include <windows.h>
int main(int argc, char* argv[])
{
LoadLibrary("user32.dll");
_asm
{
// Locate Kernelbase.dll address
XOR ECX, ECX // zero out ECX
MOV EAX, FS:[ecx + 0x30] // EAX = PEB
MOV EAX, [EAX + 0x0c] // EAX = PEB->Ldr
MOV ESI, [EAX + 0x14] // ESI = PEB->Ldr.InMemoryOrderModuleList
LODSD // memory address of the second list entry structure
XCHG EAX, ESI // EAX = ESI , ESI = EAX
LODSD // memory address of the third list entry structure
XCHG EAX, ESI // EAX = ESI , ESI = EAX
LODSD // memory address of the fourth list entry structure
MOV EBX, [EAX + 0x10] // EBX = Base address
// Export Table
MOV EDX, DWORD PTR DS:[EBX + 0x3C] //EDX = DOS->e_lfanew
ADD EDX, EBX //EDX = PE Header
MOV EDX, DWORD PTR DS:[EDX + 0x78] //EDX = Offset export table
ADD EDX, EBX //EDX = Export table
MOV ESI, DWORD PTR DS:[EDX + 0x20] //ESI = Offset names table
ADD ESI, EBX //ESI = Names table
XOR ECX, ECX //EXC = 0
GetFunction :
INC ECX //increment counter
LODSD //Get name offset
ADD EAX, EBX //Get function name
CMP[EAX], 0x50746547 //"PteG"
JNZ SHORT GetFunction //jump to GetFunction label if not "GetP"
CMP[EAX + 0x4], 0x41636F72 //"rocA"
JNZ SHORT GetFunction //jump to GetFunction label if not "rocA"
CMP[EAX + 0x8], 0x65726464 //"ddre"
JNZ SHORT GetFunction //jump to GetFunction label if not "ddre"
MOV ESI, DWORD PTR DS:[EDX + 0x24] //ESI = Offset ordinals
ADD ESI, EBX //ESI = Ordinals table
MOV CX, WORD PTR DS:[ESI + ECX * 2] //CX = Number of function
DEC ECX //Decrement the ordinal
MOV ESI, DWORD PTR DS:[EDX + 0x1C] //ESI = Offset address table
ADD ESI, EBX //ESI = Address table
MOV EDX, DWORD PTR DS:[ESI + ECX * 4] //EDX = Pointer(offset)
ADD EDX, EBX //EDX = GetProcAddress
// Get the Address of LoadLibraryA function
XOR ECX, ECX //ECX = 0
PUSH EBX //Kernelbase base address
PUSH EDX //GetProcAddress
PUSH ECX //0
PUSH 0x41797261 //"Ayra"
PUSH 0x7262694C //"rbiL"
PUSH 0x64616F4C //"daoL"
PUSH ESP //"LoadLibrary"
PUSH EBX //Kernelbase base address
MOV ESI, EBX //save the kernelbase address in esi for later
CALL EDX //GetProcAddress(LoadLibraryA)
}
return 0;
}
Furthermore, if we load the testasm.exe
in WinDbg debugger, we see that after the execution of the last instruction CALL EDX
, the eax
register will finally hold the return value from the GetProcAddress
function, which is the address of the LoadLobraryA
function.
This was the second part of the custom win32 reverse tcp shellcode development series. At this second part, we have achieved to be in a position to use the GetProcAddress
function from Kernelbase.dll
library. In conclusion, after reading this post, we understand that we are at the point where we can use the GetProcAddress
function, and this is a crucial part before we continue with the reverse tcp shellcode construction, as we will see in a later blog post. What is important here, is that we are now able to find the address of LoadLibraryA
function, which can help us loading other libraries where we can further use their functions. At the thirt part of this custom win32 reverse tcp shellcode development series, we will be focusing on the rest of the construction of the reverse tcp shellcode.
References