Mitigación de Ataques de Programación Orientada a Retorno (ROP)

La Programación Orientada a Retorno (ROP) es una técnica de explotación avanzada utilizada para sortear mecanismos de protección como la Prevención de Ejecución de Datos (DEP). Consiste en encadenar "gadgets" de código existentes en la memoria del programa para lograr el control sobre el flujo de ejecución, sin inyectar código nuevo. Un "gadget" es una pequeña secuencia de instrucciones que termina con una instrucción ret (retorno).

Principios Fundamentales de ROP

En el contexto de las arquitecturas x86/x64, la instrucción ret desapila la dirección de retorno de la pila y salta a ella, continuando la ejecución. Los atacantes manipulan la pila para colocar direcciones de "gadgets" secuencialmente, permitiendo que cada ret salte al siguiente "gadget", construyendo así una cadena lógica compleja a partir de fragmentos de código legítimos.

Para que un ataque ROP sea exitoso, generalmente se requieren dos condiciones:

  • Una vulnerabilidad que permita la sobrescritura de la pila, concediendo al atacante control sobre la dirección de retorno.
  • La disponibilidad de "gadgets" adecuados en el espacio de memoria del programa, junto con sus direcciones.

Tipos Comunes de Ataques ROP

Existen varias modalidades de ataques ROP, diferenciadas por el objetivo de la cadena de "gadgets":

  • Ret2text: Redirige la ejecución a código existente y ejecutable dentro del segmento .text del programa.
  • Ret2shellcode: Utiliza ROP para deshabilitar las protecciones de memoria y luego ejecutar un "shellcode" inyectado.
  • Ret2syscall: Permite realizar llamadas al sistema directamente, manipulando los registros para pasar argumentos a funciones del kernel.
  • Ret2libc: Controla la ejecución de funciones presentes en la biblioteca C estándar (libc), como system() o execve().

Mecanismos de Defensa contra ROP

Las defensas contra ROP se centran en detectar patrones de ejecución anómalos que son característicos de estos ataques, especialmente cuando interactúan con funciones críticas del sistema. Una "función crítica" se define como aquella cuya invocación por parte de un atacante puede eludir las mitigaciones existentes o permitir la ejecución de código arbitrario.

Ejemplos de funciones críticas:

  • CreateProcess: Para iniciar procesos arbitrarios.
  • VirtualProtect, VirtualAlloc, LoadLibrary: Para manipular las protecciones de memoria o cargar bibliotecas maliciosas.
  • OpenFile, WriteFile: Para interactuar con el sistema de archivos de manera no autorizada.

Los mecanismos de mitigación examinan varios aspectos cuando estas funciones son invocadas:

  • ¿Cómo se realizó la llamada a la función crítica? (¿Fue una llamada legítima o una redirección de ret?)
  • ¿Qué ocurriría inmediatamente después de la ejecución de la función crítica?
  • ¿El estado actual del sistema es coherente con una ejecución normal del programa?
  • ¿La ejecución de esta función en el contexto actual comprometería la seguridad del sistema?

1. Verificaciones de Carga de Bibliotecas (Load Libray Checks)

Esta técnica previene la carga de módulos o bibliotecas dinámicas desde rutas UNC (Universal Naming Convention), que a menudo son explotadas para ejecutar binarios maliciosos almacenados en recursos compartidos de red o servidores web remotos.

Concepto de Ruta UNC

Una ruta UNC es una forma estándar de especificar la ubicación de recursos en una red, siguiendo el formato \\<nombre_computadora>\<directorio_compartido>\.... Por ejemplo, \\servidor_malicioso\recursos\payload.dll.

La mitigación interviene las APIs de carga de bibliotecas (como LoadLibraryA/W, LoadLibraryExA/W, LdrLoadDll) para inspeccionar la ruta del archivo. Si la ruta es identificada como UNC, la operación es blouqeada y el proceso puede ser terminado.

Implementación de ejemplo:

Esta lógica intercepta las llamadas a funciones de carga de bibliotecas. Si la ruta de la DLL es una ruta UNC, se activa una alerta o se termina el proceso para prevenir una posible ejecución de código malicioso.


#include <windows.h>
#include <string>
#include <cstdio> // For sprintf_s

