Skip to content
KitploitKITPLOIT
HerramientasBlog
Enviar
HerramientasBlog
Enviar

¡Herramientas de Hacking, PenTest y Ciberseguridad para tu Arsenal de Seguridad!

Kitploit es un directorio de herramientas de hacking, ciberseguridad y pentesting. Descubre las últimas actualizaciones de proyectos para encontrar vulnerabilidades, analizar sistemas, automatizar pruebas y fortalecer tu seguridad.

··Feeds·Contacto·Privacidad·© 2026 Kitploit

Directorio de Herramientas

Categorías

Ver todas las categorías
Loading categories
LdrShuffle — Técnica de ejecución/inyección de código mediante la manipulación de la estructura de módulos DLL en el PEB | Kitploit
Herramientas/GitHubGitHub/rwxstoned/ldrshuffle
Análisis de CódigoExplotaciónMovimiento LateralShellcodePost-ExplotaciónPruebas de PenetraciónRed TeamingDesarrollo de PayloadsExplotación de Binarios
GitHubrwxstoned/ldrshuffle

LdrShuffle

Técnica de ejecución/inyección de código mediante la manipulación de la estructura de módulos DLL en el PEB

28944hace 1 añoRevisado por Kitploit

Más Populares

Ver todos →

Descubre las herramientas más usadas por nuestra comunidad.

Explora todas las herramientas

Explora nuestra colección de herramientas

Ver todas las herramientas →
Compartir
Ver Repositorio

LdrShuffle

Ejecución de código sigilosa mediante la modificación del EntryPoint de los módulos cargados en tiempo de ejecución.

Resumen

Los procesos de Windows tienen varios módulos cargados en tiempo de ejecución. Cada uno de estos módulos tiene una función DllMain() definida, que se invocará en la creación/destrucción de procesos o subprocesos (cuatro escenarios posibles).

Para llamar correctamente a esas funciones durante la vida del proceso, las funciones del cargador de Windows (ntdll!Ldrp*) consultan una lista de entradas que contienen parámetros clave (incluido el campo EntryPoint) para cada módulo.

Al sobrescribir este EntryPoint de una DLL, conseguimos que la ejecución de código se redirija a un lugar de nuestra elección.

Casos de uso

Esto puede utilizarse tanto como primitiva de ejecución de código como para el proxying de API, es decir, para ejecutar ciertas APIs con una pila de llamadas no sospechosa, ya que serán invocadas por funciones legítimas de Windows.

También puede utilizarse para desencadenar la ejecución en un proceso remoto, siempre que el atacante tenga la capacidad de leer y escribir memoria en el proceso objetivo. De forma similar a la Threadless Injection, esto permite ejecutar código en un proceso sin invocar las API clásicas relacionadas con la ejecución (CreateRemoteThread, QueueUserAPC).

Desafíos

La carga/descarga de módulos dentro de un proceso de Windows es un tema complejo que presenta muchos desafíos, potencial de inestabilidad, condiciones de carrera y fallos. Un obstáculo bien conocido relacionado con la ejecución de código como parte de una función DllMain(), por ejemplo, radica en que hay un Loader Lock activo y en que nos encontramos en un subproceso que no se ha configurado por completo, o que está en proceso de terminación.

Por lo tanto, he intentado documentar adecuadamente qué es posible y qué no. Por ejemplo, aunque la mayoría de las llamadas API habituales pueden realizarse, ejecutar un beacon completo conlleva ciertos requisitos de estar en un proceso separado, para evitar interbloqueos causados por las funciones utilizadas en wininet.dll o winhttp.dll.

Implementación

Repaso sobre la carga de DLL en Windows

Cada proceso mantiene una lista de estructuras _LDR_DATA_TABLE_ENTRY en tiempo de ejecución. Estas estructuras contienen muchos detalles relevantes de la DLL, como su EntryPoint (que sobrescribiremos), su nombre, ciertos hashes, marcas de tiempo, varias banderas, etc. Algunas de estas estructuras están documentadas y otras no.

Estas pueden visualizarse mediante este comando de WinDbg:

dt nt!_LDR_DATA_TABLE_ENTRY 0xdeadbeef

