Volver al blog

Cifrado XOR: Destripando el Cifrado y Evadiendo Firmas

Aprende a cifrar y ofuscar shellcodes usando XOR. Desde la teoría criptográfica hasta implementaciones prácticas que hacen detecciones más difíciles.

David Herrera
David Herrera Shac0x
35 min
Cifrado XOR: Destripando el Cifrado y Evadiendo Firmas

Cifrado de Shellcode con XOR: Protegiendo Tu Payload

En el post anterior sobre inyección de shellcode, aprendimos cómo crear e inyectar código máquina directamente en memoria. Pero hay un problema grave que no podemos ignorar: si dejas tu shellcode sin cifrar, los antivirus lo detectarán en segundos.

Los antivirus y sistemas EDR (Endpoint Detection and Response) escanean los binarios buscando patrones de shellcode conocidos. Es como dejar un cartel que dice “¡Código malicioso aquí!”. Para evadir esta detección, necesitamos ofuscar nuestro payload, y XOR es la herramienta más simple, rápida y efectiva para hacerlo.

En este tutorial completo, no solo aprenderás cómo implementar XOR paso a paso, sino que entenderás la matemática detrás de la operación, explorarás tres variantes diferentes (desde la más básica hasta la más robusta), y descubrirás exactamente por qué funciona… y también sus limitaciones.

¿Qué es XOR? La Puerta a la Criptografía Simple

Antes de implementar, necesitamos entender qué es XOR realmente y por qué funciona tan bien para ofuscar shellcode.

La Operación XOR (OR Exclusivo)

XOR es una operación lógica fundamental en computación. Si nunca has trabajado con bits, no te preocupes: es muy simple. XOR compara dos bits y devuelve 1 si son diferentes, y 0 si son iguales.

Tabla de Operación XOR

ABA XOR B
000
011
101
110

En código C, XOR se representa con el operador ^:

unsigned char resultado = 0xAB ^ 0x42;  // Ejemplo: 171 XOR 66 = 213

La Propiedad Fundamental: Reversibilidad

La característica más importante de XOR es su reversibilidad. Esto es lo que lo hace perfecto para ofuscación: si aplicas XOR dos veces con la misma clave, obtienes el valor original.

Matemáticamente:

A XOR B = C
C XOR B = A

Ejemplo práctico:

unsigned char dato_original = 0x72;  // 114 en decimal (letra 'r')
unsigned char clave = 0x33;          // 51 en decimal

// Cifrar
unsigned char cifrado = dato_original ^ clave;  // 0x72 ^ 0x33 = 0x41 (65)

// Descifrar (¡la MISMA operación!)
unsigned char descifrado = cifrado ^ clave;     // 0x41 ^ 0x33 = 0x72 (114)
// Resultado: descifrado == dato_original ✓

¿Por qué es importante? Porque podemos usar exactamente la misma función tanto para cifrar como para descifrar. No necesitamos lógica diferente para cada una. Aplicas XOR con la clave y listo.

¿Por Qué XOR para Shellcode?

XOR es la estrella de la ofuscación de shellcode. No porque sea el mejor cifrado (spoiler: no lo es), sino porque es prácticamente perfecto para este uso específico:

Ventajas:

  • Velocidad extrema - Una operación por byte, sin latencia perceptible
  • 📦 Sin dependencias - No necesitas librerías criptográficas complejas
  • 🔄 Bidireccional - Usa la misma función para cifrar y descifrar
  • 📝 Código minimalista - Solo unas pocas líneas de C
  • 🎭 Rompe firmas - Transforma datos reconocibles en “ruido” aleatorio

Las limitaciones son importantes:

  • No es seguro contra expertos - Si alguien dedicado quiere quebrarlo, lo hará
  • Vulnerable si se repite - Si el atacante ve múltiples ejemplos del mismo shellcode, puede encontrar patrones

La Matemática Detrás de XOR: Entendiendo la Ofuscación

Cómo XOR Transforma Bytes

Aquí ocurre la magia. Cuando XOReas un byte, cada uno de sus bits se compara con el bit correspondiente de la clave. Veamos un ejemplo real:

Shellcode original:  0x72    (114 en decimal)
Clave:               0x33    (51 en decimal)
                     ----
Resultado:           0x41    (65 en decimal)

En binario:

01110010  (0x72)
00110011  (0x33)
--------
01000001  (0x41)  ← Este es completamente diferente

Lo importante es que este proceso se repite para cada byte del shellcode. Un shellcode de 1000 bytes se transforma completamente en 1000 bytes “ruidosos” que no parecen código.