// Función auxiliar para verificar si una cadena de ruta es una ruta UNC.
BOOL EsRutaUNC(LPCSTR pathName) {
    if (!pathName || strlen(pathName) < 2) {
        return FALSE;
    }

    // Una ruta UNC estándar comienza con "\\".
    if (pathName[0] == '\\' && (pathName[1] == '\\' || pathName[1] == '/')) {
        return TRUE;
    }
    
    // También considerar rutas UNC con el prefijo extendido "\\?\" o "\\.\".
    // Por ejemplo, "\\?\UNC\server\share\".
    if (strlen(pathName) >= 4 && strncmp(pathName, "\\\\?\\", 4) == 0) {
        if (strlen(pathName) >= 8 && _strnicmp(pathName + 4, "UNC\\", 4) == 0) {
            return TRUE;
        }
    }
    return FALSE;
}

// Lógica de mitigación para la carga de bibliotecas.
// Se invoca cuando una API de carga de bibliotecas es interceptada.
// nombreModuloPtr: Puntero al nombre de la DLL (char* o wchar_t*).
// esUnicode: TRUE si nombreModuloPtr es Unicode (wchar_t*).
// flagsCarga: Flags pasados a la función LoadLibrary (ej. LOAD_LIBRARY_AS_DATAFILE).
BOOL VerificarCargaModulo(LPCVOID nombreModuloPtr, BOOL esUnicode, DWORD flagsCarga) {
    // Si la DLL se carga únicamente como archivo de datos o recurso, no se intercepta.
    // Esto es común para LOAD_LIBRARY_AS_DATAFILE, LOAD_LIBRARY_AS_IMAGE_RESOURCE, etc.
    // (0x70 es una máscara de bits que cubre estas banderas comunes)
    if ((flagsCarga & 0x70) != 0) {
        return TRUE; // Permitir la operación
    }

    LPSTR moduleNameAnsi = NULL;
    int neededSize = 0;

    if (esUnicode) {
        neededSize = WideCharToMultiByte(CP_ACP, 0, (LPCWSTR)nombreModuloPtr, -1, NULL, 0, NULL, NULL);
        if (neededSize > 0) {
            moduleNameAnsi = new char[neededSize];
            if (moduleNameAnsi) {
                WideCharToMultiByte(CP_ACP, 0, (LPCWSTR)nombreModuloPtr, -1, moduleNameAnsi, neededSize, NULL, NULL);
            }
        }
    } else {
        moduleNameAnsi = (LPSTR)nombreModuloPtr;
    }

    BOOL uncDetected = FALSE;
    if (moduleNameAnsi) {
        uncDetected = EsRutaUNC(moduleNameAnsi);
    }
    
    // Limpiar memoria si se asignó dinámicamente.
    if (esUnicode && moduleNameAnsi) {
        delete[] moduleNameAnsi;
        moduleNameAnsi = NULL;
    }
    
    if (uncDetected) {
        char debugMessage[256];
        sprintf_s(debugMessage, sizeof(debugMessage), "MITIGACION ROP: Intento de cargar DLL desde ruta UNC detectado y bloqueado: %s", (moduleNameAnsi ? moduleNameAnsi : "NULL"));
        OutputDebugStringA(debugMessage);
        return FALSE; // Indicar que la operación debe ser bloqueada.
    }
    return TRUE; // Permitir la operación.
}

2. Verificaciones de Protección de Memoria (Memory Protection Checks)

Este mecanismo se enfoca en impedir que el código malicioso modifique las propiedades de protección de la memoria de la pila, especialmente para hacerla ejecutable. La activación de la ejecución en la pila es una técnica común en los ataques ROP para ejecutar "shellcode".

Las funciones objetivo para esta mitigación son las APIs que pueden alterar las protecciones de páginas de memoria, como VirtualProtect, VirtualProtectEx y NtProtectVirtualMemory.

Implementación de ejemplo:

El detector intercepta las llamadas a estas funciones y verifica los parámetros. Si se intenta establecer el bit de "ejecutable" (por ejemplo, PAGE_EXECUTE_READWRITE) en una región de memoria que pertenece a la pila del hilo actual, se considera una actividad sospechosa y se bloquea.


#include <windows.h>
#include <winnt.h> // Para NT_TIB
#include <cstdio> // For sprintf_s

