
Call stack spoofing for Rust
توفر Unwinder تسليحًا كاملًا لتقنية SilentMoonWalk، مما يسمح بالحصول على تزييف كامل ومستقر لمكدس الاستدعاءات في Rust.
تتميز هذه التقنية بالخصائص التالية:
كل التقدير لمبتكري تقنية SilentMoonWalk:
وبالطبع تحية كبيرة إلى namazso بفضل السلسلة على تويتر التي ألهمت هذا المشروع بأكمله.
استورد هذه الحزمة إلى مشروعك بإضافة السطر التالي إلى ملف cargo.toml وقم بالترجمة في وضع release:
[dependencies]
unwinder = "=0.1.4"
تم تغليف الوظيفة الرئيسية لهذه الحزمة في ماكرون:
call_function!() تشغيل أي دالة تعسفية بمكدس استدعاءات نظيف.indirect_syscall!() استدعاء النظام (غير المباشر) المحدد بمكدس استدعاءات نظيف.لاستخدام أي من هذين الماكروين، يلزم استيراد نوع البيانات std::ffi::c_void.
يعيد كلا الماكروين قيمة من نوع *mut c_void يمكن استخدامها لاسترجاع القيمة التي تُرجعها الدالة المنفذة. معلومات أكثر تفصيلًا في قسم الأمثلة.
يُستخدم هذا الماكرو لاستدعاء أي دالة مرغوبة بمكدس استدعاءات نظيف. يتوقع الماكرو الوسائط التالية:
usize أو isize أو مؤشر.يُستخدم هذا الماكرو لتنفيذ أي استدعاء نظام غير مباشر مرغوب بمكدس استدعاءات نظيف. يتوقع الماكرو الوسائط التالية:
من أجل تمرير وسائط من أنواع مختلفة إلى هذين الماكروين، يجب مراعاة الاعتبارات التالية:
usize (u8-u64، i8-i64، bool، إلخ) مباشرة إلى الماكروين.&str و String) كمؤشر.ptr::null()، ptr::null_mut()، إلخ) كقيمة 0 (بغض النظر عما إذا كانت u8 أو u16 أو i32 أو غير ذلك).let k32 = dinvoke_rs::dinvoke::get_module_base_address("kernel32.dll");
let sleep = dinvoke_rs::dinvoke::get_function_address(k32, "Sleep"); // Memory address of kernel32.dll!Sleep()
let miliseconds = 1000i32;
unwinder::call_function!(sleep, false, miliseconds);
let k32 = dinvoke_rs::dinvoke::get_module_base_address("kernel32.dll");
let open_process: isize = dinvoke_rs::dinvoke::get_function_address(k32, "Openprocess");
let desired_access: u32 = 0x1000;
let inherit = 0i32;
let pid = 20628i32;
let handle = unwinder::call_function!(open_process, false, desired_access, inherit, pid); // returns *mut c_void
let handle: HANDLE = std::mem::transmute(handle);
println!("Handle id: {:x}", handle.0);
لاحظ أن الماكرو يُرجع قيمة من نوع *mut c_void يمكن تحويلها مباشرة إلى HANDLE نظرًا لأن كلا النوعين لهما نفس الحجم. وهذا يتيح الوصول إلى القيمة التي يُرجعها OpenProcess، وهي المقبض الجديد للعملية الهدف.
let large = 0x8000000000000000 as u64; // Sleep indefinitely
let large: *mut i64 = std::mem::transmute(&large);
let alertable = false;
let ntstatus = unwinder::indirect_syscall!("NtDelayExecution", false, alertable, large); // returns *mut c_void
println!("ntstatus: {:x}", ntstatus as i32);
لاحظ أن الماكرو يُرجع قيمة من نوع *mut c_void يمكن استخدامها لاسترجاع NTSTATUS التي يُرجعها NtDelayExecution.
يمكن تسلسل عملية التزييف أي عدد من المرات دون زيادة غير طبيعية في حجم مكدس الاستدعاءات. كما سيتم الحفاظ على تدفق التنفيذ. الكود التالي مثال على ذلك:
fn main()
{
function_a();
}
fn function_a()
{
unsafe
{
let func_b = function_b as usize;
call_function!(func_b, false);
println!("function_a done.");
}
}
fn function_b()
{
unsafe
{
let func_c = function_c as usize;
call_function!(func_c, false);
println!("function_b done.")
}
}
fn function_c()
{
unsafe
{
let large = 0x0000000000000000 as u64; // Don't sleep so we return to function_b, allowing to check the execution flow preservation.
let large: *mut i64 = std::mem::transmute(&large);
let alertable = false;
let ntstatus = unwinder::indirect_syscall!("NtDelayExecution", false, alertable, large);
println!("ntstatus: {:x}", (ntstatus as usize) as i32); //NTSTATUS is a i32, although that second casting is not really required in this case.
}
}
إذا ضبطت الوسيط الثاني على true (في كلا الماكروين)، فستحاول عملية التزييف الاحتفاظ بإطار عنوان بداية الخيط في مكدس الاستدعاءات لزيادة الشرعية.