root@kitploit:~
0:006> dt nt!_LDR_DATA_TABLE_ENTRY 0x18d1c4838c0
ntdll!_LDR_DATA_TABLE_ENTRY
   +0x000 InLoadOrderLinks : _LIST_ENTRY [ 0x0000018d`1c485de0 - 0x0000018d`1c4832b0 ]
   +0x010 InMemoryOrderLinks : _LIST_ENTRY [ 0x0000018d`1c485df0 - 0x0000018d`1c4832c0 ]
   +0x020 InInitializationOrderLinks : _LIST_ENTRY [ 0x0000018d`1c4832d0 - 0x0000018d`1c482c80 ]
   +0x030 DllBase          : 0x00007ffe`e87e0000 Void
   +0x038 EntryPoint       : 0x00007ffe`e8838d00 Void
   +0x040 SizeOfImage      : 0x2fe000
   +0x048 FullDllName      : _UNICODE_STRING "C:\WINDOWS\System32\KERNELBASE.dll"
   +0x058 BaseDllName      : _UNICODE_STRING "KERNELBASE.dll"
   +0x068 FlagGroup        : [4]  "???"
   +0x068 Flags            : 0x8a2cc
   +0x068 PackagedBinary   : 0y0
   +0x068 MarkedForRemoval : 0y0
   +0x068 ImageDll         : 0y1
(...)
   +0x0e0 MappingInfoIndexNode : _RTL_BALANCED_NODE
   +0x0f8 OriginalBase     : 0x00007ffe`e87e0000
   +0x100 LoadTime         : _LARGE_INTEGER 0x01db5f86`2fa735fc
   +0x108 BaseNameHashValue : 0x235bec4
   +0x10c LoadReason       : 0 ( LoadReasonStaticDependency )
   +0x110 ImplicitPathOptions : 0x4000
   +0x114 ReferenceCount   : 1
   +0x118 DependentLoadFlags : 0x800
   +0x11c SigningLevel     : 0 ''

Tenga en cuenta la bandera DontCallForThreads. Como su nombre indica, si esa bandera está establecida, el sistema operativo NO llamará al DllMain() de ese módulo para eventos de subproceso (es decir, DLL_THREAD_ATTACH o DLL_THREAD_DETACH).

Al crear una DLL, se debe seguir la siguiente plantilla para trabajar en conjunto con las funciones del cargador del sistema operativo:

root@kitploit:~
BOOL WINAPI DllMain(
    HINSTANCE hinstDLL,  // handle to DLL module
    DWORD fdwReason,     // reason for calling function
    LPVOID lpvReserved )  // reserved
{
    // Perform actions based on the reason for calling.
    switch( fdwReason ) 
    { 
        case DLL_PROCESS_ATTACH:
         // Initialize once for each new process.
         // Return FALSE to fail DLL load.
            break;

        case DLL_THREAD_ATTACH:
         // Do thread-specific initialization.
            break;

        case DLL_THREAD_DETACH:
         // Do thread-specific cleanup.
            break;

        case DLL_PROCESS_DETACH:
        
            if (lpvReserved != nullptr)
            {
                break; // do not do cleanup if process termination scenario
            }
            
         // Perform any necessary cleanup.
            break;
    }
    return TRUE;  // Successful DLL_PROCESS_ATTACH.
}

Detalles técnicos sobre la implementación

Configuración de una llamada API

Como se ha descrito anteriormente, la técnica sobrescribe temporalmente el EntryPoint de una DLL para redirigir la ejecución. Dado que no tenemos control más allá de la redirección de la ejecución, hay que hacer algunos arreglos en paralelo para manejar qué queremos ejecutar, con qué argumentos y cómo recuperar el valor de retorno.

Esto se logra definiendo una estructura DATA_T en el heap, de modo que permanezca accesible a lo largo de los distintos pasos.

Esa estructura se define de la siguiente manera:

root@kitploit:~
typedef struct _DATA_T {
    // LDR structures manipulation
    ULONG_PTR   runner;             // malicious entry point to execute
    ULONG_PTR   bakOriginalBase;    // backup of overwritten OriginalBase
    ULONG_PTR   bakEntryPoint;      // backup of overwritten EntryPoint
    HANDLE      event;              // event signalling that the Runner has executed
    // function call
    ULONG_PTR   ret;                // return value
    DWORD       createThread;       // run this API call in a new thread (required for wininet/winhttp)
    ULONG_PTR   function;           // Windows API to call
    DWORD       dwArgs;             // number of args
    ULONG_PTR   args[MAX_ARGS];     // array of args
} DATA_T, * PDATA_T;