// Estructura para capturar los argumentos relevantes de las APIs de protección de memoria
struct ProteccionMemoriaSolicitud {
    HANDLE procesoObjetivo;
    LPVOID direccionInicial;
    SIZE_T tamanoArea;
    DWORD nuevaProteccionFlags;
};

// Función de mitigación de protección de memoria
// Verifica si una solicitud de cambio de protección de memoria es maliciosa.
BOOL VerificarProteccionPila(const ProteccionMemoriaSolicitud* requestInfo) {
    // Solo aplicar la mitigación si se intenta establecer permisos de ejecución.
    // PAGE_EXECUTE, PAGE_EXECUTE_READ, PAGE_EXECUTE_READWRITE, PAGE_EXECUTE_WRITECOPY
    DWORD executableFlags = PAGE_EXECUTE | PAGE_EXECUTE_READ | PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY;
    if (!(requestInfo->nuevaProteccionFlags & executableFlags)) {
        return TRUE; // No hay intento de hacer ejecutable, permitir.
    }

    // Si la llamada es para otro proceso, no aplicar esta mitigación (podría ser legítimo).
    if (requestInfo->procesoObjetivo && requestInfo->procesoObjetivo != GetCurrentProcess()) {
        return TRUE;
    }

    // Obtener límites de la pila del hilo actual.
    // En arquitecturas Windows, el Thread Environment Block (TEB) contiene los límites de la pila.
    // La pila crece hacia abajo, por lo que StackLimit es la dirección más baja, y StackBase la más alta.
    NT_TIB* teb = (NT_TIB*)NtCurrentTeb(); 
    DWORD_PTR stackLowAddr = (DWORD_PTR)teb->StackLimit;
    DWORD_PTR stackHighAddr = (DWORD_PTR)teb->StackBase;

    DWORD_PTR requestedStartAddr = (DWORD_PTR)requestInfo->direccionInicial;
    DWORD_PTR requestedEndAddr = requestedStartAddr + requestInfo->tamanoArea;

    // Verificar si la región solicitada se superpone con la pila del hilo.
    // Se considera superposición si:
    // 1. La región solicitada comienza o termina dentro de la pila.
    // 2. La pila está completamente contenida dentro de la región solicitada.
    // 
    // Para una detección más robusta, se puede alinear a páginas de memoria.
    DWORD_PTR pageAlignedRequestedStart = requestedStartAddr & (~0xFFF); // Alineación a página de 4KB
    DWORD_PTR pageAlignedRequestedEnd = (requestedEndAddr + 0xFFF) & (~0xFFF);

    if ( // Check for overlap with the stack region
        (pageAlignedRequestedStart >= stackLowAddr && pageAlignedRequestedStart < stackHighAddr) ||
        (pageAlignedRequestedEnd > stackLowAddr && pageAlignedRequestedEnd <= stackHighAddr) ||
        (pageAlignedRequestedStart <= stackLowAddr && pageAlignedRequestedEnd >= stackHighAddr)
       ) {
        char debugMessage[256];
        sprintf_s(debugMessage, sizeof(debugMessage), "MITIGACION ROP: Intento de hacer ejecutable la pila detectado en 0x%p - 0x%p. Bloqueado.", 
                  (void*)requestedStartAddr, (void*)requestedEndAddr);
        OutputDebugStringA(debugMessage);
        return FALSE; // Bloquear la operación.
    }

    return TRUE; // Permitir la operación.
}

3. Verificaciones de Origen de Llamada (Caller Checks)

Este control tiene como objetivo detectar y bloquear las invocaciones a funciones críticas que no provienen de una instrucción CALL legítima, sino de un RET. En un ataque ROP, el flujo de control se transfiere a una función crítica a través de una instrucción RET que desapila la dirección de la función crítica. Este mecanismo analiza la instrucción precedente a la dirección de retorno para determinar si fue un CALL o un RET.

Implementación de ejemplo:

Cuando una función crítica es interceptada, la dirección de retorno en la pila se examina. Se busca la instrucción inmediatamente anterior a la dirección de retorno para verificar si es una instrucción CALL. Si no lo es, o si el objetivo del CALL no coincide con la función crítica, se levanta una alerta.


#include <windows.h>
#include <string>
#include <cstdio> // For sprintf_s