¿Por Qué Evita la Detección?

Los antivirus tienen bases de datos gigantescas con “firmas” de shellcode conocidas. Buscan patrones como: \xfc\x48\x83\xe4\xf0...

Pero cuando cifras con XOR:

  • El original \xfc\x48\x83\xe4... se convierte en \xaf\x7b\xb0\xd7... (completamente diferente)
  • La firma no coincide
  • El antivirus ve datos “aleatorios” y los deja pasar

El problema es que cuando el código se ejecuta en memoria, tiene que desencriptarse, y en ese momento queda expuesto. Por eso es crítico desencriptarlo lo más tarde posible durante la ejecución.

Variante 1: XOR con Clave Única (Simple)

Esta es la forma más básica: tomas un solo byte (por ejemplo: 0x42) y lo usas como clave para cifrar cada byte del shellcode. Es simple, directo, y funciona. Es perfecta para aprender el concepto.

Sin embargo, hay un problema de seguridad grave: si alguien quiere quebrar tu cifrado, tiene que probar solo 256 combinaciones posibles (0x00 a 0xFF). En un ordenador moderno, esto toma milisegundos. Así que es fácil de entender y rápido de ejecutar, pero terrible para la seguridad real.

Implementación Básica

/**
 * Encripta/Desencripta un shellcode usando XOR con una clave de un byte
 *
 * Parámetros:
 *  - pShellcode: Puntero al buffer que contiene el shellcode
 *  - sShellcodeSize: Tamaño en bytes del shellcode
 *  - bKey: Un byte que actúa como clave
 */
VOID XorByOneKey(IN PBYTE pShellcode, IN SIZE_T sShellcodeSize, IN BYTE bKey) {
    for (size_t i = 0; i < sShellcodeSize; i++) {
        pShellcode[i] = pShellcode[i] ^ bKey;
    }
}

Análisis de Seguridad

Ventajas: Es ultra simple de implementar (solo 3 líneas de código) y corre a la velocidad de la luz.

Problemas serios: Solo hay 256 claves posibles. Si tu shellcode contiene bytes repetidos (como múltiples ceros), estos siempre se cifran igual, dejando pistas. Alguien con herramientas básicas puede romper esto en segundos probando todas las 256 combinaciones automáticamente. No uses esto en producción si esperas resistir un análisis dedicado.

Variante 2: XOR Dinámico (i + clave)

Aquí subimos de nivel. En lugar de usar el mismo byte como clave para todo, cambiamos la clave basándote en la posición del byte.

Por ejemplo, si tu clave base es 0x42:

  • Byte 0 se cifra con: 0x42 + 0 = 0x42
  • Byte 1 se cifra con: 0x42 + 1 = 0x43
  • Byte 2 se cifra con: 0x42 + 2 = 0x44
  • …y así sucesivamente

Esto es mucho más seguro que la variante 1. El espacio de claves ahora es 256 × tamaño_del_shellcode. Si tu shellcode tiene 1000 bytes, tienes 256,000 combinaciones posibles en lugar de solo 256. Aun así, no es impenetrable si alguien conoce parte del shellcode original.

Implementación Mejorada

/**
 * Encripta/Desencripta usando XOR dinámico
 * La clave cambia para cada byte: (bKey + i) donde i es el índice
 *
 * Parámetros:
 *  - pShellcode: Puntero al buffer
 *  - sShellcodeSize: Tamaño del shellcode
 *  - bKey: Clave base
 */
VOID XorByIndexKey(IN PBYTE pShellcode, IN SIZE_T sShellcodeSize, IN BYTE bKey) {
    for (size_t i = 0; i < sShellcodeSize; i++) {
        pShellcode[i] = pShellcode[i] ^ (bKey + i);
    }
}

Análisis de Seguridad

Mejor: Cada byte usa una clave diferente, lo que hace el análisis de frecuencia más complicado.

Aún vulnerable: Si alguien tiene un ejemplo del shellcode original, puede deducir la clave base. Además, si ven múltiples ejemplos del mismo shellcode cifrado con este método, los patrones serán idénticos cada vez, exponiendo que es la misma carga.

Variante 3: XOR con Clave Múltiple (Rotor)

Esta es la que deberías usar si quieres seguridad. En lugar de una sola clave o una clave basada en índice, usas un array de múltiples bytes que actúa como un “rotor”.

Imagina que tu clave es: [0x12, 0x34, 0x56, 0x78, 0x9A, ...] (digamos, 32 bytes)

