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.

RAW vs Virtual

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.

Relocations

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.

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.

IMAGE_IMPORT_DESCRIPTORs

So what we are interested is the FirstThunk. That is an RVA pointing to an array of _IMAGE_THUNK_DATA

When looking at the struct in memory we come across another RVA which essentially points to the name of the function.

Names of Imported functions

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

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.

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.

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

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.

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

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

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.

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.

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.

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.

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

Also the Image Import Descriptor struct has to be defined

We then go ahead and calculate the address from the RVA

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

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

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

DLL executed successfully

Complete Code

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

Was this helpful?