// Función auxiliar para determinar si la instrucción en una dirección dada es un CALL.
// Esta es una implementación simplificada y heurística; un desensamblador completo
// sería necesario para una detección robusta.
BOOL IsCallInstruction(DWORD_PTR instructionAddress) {
    BYTE opcodeBytes[5]; // Suficiente para CALL rel32 y otros CALLs comunes
    SIZE_T bytesRead;

    if (!ReadProcessMemory(GetCurrentProcess(), (LPCVOID)instructionAddress, opcodeBytes, sizeof(opcodeBytes), &bytesRead)) {
        return FALSE; // Error al leer la memoria.
    }

    if (bytesRead < 1) return FALSE;

    // Patrones de CALL comunes en x86:
    // E8 cd          CALL rel32      (cd es el offset de 4 bytes)
    // FF /2          CALL r/m16/32   (ej. FF D0 para CALL EAX)
    if (opcodeBytes[0] == 0xE8) { // CALL rel32
        return TRUE;
    }
    if (opcodeBytes[0] == 0xFF) { // CALL indirecto
        if (bytesRead < 2) return FALSE;
        // La segunda parte del opcode (ModR/M) determina el tipo de operando.
        // Los opcodes FF D0-D7 corresponden a CALL REG.
        if ((opcodeBytes[1] >= 0xD0) && (opcodeBytes[1] <= 0xD7)) {
            return TRUE; // CALL EAX, ECX, EDX, EBX, ESP, EBP, ESI, EDI
        }
        // Otros patrones como CALL [mem] también existen (FF /2).
        // Se requeriría un análisis más profundo del byte ModR/M.
        // Para simplificar, nos centramos en los más directos.
    }

    return FALSE;
}

// Función de mitigación para verificar el origen de la llamada.
// returnAddress: La dirección de retorno que está en la pila (apunta a la instrucción después del CALL).
// targetFunctionEntryPoint: La dirección de entrada de la función crítica interceptada.
BOOL VerificarOrigenDeLlamada(DWORD_PTR returnAddress, DWORD_PTR targetFunctionEntryPoint) {
    // La dirección de retorno apunta a la instrucción *después* de la instrucción CALL.
    // Necesitamos buscar la instrucción CALL en las posiciones *anteriores* a returnAddress.
    // Un CALL rel32 típico ocupa 5 bytes. Buscaremos en un rango pequeño.
    const int MAX_CALL_INSTRUCTION_LEN = 5; // Longitud máxima para CALL rel32

    for (int offset = 1; offset <= MAX_CALL_INSTRUCTION_LEN; ++offset) {
        DWORD_PTR probableCallAddr = returnAddress - offset;
        if (IsCallInstruction(probableCallAddr)) {
            // Si encontramos un CALL, asumimos que fue una llamada legítima.
            // Para ser más estrictos, podríamos desensamblar y verificar si el destino
            // del CALL coincide con targetFunctionEntryPoint.
            // Esto es significativamente más complejo y requiere un desensamblador real.
            return TRUE; // Llamada legítima detectada.
        }
    }
    
    char debugMessage[256];
    sprintf_s(debugMessage, sizeof(debugMessage), "MITIGACION ROP: Llamada a función crítica (0x%p) sin CALL previo detectada. Bloqueado.", (void*)targetFunctionEntryPoint);
    OutputDebugStringA(debugMessage);
    return FALSE; // No se encontró un CALL precedente válido, posible ataque ROP.
}

// Ejemplo de cómo se usaría en un hook de función:
// extern "C" __declspec(naked) void Hooked_CriticalFunction() {
//     __asm {
//         PUSHAD // Guardar registros
//         PUSHFD
//         // Preparar argumentos para VerificarOrigenDeLlamada (ej. ESP+32+4 para RetAddr, y EIP de la función hookeada)
//         MOV EAX, [ESP + 32 + 4] // Obtener la dirección de retorno original de la pila
//         PUSH EAX // returnAddress
//         PUSH DWORD PTR [OriginalFunctionAddress] // targetFunctionEntryPoint
//         CALL VerificarOrigenDeLlamada
//         ADD ESP, 8 // Limpiar la pila de los argumentos de VerificarOrigenDeLlamada
//         TEST EAX, EAX
//         JZ ROP_DETECTED
//         POPFD
//         POPAD
//         JMP [OriginalFunctionAddress] // Saltar a la función original
//     ROP_DETECTED:
//         // Lógica para terminar el proceso o registrar el evento.
//         // ExitProcess(0);
//     }
// }