El rotor cicla así:

  • Byte 0 se cifra con: clave[0] = 0x12
  • Byte 1 se cifra con: clave[1] = 0x34
  • Byte 2 se cifra con: clave[2] = 0x56
  • Byte 31 se cifra con: clave[31] = 0x??
  • Byte 32 se cifra con: clave[0] = 0x12 (¡el rotor vuelve al inicio!)

Esto multiplica exponencialmente el espacio de claves: 256^32 combinaciones. Buena suerte haciendo fuerza bruta. Además, es mucho más realista criptográficamente (aunque sigue siendo XOR, no es un algoritmo “serio” como AES).

Implementación Avanzada

/**
 * Encripta/Desencripta usando XOR con array de claves (rotor)
 * La clave se repite cíclicamente a través del shellcode
 *
 * Parámetros:
 *  - pShellcode: Puntero al buffer del shellcode
 *  - sShellcodeSize: Tamaño del shellcode
 *  - pKeyBuffer: Puntero a array de bytes que contiene la clave
 *  - sKeySize: Tamaño del array de clave
 */
VOID XorByMultipleKeys(IN PBYTE pShellcode, IN SIZE_T sShellcodeSize,
                       IN PBYTE pKeyBuffer, IN SIZE_T sKeySize) {
    for (size_t i = 0, j = 0; i < sShellcodeSize; i++) {
        pShellcode[i] = pShellcode[i] ^ pKeyBuffer[j];

        // Hacer rotar el índice de la clave
        j = (j + 1) % sKeySize;  // o simplemente: if (++j >= sKeySize) j = 0;
    }
}

Análisis de Seguridad

Lo bueno: El espacio de claves es masivo (256^32 si usas 32 bytes). El análisis de frecuencia es mucho más complicado porque los patrones se distribuyen entre múltiples bytes de clave. Es profesional y funciona bien en la práctica.

Las limitaciones: Sigue siendo XOR, no un algoritmo “serio” como AES. Si alguien logra recuperar parte de la clave, el resto del shellcode cae. También necesitas guardar el array de clave en algún lado, y debes hacerlo de forma segura.

Implementación Completa: Paso a Paso

A continuación, crearemos un programa que demuestra el cifrado y descifrado de XOR de forma interactiva. El programa toma un shellcode, lo muestra original, lo cifra con una clave de 8 bytes, y luego lo descifra para verificar que la reversibilidad de XOR funciona correctamente.

Paso 1: Función XOR (La Base)

// Cifra o descifra un buffer con XOR
void XorEncryptDecrypt(unsigned char *data, int size,
                       unsigned char *key, int key_size) {
    for (int i = 0, j = 0; i < size; i++) {
        data[i] = data[i] ^ key[j];
        j = (j + 1) % key_size;  // Rotar la clave
    }
}

Paso 2: Programa Principal Completo

#include <windows.h>
#include <stdio.h>
#include <string.h>

#define XOR_KEY_SIZE 32

int main() {
    unsigned char shellcode[] = { 0xfc, 0x48, 0x83, 0xe4, 0xf0, 0xe8, 0xc0, 0x00 };
    unsigned char encrypted[8] = {0};
    unsigned char xor_key[8] = { 0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0 };
    int size = 8;

    printf("ORIGINAL:   ");
    for (int i = 0; i < size; i++) printf("%02x ", shellcode[i]);
    printf("\nPresiona ENTER...\n");
    getchar();

    for (int i = 0; i < size; i++) encrypted[i] = shellcode[i];

    XorEncryptDecrypt(encrypted, size, xor_key, 8);
    printf("\nCIFRADO:    ");
    for (int i = 0; i < size; i++) printf("%02x ", encrypted[i]);
    printf("\nPresiona ENTER...\n");
    getchar();

    XorEncryptDecrypt(encrypted, size, xor_key, 8);
    printf("\nDESCIFRADO: ");
    for (int i = 0; i < size; i++) printf("%02x ", encrypted[i]);
    printf("\n");

    return 0;
}

Ofuscación Avanzada: Técnicas Complementarias

XOR es efectivo por sí solo, pero es aún más poderoso cuando lo combinas con otras técnicas defensivas.

Técnica 1: Cifrado en Capas

En lugar de una sola ronda de XOR, aplica XOR dos veces con claves diferentes. Primero cifra con la clave 1, luego cifra el resultado con la clave 2:

// Cifrar en dos capas
XorEncryptDecrypt(pShellcode, sShellcodeSize, key1, key1Size);
XorEncryptDecrypt(pShellcode, sShellcodeSize, key2, key2Size);

