RC4: Del XOR Básico a la Criptografía Stream
En posts anteriores exploramos XOR, nuestro primer aliado en la ofuscación de payloads. Es simple, rápido, y cumple su función. Pero aquí viene el problema:
XOR toma una clave y la repite. Si ves el mismo payload múltiples veces, los patrones son obvios. Los AV de hace 15 años ya podían detectarlo.
Entra RC4: el hermano mayor de XOR. Durante los 90s y 2000s, RC4 fue el estándar de facto en navegadores, TLS, y sistemas empresariales. Hoy está deprecado por NIST, pero para nuestros propósitos: “Evadir firmas estaticas” sigue siendo exponencialmente más efectivo que XOR.
¿Por qué? Porque RC4 no repite la clave. Genera una secuencia pseudoaleatoria diferente cada vez, haciendo que el mismo payload cifrado sea irreconocible.
En este tutorial, aprenderás tres formas de implementar RC4: desde una implementación pura en Rust (máximo control) hasta APIs nativas de Windows. Elige tu camino.
¿Qué es RC4? Entendiendo el Algoritmo
¿Sabías que…?
RC4 fue diseñado en 1987 por Ron Rivest como un cifrado stream simple y rápido. Durante años fue el estándar de facto en navegadores y protocolos como TLS/SSL. Sin embargo, en 2015 fue oficialmente prohibido en TLS 1.3 debido a vulnerabilidades descubiertas. Hoy es considerado criptográficamente débil… pero para ofuscación de payloads sigue siendo más que efectivo.
El Concepto: Un Generador de Secuencia Pseudoaleatoria
Aquí está el giro: XOR repite la clave. RC4 es completamente diferente.
RC4 genera una secuencia de bytes que PARECE aleatoria, pero es reproducible si conoces la clave. Luego, XORea tu payload con esa secuencia.
Visualicémoslo:
Tu clave: "malghost123"
↓
RC4 KSA (Key Scheduling)
[mezcla la clave]
↓
Secuencia generada: 0x7E, 0xC2, 0x5A, 0x91, 0x23, 0xD4...
↓
Tu payload: 0x90, 0xCC, 0x44, 0x2E, 0xAA, 0x3F...
↓
XOR resultante: 0xEE, 0x2E, 0x1E, 0xBD, 0x89, 0xEB...Aquí está la magia: La secuencia 0x7E, 0xC2, 0x5A… NO repite. Cada byte es diferente, generado dinámicamente. Así que si cifras el mismo payload dos veces con la misma clave, obtienes la MISMA salida (reproducible). Pero desde fuera, parece completamente aleatorio.
Con XOR: Misma clave = mismos bytes repetidos = patrones detectables.
Con RC4: Misma clave = secuencia única que parece aleatoria = casi imposible de detectar.
Lo honesto: Limitaciones reales
RC4 no es magia, tiene limitaciones que debes entender:
- ⚠️ RC4 está deprecado - NIST lo prohibió en TLS 1.3 por vulnerabilidades criptográficas serias.
- ⚠️ El payload se expone en memoria - Tarde o temprano, cuando lo ejecutes, aparecerá sin cifrar.
- ⚠️ EDR + Behavioral Analysis - Los EDRs modernos detectan comportamiento malicioso, no solo firmas.
Pero aquí está la clave: RC4 EVADE detecciones basadas en firmas estáticas. Es excelente para eludir el primer paso (análisis estático de archivos).
Para maximizar su efectividad, combina RC4 con:
- 🔑 Claves generadas dinámicamente
- 🛡️ Anti-debugging y anti-VM
- 👻 Comportamiento que parece legítimo
- 🔍 APIs menos obvias para inyección
Arquitectura de RC4: Los Dos Componentes Críticos
RC4 es simple en concepto pero efectivo en práctica. Funciona en dos fases:
- KSA (Key Scheduling Algorithm) - Setup inicial
- PRGA (Pseudo-Random Generation Algorithm) - Generación de bytes
Veamos cada una:
Fase 1: KSA (Key Scheduling Algorithm)
Piensa en esto: tienes un array de 256 bytes [0, 1, 2, 3, ..., 255]. El objetivo del KSA es mezclar completamente este array basándose en tu clave, para que la salida parezca aleatoria.
¿Por qué? Porque la siguiente fase (PRGA) usará este array “mezclado” para generar bytes de la secuencia. Sin esta mezcla, el cifrado sería predecible.
fn rc4_init(context: &mut Rc4Context, key: &[u8]) {
// Inicializa S-box con valores 0-255
for i in 0..256 {
context.s[i] = i as u8;
}
// Permuta S usando la clave
let mut j: u8 = 0;
for i in 0..256 {
j = j.wrapping_add(context.s[i]).wrapping_add(key[i % key.len()]);
// Intercambia valores
context.s.swap(i, j as usize);
}
context.i = 0;
context.j = 0;
}Qué ocurre: El S-box original [0, 1, 2, 3, ..., 255] se transforma en algo como [0xAB, 0x3D, 0xF2, ..., 0x5C] basado en tu clave.
Fase 2: PRGA (Pseudo-Random Generation Algorithm)
Aquí es donde ocurre la magia. Usamos el S-box “mezclado” del KSA para generar un stream de bytes que parecen aleatorios pero son reproducibles.
En cada iteración:
- Movemos dos índices (i, j) dentro del S-box
- Intercambiamos valores en el S-box
- Extraemos un byte de la secuencia
- XORea ese byte con tu payload
El resultado es una secuencia única que parece aleatoria pero es idéntica si repites el proceso con la misma clave.
fn rc4_cipher(context: &mut Rc4Context, input: &[u8], output: &mut [u8]) {
assert_eq!(input.len(), output.len());
for (in_byte, out_byte) in input.iter().zip(output.iter_mut()) {
// Genera el siguiente índice pseudoaleatorio
context.i = context.i.wrapping_add(1);
context.j = context.j.wrapping_add(context.s[context.i as usize]);
// Intercambia valores en el S-box
context.s.swap(context.i as usize, context.j as usize);
// Genera byte de la secuencia y XORea
let k_index = (context.s[context.i as usize] as u16 + context.s[context.j as usize] as u16) % 256;
*out_byte = in_byte ^ context.s[k_index as usize];
}
}Qué ocurre: Para cada byte del payload, se modifica el S-box de forma pseudoaleatoria y se genera un byte “impredecible” que XORea con tu dato.
Método 1: Implementación Pura en Rust
¿Por qué elegir este método?
- 🎓 Es la más educativa, entiendes cómo funciona realmente RC4
- 🔧 Control total sobre cada paso
- 🖥️ Funciona en cualquier plataforma (Windows, Linux, macOS)
- 🛡️ Seguridad de memoria garantizada por Rust (sin buffer overflows)
- ⚠️ Es la más “obvia” para análisis estático (firmas reconocen esta implementación)
Esta es la forma más directa: implementar RC4 desde cero en puro Rust.
Estructura y Módulos Base
#[derive(Clone)]
struct Rc4Context {
i: u8,
j: u8,
s: [u8; 256],
}
impl Rc4Context {
fn new() -> Self {
Rc4Context {
i: 0,
j: 0,
s: [0; 256],
}
}
}Inicialización del Contexto (KSA)
/// Inicializa el contexto de RC4 con la clave proporcionada
///
/// Parámetros:
/// - context: Contexto RC4 mutable
/// - key: La clave de cifrado
fn rc4_init(context: &mut Rc4Context, key: &[u8]) {
// Inicializa S-box: [0, 1, 2, ..., 255]
for i in 0..256 {
context.s[i] = i as u8;
}
// Permutación KSA: mezcla S-box usando la clave
let mut j: u8 = 0;
for i in 0..256 {
j = j.wrapping_add(context.s[i]).wrapping_add(key[i % key.len()]);
context.s.swap(i, j as usize);
}
context.i = 0;
context.j = 0;
}Cifrado/Descifrado (PRGA)
/// Cifra o descifra datos usando RC4
///
/// Parámetros:
/// - context: Contexto RC4 ya inicializado
/// - input: Datos de entrada (plaintext o ciphertext)
/// - output: Buffer de salida mutable
fn rc4_cipher(context: &mut Rc4Context, input: &[u8], output: &mut [u8]) {
assert_eq!(input.len(), output.len(), "Input y output deben tener el mismo tamaño");
for (in_byte, out_byte) in input.iter().zip(output.iter_mut()) {
// Genera el siguiente índice pseudoaleatorio
context.i = context.i.wrapping_add(1);
context.j = context.j.wrapping_add(context.s[context.i as usize]);
// Intercambia elementos en el S-box
context.s.swap(context.i as usize, context.j as usize);
// Genera byte de la secuencia y XORea
let k_index = (context.s[context.i as usize] as u16 + context.s[context.j as usize] as u16) % 256;
*out_byte = in_byte ^ context.s[k_index as usize];
}
}Programa Completo: Demostración Interactiva
Aquí usamos las funciones Rc4Context, rc4_init y rc4_cipher definidas en las secciones anteriores. El main() demuestra el flujo completo:
use std::io::{self};
// Usa las definiciones de Rc4Context, rc4_init y rc4_cipher de las secciones anteriores
fn main() {
// Payload de ejemplo (datos binarios simulando código máquina)
let payload = vec![
0x55, 0x89, 0xE5, 0x83, 0xEC, 0x10, 0xC7, 0x45, 0xF8, 0x00, 0x00, 0x00, 0x00,
0x8B, 0x45, 0xF8, 0x83, 0xC0, 0x01, 0x89, 0x45, 0xF8, 0x8B, 0x45, 0xF8
];
let rc4_key = b"malghost123";
let mut encrypted = vec![0u8; payload.len()];
let mut decrypted = vec![0u8; payload.len()];
println!("[*] Demostración RC4 Encryption/Decryption");
println!("[*] Tamaño de payload: {} bytes\n", payload.len());
// Mostrar original
print!("[+] PAYLOAD ORIGINAL:\n ");
for byte in &payload {
print!("{:02x} ", byte);
}
println!("\n\nPresiona ENTER para continuar...");
let mut input = String::new();
io::stdin().read_line(&mut input).unwrap();
// PASO 1: Cifrado
println!("\n[*] Cifrando con RC4...");
let mut ctx1 = Rc4Context::new();
rc4_init(&mut ctx1, rc4_key);
rc4_cipher(&mut ctx1, &payload, &mut encrypted);
print!("[+] PAYLOAD CIFRADO:\n ");
for byte in &encrypted {
print!("{:02x} ", byte);
}
println!("\n\nPresiona ENTER para continuar...");
let mut input = String::new();
io::stdin().read_line(&mut input).unwrap();
// PASO 2: Descifrado
println!("\n[*] Descifrando con RC4...");
let mut ctx2 = Rc4Context::new();
rc4_init(&mut ctx2, rc4_key);
rc4_cipher(&mut ctx2, &encrypted, &mut decrypted);
print!("[+] PAYLOAD DESCIFRADO:\n ");
for byte in &decrypted {
print!("{:02x} ", byte);
}
println!();
// Verificación
if payload == decrypted {
println!("\n[✓] Verificación EXITOSA: Descifrado coincide con original");
} else {
println!("\n[!] Verificación FALLIDA: Datos corruptos");
}
println!("\n\nPresiona ENTER para salir...");
let mut input = String::new();
io::stdin().read_line(&mut input).unwrap();
}Demo:

