Reflective DLL injection is one of the most common techniques used for loading code in memory. Most c2 frameworks are using some variation of this code to reflectively and dynamically load additional functionality (, ).
It is also common for these frameworks to provide SRDI functionality which is basically the same code as described in this article, turned into assembly and appended (or prepended) to the dll bytes. That provides more flexibility since the code could be handled the same way as any PIC shellcode.
Background knowledge
In an ideal scenario we would load the dll in memory, and point execution to the entry point and that would be the end of it. Unfortunately, it's not that simple and quite a few steps are required before executing the dll.
The code needs to be broken into 3 parts.
1. RAW offsets -> RVA
When the dll is on disc the different sections are addressed with the raw offset but when the dll is in memory the different sections are addressed using the RVA. Let's take a look into the.
In this case we are particularly interested in lines 4,5 and 6.
Let's see the definitions from Microsoft's documentation:
VirtualAddress
For executable images, the address of the first byte of the section relative to the image base when the section is loaded into memory. For object files, this field is the address of the first byte before relocation is applied; for simplicity, compilers should set this to zero. Otherwise, it is an arbitrary value that is subtracted from offsets during relocation.
SizeOfRawData
The size of the section (for object files) or the size of the initialized data on disk (for image files). For executable images, this must be a multiple of FileAlignment from the optional header. If this is less than VirtualSize, the remainder of the section is zero-filled. Because the SizeOfRawData field is rounded but the VirtualSize field is not, it is possible for SizeOfRawData to be greater than VirtualSize as well. When a section contains only uninitialized data, this field should be zero.
PointerToRawData
The file pointer to the first page of the section within the COFF file. For executable images, this must be a multiple of FileAlignment from the optional header. For object files, the value should be aligned on a 4-byte boundary for best performance. When a section contains only uninitialized data, this field should be zero.
So in summary the PointerToRawData points to the data when the dll is at rest, the SizeOfRawData is the actual size of the data pointed to by the PointerToRawData and the VirtualAddress is the Relative address from the base address to be used during execution. When the dll is loaded in memory the image size expands and therefore any additional space will be filled with 0s. To better understand the above concepts we can take a look at PE bear for a visual representation of the dll on disk and in memory.
The sections on the left represent the dll on disk while the data on the right represent the dll in memory. The grey padding in between sections is zero-filled.
2. Relocations
When a DLL is loaded into memory it's base address is the same as the Image Base address from the optional header right? Nope, Wrong. Ever since the introduction of the ASLR (dll base addresses are randomised by the OS) it is highly improbable for a dll to be loaded at its preferred address.
We could try allocate the specific memory using VirtualAlloc, but it could potentially fail if the memory is already reserved. If we fail to allocate the preferred address the dll execution will fail since the compiler hardcodes addresses in the dll at compile time.
Thankfully for us the hardcoded values are documented in the .reloc section of the executable. Let's have a quick look in the following c structures.
We are interested in the PageAddress and the Offset. Adding these two should give us the RVA of the address that needs to be relocated.
Let's go back to PE-Bear again in order to understand exactly what we are supposed to do.
We can see that we have a relocation at page with RVA 0x2000. The offset of the relocation is at 0x558. That gives us the relocation RVA of 0x2558 (0x2000+0x558).
At the very top we can see the memory that has the value 40 25 40 AE 03. That is essentially an address pointer 0x03ae722540.
From the optional header we can see that the preferred image Base of the DLL is 0x3ae720000.
To get the correct value we need to perform the following calculation:
0x03ae722540 - 0x3ae720000 which will give us the RVA and add the new dll base address returned from VirtualAlloc.
We then have to loop through all pages and modify all relocations for all pages.
3. Import Directory
type IMAGE_IMPORT_DESCRIPTOR struct {
Characteristics uint32
TimeDateStamp uint32
ForwarderChain uint32
Name uint32
FirstThunk uint32
}
So in the .idata section we can find an array of IMAGE_IMPORT_DESCRIPTORS, one for each external dll requirement. This is how it looks in pe-bear.
So what we are interested is the FirstThunk. That is an RVA pointing to an array of _IMAGE_THUNK_DATA
typedef struct _IMAGE_THUNK_DATA {
union {
uint32_t* Function; // address of imported function
uint32_t Ordinal; // ordinal value of function
PIMAGE_IMPORT_BY_NAME AddressOfData; // RVA of imported name
DWORD ForwarderStringl // RVA to forwarder string
} u1;
} IMAGE_THUNK_DATA, *PIMAGE_THUNK_DATA;
When looking at the struct in memory we come across another RVA which essentially points to the name of the function.
So by navigating the IMAGE_IMPORT_DESCRIPTOR and the _IMAGE_THUNK_DATA we can get the names (or ordinals) of the functions we need to import and add the memory addresses at the location of the first thunk and then increase by 8 bytes in a loop for all subsequent functions.
The following image demonstrates how the end result will look like after the import addresses are written in the IAT:
Code analysis
DLL code
All this code does is to display a MessageBox when the dll is attached to a process. The only exported function is DllMain.
To compile it simply run gcc -shared -o mydll.dll dll.c
#include <windows.h>
#define DLLEXPORT __declspec( dllexport )
DLLEXPORT BOOL DllMain( HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved);
BOOL APIENTRY DllMain( HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) {
switch (ul_reason_for_call) {
case DLL_PROCESS_ATTACH:
MessageBoxA(NULL, "DLL PROCESS ATTACH", "Bingo!", 0);
break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
default:
break;
}
return TRUE;
}
1. Read DLL contents
This can be modified so the bytes can be received from the internet, but for the sake of simplicity we just used the ReadFile function to read the contents of the dll in a byte slice.
We then get a pointer to the first byte of the slice to use as the base address.
dllBytes, err := os.ReadFile("mydll.dll")
if err != nil {
log.Fatalf("Failed to open file %v", err)
}
dllPtr := uintptr(unsafe.Pointer(&dllBytes[0]))
Once the dll is loaded to memory we can then go ahead and capture the contents of the PE headers.
The headers hold a lot of information that will be useful throughout our program. To cast the dll pointer to the above structure the following code is used:
From now on instead of having to read contents of the memory we can use this struct to get our desired values.
2. Allocate memory for the dll
As discussed in the previous section we will have to rearrange the layout of the dll to match the expected RVA offsets. In order to do that we will used VirtualAlloc Windows API. In order to to avoid having the highly suspicious RWX permissions we will first assign the RW permissions and then change the permissions of the .text section to RX.
The allocated memory will have the size specified in the OptionalHeader. We also specify the desired address from the optional header. If the memory is being reserved, the specified address is rounded down to the nearest multiple of the allocation granularity. If the memory is already reserved and is being committed, the address is rounded down to the next page boundary.
The first step is to copy the headers of the dll to our newlly allocated memory. The headers start from offset 0 and the size can be found in the optional header (SizeOfHeaders).
var numberOfBytesWritten uintptr
err = windows.WriteProcessMemory(windows.CurrentProcess(),
dllBase,
&dllBytes[0],
uintptr(nt_header.OptionalHeader.SizeOfHeaders),
&numberOfBytesWritten)
if err != nil {
log.Fatalf("[!] WriteProcessMemory Failed")
}
4. Copy the DLL sections
Firstly we need to identify how many sections there are in our dll. The IMAGE_FILE_HEADER holds that value in the NumberOfSection variable.
We then go ahead and calculate the address of our section headers.
We then create a for loop to read the section header entries and copy each section to the right location. Firstly we have to define the IMAGE_SECTION_HEADER struct
A few nested loops are used in order to capture the relocation blocks and the base relocation entries. Before we get into the code let's define a few structures first and a few helper functions.
type BASE_RELOCATION_BLOCK struct {
PageAddress uint32
BlockSize uint32
}
// BASE_RELOCATION_ENTRY represents the base relocation entry structure
type BASE_RELOCATION_ENTRY struct {
OffsetType uint16 // Combined field for Offset and Type
}
// Offset extracts the Offset from the combined field
func (bre BASE_RELOCATION_ENTRY) Offset() uint16 {
return bre.OffsetType & 0xFFF
}
// Type extracts the Type from the combined field
func (bre BASE_RELOCATION_ENTRY) Type() uint16 {
return (bre.OffsetType >> 12) & 0xF
}
The BASE_RELOCATION_ENTRY is not defined the same way it would in C (see below). Since we don't have bit level control to define the last 4 bits we go ahead and combine the two values in a sturct and use the Offset() and Type() helper functions to extract the desired values.
Line 3: Calculates the address of the first relocation block
Line 4: Calculates the address of the first relocation entry
Line 8: Calculates how many relocations entries in the block
Line 11-15 : Creates a slice of BASE_RELOCATION_ENTRY and fills it in from the data on memory
Line 16: Loops through the contents of the slice
Line 23: Calculates the relocationRVA
Line 25: Reads the contents of the memory from memory
Line 29-31: Turns the byte slice to uintptr, Calculates the new address and turn it back to byte slice
Line 32-35: Writes the new address back to memory
6. Imports
The last piece of the puzzle is to fill in the addresses of the imported functions in IAT. Similarly with the relocations section, we have to find the address of our section from the import header.
Let's define our constant for the Data Directory index
IMAGE_DIRECTORY_ENTRY_IMPORT = 0x1
Also the Image Import Descriptor struct has to be defined
type IMAGE_IMPORT_DESCRIPTOR struct {
Characteristics uint32
TimeDateStamp uint32
ForwarderChain uint32
Name uint32
FirstThunk uint32
}
We then go ahead and calculate the address from the RVA
Line 2: Casting the pointer to IMAGE_IMPORT_DESCRIPTOR
Lines 6-8: Get the name of the dll to load. We can get the DLL name RVA from the importDescriptor.Name. We then get the name from the byte pointer located in the RVA.
Lines 9-12: Load the dll using windows API LoadLibrary
Line 13: Points to the thunk of the first imported function
Line 16: We then read the first 2 bytes to get the RVA to the Hint/Name of the function
Lines 20: We add 2 to the RVA to skip the hint and point directly to the Name bytes
Line 22: Get the Name of the function
Lines 23-27: Get the address of the function using the GetProcAddress API
Line 28: Turn the uintptr to a byte slice to be used in WriteProcessMemory
Lines 30-34: Overwrite the contents of the thunk with the address of the function.
Line 35: Adds 0x8 bytes to jump to the next thunk
Line 38: Adds 0x14 bytes to jump to the next image descriptor
7. Execution
With everything in place we can now use the SyscallN function to execute our code. We first define the DLL_PROCESS_ATTACH constant
DLL_PROCESS_ATTACH = 0x1
syscall.SyscallN(dllBase+uintptr(nt_header.OptionalHeader.AddressOfEntryPoint),
dllBase,
DLL_PROCESS_ATTACH,
0)
fmt.Println("[+] DLL function executed")
err = windows.VirtualFree(dllBase, 0x0, windows.MEM_RELEASE)
if err != nil {
log.Fatalln("[ERROR] Failed to Free Memory")
}
fmt.Printf("[+] Freed Memory at 0x%x\n", dllBase)
Lines 1-4: We run the SyscallN function.
The first argument is the address to be executed in our case the entry point of the DLL, In the future we can go through the exports table and run any exported function
We then have to provide the base address of the dll
We then have to define the DLL_PROCESS_ATTACH in order to execute
Line 6: We perform some clean up actions after we are done executing the dll
Complete Code
package main
import (
"encoding/binary"
"fmt"
"log"
"os"
"syscall"
"unsafe"
"golang.org/x/sys/windows"
)
type IMAGE_NT_HEADERS64 struct {
Signature uint32
FileHeader IMAGE_FILE_HEADER
OptionalHeader IMAGE_OPTIONAL_HEADER64
}
// IMAGE_OPTIONAL_HEADER64 represents the optional header for 64-bit architecture.
type IMAGE_OPTIONAL_HEADER64 struct {
Magic uint16
MajorLinkerVersion uint8
MinorLinkerVersion uint8
SizeOfCode uint32
SizeOfInitializedData uint32
SizeOfUninitializedData uint32
AddressOfEntryPoint uint32
BaseOfCode uint32
ImageBase uint64
SectionAlignment uint32
FileAlignment uint32
MajorOperatingSystemVersion uint16
MinorOperatingSystemVersion uint16
MajorImageVersion uint16
MinorImageVersion uint16
MajorSubsystemVersion uint16
MinorSubsystemVersion uint16
Win32VersionValue uint32
SizeOfImage uint32
SizeOfHeaders uint32
CheckSum uint32
Subsystem uint16
DllCharacteristics uint16
SizeOfStackReserve uint64
SizeOfStackCommit uint64
SizeOfHeapReserve uint64
SizeOfHeapCommit uint64
LoaderFlags uint32
NumberOfRvaAndSizes uint32
DataDirectory [16]IMAGE_DATA_DIRECTORY
}
// IMAGE_DATA_DIRECTORY represents a data directory entry.
type IMAGE_DATA_DIRECTORY struct {
VirtualAddress uint32
Size uint32
}
// IMAGE_FILE_HEADER represents the file header in the IMAGE_NT_HEADERS structure.
type IMAGE_FILE_HEADER struct {
Machine uint16
NumberOfSections uint16
TimeDateStamp uint32
PointerToSymbolTable uint32
NumberOfSymbols uint32
SizeOfOptionalHeader uint16
Characteristics uint16
}
type IMAGE_SECTION_HEADER struct {
Name [8]byte
VirtualSize uint32
VirtualAddress uint32
SizeOfRawData uint32
PointerToRawData uint32
PointerToRelocations uint32
PointerToLinenumbers uint32
NumberOfRelocations uint16
NumberOfLinenumbers uint16
Characteristics uint32
}
type BASE_RELOCATION_BLOCK struct {
PageAddress uint32
BlockSize uint32
}
// BASE_RELOCATION_ENTRY represents the base relocation entry structure
type BASE_RELOCATION_ENTRY struct {
OffsetType uint16 // Combined field for Offset and Type
}
// Offset extracts the Offset from the combined field
func (bre BASE_RELOCATION_ENTRY) Offset() uint16 {
return bre.OffsetType & 0xFFF
}
// Type extracts the Type from the combined field
func (bre BASE_RELOCATION_ENTRY) Type() uint16 {
return (bre.OffsetType >> 12) & 0xF
}
type IMAGE_IMPORT_DESCRIPTOR struct {
Characteristics uint32
TimeDateStamp uint32
ForwarderChain uint32
Name uint32
FirstThunk uint32
}
func uintptrToBytes(ptr uintptr) []byte {
// Create a pointer to the uintptr value
ptrPtr := unsafe.Pointer(&ptr)
// Convert the pointer to a byte slice
byteSlice := make([]byte, unsafe.Sizeof(ptr))
for i := 0; i < int(unsafe.Sizeof(ptr)); i++ {
byteSlice[i] = *(*byte)(unsafe.Pointer(uintptr(ptrPtr) + uintptr(i)))
}
return byteSlice
}
const (
IMAGE_DIRECTORY_ENTRY_IMPORT = 0x1
IMAGE_DIRECTORY_ENTRY_BASERELOC = 0x5
DLL_PROCESS_ATTACH = 0x1
)
func main() {
fmt.Println()
dllBytes, err := os.ReadFile("mydll.dll")
if err != nil {
log.Fatalf("Failed to open file %v", err)
}
dllPtr := uintptr(unsafe.Pointer(&dllBytes[0]))
fmt.Printf("[+] DLL of size %d is loaded in memory at 0x%x\n", len(dllBytes), dllPtr)
e_lfanew := *((*uint32)(unsafe.Pointer(dllPtr + 0x3c)))
nt_header := (*IMAGE_NT_HEADERS64)(unsafe.Pointer(dllPtr + uintptr(e_lfanew)))
dllBase, err := windows.VirtualAlloc(uintptr(nt_header.OptionalHeader.ImageBase),
uintptr(nt_header.OptionalHeader.SizeOfImage),
windows.MEM_RESERVE|windows.MEM_COMMIT,
windows.PAGE_EXECUTE_READWRITE,
)
if err != nil {
log.Fatalf("[!] VirtualAlloc Failed")
}
fmt.Printf("[+] Allocated address at 0x%x\n\n", dllBase)
var numberOfBytesWritten uintptr
err = windows.WriteProcessMemory(windows.CurrentProcess(), dllBase, &dllBytes[0], uintptr(nt_header.OptionalHeader.SizeOfHeaders), &numberOfBytesWritten)
if err != nil {
log.Fatalf("[!] WriteProcessMemory Failed")
}
numberOfSections := int(nt_header.FileHeader.NumberOfSections)
var sectionAddr uintptr
sectionAddr = dllPtr + uintptr(e_lfanew) + unsafe.Sizeof(nt_header.Signature) + unsafe.Sizeof(nt_header.OptionalHeader) + unsafe.Sizeof(nt_header.FileHeader)
for i := 0; i < numberOfSections; i++ {
section := (*IMAGE_SECTION_HEADER)(unsafe.Pointer(sectionAddr))
sectionDestination := dllBase + uintptr(section.VirtualAddress)
sectionBytes := (*byte)(unsafe.Pointer(dllPtr + uintptr(section.PointerToRawData)))
fmt.Printf("[+] Copying %d bytes from 0x%x -> 0x%x for section : %s", section.SizeOfRawData, dllPtr+uintptr(section.PointerToRawData), sectionDestination, windows.ByteSliceToString(section.Name[:]))
err = windows.WriteProcessMemory(windows.CurrentProcess(), sectionDestination, sectionBytes, uintptr(section.SizeOfRawData), &numberOfBytesWritten)
if err != nil {
log.Fatalf("[!] WriteProcessMemory Failed: %v \n", err)
}
fmt.Printf(" ... Bytes 0x%x/0x%x Written\n", section.SizeOfRawData, numberOfBytesWritten)
if windows.ByteSliceToString(section.Name[:]) == ".text" {
var oldprotect uint32
err := windows.VirtualProtect(sectionDestination, uintptr(section.SizeOfRawData), windows.PAGE_EXECUTE_READ, &oldprotect)
if err != nil {
log.Fatalln("[ERROR] Failed to change memory permissions")
}
}
sectionAddr += unsafe.Sizeof(*section)
}
fmt.Println()
relocations := nt_header.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC]
relocation_table := uintptr(relocations.VirtualAddress) + dllBase
fmt.Printf("[+] Relocation table Address: 0x%x\n\n", relocation_table)
var relocations_processed int = 0
deltaImageBase := dllBase - uintptr(nt_header.OptionalHeader.ImageBase)
for {
relocation_block := *(*BASE_RELOCATION_BLOCK)(unsafe.Pointer(uintptr(relocation_table + uintptr(relocations_processed))))
relocEntry := relocation_table + uintptr(relocations_processed) + 8
if relocation_block.BlockSize == 0 && relocation_block.PageAddress == 0 {
break
}
relocationsCount := (relocation_block.BlockSize - 8) / 2
fmt.Printf("[+] PAGERVA : 0x%04x Size: 0x%02x Entries Count: 0x%02x\n", relocation_block.PageAddress, relocation_block.BlockSize, relocationsCount)
relocationEntries := make([]BASE_RELOCATION_ENTRY, relocationsCount)
for i := 0; i < int(relocationsCount); i++ {
relocationEntries[i] = *(*BASE_RELOCATION_ENTRY)(unsafe.Pointer(relocEntry + uintptr(i*2)))
}
for _, relocationEntry := range relocationEntries {
if relocationEntry.Type() == 0 {
continue
}
fmt.Printf(" --> Value: %X Offset: %x\n", relocationEntry.OffsetType, relocationEntry.Offset())
var size uintptr
byteSlice := make([]byte, unsafe.Sizeof(size))
relocationRVA := relocation_block.PageAddress + uint32(relocationEntry.Offset())
err = windows.ReadProcessMemory(windows.CurrentProcess(), dllBase+uintptr(relocationRVA), &byteSlice[0], unsafe.Sizeof(size), nil)
if err != nil {
log.Fatalf("[ERROR] Failed to ReadProcessMemory")
}
addressToPatch := uintptr(binary.LittleEndian.Uint64(byteSlice))
addressToPatch += deltaImageBase
a2Patch := uintptrToBytes(addressToPatch)
err = windows.WriteProcessMemory(windows.CurrentProcess(), dllBase+uintptr(relocationRVA), &a2Patch[0], uintptr(len(a2Patch)), nil)
if err != nil {
log.Fatalf("[ERROR] Failed to WriteProcessMemory")
}
}
relocations_processed += int(relocation_block.BlockSize)
}
//time.Sleep(10 * time.Second)
importsDirectory := nt_header.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]
importDescriptorAddr := dllBase + uintptr(importsDirectory.VirtualAddress)
fmt.Printf("[+] Import Descripton address: 0x%x\n\n", importDescriptorAddr)
for {
importDescriptor := *(*IMAGE_IMPORT_DESCRIPTOR)(unsafe.Pointer(importDescriptorAddr))
if importDescriptor.Name == 0 {
break
}
libraryName := uintptr(importDescriptor.Name) + dllBase
dllName := windows.BytePtrToString((*byte)(unsafe.Pointer(libraryName)))
fmt.Printf("[+] Importing DLL : %s\n", dllName)
hLibrary, err := windows.LoadLibrary(dllName)
if err != nil {
log.Fatalln("[ERROR] LoadLibrary Failed")
}
addr := dllBase + uintptr(importDescriptor.FirstThunk)
//char := dllBase + uintptr(importDescriptor.Characteristics)
//fmt.Printf("First Thunk: 0%x\n", addr)
//fmt.Printf("Chars: 0%x\n", char)
for {
thunk := *(*uint16)(unsafe.Pointer(addr))
if thunk == 0 {
break
}
functionNameAddr := dllBase + uintptr(thunk+2)
functionName := windows.BytePtrToString((*byte)(unsafe.Pointer(functionNameAddr)))
proc, err := windows.GetProcAddress(hLibrary, functionName)
if err != nil {
log.Fatalln("[ERROR] Failed to GetProcAddress")
}
fmt.Printf(" --> Importing Function %s -> Addr: 0x%x\n", functionName, proc)
procBytes := uintptrToBytes(proc)
// https://reverseengineering.stackexchange.com/questions/16870/import-table-vs-import-address-table
var numberOfBytesWritten uintptr
err = windows.WriteProcessMemory(windows.CurrentProcess(), addr, &procBytes[0], uintptr(len(procBytes)), &numberOfBytesWritten)
if err != nil {
log.Fatalln("[ERROR] Failed to WriteProcessMemory")
}
addr += 0x8
}
importDescriptorAddr += 0x14
}
//fmt.Printf("BreakPoint %x", dllBase+0x1251)
//time.Sleep(time.Second * 10)
syscall.SyscallN(dllBase+uintptr(nt_header.OptionalHeader.AddressOfEntryPoint), dllBase, DLL_PROCESS_ATTACH, 0)
fmt.Println("[+] DLL function executed")
err = windows.VirtualFree(dllBase, 0x0, windows.MEM_RELEASE)
if err != nil {
log.Fatalln("[ERROR] Failed to Free Memory")
}
fmt.Printf("[+] Freed Memory at 0x%x\n", dllBase)
}
References
The last obstacle to executing the dll is to dynamically resolve the Import addresses and modify the IAT to include them. The answer in this article provides a very good explanation of the process.
A compiled version of the dll can be find on my
As mentioned, copying sections is not straight forward. We will have to turn RAW offsets to RVAs and fill the resulting memory gaps with zeros. Thankfully for us VirtualAlloc allocates a page of zeros therefore we only have to write the sections at the correct offset.