Para configurar una ejecución de API, hay que preparar estos campos. El valor ret es el que recogerá el valor de retorno tras la ejecución. El event se utiliza para la sincronización, para señalar que la ejecución ha terminado. Todos los demás campos son entradas que definen qué API llamar (function), con qué argumentos (dwArgs y args[]), la dirección de la función Runner() a la que se redirige la ejecución, y copias de respaldo de las entradas DLL originales sobrescritas (bakOriginalBase y bakEntryPoint).

El campo createThread debe establecerse a 1 para aquellas funciones API complejas que no se ejecutan bien en un entorno DllMain() (esto incluye muchas librerías wininet y winhttp).

Aquí hay un ejemplo de configuración de una llamada a MessageBoxA() como se ve en el PoC:

root@kitploit:~
 pDataT->dwArgs = 4;
 pDataT->runner = (ULONG_PTR)Runner;
 pDataT->function = (ULONG_PTR)MessageBoxA;
 pDataT->args[0] = (ULONG_PTR)0;
 pDataT->args[1] = (ULONG_PTR)"Hello";
 pDataT->args[2] = (ULONG_PTR)"LDRSHUFFLE";
 pDataT->args[3] = (ULONG_PTR)MB_OKCANCEL;
 pDataT->event = CreateEventA(NULL, FALSE, FALSE, "ExecEvt");

Modificación de la _LDR_DATA_TABLE_ENTRY

La función UpdateLdr() es la responsable de realizar la modificación adecuada en la _LDR_DATA_TABLE_ENTRY del módulo objetivo.

RestoreLdr() restaurará esos cambios en una etapa posterior (invocada por Runner()).

Estas funciones localizan esencialmente el PEB y recorren las estructuras de módulos para identificar la DLL correcta y sus campos. En los archivos de cabecera, reutilizo definiciones utilizadas por Batsec en su DarkLoadLibrary y animo a los lectores a revisar este proyecto y el blogpost asociado de MDSec para beneficiarse del excelente trabajo que ha realizado sobre los entresijos de la carga de módulos en Windows.

Nota: este PoC carga una DLL sacrificial (SACRIFICIAL_DLL_NAME) y realiza esos cambios sobre esa DLL. Sin embargo, es perfectamente factible modificar una DLL ya cargada. De hecho, este es el enfoque utilizado para la inyección entre procesos. Por razones de estabilidad, recomendaría evitar tocar DLL importantes como ntdll o kernel32, que también tienden a ser más examinadas por las soluciones de seguridad.

Ejecución

En la creación o destrucción de un subproceso, la ejecución se redirige a Runner(), que actúa como un DllMain() falso para el módulo. Esta función entonces:

  • localizar en el heap dónde está la estructura de datos utilizada para ejecutar llamadas/obtener el valor de retorno (estructura PDATA_T)
  • restaurar el PEB RestoreLdr() a su estado original
  • realizar la llamada normal a DllMain() (esencialmente haciendo de proxy de la llamada DLL normal).

En este punto, se ha realizado la ejecución "normal" del sistema operativo. A continuación continúa con nuestras cargas útiles:

  • ejecutar nuestra llamada API maliciosa, según los valores y argumentos almacenados en la estructura DATA_T. Si esta API ha sido marcada para ejecutarse en un nuevo subproceso (createThread = 1), esta llamada se realizará en un nuevo subproceso.
  • finalmente, señalar un evento (pDataT->event) para que nuestro código principal sepa que la llamada se ha realizado.

Cuando Windows termina invocando nuestro EntryPoint falso (que es la dirección de la función Runner()), la pila de llamadas se ve de la siguiente manera:

Pila de llamadas en MessageBoxA()

Ejemplo de proxying de API

El PoC proporcionado contiene un ejemplo que invoca MessageBoxA().

También contiene una demostración de una descarga HTTP usando wininet. Defina la variable HTTP para habilitar ese código.

