Home of the Plackyhacker
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.
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.
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.
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:
4
through 7
._IMAGE_DATA_DIRECTORY
pointer by adding 0x88
to the address of the NT Header; line 9
.11
and 12
.AddressOfNames
table, lines 14
through 16
.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.
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:
2
.AddressOfNames
table in to eax
(lower 8 bytes) on line 5
and:kernel32.dll
in rbx
) to rax
which gives us the address of the name on line 6
.8
compares the name we pass in via r9
to the current name in the table.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.
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
:
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.
Upon obtaining the function ordinal, our next objective is to convert it into its corresponding Virtual Memory Address, as this is essential for invoking Win32 APIs.
In the last section we were able to find functions in a module by name, this is only the first step in resolving the Virtual Memory Address (VMA) of a function. In order to call a function we need to know its VMA.
A VMA is a concept used in computer systems, particularly in the context of virtual memory management. It refers to a unique address or location in the virtual memory space of a process.
Virtual memory allows programs to operate on a larger address space than the physical memory (RAM) available in the system.
The Export Address Table (EAT) contains three lists:
When we looped over the AddressOfNames
table we found a match for our function name that we wanted to resolve.
We can now find that index in the AddressOfNameOrdinals
which will point to the AddressOfFunctions
table which contains pointers to the VMA of that function.
We can continue from where we left off in the previous section (the addresses may have changed but the offsets are the same).
We can see in the output below that third entry in the EAT is ActivateActCtx
at offset 0x9e3e5
in the AddressOfNames
table:
0:006> dd kernel32+9a370+20 L1
00007ffc`ebf4a390 0009bd20
0:006> dd kernel32+0009bd20
00007ffc`ebf4bd20 0009e379 0009e3b2 0009e3e5 0009e3f4
00007ffc`ebf4bd30 0009e409 0009e412 0009e41b 0009e42c
00007ffc`ebf4bd40 0009e43d 0009e482 0009e4a8 0009e4c7
00007ffc`ebf4bd50 0009e4e6 0009e4f3 0009e506 0009e51e
00007ffc`ebf4bd60 0009e539 0009e54e 0009e56b 0009e5aa
00007ffc`ebf4bd70 0009e5eb 0009e5fe 0009e60b 0009e625
00007ffc`ebf4bd80 0009e643 0009e67a 0009e6bf 0009e70a
00007ffc`ebf4bd90 0009e765 0009e7ba 0009e80d 0009e862
0:006> da kernel32+9e3e5
00007ffc`ebf4e3e5 "ActivateActCtx"
Next we can take a look at the AddressOfNameOrdinals
table. We can see that the third entry is the same index that the ActivateActCtx
is located at, so this is easy to resolve:
0:006> dd kernel32+9a370+24 L1
00007ffc`ebf4a394 0009d6a8
0:006> dw kernel32+9d6a8
00007ffc`ebf4d6a8 0000 0001 0002 0003 0004 0005 0006 0007
00007ffc`ebf4d6b8 0008 0009 000a 000b 000c 000d 000e 000f
00007ffc`ebf4d6c8 0010 0011 0012 0013 0014 0015 0016 0017
00007ffc`ebf4d6d8 0018 0019 001a 001b 001c 001d 001e 001f
00007ffc`ebf4d6e8 0020 0021 0022 0023 0024 0025 0026 0027
00007ffc`ebf4d6f8 0028 0029 002a 002b 002c 002d 002e 002f
00007ffc`ebf4d708 0030 0031 0032 0033 0034 0035 0036 0037
00007ffc`ebf4d718 0038 0039 003a 003b 003c 003d 003e 003f
Finally we can look at the AddressOfFunctions
table at offset 0x1c
.
We can then look at the third entry to get the offset address of 0x1be90
. If we uncompile this we find that we now have the VMA of KERNEL32!ActivateActCtxWorker
:
0:006> dd kernel32+9a370+1c L1
00007ffc`ebf4a38c 0009a398
0:006> dd kernel32+9a398+0c L1
00007ffc`ebf4a3a4 0001be90
0:006> u kernel32+0001be90
KERNEL32!ActivateActCtxWorker:
00007ffc`ebecbe90 4883ec28 sub rsp,28h
00007ffc`ebecbe94 4883f9ff cmp rcx,0FFFFFFFFFFFFFFFFh
00007ffc`ebecbe98 0f846a940100 je KERNEL32!ActivateActCtxWorker+0x19478 (00007ffc`ebee5308)
00007ffc`ebecbe9e 4c8bc2 mov r8,rdx
00007ffc`ebecbea1 488bd1 mov rdx,rcx
00007ffc`ebecbea4 33c9 xor ecx,ecx
00007ffc`ebecbea6 48ff157b790600 call qword ptr [KERNEL32!_imp_RtlActivateActivationContext (00007ffc`ebf33828)]
00007ffc`ebecbead 0f1f440000 nop dword ptr [rax+rax]
Now that we have resolved a VMA manually we can write some shellcode to do this automatically and on demand.
The shellcode to resolve the VMA of a function is shown below:
; RCX contains the ordinal of the function we want to resolve
; RBX = Base address of KERNEL32
; R8 = VMA of the Export Address Table
found_function:
xor rsi, rsi ; RSI = 0
mov esi, [r8 + 0x24] ; ESI = Offset to AddressOfNameOrdinals
add rsi, rbx ; RSI = VMA of AddressOfNameOrdinals
mov cx, [rsi + rcx * 2] ; The number of the function
xor rsi, rsi ; RSI = 0
mov esi, [r8 + 0x1c] ; Offset to AddressOfFunctions
add rsi, rbx ; RSI = VMA of AddressOfFunctions
xor rdx, rdx ; RDX = 0
mov edx, [rsi + rcx * 4] ; EDX = Index of the function VMA needed
add rdx, rbx ; RDX = Function Address
mov rdi, rdx ; Save Function Address in RDI
ret ;
The shellcode corresponds to what we carried out manually using Windbg, it:
AddressOfNamesOrdinals
on lines 5
through 7
.9
. Notice that the table entries are 2 bytes in size.11
through 13
are used to locate the AddressOfFunctions
table.16
.kernel32.dll
on line 17
.rdi
and the function returns.In the next post we will put everything we have learned together to create shellcode that displays a message box.