4. Simulación del Flujo de Ejecución (Simulate Execution Flow)

Esta técnica analiza el flujo de ejecución inmediatamente después de una llamada a una función crítica. Dado que los "gadgets" ROP son secuencias cortas que terminan en RETN, este mecanismo simula un número limitado de instrucciones (por ejemplo, 15) después de la función crítica. Se rastrean los cambios en los punteros de la pila (ESP, EBP) y se detecta si la ejecución resultante es coherente con un flujo legítimo, o si apunta a otra cadena de "gadgets" maliciosos.

Se presta especial atención a las instrucciones que manipulan la pila (PUSH, POP, ADD ESP, SUB ESP, RETN). La simulación se detiene si se encuentra un número predefinido de instrucciones, o si se detecta una instrucción que altera el flujo de ejecución de manera inesperada (excepto RETN).

Implementación de ejemplo:


#include <windows.h>
#include <string>
#include <cstdio> // For sprintf_s

// Contexto de la CPU simulado para rastrear el estado de la pila y ejecución
typedef struct _CONTEXTO_SIMULADO
{
    DWORD_PTR ebp_reg;
    DWORD_PTR esp_reg;
    DWORD_PTR eip_reg; // Puntero de instrucción actual
} CONTEXTO_SIMULADO;

// Tipos de flujo de ejecución
enum TipoFlujoEjecucion
{
    FLUJO_ERROR_SIM = 0,
    FLUJO_RAMA_SIM,     // CALL o JMP
    FLUJO_SECUENCIAL_SIM, // Ejecución normal
    FLUJO_RETORNO_SIM   // RET
};

// Categorías de instrucciones para simulación
enum CategoriaInstruccion
{
    INS_SIM_ERROR = 0,
    INS_SIM_CALL,
    INS_SIM_JMP,
    INS_SIM_MOV_ESP_EBP,
    INS_SIM_PUSH,
    INS_SIM_POP,
    INS_SIM_POP_ESP,
    INS_SIM_POP_EBP,
    INS_SIM_ADD_ESP,
    INS_SIM_SUB_ESP,
    INS_SIM_LEAVE,
    INS_SIM_RETURN,
    INS_SIM_UNKNOWN_SEQ // Instrucción secuencial no manipuladora de pila directa
};

// Función auxiliar para obtener la categoría y el tamaño de una instrucción.
// Esto es una simplificación extrema; un desensamblador real sería necesario.
CategoriaInstruccion ObtenerInfoInstruccion(DWORD_PTR eip, UINT* instructionSize) {
    BYTE opcode;
    if (!ReadProcessMemory(GetCurrentProcess(), (LPCVOID)eip, &opcode, sizeof(opcode), NULL)) {
        *instructionSize = 0;
        return INS_SIM_ERROR;
    }

    *instructionSize = 1; // Tamaño por defecto.

    switch (opcode) {
        case 0xE8: *instructionSize = 5; return INS_SIM_CALL;    // CALL rel32
        case 0xE9: *instructionSize = 5; return INS_SIM_JMP;     // JMP rel32
        case 0xC3: *instructionSize = 1; return INS_SIM_RETURN;  // RET
        case 0xC2: *instructionSize = 3; return INS_SIM_RETURN;  // RET imm16
        case 0x50: case 0x51: case 0x52: case 0x53: case 0x54: case 0x55: case 0x56: case 0x57:
        case 0x6A: case 0x68: *instructionSize = (opcode == 0x6A ? 2 : (opcode == 0x68 ? 5 : 1)); return INS_SIM_PUSH; // PUSH reg/imm
        case 0x58: case 0x59: case 0x5A: case 0x5B: case 0x5C: case 0x5D: case 0x5E: case 0x5F:
            if (opcode == 0x5C) return INS_SIM_POP_ESP; // POP ESP (x86)
            if (opcode == 0x5D) return INS_SIM_POP_EBP; // POP EBP (x86)
            return INS_SIM_POP; // POP reg
        case 0xC9: *instructionSize = 1; return INS_SIM_LEAVE;   // LEAVE
        case 0x8B: // Puede ser MOV ESP, EBP (8B E5)
            {
                BYTE nextByte;
                if (ReadProcessMemory(GetCurrentProcess(), (LPCVOID)(eip + 1), &nextByte, sizeof(nextByte), NULL) && nextByte == 0xE5) {
                    *instructionSize = 2; return INS_SIM_MOV_ESP_EBP;
                }
            }
            break;
        case 0x83: // ADD/SUB ESP, imm8 (ej. 83 C4 imm8, 83 EC imm8)
            {
                BYTE nextByte;
                if (ReadProcessMemory(GetCurrentProcess(), (LPCVOID)(eip + 1), &nextByte, sizeof(nextByte), NULL)) {
                    if (nextByte == 0xC4) { *instructionSize = 3; return INS_SIM_ADD_ESP; }
                    if (nextByte == 0xEC) { *instructionSize = 3; return INS_SIM_SUB_ESP; }
                }
            }
            break;
        case 0x81: // ADD/SUB ESP, imm32 (ej. 81 C4 imm32, 81 EC imm32)
            {
                BYTE nextByte;
                if (ReadProcessMemory(GetCurrentProcess(), (LPCVOID)(eip + 1), &nextByte, sizeof(nextByte), NULL)) {
                    if (nextByte == 0xC4) { *instructionSize = 6; return INS_SIM_ADD_ESP; }
                    if (nextByte == 0xEC) { *instructionSize = 6; return INS_SIM_SUB_ESP; }
                }
            }
            break;
    }
    return INS_SIM_UNKNOWN_SEQ; // Por defecto, es una instrucción secuencial que no se rastrea.
}