أحيانًا، لا تنفذ دالة بداية الخيط استدعاء call لدالة لاحقة (على سبيل المثال، يتم تنفيذ تعليمة jmp بدلاً من ذلك)، مما يعني عدم وجود عنوان عودة مدفوع على المكدس. في هذا السيناريو (وأيضًا إذا ضبطت الوسيط الثاني على false)، سيبدأ مكدس الاستدعاءات المُزيَّف من إطار BaseThreadInitThunk.

من أجل اختبار تنفيذ التقنية، تم استخدام PE-sieve مع العلم /threads. تُظهر نتائج الاختبار أن فحص مكدس الاستدعاءات لا يكشف عن وجود الحمولة عند استخدام وظائف هذه الحزمة. وكما يظهر في الصورة الثانية، يتم اكتشاف الحمولة عند عدم استخدام unwinder.

هذه بديل لتزييف مكدس الاستدعاءات يعتمد على تقنية SilentMoonWalk، ويتيح الحفاظ على مكدس استدعاءات نظيف أثناء تنفيذ برنامجك. الفكرة الرئيسية وراء هذه التقنية هي أن كل دالة تُستدعى داخل وحدتك (module) تعتني بعنوان العودة الذي دُفع سابقًا، فتبحث في وقت التشغيل عن دالة شرعية بنفس حجم الإطار الذي يجب تزييف عنوان العودة فيه. وبمجرد العثور على دالة شرعية بنفس حجم الإطار، يُحسب إزاحة (offset) بداخلها ويُستخدم العنوان النهائي لاستبدال عنوان العودة الأخير، مما يخفي أي إدخال شاذ في مكدس الاستدعاءات ويُبقيه قابلًا للفك (unwindable). يحتفظ unwinder بعنوان العودة الأصلي ويُعيده إلى الموضع الصحيح في المكدس قبل تنفيذ تعليمة return، مما يتيح مواصلة التدفق الطبيعي للبرنامج.
هذه ميزة تجريبية، ورغم أنها تعمل بشكل كامل إلا أنها ما تزال قيد التطوير والبحث، لذا تأكد من اختبار كودك إذا قررت دمج هذه التقنية فيه.
لاستخدام وظيفة استبدال المكدس، يجب إضافة السطر التالي إلى ملف cargo.toml والترجمة في وضع release:
[dependencies]
unwinder = {version = "0.1.4", features = ["Experimental"]}
تم تغليف الوظيفة الرئيسية لهذه الميزة في الماكروهات التالية:
start_stack_replacement!()/end_replacement!() يوجّه unwinder لبدء/إنهاء عملية استبدال المكدس. يجب استدعاء هذين الماكروين في نقطة دخول كودك (مثل الدوال المُصدَّرة من ملف dll).replace_and_continue!()/restore!() ينفذ استبدال/استعادة عنوان العودة الأخير.replace_and_call!()/replace_and_syscall!() لتنفيذ استبدال المكدس عندما نريد استدعاء دوال خارج الوحدة الحالية (مثل استخدام Windows API أو استدعاء كود من أي ملف dll آخر). يُرجع كلا الماكروين قيمة من نوع *mut c_void تحتوي على القيمة التي تُرجعها الدالة المستدعاة بهذه الطريقة (أي أنهما يعملان بنفس الطريقة الموصوفة للماكروين call_function و indirect_syscall المستخدمين لتنفيذ SilentMoonWalk).لاستخدام هذه الماكروهات، يلزم استيراد نوع البيانات std::ffi::c_void.
يجب أن تُوسم جميع الدوال التي تستخدم أيًا من هذه الماكروهات بالسمات #[no_mangle] أو #[inline(never)] لمنع مترجم Rust من تضمينها أثناء عملية التحسين.
قبل الخوض في مثال عملي يوضح كيفية استخدام كل هذا، إليك نظرة سريعة على زوج الماكروين replace_and_call/replace_and_syscall وكيفية تمرير الوسائط المتوقعة إليهما.
يُستخدم هذا الماكرو لاستدعاء أي دالة مرغوبة خارج الوحدة الحالية بمكدس استدعاءات نظيف مع استخدام استبدال المكدس. يتوقع الماكرو الوسائط التالية:
usize أو isize أو مؤشر.يُستخدم هذا الماكرو لتنفيذ أي استدعاء نظام غير مباشر مرغوب بمكدس استدعاءات نظيف مع استخدام استبدال المكدس. يتوقع الماكرو الوسائط التالية:
أعتقد أن أفضل طريقة لإظهار كيفية استخدام هذه الماكروهات هي من خلال مثال عملي. لنفترض أننا ننشئ ملف dll سيتم حقنه انعكاسيًا في الذاكرة. سيُصدّر هذا الملف دالتين ExportA و ExportB، لذا سنعتبر هاتين الدالتين نقطتي دخول للوحدة. يجب على كلتيهما استدعاء ماكرو start_stack_replacement في البداية مباشرة، كما يجب عليهما استدعاء الماكرو المعاكس end_replacement قبل العودة. يتوقع ماكرو start_stack_replacement عنوان الأساس للوحدة كوسيط، أو يمكنك تمرير 0 إذا كنت لا تعرف هذا العنوان في وقت التشغيل، فسيحاول الماكرو اكتشافه بنفسه.
#[no_mangle]
fn ExportedA(base_address: usize) -> bool
{
unwinder::start_replacement!(base_address);
...
unwinder::end_replacement!();
true
}
#[no_mangle]
fn ExportedB() -> bool
{
unwinder::start_replacement!(0);
...
unwinder::end_replacement!();
true
}
تبدأ عملية استبدال المكدس بإنشاء مكدس جديد يدويًا سيتم استخدامه حتى يتم استدعاء ماكرو end_replacement. توضح الصورة التالية ما يحدث في الخلفية:
على الرغم من أنه من الناحية النظرية لن يكون من الضروري البدء بمكدس جديد من الصفر، فقد قررت تنفيذ العملية بهذه الطريقة لضمان الاستقرار ومنع أي شيء من الانهيار.
الآن، لنفترض أن دالتنا ExportedA تقوم بعدة استدعاءات لدالتين داخليتين أخريين. هاتان الدالتان الداخليتان مسؤولتان عن استبدال/استعادة عنوان العودة الأصلي الذي سيشير إلى مكان ما داخل ExportedA، مما يكسر مكدس الاستدعاءات ما لم نتعامل معه. تتضمن عملية الاستبدال هذه تغليف كود الدالة الداخلية بين الماكروين replace_and_continue و restore:
#[no_mangle]
fn ExportedA(base_address: usize) -> bool
{
unwinder::start_replacement!(base_address);
let ret_a = internal_a();
let ret_b = internal_b(ret_a);
unwinder::end_replacement!();
ret_b
}
#[inline(never)] // This attribute is mandatory
fn internal_a() -> bool
{
unwinder::replace_and_continue();
...
unwinder::restore();
some_value
}
#[inline(never)] // This attribute is mandatory
fn internal_b(value: bool) -> bool
{
unwinder::replace_and_continue();
...
unwinder::restore();
some_value
}
أخيرًا، تستخدم كلتا الدالتين internal_a و internal_b بعض وظائف Windows API. للحفاظ على مكدس استدعاءات قابل للفك، يجب تنفيذ هذه الاستدعاءات عبر الماكروين replace_and_call (استدعاء عادي) أو replace_and_syscall (استدعاء نظام غير مباشر).
#[no_mangle] // This attribute is mandatory
fn ExportedA(base_address: usize) -> bool
{
unwinder::start_replacement!(base_address);
let ret_a = internal_a();
let ret_b = internal_b(ret_a);
unwinder::end_replacement!();
ret_b
}
#[inline(never)] // This attribute is mandatory
fn internal_a() -> bool
{
unwinder::replace_and_continue();
...
let module_name = "advapi32.dll";
let module_name = CString::new(module_name.to_string()).expect("");
let module_name_ptr: *mut u8 = std::mem::transmute(module_name.as_ptr());
let k32 = dinvoke_rs::dinvoke::get_module_base_address("kernel32.dll");
let load_library = dinvoke_rs::dinvoke::get_function_address(k32, "LoadLibraryA");
let ret = unwinder::replace_and_call!(load_library, module_name_ptr); // Load a dll with an unwindable call stack
println!("advapi.dll base address: 0x{:x}", ret as usize);
...
unwinder::restore();
some_value
}
#[inline(never)] // This attribute is mandatory
fn internal_b(value: bool) -> bool
{
unwinder::replace_and_continue();
...
let large = 0xFFFFFFFFFF676980 as u64; // Sleep one second
let large: *mut i64 = std::mem::transmute(&large);
let alertable = false;
let ntstatus = unwinder::replace_and_syscall!("NtDelayExecution", alertable, large);
println!("ntstatus: {:x}", ntstatus as usize);
...
unwinder::restore();
some_value
}
نظرًا لأن هذه ميزة قيد التطوير، يجب مراعاة بعض الأمور:
start_stack_replace. حاليًا، لن يتمكن من العثور عليه بنفسه (سيتم حل ذلك في التحديث القادم).jmp rbx + إطار الإخفاء (concealment frame) المستخدمة في تقنية SilentMoonWalk. يحدث هذا فقط عند استخدام الماكروين replace_and_call و replace_and_syscall ومن المخطط تغييره في التحديث القادم.replace_and_call و replace_and_syscall قيمة من نوع *mut c_void يمكن استخدامها لاسترجاع القيمة التي تُرجعها الدالة المنفذة من خلالهما. هذا هو نفس السلوك الموصوف للماكروين call_function و indirect_syscall.replace_and_call و replace_and_syscall بحد أقصى 11 وسيطًا.يُرجى إبلاغي بأي خطأ قد يظهر عند استخدام هذه الميزة.