Pila de llamadas en MessageBoxA()

Ejemplo de inyección entre procesos

Los principios descritos anteriormente se resumen en leer y escribir en el espacio de memoria del proceso, con el fin de provocar la ejecución de código en un punto arbitrario del futuro.

Con algunos ajustes, estas operaciones de lectura y escritura pueden aplicarse a un proceso remoto para sobrescribir el EntryPoint de una de sus DLL.

Un requisito previo es la capacidad de leer y escribir en el espacio de memoria del proceso, es decir:

OpenProcess(PROCESS_VM_READ, FALSE, dwPid) y OpenProcess(PROCESS_VM_WRITE | PROCESS_VM_OPERATION, FALSE, PID)

En el PoC hay un proyecto adicional, llamado LdrInject, que demuestra cómo realizar estos pasos. En resumen, hace lo siguiente:

  • en ReadPEB(), recorre la lista _LDR_DATA_TABLE_ENTRY en el proceso objetivo para identificar una DLL adecuada a sobrescribir. Tenga en cuenta que esta DLL debe tener DontCallForThreads == 0 porque queremos que Windows invoque el EntryPoint de esa DLL en la creación de subprocesos. Tampoco elegimos las primeras DLL de la lista, ya que tienden a estar más examinadas por los productos de seguridad (ntdll.dll, kernel32.dll...).

  • los detalles de esa DLL se almacenan en una estructura de datos PEBINJ_DATA.

  • el shellcode (en este caso un beacon) se escribe en el espacio del proceso remoto con InjectShellcodeToRemoteProcess()

  • dos llamadas a WriteProcessMemory() sobrescriben el EntryPoint de la DLL y lo respaldan en OriginalBase para que pueda restaurarse más tarde.

En ese momento, el siguiente evento DLL_THREAD_ATTACH o DLL_THREAD_DETACH dará como resultado la invocación del shellcode. Esto conlleva ciertas limitaciones y advertencias en el contexto de la ejecución de un beacon, que se detallan en la siguiente sección.

Cobalt Strike Beacon

Esta técnica da como resultado la ejecución de un shellcode en una situación muy específica. El Loader Lock está activo (ya que el sistema operativo cree que está en proceso de cargar/descargar una DLL); un subproceso se está creando o destruyendo; y, en términos generales, existe potencial de problemas de sincronización de subprocesos, interbloqueos, etc.

Durante las pruebas, se han observado dos desafíos:

  • ejecutar un beacon típico de Cobalt Strike provocaría un interbloqueo al usar APIs en wininet.dll o winhttp.dll.

  • ejecutarse en la destrucción de un subproceso causa problemas de estabilidad, ya que nos encontramos en un subproceso que está en proceso de ser destruido.

Para aumentar la estabilidad, tenemos que:

  • asegurarnos de que el beacon se ejecutará en un nuevo subproceso. Por lo tanto, el UDRL hará CreateThread antes de invocar el punto de entrada habitual de la DLL reflectiva de Cobalt Strike.

  • ejecutarse solo en un subproceso que se está creando y no en uno que está muriendo. Para ello, nos aseguramos de que cuando el sistema operativo llame al EntryPoint, el motivo invocado sea fdwReason == DLL_THREAD_ATTACH:

winApi.CreateThread(NULL, 4096, (LPTHREAD_START_ROUTINE)&runner, (LPVOID)&ct_data, 0, &dwThId);

en lugar de lo habitual

((DLLMAIN)entryPoint)((HINSTANCE)loaderStart, 4, NULL);

root@kitploit:~
    ULONG_PTR __cdecl ReflectiveLoader(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved) {
        // only run for a Thread creation event
        
        if (fdwReason != DLL_THREAD_ATTACH) {
            return TRUE;
        }

        ...
    }

Estos dos pasos adicionales se han incorporado en una demo para un UDRL.

Pruebas

Lista de APIs probadas para LdrShuffle

VirtualAlloc

VirtualProtect

CreateThread

Sleep

MessageBoxA

InternetOpenW (necesita ejecutarse con createThread = 1)

InternetOpenUrlA (necesita ejecutarse con createThread = 1)

TODO

  • Seguir probando más APIs para LdrShuffle
Descargar herramienta