// Simula un solo paso de ejecución y actualiza el contexto.
TipoFlujoEjecucion SimularPasoEjecucion(CONTEXTO_SIMULADO* ctx) {
    UINT instrLen = 0;
    CategoriaInstruccion instrType = ObtenerInfoInstruccion(ctx->eip_reg, &instrLen);

    if (instrLen == 0 || instrType == INS_SIM_ERROR) {
        return FLUJO_ERROR_SIM;
    }

    switch (instrType) {
        case INS_SIM_CALL:
        case INS_SIM_JMP:
            return FLUJO_RAMA_SIM;
        case INS_SIM_MOV_ESP_EBP:
            ctx->esp_reg = ctx->ebp_reg;
            break;
        case INS_SIM_PUSH:
            ctx->esp_reg -= sizeof(DWORD_PTR);
            break;
        case INS_SIM_POP:
            ctx->esp_reg += sizeof(DWORD_PTR);
            break;
        case INS_SIM_POP_EBP:
            ctx->ebp_reg = *(DWORD_PTR*)ctx->esp_reg; // El valor desapilado se va a EBP
            ctx->esp_reg += sizeof(DWORD_PTR);
            break;
        case INS_SIM_POP_ESP:
            // Esto es complejo; POP ESP carga un valor de la pila a ESP, cambiando la base de la pila.
            // La lógica de EMET es una heurística para detectar manipulación de ESP.
            // Para una simulación real, necesitaríamos leer el valor de la pila.
            ctx->esp_reg = *(DWORD_PTR*)ctx->esp_reg; // Asumimos que el valor de la pila se convierte en el nuevo ESP
            // No se incrementa ESP después de esto, ya que ahora es el nuevo ESP.
            // En el contexto de EMET, a veces se veía un incremento aquí para "limpiar" el viejo ESP de la pila.
            break;
        case INS_SIM_LEAVE:
            ctx->esp_reg = ctx->ebp_reg;
            ctx->ebp_reg = *(DWORD_PTR*)ctx->esp_reg;
            ctx->esp_reg += sizeof(DWORD_PTR);
            break;
        case INS_SIM_ADD_ESP: // Para ADD ESP, immX
            if (instrLen == 3) ctx->esp_reg += *(char*)(ctx->eip_reg + 2); // Inmediato de 1 byte
            else if (instrLen == 6) ctx->esp_reg += *(DWORD*)(ctx->eip_reg + 2); // Inmediato de 4 bytes
            break;
        case INS_SIM_SUB_ESP: // Para SUB ESP, immX
            if (instrLen == 3) ctx->esp_reg -= *(char*)(ctx->eip_reg + 2); // Inmediato de 1 byte
            else if (instrLen == 6) ctx->esp_reg -= *(DWORD*)(ctx->eip_reg + 2); // Inmediato de 4 bytes
            break;
        case INS_SIM_RETURN:
            return FLUJO_RETORNO_SIM;
        case INS_SIM_UNKNOWN_SEQ:
        default:
            // No se hace nada especial con la pila para otras instrucciones secuenciales.
            break;
    }

    // Actualizar EIP para el siguiente paso de ejecución, excepto si es un retorno o una rama que la simulación no sigue.
    if (instrType != INS_SIM_RETURN && instrType != INS_SIM_JMP && instrType != INS_SIM_CALL) {
        ctx->eip_reg += instrLen;
    }
    
    return FLUJO_SECUENCIAL_SIM;
}