Método 2: SystemFunction032 (API Nativa de Windows)
¿Sabías que Windows ya tiene RC4 built-in? Existe una API poco conocida llamada SystemFunction032 que implementa RC4 nativamente.
¿Por qué elegir este método?
- 🛡️ Usa el código criptográfico oficial de Windows
- ⚡ Rendimiento nativo más rápido
- 👻 Menos firmas estáticas
- ⚠️ API no documentada (undocumented). Microsoft no garantiza compatibilidad futura
La contrapartida: Es una API “undocumented”, así que puede cambiar. Pero ha estado disponible desde Windows XP y sigue funcionando en Windows 11.
Cargo.toml
Para usar APIs de Windows desde Rust, necesitas la crate windows-rs:
[package]
name = "rc4_systemfunction032"
version = "0.1.0"
edition = "2024"
[dependencies]
windows = { version = "0.51", features = [
"Win32_Foundation",
"Win32_System_LibraryLoader",
] }Definición de SystemFunction032
Firma de la función SystemFunction032 en C:
// Declaración de la función (exportada desde Advapi32.dll)
NTSTATUS WINAPI SystemFunction032(
struct USTRING *Data, // Puntero a estructura con datos a cifrar/descifrar
const struct USTRING *Key // Puntero a estructura con la clave RC4
);Parámetros:
Data: Estructura USTRING que apunta a tus datos. Importante: se modifican IN-PLACE (el cifrado sobrescribe los datos originales)Key: Estructura USTRING que apunta a tu clave RC4- Retorno: NTSTATUS (0 = éxito, valores negativos = error)
Usando SystemFunction032
use windows::core::PCSTR;
use windows::Win32::System::LibraryLoader::{LoadLibraryA, GetProcAddress};
#[repr(C)]
struct USTRING {
length: u32,
maximum_length: u32,
buffer: *mut u8,
}
type SystemFunction032Fn = unsafe extern "system" fn(
*mut USTRING,
*mut USTRING,
) -> i32;
fn rc4_encryption_via_systemfunc032(
rc4_key: &[u8],
payload_data: &mut [u8],
) -> bool {
unsafe {
let advapi32 = match LoadLibraryA(PCSTR(b"Advapi32.dll\0".as_ptr())) {
Ok(lib) => lib,
Err(_) => {
println!("[!] No se pudo cargar Advapi32");
return false;
}
};
if advapi32.is_invalid() {
println!("[!] Handle inválido de Advapi32");
return false;
}
let system_function_032 = GetProcAddress(
advapi32,
PCSTR(b"SystemFunction032\0".as_ptr()),
);
if system_function_032.is_none() {
println!("[!] No se pudo obtener SystemFunction032");
return false;
}
let mut data = USTRING {
length: payload_data.len() as u32,
maximum_length: payload_data.len() as u32,
buffer: payload_data.as_mut_ptr(),
};
let mut key = USTRING {
length: rc4_key.len() as u32,
maximum_length: rc4_key.len() as u32,
buffer: rc4_key.as_ptr() as *mut u8,
};
let func: SystemFunction032Fn = std::mem::transmute(system_function_032.unwrap());
let status = func(&mut data, &mut key);
if status != 0 {
println!("[!] SystemFunction032 falló: 0x{:08X}", status);
return false;
}
println!("[+] RC4 cifrado exitoso con SystemFunction032");
true
}
}Programa Completo: Demostración
Aquí usamos la función rc4_encryption_via_systemfunc032 definida en la sección anterior. El main() muestra el flujo completo:
use windows::core::PCSTR;
use windows::Win32::System::LibraryLoader::{LoadLibraryA, GetProcAddress};
// Usa las definiciones de USTRING, SystemFunction032Fn y rc4_encryption_via_systemfunc032 de la sección anterior
fn main() {
let mut payload = vec![
0x4D, 0x5A, 0x90, 0x00, 0x03, 0x00, 0x00, 0x00,
0x04, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00,
0xB8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
];
let rc4_key = b"malghost123";
println!("[*] Cifrado RC4 usando SystemFunction032");
println!("[+] Tamaño: {} bytes\n", payload.len());
// Mostrar original
print!("[*] ORIGINAL:\n ");
for byte in &payload {
print!("{:02x} ", byte);
}
println!("\n");
// Cifrar
if !rc4_encryption_via_systemfunc032(rc4_key, &mut payload) {
println!("[!] Error en cifrado");
return;
}
print!("[+] CIFRADO:\n ");
for byte in &payload {
print!("{:02x} ", byte);
}
println!("\n");
// Descifrar (RC4 es bidireccional)
if !rc4_encryption_via_systemfunc032(rc4_key, &mut payload) {
println!("[!] Error en descifrado");
return;
}
print!("[+] DESCIFRADO:\n ");
for byte in &payload {
print!("{:02x} ", byte);
}
println!();
}Compilación
# Compilar para Windows x86_64
cargo build --release
# Ejecutar
cargo run --releaseMétodo 3: SystemFunction033 (Alternativa Nativa)
Si SystemFunction032 está patcheado o detectado, existe una hermana gemela: SystemFunction033. Implementa el mismo RC4 que SystemFunction032.
Definición de la Función SystemFunction033
La firma de SystemFunction033 es idéntica a SystemFunction032:
// Declaración de la función (exportada desde Advapi32.dll)
NTSTATUS WINAPI SystemFunction033(
struct USTRING *Data, // Puntero a estructura con datos a cifrar/descifrar
const struct USTRING *Key // Puntero a estructura con la clave RC4
);¿Cuál es la diferencia entonces?
Ambas funciones tienen la misma firma y usan la misma estructura USTRING. La diferencia interna está en cómo Windows implementa internamente cada una, pero para el usuario externo, son intercambiables.
Implementación de SystemFunction033
use windows::core::PCSTR;
use windows::Win32::System::LibraryLoader::{LoadLibraryA, GetProcAddress};
#[repr(C)]
struct USTRING {
length: u32,
maximum_length: u32,
buffer: *mut u8,
}
type SystemFunction033Fn = unsafe extern "system" fn(
*mut USTRING,
*mut USTRING,
) -> i32;
/// Cifra/descifra datos usando SystemFunction033 (RC4 nativo de Windows)
///
/// Parámetros:
/// - rc4_key: Clave de cifrado RC4
/// - payload_data: Buffer del payload (mutable)
fn rc4_encryption_via_systemfunc033(
rc4_key: &[u8],
payload_data: &mut [u8],
) -> bool {
unsafe {
// Cargar Advapi32.dll
let advapi32 = match LoadLibraryA(PCSTR(b"Advapi32.dll\0".as_ptr())) {
Ok(lib) => lib,
Err(_) => {
println!("[!] No se pudo cargar Advapi32");
return false;
}
};
if advapi32.is_invalid() {
println!("[!] Handle inválido de Advapi32");
return false;
}
// Obtener dirección de SystemFunction033
let system_function_033 = GetProcAddress(
advapi32,
PCSTR(b"SystemFunction033\0".as_ptr()),
);
if system_function_033.is_none() {
println!("[!] No se pudo obtener SystemFunction033");
return false;
}
// Crear estructuras USTRING (orden diferente)
let mut key = USTRING {
length: rc4_key.len() as u32,
maximum_length: rc4_key.len() as u32,
buffer: rc4_key.as_ptr() as *mut u8,
};
let mut data = USTRING {
length: payload_data.len() as u32,
maximum_length: payload_data.len() as u32,
buffer: payload_data.as_mut_ptr(),
};
// Llamar a SystemFunction033
let func: SystemFunction033Fn = std::mem::transmute(system_function_033.unwrap());
let status = func(&mut data, &mut key);
if status != 0 {
println!("[!] SystemFunction033 falló: 0x{:08X}", status);
return false;
}
println!("[+] RC4 cifrado exitoso con SystemFunction033");
true
}
}Mejores Prácticas de Seguridad
1. Usa Claves Largas
¿Cuánto es “suficientemente largo”?
- 8 bytes = mínimo absoluto (débil)
- 16 bytes = recomendado (bueno)
- 32 bytes = excelente (fuerte)
// Clave de 32 bytes (256 bits)
let rc4_key: [u8; 32] = [
0x6d, 0x61, 0x6c, 0x64, 0x65, 0x76, 0x31, 0x32,
0x33, 0x2d, 0x6c, 0x6f, 0x6e, 0x67, 0x2d, 0x6b,
0x65, 0x79, 0x2d, 0x66, 0x6f, 0x72, 0x2d, 0x72,
0x63, 0x34, 0x2d, 0x65, 0x6e, 0x63, 0x72, 0x79
];2. Cifrado en Capas (Defense in Depth)
Aplica RC4 dos veces con claves diferentes. Así, aunque alguien rompa una capa, la otra aún protege:
// Cifrado con dos capas
let mut ctx1 = Rc4Context::new();
rc4_init(&mut ctx1, &key1);
let mut encrypted1 = vec![0u8; payload.len()];
rc4_cipher(&mut ctx1, &payload, &mut encrypted1);
let mut ctx2 = Rc4Context::new();
rc4_init(&mut ctx2, &key2);
let mut encrypted2 = vec![0u8; encrypted1.len()];
rc4_cipher(&mut ctx2, &encrypted1, &mut encrypted2);
// Descifrado (orden inverso)
let mut ctx3 = Rc4Context::new();
rc4_init(&mut ctx3, &key2);
let mut decrypted1 = vec![0u8; encrypted2.len()];
rc4_cipher(&mut ctx3, &encrypted2, &mut decrypted1);
let mut ctx4 = Rc4Context::new();
rc4_init(&mut ctx4, &key1);
let mut decrypted2 = vec![0u8; decrypted1.len()];
rc4_cipher(&mut ctx4, &decrypted1, &mut decrypted2);Limitaciones y Realidades
Siendo completamente honesto: RC4 no es una bala de plata. Tiene limitaciones reales que debes entender.
Lo que RC4 SÍ hace bien
- ✅ Elude firmas estáticas - Antivirus basado en patrones no te detecta.
- ✅ Evade análisis superficial - Un análisis rápido no revela el payload.
- ✅ Es rápido y simple - Bajo uso de CPU y memoria.
Lo que RC4 NO puede hacer
- ❌ No protege contra análisis profundo - Un investigador de malware rompe RC4 en minutos.
- ❌ El payload se expone en memoria - Cuando lo ejecutas, aparece sin cifrar (inevitablemente).
- ❌ EDR es más inteligente ahora - Los EDRs modernos detectan comportamiento, no solo firmas.
RC4 es una primera línea de defensa. Detiene detecciones estáticas automáticas. Pero si un analista humano mira tu binario + comportamiento, RC4 solo es un pequeño obstáculo.
Resumen: Lo Que Hemos Aprendido
En este tutorial sobre RC4 encryption, cubrimos:
- ✅ Teoría de cifrado stream - Cómo RC4 genera secuencias que parecen aleatorias
- ✅ Arquitectura RC4 - KSA (mezcla) y PRGA (generación) explicadas claramente
- ✅ Tres formas de implementar RC4 en Rust: Puro, SystemFunction032 y SystemFunction033
- ✅ Mejores prácticas de seguridad - Claves dinámicas, largas, cifrado en capas
- ✅ Limitaciones reales - Qué protege y qué no protege RC4
La Reflexión Final: Verdades Incómodas
RC4 es exponencialmente más robusto que XOR. Evade firmas estáticas que pararían a la mayoría de payloads. Pero NO es una bala de plata. Si crees que RC4 alone te hace invisible, estás equivocado. Los EDRs, analistas de malware, y sistemas modernos de defensa son MUCHO más sofisticados.
RC4 es el primer paso. Pero si alguien realmente quiere analizarte, rompe RC4 en segundos.
Espero que este tutorial te haya ayudado a entender RC4 no como “magia”, sino como lo que realmente es: un algoritmo bien diseñado que genera secuencias pseudoaleatorias para ofuscación de payloads.
¿Listo para el siguiente nivel? Explora anti-debugging, anti-VM, o técnicas avanzadas de inyección en los próximos posts de MalGhost. 🔓
Sobre el autor
Pentester specializing in penetration testing, vulnerability identification, and securing systems against advanced threats.
Artículos relacionados

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.

Inyección de Shellcode en Windows: Guía Práctica Paso a Paso
La inyección de shellcode es una de las técnicas fundamentales en investigación de seguridad y desarrollo de malware. Esta guía te proporcionará tanto el conocimiento teórico como las habilidades prácticas necesarias para dominar esta técnica.