// Descifrar en orden inverso
XorEncryptDecrypt(pShellcode, sShellcodeSize, key2, key2Size);
XorEncryptDecrypt(pShellcode, sShellcodeSize, key1, key1Size);

¿Por qué? Hace que el análisis sea exponencialmente más difícil. Alguien que quiera quebrarlo debe descubrir ambas claves, no solo una.

Técnica 2: Oculta la Clave

Guardar la clave en claro en el binario es el punto débil. Mejores alternativas:

// Opción 1: Clave encriptada en el binario
BYTE encrypted_key[] = { 0x45, 0x67, 0x89... };
// Desencríptala primero con otra técnica

// Opción 2: Genera la clave en tiempo de ejecución
BYTE key[32];
GenerateKeyFromEnvironment(key);
// Basada en info del sistema actual

// Opción 3: Lee la clave del registro
GetKeyFromRegistry(HKEY_LOCAL_MACHINE, "Software\\...", key);

De esta forma, el analizador no puede simplemente leer la clave del ejecutable.

Limitaciones de XOR: Entendiendo las Realidades

Siendo honesto: XOR tiene límites, y es importante saberlos.

1. XOR No es “Cifrado Serio”

XOR se usó en máquinas Enigma en la Segunda Guerra Mundial. Hoy es considerado un juguete criptográfico. No lo uses para:

  • Proteger datos sensibles contra adversarios profesionales
  • Cumplir estándares bancarios o de gobierno
  • Nada que requiera confidencialidad real

Pero para ofuscación de shellcode? Es más que suficiente.

2. El Shellcode Siempre Se Expone

Aquí está la verdad incómoda: No importa cuánto lo ocultes, el shellcode DEBE estar desencriptado en memoria para ejecutarse. Es un problema fundamental, no una limitación de XOR.

La clave es retrasar ese momento lo máximo posible en la ejecución.

3. EDR Sigue Vigilante

XOR disfraza el código, pero EDR (especialmente moderno) puede detectar:

  • El patrón de desencriptación - El loop XOR tiene una “firma” reconocible
  • APIs sospechosas - VirtualAlloc + VirtualProtect + CreateThread juntos = actividad maliciosa
  • Comportamiento post-ejecución - Qué hace el código una vez ejecutado

Análisis Práctico: Detectabilidad

En el mundo real, los EDRs pueden detectar tu código por tres razones principales:

  1. El patrón XOR es reconocible - El loop for(i=0; i<size; i++) buf[i] ^= key[j] tiene una “firma” característica. Los patrones de desencriptación son bien conocidos.

  2. La combinación de APIs es sospechosa - VirtualAlloc → VirtualProtect → CreateThread es un patrón clásico de inyección de código. EDR busca específicamente esta secuencia.

  3. Lo que hace después importa más - EDR puede estar menos interesado en cómo se carga el código y más en QUÉ hace. Si tu payload abre cmd.exe o exfiltra datos, eso se detecta con análisis de comportamiento.

Conclusión realista: XOR añade una capa de ofuscación y esquiva detecciones basadas en firmas, pero no es una bala de plata. Es una pieza de un puzzle más grande.

Lo Que Hemos Aprendido

Hemos cubierto bastante terreno:

  1. La matemática de XOR - Bits, operaciones reversibles, por qué evita firmas
  2. Tres variantes - Desde ultra simple hasta profesional con rotor
  3. Por qué cada una es mejor (o peor) - Los trade-offs reales
  4. Código completo y funcional - De teoría a práctica
  5. Técnicas avanzadas - Cifrado en capas y ocultación de claves
  6. La verdad dura - Qué funciona y qué no en el mundo real

Reflexión Final

XOR es tu primer escudo defensivo contra detecciones estáticas. Los antivirus que buscan firmas no te verán. EDR es otro cuento - eso requiere técnicas más sofisticadas como anti-análisis, inyección sin APIs sospechosas, o simplemente comportamiento “normal”.

Pero aquí viene lo importante: No existe la ofuscación perfecta. Todo código que se ejecuta debe estar en claro en algún punto. La meta realista no es “indetectable” (spoiler: no existe). La meta es hacer que el costo de análisis sea tan alto que no valga la pena.

Combina XOR con:

  • Cifrado en capas (múltiples rondas)
  • Ofuscación de claves
  • APIs menos obvias
  • Comportamiento que parece legítimo
Compartir este artículo

Sobre el autor

Pentester specializing in penetration testing, vulnerability identification, and securing systems against advanced threats.

📍 España
Ver perfil en GitHub

Artículos relacionados

Enlace copiado
Enlace copiado al portapapeles