// Función principal de mitigación por simulación de ejecución.
// startEIP: El EIP inicial para la simulación (ej. la dirección de retorno después de la función hookeada).
// startESP: El ESP inicial para la simulación.
// startEBP: El EBP inicial para la simulación.
BOOL MitigarPorSimulacionEjecucion(DWORD_PTR startEIP, DWORD_PTR startESP, DWORD_PTR startEBP) {
    CONTEXTO_SIMULADO simulatedContext;
    simulatedContext.eip_reg = startEIP;
    simulatedContext.esp_reg = startESP;
    simulatedContext.ebp_reg = startEBP;

    const int MAX_INSTRUCTIONS_TO_SIMULATE = 15; 

    for (int i = 0; i < MAX_INSTRUCTIONS_TO_SIMULATE; ++i) {
        TipoFlujoEjecucion flowType = SimularPasoEjecucion(&simulatedContext);

        if (flowType == FLUJO_ERROR_SIM) {
            OutputDebugStringA("MITIGACION ROP: Error en la simulación del flujo de ejecución. Bloqueado.");
            return FALSE; // Error en la simulación, potencialmente malicioso.
        } else if (flowType == FLUJO_RAMA_SIM) {
            // Si la simulación encuentra un CALL o JMP, se asume que el control de flujo
            // ha salido de la cadena de gadgets ROP simple. Esto podría ser legítimo.
            // En un sistema muy estricto, también podría ser una alerta.
            return TRUE; 
        } else if (flowType == FLUJO_RETORNO_SIM) {
            // Se encontró un RET. Ahora simulamos el POP de la dirección de retorno.
            DWORD_PTR nextReturnAddress = *(DWORD_PTR*)simulatedContext.esp_reg;
            simulatedContext.esp_reg += sizeof(DWORD_PTR); // Simular pop

            // Ahora, 'nextReturnAddress' es el siguiente EIP en la cadena ROP (el próximo gadget).
            // Se verifica si esta dirección es ejecutable.
            MEMORY_BASIC_INFORMATION memInfo;
            if (VirtualQuery((LPCVOID)nextReturnAddress, &memInfo, sizeof(memInfo)) == 0 ||
                !(memInfo.Protect & (PAGE_EXECUTE | PAGE_EXECUTE_READ | PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY))) {
                char debugMessage[256];
                sprintf_s(debugMessage, sizeof(debugMessage), "MITIGACION ROP: Flujo simulado detecta retorno a dirección no ejecutable: 0x%p. Bloqueado.", (void*)nextReturnAddress);
                OutputDebugStringA(debugMessage);
                return FALSE; // Retorno a una dirección no ejecutable, muy sospechoso.
            }
            // Opcional: Podríamos también hacer un 'Caller Check' aquí para 'nextReturnAddress'.
            
            // Actualizar EIP para continuar la simulación desde el próximo gadget.
            simulatedContext.eip_reg = nextReturnAddress;
        }
        // Si flowType es FLUJO_SECUENCIAL_SIM, el EIP ya fue actualizado en SimularPasoEjecucion.
    }

    return TRUE; // La simulación de X instrucciones no detectó anomalías.
}

5. Detección de Pivote de Pila (Stack Pivot)

Esta mitigación se centra en detectar manipulaciones maliciosas del puntero de la pila (ESP). Un "pivote de pila" ocurre cuando un atacante desvía el ESP a una región de memoria controlada por él, típicamente fuera de la región legítima de la pila del hilo. Este control permite al atacante construir y controlar completamente la pila para su cadena ROP.

Implementación de ejemplo:

Al interceptar una función crítica, se examina el valor actual del puntero de la pila. Si el ESP no se encuentra dentro de los límites esperados de la pila del hilo en ejecución, se considera un intento de pivote de pila y se bloquea la ejecución.


#include <windows.h>
#include <winnt.h> // Para NT_TIB
#include <cstdio> // For sprintf_s

// Función para obtener el Thread Environment Block (TEB) del hilo actual.
// Esto es una macro o intrínseca de MSVC; para otros compiladores, podría requerir asm.
// extern "C" NT_TIB* __stdcall NtCurrentTeb(); 

// Función de mitigación para detectar pivotes de pila
// currentStackPointer: El valor actual del puntero de pila (ESP o RSP) en el momento de la llamada a la función crítica.
BOOL DetectarPivoteDePila(DWORD_PTR currentStackPointer) {
    // Obtener información del Thread Environment Block (TEB) del hilo actual
    // para acceder a los límites de la pila.
    NT_TIB* teb = (NT_TIB*)NtCurrentTeb(); 
    if (!teb) {
        OutputDebugStringA("MITIGACION ROP: No se pudo obtener el TEB del hilo. No se pudo verificar el pivote de pila.");
        return TRUE; // Fallo seguro, pero podría ser una brecha.
    }

    DWORD_PTR stackLimit = (DWORD_PTR)teb->StackLimit;
    DWORD_PTR stackBase = (DWORD_PTR)teb->StackBase;

    // La pila en Windows crece hacia abajo. Por lo tanto, stackLimit es la dirección más baja
    // y stackBase es la dirección más alta de la región de la pila asignada.
    // Un puntero de pila válido (ESP/RSP) debe estar dentro de este rango.
    // currentStackPointer debe ser >= stackLimit y < stackBase.
    if (currentStackPointer < stackLimit || currentStackPointer >= stackBase) {
        // El puntero de pila está fuera de los límites de la pila legítima del hilo.
        char debugMessage[256];
        sprintf_s(debugMessage, sizeof(debugMessage), "MITIGACION ROP: Pivote de pila detectado. ESP (0x%p) fuera del rango [0x%p - 0x%p]. Bloqueado.",
                  (void*)currentStackPointer, (void*)stackLimit, (void*)stackBase);
        OutputDebugStringA(debugMessage);
        return FALSE; // Bloquear la operación.
    }

    return TRUE; // El puntero de pila parece estar en una región de pila legítima.
}

// Ejemplo de uso en un hook de función (requiere ensamblador inline o intrínsecas):
// extern "C" __declspec(naked) void Hooked_ExampleFunction() {
//     __asm {
//         PUSHAD // Guardar todos los registros generales
//         PUSHFD // Guardar los flags de registro
//
//         // Obtener el valor actual de ESP (original, antes de PUSHAD/PUSHFD)
//         // El ESP después de PUSHFD apunta a la dirección de los flags,
//         // y antes de PUSHFD apuntaba a la dirección de retorno de la función.
//         // Si queremos el ESP de la pila original de la función, necesitamos
//         // calcularlo basándonos en los PUSH que se acaban de hacer.
//         // ESP actual (después de PUSHFD) + (sizeof(DWORD) * 9) para POPFD + POPAD = ESP original.
//         MOV EAX, ESP
//         ADD EAX, (4 * 9) // 8 registros + 1 flags = 9 DWORDS
//         
//         PUSH EAX // Pasar el ESP original como argumento a DetectarPivoteDePila
//         CALL DetectarPivoteDePila
//         ADD ESP, 4 // Limpiar el argumento
//
//         TEST EAX, EAX // Verificar el resultado de DetectarPivoteDePila
//         JZ ROP_PIVOT_DETECTED
//
//         POPFD // Restaurar flags
//         POPAD // Restaurar registros
//         JMP [OriginalExampleFunctionAddress] // Saltamos a la función original
//
//     ROP_PIVOT_DETECTED:
//         // Lógica para manejar la detección de ROP, por ejemplo, terminar el proceso.
//         // ExitProcess(0);
//     }
// }

Etiquetas: seguridad explotación rop mitigación Windows API

Publicado el 8-8 12:57