Resolving Symbols

Symbol resolution is the process of associating function names with corresponding memory addresses, without which we cannot make the necessary API calls.

As exploit developers, discovering the Virtual Memory Addresses (VMAs) of functions within a DLL module involves enumerating its DOS and PE headers. These headers provide essential information about the module's executable file structure. By analysing these headers, we can locate the precise VMAs of functions, enabling effective vulnerability identification and exploitation.

Using Windbg

The first step in finding the VMAs of functions is to find the index of those functions, to do that we need to find the name of the function in the Export Address Table (EAT).

We will do this using Windbg first, so it is clear what the shellcode is doing.

The EAT is a data structure found in the Portable Executable (PE) file format used by Windows operating systems. It is part of the DLL file and contains a list of exported functions and their corresponding memory addresses.

The EAT acts as a lookup table, allowing other modules or executables to easily access and utilise the functions provided by the DLL. When a DLL is loaded, the EAT provides a mechanism for resolving function addresses dynamically at runtime. This enables the calling module to invoke the DLL's exported functions without needing to know their memory addresses in advance.

The EAT also includes the names of the exported functions, which facilitates the identification and usage of specific functions within the DLL. By referencing the EAT, programs can dynamically link to and utilize the functions provided by the DLL, enhancing code modularity, reusability, and extensibility.

In the previous section we found the base address of kernel32.dll, we can use this address to find the EAT and the AddressOfNames linked list:

Load up any executable in Windbg, such as Notepad. When the program breaks we can input the following commands.

We can locate the AddressOfNames table manually in Windbg, by finding the DOS Header in kernel32.dll:

0:000> da kernel32
00007ffc`c5f90000  "MZ."

The DOS Header begins at offset 0x00 in the module (the signature is "MZ"). The next field of interest is an offset to the PE Header which is stored in e_lfanew field at an offset of 0x3c from the module base address. We can find this offset and then check it against the PE Header signature:

0:000> db kernel32+3c L1
00007ffc`c5f9003c  f8                                               
0:000> da kernel32+f8
00007ffc`c5f900f8  "PE"

At an offset of 0x88 from the PE header we will find another offset, this is the offset to the ETA. We refer to this as the ETA VMA:

0:000> dd kernel32+f8+88 L1
00007ffc`c5f90180  0009a370

The number of ordinals (functions) in the ETA is recorded at offset 0x14 from the address of the ETA, we will use this in the next section:

0:000> dd kernel32+9a370+14 L1
00007ffc`c602a384  00000661

The AddressOfNames table offset is stored at offset 0x20. This table is a sequential list of 4 byte offsets. The value that is stored in this entry is another offset, this time to the actual table:

0:000> dd kernel32+9a370+20 L1
00007ffc`c602a390  0009bd1c

The following depivts how we list the first 32 pointers to function names, then view the first two by using the base address and the offsets in the table:

0:000> dd kernel32+9bd1c
00007ffc`c602bd1c  0009e36f 0009e3a8 0009e3db 0009e3ea
00007ffc`c602bd2c  0009e3ff 0009e408 0009e411 0009e422
00007ffc`c602bd3c  0009e433 0009e478 0009e49e 0009e4bd
00007ffc`c602bd4c  0009e4dc 0009e4e9 0009e4fc 0009e514
00007ffc`c602bd5c  0009e52f 0009e544 0009e561 0009e5a0
00007ffc`c602bd6c  0009e5e1 0009e5f4 0009e601 0009e61b
00007ffc`c602bd7c  0009e639 0009e670 0009e6b5 0009e700
00007ffc`c602bd8c  0009e75b 0009e7b0 0009e803 0009e858
0:000> da kernel32+9e36f
00007ffc`c602e36f  "AcquireSRWLockExclusive"
0:000> da kernel32+9e3a8
00007ffc`c602e3a8  "AcquireSRWLockShared"

Now we understand how to resolve symbols in a module we can write the shellcode.

Shellcode

Let's break this down in to two small sections of shellcode. First we locate the AddressOfNames table, then we loop through the strings to find what we are looking for.

AddressOfNames

The shellcode is shown below:

Resolve symbols
; RBX = Base address of KERNEL32
; R9  = First 8 bytes of the function we want to find 
get_function:
    xor r8, r8                      ; R8 = 0
    mov r8d, [rbx + 0x3c]           ; R8D = DOS->e_lfanew offset
    mov rdx, r8                     ; RDX = DOS->e_lfanew
    add rdx, rbx                    ; RDX = PE Header

    add rdx, 0x88                   ; add 0x88 to RDX to avoid null bytes

    mov r8d, [rdx]                  ; R8D = Offset to EAT
    add r8, rbx                     ; R8 = EAT
    
    xor rsi, rsi                    ; Clear RSI
    mov esi, [r8 + 0x20]            ; RSI = Offset AddressOfNames table
    add rsi, rbx                    ; RSI = AddressOfNames table
    xor rcx, rcx                    ; RCX = 0, this is used as the ordinal/index

The shellcode corresponds to what we carried out manually using Windbg, it:

  • Locates the PE signature (NT Header), on lines 4 through 7.

  • Locates the _IMAGE_DATA_DIRECTORY pointer by adding 0x88 to the address of the NT Header; line 9.

  • Locates the EAT, on lines 11 and 11.

  • Locates the AddressOfNames table, lines 14 through 16.

  • Sets the index for our loop to 0 on line 17.

Essentially the shellcode ends with the address of the AddressOfNames table in rsi and is ready to traverse the table to find the function we require.

Note: Line 9 produces NULL bytes, this will be dealt with later.

Next Function Name Loop

The next piece of shellcode loops over the AddressOfNames table

Next function loop
next_function_name:
    inc rcx                         ; Increment the ordinal
    
    xor rax, rax                    ; RAX = 0
    mov eax, [rsi + rcx * 4]        ; Get string offset (function name)
    add rax, rbx                    ; RAX = function name
    
    cmp qword [rax], r9             ; Does it match the function name in R9 ?
    jnz next_function_name          ; Loop if it doesn't

The shellcode carries out the following:

  • Increments the ordinal/index on line 2.

  • Moves the next string offset in the AddressOfNames table in to eax (lower 8 bytes) on line 5 and adds the base address of the module (kernel32.dll in rbx) to rax which gives us the address of the name; line 6.

  • Line 8 compares the name we pass in via r9 to the current name in the table.

  • If they match then exit the loop, otherwise jump back to next_function_name and loop.

At the end of the loop we should have the ordinal of the function address we require in rcx. We will use this in the next section.

Calling GetFunction

We can call the function using the following shellcode. Notice that we only put the first eight bytes of the function name we are looking for in to r9, this is sufficient for our purposes (but be aware of this). Line 3 makes the call and line 4 stores the address of the found function in a variable offset from rbp:

Calling GetFunction
get_getprocaddress:
    mov r9, 0x41636f7250746547      ; GetProcA (in ASCII AcorPteG)
    call QWORD [rbp-0x20]           ; CALL get_function
    mov [rbp-0x18], rdi             ; [RBP-0x18] = *GetProcAddress

Note: we have not resolved the functions Virtual Memory Addresses yet (we have only found the ordinal/index of the function), this follows in the next section.

Last updated