Home of the Plackyhacker

Plackyhacker Home Posts Contact

Home > Posts > 64-bit Custom Shellcode Part 2

64-bit Custom Shellcode Part 2


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 depicts 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:

; 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:

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_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: