
Offuscamento del sonno basato su ROP per eludere gli scanner di memoria
Shelter è una tecnica di offuscamento del sonno completamente armata che permette di crittografare completamente il tuo payload in memoria facendo un uso estensivo di ROP.
Questo crate presenta le seguenti caratteristiche:
Importa questo crate nel tuo progetto aggiungendo la seguente riga al tuo cargo.toml:
[dependencies]
shelter = "=0.1.2"
Quindi, compila il tuo progetto in modalità --release.
La funzionalità principale di questo crate è stata racchiusa in tre funzioni:
fluctuate() permette di crittografare sia la regione di memoria corrente che l'intero PE. Questa funzione richiede che i byte MZ del PE siano presenti per recuperare dinamicamente il suo indirizzo di base.fluctuate_from_address() crittografa completamente il PE. Questa funzione si aspetta come parametro di input l'indirizzo di base del PE.fluctuate_from_pattern() crittografa anch'essa completamente il PE. Questa funzione si aspetta come parametro di input un set personalizzato di due byte da utilizzare per determinare l'indirizzo di base del PE. Questi byte magici personalizzati sostituiscono il classico pattern MZ.Ogni volta che l'intero PE viene crittografato, le protezioni di memoria delle sezioni originali vengono memorizzate nell'heap per essere ripristinate successivamente.
Shelter utilizza NtWaitForSingleObject per dormire. Oltre a indicare per quanti secondi si desidera dormire, è possibile passare anche un handle di evento e segnalarlo in qualsiasi momento per tornare prima della scadenza del timeout (usando SetEvent per esempio). Tieni presente che se l'intero payload è crittografato (che è il punto principale, immagino), avrai bisogno di un modo alternativo per segnalare l'evento nel caso in cui tu abbia dormito indefinitamente.
La funzione si aspetta i seguenti parametri:
true richiede che i byte MZ siano presenti in memoria.None, il timeout sarà infinito, il che significa che l'esecuzione non tornerà fino a quando l'evento passato a NtWaitForSingleObject non viene segnalato.None. Il programma si bloccherà se imposti questo parametro e il timeout entrambi a None.let time_to_sleep = Some(10); // Dormi per 10 secondi
let _ = shelter::fluctuate(false, time_to_sleep, None); // Crittografa solo la regione di memoria corrente
let time_to_sleep = Some(10); // Dormi per 10 secondi
let _ = shelter::fluctuate(true, time_to_sleep, None); // Crittografa l'intero 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; // Dormi indefinitamente
let _ = shelter::fluctuate(true, time_to_sleep, event_handle); // Crittografa l'intero PE finché l'evento non viene segnalato
La funzione si aspetta i seguenti parametri:
None, il timeout sarà infinito, il che significa che l'esecuzione non tornerà fino a quando l'evento passato a NtWaitForSingleObject non viene segnalato.None. Il programma si bloccherà se imposti questo parametro e il timeout entrambi a None.Un modo per utilizzare questa funzione sarebbe quello di mappare manualmente il nostro payload con Dinvoke_rs. In questo modo, il loader può inviare al payload il proprio indirizzo di base, così che il payload possa usarlo per offuscarsi ogni volta che è necessario. In questo modo, il loader può rimuovere in sicurezza le intestazioni del PE per ottenere un certo livello di furtività.
Esempio di loader:
let payload: Vec<u8> = your_download_function();
let mut m = dinvoke_rs::manualmap::manually_map_module(payload.as_ptr(), true).unwrap();
println!("La dll è caricata all'indirizzo di base 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);
Esempio di payload:
#[no_mangle]
fn run(base_address: usize)
{
...
let time_to_sleep = Some(10); // Dormi per 10 secondi
let _ = shelter::fluctuate_from_address(time_to_sleep, None, base_address); // Crittografa l'intero PE da questo specifico indirizzo di base
...
}
La funzione si aspetta i seguenti parametri:
None, il timeout sarà infinito, il che significa che l'esecuzione non tornerà fino a quando l'evento passato a NtWaitForSingleObject non viene segnalato.None. Il programma si bloccherà se imposti questo parametro e il timeout entrambi a None.[u8;2] contenente byte magici personalizzati da cercare per ottenere l'indirizzo di base del PE.Lo scopo di creare questa funzione è di permettere al loader di rimuovere l'intestazione del PE e altre firme, inclusi i classici byte MZ. In questo modo, quei byte possono essere sostituiti da un pattern personalizzato che Shelter cercherà per recuperare l'indirizzo di base del PE.
let time_to_sleep = Some(10); // Dormi per 10 secondi
let pattern = [0x29,0x07];
let _ = shelter::fluctuate_from_pattern(time_to_sleep, None, pattern); // Crittografa l'intero PE usando un pattern personalizzato come byte magici
Per testare l'implementazione della tecnica, è stato utilizzato principalmente PE-sieve. Per impostazione predefinita, PE-sieve cerca impianti all'interno di regioni di memoria eseguibili, il che significa che anche offuscare esclusivamente la regione di memoria corrente (.text) è sufficiente per evitare rilevamenti:

Nota che, poiché stiamo usando Unwinder, lo stack di chiamate viene falsificato e quindi il flag /threads non rileva nemmeno la dll mappata.
Ora, PE-sieve permette di ispezionare anche le regioni di memoria non eseguibili usando il flag /data. Secondo la documentazione ufficiale dello strumento, questo flag impostato su always può "produrre molto rumore/falsi positivi". Nonostante ciò, abbiamo deciso di usarlo per verificare l'efficacia della capacità di crittografia dell'intero PE, poiché permette di nascondere le regioni dati del PE che potrebbero contenere indicatori della presenza di un impianto in memoria.
Come si può vedere, nella prima immagine viene mostrato come offuscare solo la sezione .text non sia sufficiente quando PE-sieve esegue la scansione delle pagine di memoria non eseguibili, poiché alcune regioni potrebbero contenere stringhe che rivelano la presenza di una DLL (MZ, intestazione DOS, nomi di sezioni, ecc.). D'altra parte, la seconda immagine mostra come questo problema possa essere risolto utilizzando il meccanismo di offuscamento dell'intero PE di Shelter. In ogni caso, come affermato nel wiki di PE-sieve, questa opzione porta a tonnellate di falsi positivi poiché la semplice presenza nell'heap di stringhe come ".data" o "rdata" già avverte di un possibile PE impiantato, nonostante non sia in grado di dumpare nulla dalla memoria (poiché non c'è alcun contenuto reale del PE in quella regione).
Infine, PE-sieve ha un'opzione relativamente nuova per rilevare la presenza di impianti offuscati cercando regioni di memoria ad alta entropia. Questa opzione (/obfusc) in combinazione con /data è in grado di rilevare la presenza del payload a causa dell'alta entropia della regione di memoria che lo contiene (anche se non può recuperare il PE poiché è completamente crittografato):
Sebbene Shelter sia pronto per l'uso e sia stato sviluppato tenendo conto dell'OPSEC, ci sono ancora alcuni miglioramenti che verranno aggiunti nel prossimo futuro:
BCryptEncrypt/BCryptDecrypt con la corrispondente funzione Nt.