How C2 Works In-depth
Today's Table of Content [Part 2]
Second Part:
- Reflective Loading & Reflective DLL Injection ( RDI )
- What it's happening in the RDI Code - Walkthrough
- Important Notes About the RDI
- How payload loaded into victim's memory
- How is the ReflectiveLoader Function Called in a DLL
Reflective Loading
Introduction to Reflective Loading:
Malicious software and DLLs as soon as saved on computer hard drives made it clean for Endpoint Detection and Response ( EDR ) systems to locate them. Stephen Fewer introduced Reflective DLL Injection ( RDI ) around 2010, converting how those threats function. RDI allows DLLs to inject into a program's memory without touching the tough force, making it tougher for EDR structures to spot the intrusion. This approach has drastically impacted cybersecurity, forcing both defenders and attackers to evolve their strategies. Stephen Fewer's work on RDI has been pivotal in advancing digital security measures. Now, that specialize in Command and Control ( C2 ) operations, let's explore how something like Meterpreter executes a DLL with out detection.
Reflective DLL loading is a technique used by attackers to load a dynamic-link library ( DLL ) directly from memory instead of loading it from the disk. Unlike conventional DLL loading, where Windows handles the loading process, reflective DLL injection involves bypassing standard Windows functions, potentially allowing attackers to evade detection.
Source code: https://github.com/rapid7/ReflectiveDLLInjection/
This image will make us understand correctly the Reflective Loading explanation.
The process of reflective DLL injection unfolds as follows:
-
Accessing the Target Process: The attacker gains access to the target process with read-write-execute permissions. This access enables the attacker to manipulate the process's memory.
Why do we use the OpenProcess API for it !? This API is used to obtain a handle to the target process, which is required to perform operations such as memory allocation and thread creation within that process.
MSDN Signature:HANDLE OpenProcess( [in] DWORD dwDesiredAccess, [in] BOOL bInheritHandle, [in] DWORD dwProcessId );
Explanation:
- dwDesiredAccess: This defines the type of access to the process. It's checked against the process's security settings. You can use different access rights here. If you have the SeDebugPrivilege privilege, you get access no matter what the security settings say.
- bInheritHandle: When TRUE, new processes created by this one inherit the handle. If FALSE, they don't.
dwProcessId
: This is the ID of the process you want to open. Opening the System Idle Process ( 0x00000000 ) fails with ERROR_INVALID_PARAMETER. Trying to open the System process or a Client Server Run-Time Subsystem ( CSRSS ) process fails with ERROR_ACCESS_DENIED due to their high security.
-
Memory Allocation: Within the target process, the attacker allocates a section of memory that is spacious enough to accommodate the entire DLL. This allocated memory space serves as a staging area for the malicious code.
Why do we use the VirtuallAllocEx API? To create a buffer within the target process's address space, which will be used to store the DLL before it's executed.
MSDN Signature:LPVOID VirtualAllocEx( [in] HANDLE hProcess, [in] LPVOID lpAddress, [in] SIZE_T dwSize, [in] DWORD flAllocationType, [in] DWORD flProtect );
Explanation:-
hProcess: Handle to the process where memory will be allocated. It requires PROCESS_VM_OPERATION access. Details on access rights can be found in Process Security and Access Rights documentation.
-
lpAddress ( optional ): Suggests a preferred starting address for memory allocation.
- For reserving memory, the function adjusts this address to the nearest allocation granularity.
- For committing reserved memory, it adjusts to the nearest page boundary.
- Use GetSystemInfo to find page size and allocation granularity.
- If NULL, the function chooses the allocation region.
- Inside an uninitialized enclave, allocates a zeroed page if uncommitted; fails with ERROR_INVALID_ADDRESS in initialized enclaves without dynamic memory management. SGX2 enclaves allow allocation, requiring post-allocation acceptance.
-
dwSize: Defines the memory region size to allocate in bytes. Rounds up to the next page boundary if lpAddress is NULL. If not, it allocates pages covering any part of the range lpAddress to lpAddress+dwSize, potentially including entire pages for small ranges that cross page boundaries.
-
flAllocationType: Specifies the memory allocation type. Must be one of the predefined values
-
-
Copying the DLL: The attacker proceeds to copy the malicious DLL into the allocated memory space within the target process. This step places the malicious code directly into the memory of the process.
Why do we use the WriteProcessMemory API? This API writes data to the memory area within the target process, which is essential for placing the DLL into the allocated space.
MSDN Signature:BOOL WriteProcessMemory( [in] HANDLE hProcess, [in] LPVOID lpBaseAddress, [in] LPCVOID lpBuffer, [in] SIZE_T nSize, [out] SIZE_T *lpNumberOfBytesWritten );
Explanation:-
hProcess - This is the process's handle where memory will be changed. The handle needs PROCESS_VM_WRITE and PROCESS_VM_OPERATION permissions.
-
lpBaseAddress - Points to the start address in the target process where data will be written. The function checks if this area can be written to; if not, it won't proceed.
-
lpBuffer - Points to the data source to be copied into the target process's space.
-
nSize - Specifies how much data ( in bytes ) will be written to the target process.
-
lpNumberOfBytesWritten ( output ) - Points to a variable that will store the count of bytes successfully copied. If not needed, setting it to NULL skips this output.
-
-
Locating the Reflective Loader: The attacker calculates the memory offset within the loaded DLL to find the export responsible for reflective loading. This offset serves as the entry point for the injection process.
This step involves internal logic within the DLL, typically not using a standard API but rather a custom algorithm to locate the Reflective Loader function within the injected DLL memory space.
-
Thread Creation: Using functions like CreateRemoteThread or, in some cases, undocumented APIs like RtlCreateUserThread, the attacker initiates a new thread within the remote process. The reflective loader function's offset address within the DLL is used as the starting point for execution.
Why do we use the CreateRemoteThread API? It creates a thread that runs within the virtual address space of the target process, which is crucial for initiating the execution of the injected DLL code.
MSDN Signature:HANDLE CreateRemoteThread( [in] HANDLE hProcess, [in] LPSECURITY_ATTRIBUTES lpThreadAttributes, [in] SIZE_T dwStackSize, [in] LPTHREAD_START_ROUTINE lpStartAddress, [in] LPVOID lpParameter, [in] DWORD dwCreationFlags, [out] LPDWORD lpThreadId );
Explanation: -
Loader Function's Execution: The reflective loader function, once executed within the target process, begins by locating the Process Environment Block (PEB) specific to that process. It leverages CPU registers to accomplish this. With the PEB, it then identifies the memory addresses of crucial system libraries, including kernel32.dll.
-
API Function Retrieval: The reflective loader parses the exports directory of kernel32.dll to identify the memory addresses of essential API functions such as LoadLibraryA, GetProcAddress, and VirtualAlloc. These functions are pivotal for the subsequent loading of the DLL.
-
DLL Loading: With the API functions' addresses in hand, the reflective loader utilizes them to properly load the DLL into the memory of the target process. This self-loading process ensures that the malicious DLL is brought into the process's address space without traditional Windows loading mechanisms.
-
Executing the DLL:
Finally, the Reflective Loader calls the DLL's entry point function (
DllMain
) to execute the injected code. This is handled internally by the Reflective Loader logic, not by a separate API.
So basically here is a simple steps recap of how the Meterpreter is using the RDI:
- Locate the image in memory
- Find libraries/functions
- Prepare memory for new image
- Process sections
- Process Imported lib/functions
- Process relocations
- Call the Entry point
How it's happening in the RDI C Code - Walkthrough RDI
- Step 0: Calculating the Current Image's Base Address
Imagine you're in a library, and you need to find a specific book. However, all the books are placed in a way that you can't just go to a section labeled "Books" and find it. You need a clever way to figure out where you are in the library and where that book might be.
See the code from here: Rapid7 - ReflectiveDLLInjection- ReflectiveLoader.c
-
Calling caller() Function: In this step, you're asking the librarian ( represented by the caller() function ) for information about your current location. They hand you a small note with a location on it.
uiLibraryAddress = caller();
-
Searching for the MZ/PE Header: Now, you're like a detective with a magnifying glass, and you start walking backward in the library. You're looking for a specific sign that says "Library Start." This sign is like a special marker that tells you where the library begins.
// loop through memory backwards searching for our images base address // we dont need SEH style search as we shouldnt generate any access violations with this while( TRUE ) { if( ((PIMAGE_DOS_HEADER)uiLibraryAddress)->e_magic == IMAGE_DOS_SIGNATURE ) { uiHeaderValue = ((PIMAGE_DOS_HEADER)uiLibraryAddress)->e_lfanew; // some x64 dll's can trigger a bogus signature (IMAGE_DOS_SIGNATURE == 'POP r10'), // we sanity check the e_lfanew with an upper threshold value of 1024 to avoid problems. if( uiHeaderValue >= sizeof(IMAGE_DOS_HEADER) && uiHeaderValue < 1024 ) { uiHeaderValue += uiLibraryAddress; // break if we have found a valid MZ/PE header if( ((PIMAGE_NT_HEADERS)uiHeaderValue)->Signature == IMAGE_NT_SIGNATURE ) break; } } uiLibraryAddress--; }
-
Identifying the PE Header: Once you find that "Library Start" sign, you spot a map (represented by the e_lfanew field in the DOS header) on the wall that shows you where the different sections of the library are. This map helps you find the book you're looking for.
uiHeaderValue = ((PIMAGE_DOS_HEADER)uiLibraryAddress)->e_lfanew;
-
Validating PE Header: To make sure you're on the right track, you check if this map on the wall is valid by making sure it's not too small or too large. You also make sure it has the right label ("PE Header").
if (uiHeaderValue >= sizeof(IMAGE_DOS_HEADER) && uiHeaderValue < 1024) { uiHeaderValue += uiLibraryAddress; if (((PIMAGE_NT_HEADERS)uiHeaderValue)->Signature == IMAGE_NT_SIGNATURE) { // Valid PE header found. break; } }
Now, you know where the library starts, and you have access to the map that will help you find the book.
- Step 1: Processing the Kernel's Exports
Think of this step as you needing a special tool to open locked doors in the library. You know that there's a workshop ( library maintenance room ) where you can get these tools.
-
Retrieve Process Environment Block ( PEB ) : You need to know how to get to the workshop, so you ask the librarian for directions. The librarian tells you to go through a secret passage to reach the workshop.
uiBaseAddress = (ULONG_PTR)((_PPEB)uiBaseAddress)->pLdr;
-
Iterating Through Loaded Modules: You enter the secret passage, and it leads you to a corridor filled with rooms. Each room represents a loaded module (like "kernel32.dll" or "ntdll.dll"). You need to find the workshop room ( the one with the tools ).
while (uiValueA) { // ... }
-
Computing Hash Values: To recognize the workshop room without peeking inside, you write down a code (hash) for its name. When you pass by each room, you check the code you wrote against the room nameplate.
dwHashValue = _hash((char *)(uiBaseAddress + DEREF_32(uiNameArray)));
-
Identifying Kernel Functions: You wrote down the codes for the names of specific tools you need (like "LoadLibraryA" and "GetProcAddress"). When you see the nameplates on the workshop room doors, you compare them with your list to identify the right rooms.
if (dwHashValue == LOADLIBRARYA_HASH || dwHashValue == GETPROCADDRESS_HASH || dwHashValue == VIRTUALALLOC_HASH || dwHashValue == NTFLUSHINSTRUCTIONCACHE_HASH) { // Store function addresses. }
Now, you know where to find the tools you need.
- Step 2: Loading the Image into Memory
Imagine you're about to build a model airplane. You need a clear workspace, so you reserve a big table and gather all your materials there.
-
Memory Allocation: In this case, you're reserving a large, clean table to build your model airplane. You specify the size and permissions for the table.
uiBaseAddress = (ULONG_PTR)pVirtualAlloc(NULL, ((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.SizeOfImage, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
Now, you have a clean workspace ready to load the DLL.
Step 3: Loading Sections of the Image
Imagine you have a blueprint for your model airplane that's divided into different parts. To build it, you need to copy each part from your materials collection to the table.
-
Copying Headers: First, you take out the blueprint and copy it to the table. The blueprint includes instructions for how to assemble the model airplane.
while (uiValueA--) { *(BYTE *)uiValueC++ = *(BYTE *)uiValueB++; }
-
Copying Sections: Now, you start copying each part of the model airplane from your materials collection to the table. Each part has a specific place in the blueprint, and you make sure they fit together perfectly.
This process of copying sections ensures that all the pieces of the DLL are in the right place in memory, just like assembling a model airplane.
This is where we've left off in the explanation. If you'd like to continue with the next steps, please let me know!
- Step 4: Relocating the Image (Optional)
Sometimes, the table where you're building your model airplane might be smaller than the blueprint. You'd need to adjust and reposition some parts to make them fit correctly. Similarly, in memory, you may need to adjust memory addresses to accommodate the loaded DLL.
-
Checking for Relocation: You look at the blueprint and see if any notes indicate you need to adjust parts. In our case, you check if the DLL has a relocation section.
uiValueD = (ULONG_PTR)uiHeaderValue + FIELD_OFFSET(IMAGE_NT_HEADERS, OptionalHeader) + ((PIMAGE_NT_HEADERS)uiHeaderValue)->FileHeader.SizeOfOptionalHeader;
-
Performing Relocation: If there are relocation instructions in the DLL, it's like getting a set of specific guidelines for moving parts around. You carefully follow these instructions to ensure everything fits properly in memory.
if (((PIMAGE_OPTIONAL_HEADER)uiValueD)->DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress) { // Perform relocation. }
This step is optional and may not always be necessary, just as not all model airplanes require adjustments.
- Step 5: Resolving Import Address Table (IAT)
Imagine you have a list of suppliers for your model airplane parts, and you need to contact them to get the necessary pieces. You'll call each supplier and ask for the specific parts you need.
-
Finding Import Table: You refer to your blueprint and find a list of suppliers ( like a phone book ) known as the Import Address Table (IAT). These suppliers (functions from other DLLs) are essential for your DLL to work correctly.
pImportDesc = (PIMAGE_IMPORT_DESCRIPTOR)(uiBaseAddress + RvaToVa((PIMAGE_NT_HEADERS)uiHeaderValue, ((PIMAGE_OPTIONAL_HEADER)uiValueD)->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress));
-
Iterating Through Suppliers: You start calling each supplier ( function ) from the list to get the parts you need. You use the tools you found earlier (like LoadLibraryA and GetProcAddress) to make these calls.
while (pImportDesc->Name) { HMODULE hModule = pLoadLibraryA((LPCSTR)(uiBaseAddress + RvaToVa((PIMAGE_NT_HEADERS)uiHeaderValue, pImportDesc->Name))); // Iterate through functions for this supplier. // Use GetProcAddress to get function addresses. pImportDesc++; }
This way, you're making sure you have all the necessary parts ( functions ) for your model airplane ( DLL ).
- Step 6: Resolving Relocations (if necessary)
If you encountered relocation instructions earlier, it's like having to adjust some of the parts you received from suppliers to fit your model airplane.
-
Relocation Table: You check the relocation section in the DLL, which tells you which parts need adjustment.
pRelocDesc = (PIMAGE_BASE_RELOCATION)(uiBaseAddress + RvaToVa((PIMAGE_NT_HEADERS)uiHeaderValue, ((PIMAGE_OPTIONAL_HEADER)uiValueD)->DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress));
-
Adjusting Parts: You follow the instructions in the relocation section to make the necessary adjustments to the parts (memory addresses) so they fit properly.
This ensures that everything in memory aligns correctly with the DLL.
- Step 7: Cleaning Up
Now that your model airplane is assembled and ready, you need to tidy up your workspace and ensure everything is neat and organized.
-
Cleaning Up the Table: You clear away any leftover materials and tools you used during the assembly process, leaving your workspace clean and free from clutter.
-
Final Steps: You perform any additional steps needed to make your model airplane ( DLL ) ready for flight. This might include setting up initial conditions, adjusting any parameters, or preparing for its first flight ( execution ).
And there you have it! Your DLL is now loaded into memory, all the parts are correctly positioned, and it's ready to be executed.
Keep in mind that this process is a simplified analogy to help you understand how DLL loading works at a low level. In reality, the Windows loader handles these steps in a more complex and efficient manner.
Important notes about the RDI
Imagine you are to put together a neat toolkit of libraries for Reflective DLL Injection. There is this one project skeleton available under the three-clause BSD license that gives you a hint on this venture. Metasploit has buddied up with your toolkit and thrown its doors wide open to Reflective DLL Injection, featuring it as a payload stage along with a customized VNC DLL.
So, it stands to reason that when you dive down into the deepest level of ReflectiveLoader, you will find it well-crafted with only mildly position-dependent C code and sprinkled with just a dash of inlined assembler. The code is therefore highly comment-rich, like breadcrumbs through a dense forest. Before doing so, a little refresher on the PE file format will get us ready for what comes next. Matt Pietrek has been one of your peers, spelunking here and writing some brilliant articles about the PE file format.
After a deep insight into Metasploit, you would come across a payload module of Ruby named Payload::Windows::ReflectiveDllInject. The other name for this module is to be brought when the injected DLL is loaded with ReflectiveLoader. It is as though it is a jigsaw puzzle, where this step finds the piece that fits—that is, the offset to the exported ReflectiveLoader function in the library—then it crafts a tiny bootstrap shellcode, shown below in Listing 1, to take its place in the MZ header of the DLL image. With this little rearrangement, the intermediate Metasploit stager can pass off the baton of execution to ReflectiveLoader, since the intermediate stager welcomes the whole library into the host process.
-
Listing 1: Bootstrap Shellcode
dec ebp ; M pop edx ; Z call 0 ; call next instruction pop ebx ; get our location (+7) push edx ; push edx back inc ebp ; restore ebp push ebp ; save ebp mov ebp, esp ; setup fresh stack frame add ebx, 0x???????? ; add offset to ReflectiveLoader call ebx ; call ReflectiveLoader mov ebx, eax ; save DllMain for second call push edi ; our socket push 0x4 ; signal we have attached push eax ; some value for hinstance call eax ; call DllMain( somevalue, DLL_METASPLOIT_ATTACH, socket ) push 0x???????? ; our EXITFUNC placeholder push 0x5 ; signal we have detached push eax ; some value for hinstance call ebx ; call DllMain( somevalue, DLL_METASPLOIT_DETACH, exitfunk ) ; we only return if we don't set a valid EXITFUNC
This bootstrap shellcode is like a handshake between your toolkit and Metasploit. It carries the payload's socket and, in the finale, the payload's exit function through calls to DllMain. This friendly exchange ensures your toolkit dances to the rhythm of the Metasploit payload system.
How is the ReflectiveLoader Function Called in a DLL
And finally, here we go with the injector -> Reflective DLL Injection - Injector,
To understand how the ReflectiveLoader function is called within a DLL, we need to explore the LoadLibraryR.c code, which is responsible for loading or injecting ReflectiveLoader DLL. The ReflectiveLoader function is a crucial part of this process. Let's break it down step by step:
Code: Reflective DLL - LoadLibraryR.c
General idea: code is designed to achieve dynamic loading and execution of a Windows Dynamic Link Library (DLL) from memory within a Windows process. This technique is commonly known as the "ReflectiveLoader" technique and is often used for various purposes, including security research, penetration testing, and in some cases, malicious activities.
How the code works! - (Overview)
-
RVA to File Offset Conversion ( Rva2Offset function ):
- The code defines a function called Rva2Offset. This function is responsible for converting a Relative Virtual Address (RVA) to a file offset within a loaded DLL image.
- RVAs are used to represent memory addresses relative to the image's base address. Converting them to file offsets allows for reading data directly from the DLL file on disk.
-
Locating the ReflectiveLoader Function ( GetReflectiveLoaderOffset function ):
- The GetReflectiveLoaderOffset function is used to find the offset of the ReflectiveLoader function within a DLL image. This function searches the export directory of the DLL to locate the ReflectiveLoader function by name.
-
Loading a DLL from Memory ( LoadLibraryR function ):
-
The LoadLibraryR function is the main entry point for loading a DLL from memory using the ReflectiveLoader technique.
-
It takes as input a pointer to a buffer ( lpBuffer ) containing the DLL image in memory, the length of the buffer (dwLength), and the name of the ReflectiveLoader function ( cpReflectiveLoaderName ).
-
Inside this function:
-
It checks if the provided buffer and length are valid.
-
It calls GetReflectiveLoaderOffset to find the offset of the ReflectiveLoader function.
-
If the offset is found, it adjusts memory protection to make the memory region executable.
-
It then calls the ReflectiveLoader function, which is expected to perform initialization tasks and return the HMODULE of the loaded DLL.
-
-
-
Loading a Remote DLL into a Host Process ( LoadRemoteLibraryR function ):
-
The LoadRemoteLibraryR function extends the functionality to load a DLL into the address space of a remote ( host ) process.
-
It takes additional parameters, including a handle to the target process ( hProcess ) and an optional parameter ( lpParameter ) that can be passed to the ReflectiveLoader function.
-
Inside this function:
-
It allocates memory within the target process for the DLL image and writes the DLL image into the target process's memory.
-
It adjusts the memory protection of the allocated memory to make it executable.
-
It creates a remote thread within the target process, with the thread's entry point set to the ReflectiveLoader function.
-
This effectively causes the ReflectiveLoader function to run within the context of the remote process.
-
-
In summary, this code enables the loading of DLLs from memory into a local or remote process. It relies on the ReflectiveLoader technique, which allows DLLs to be loaded and executed without the need for traditional loading methods such as LoadLibrary or process injection techniques like DLL injection. This approach can be used for legitimate purposes like security research or debugging but should be used responsibly, as it can also be leveraged for malicious activities.
Entire extensions of the Meterpreter & Code Analysis
And that's what we will learn at the third part :)
Thanks for reading!