
إخفاء النوم القائم على ROP لتجنب ماسحات الذاكرة
Shelter هي تقنية إخفاء للنوم (sleep obfuscation) مسلحة بالكامل، تتيح تشفير الحمولة في الذاكرة بشكل كامل مع الاستخدام المكثف لـ ROP.
تأتي هذه الحزمة مع الخصائص التالية:
قم باستيراد هذه الحزمة إلى مشروعك بإضافة السطر التالي إلى ملف cargo.toml الخاص بك:
[dependencies]
shelter = "=0.1.2"
ثم قم بتجميع مشروعك في وضع --release.
الوظيفة الرئيسية لهذه الحزمة مغلفة في ثلاث دوال:
fluctuate() تسمح بتشفير إما منطقة الذاكرة الحالية أو الـ PE بأكمله. تتطلب هذه الدالة وجود بايتات MZ الخاصة بـ PE لاسترداد عنوان قاعدته ديناميكيًا.fluctuate_from_address() تقوم بتشفير الـ PE بالكامل. تتوقع هذه الدالة كمعامل إدخال عنوان قاعدة الـ PE.fluctuate_from_pattern() تقوم أيضًا بتشفير الـ PE بالكامل. تتوقع هذه الدالة كمعامل إدخال مجموعة مخصصة من بايتين لاستخدامها في تحديد عنوان قاعدة الـ PE. تحل هذه البايتات السحرية المخصصة محل النمط التقليدي MZ.عندما يتم تشفير الـ PE بالكامل، يتم تخزين حمايات الذاكرة الأصلية للأقسام في الكومة (heap) لاستعادتها لاحقًا.
يستخدم Shelter NtWaitForSingleObject للنوم. بالإضافة إلى تحديد عدد الثواني التي تريد النوم فيها، يمكنك أيضًا تمرير مقبض حدث (event handle) والإشارة إليه في أي وقت للعودة قبل انتهاء المهلة (باستخدام SetEvent على سبيل المثال). ضع في اعتبارك أنه إذا كانت حمولتك كاملة مشفرة (وهذا هو الهدف الأساسي على ما أعتقد)، فستحتاج إلى طريقة بديلة للإشارة إلى الحدث في حال نمت إلى أجل غير مسمى.
تتوقع الدالة المعاملات التالية:
true وجود بايتات MZ في الذاكرة.None، فستكون المهلة غير محدودة، مما يعني أن التنفيذ لن يعود حتى يتم الإشارة إلى الحدث المرسل إلى NtWaitForSingleObject.None. سيتوقف البرنامج إذا قمت بتعيين هذا المعامل والمهلة كلاهما إلى None.let time_to_sleep = Some(10); // النوم لمدة 10 ثوانٍ
let _ = shelter::fluctuate(false, time_to_sleep, None); // تشفير منطقة الذاكرة الحالية فقط
let time_to_sleep = Some(10); // النوم لمدة 10 ثوانٍ
let _ = shelter::fluctuate(true, time_to_sleep, None); // تشفير الـ PE بالكامل
pub type CreateEventW = unsafe extern "system" fn (*const SECURITY_ATTRIBUTES, i32, i32, *const u16) -> HANDLE;
let k32 = dinvoke_rs::dinvoke::get_module_base_address("kernel32.dll");
let create_event: CreateEventW;
let event_handle: Option<HANDLE>;
dinvoke_rs::dinvoke::dynamic_invoke!(k32,"CreateEventW",create_event,event_handle,ptr::null_mut(),0,0,ptr::null());
let time_to_sleep = None; // النوم إلى أجل غير مسمى
let _ = shelter::fluctuate(true, time_to_sleep, event_handle); // تشفير الـ PE بالكامل حتى يتم الإشارة إلى الحدث
تتوقع الدالة المعاملات التالية:
None، فستكون المهلة غير محدودة، مما يعني أن التنفيذ لن يعود حتى يتم الإشارة إلى الحدث المرسل إلى NtWaitForSingleObject.None. سيتوقف البرنامج إذا قمت بتعيين هذا المعامل والمهلة كلاهما إلى None.إحدى طرق استخدام هذه الدالة هي تعيين حمولتنا يدويًا باستخدام Dinvoke_rs. بهذه الطريقة، يمكن للمحمل (loader) إرسال حمولته عنوان قاعدتها الخاص، بحيث يمكن للحمولة استخدامه لإخفاء نفسها كلما دعت الحاجة. بهذه الطريقة، يمكن للمحمل إزالة رؤوس الـ PE بأمان لتحقيق مستوى معين من التخفي.
مثال على المحمل:
let payload: Vec<u8> = your_download_function();
let mut m = dinvoke_rs::manualmap::manually_map_module(payload.as_ptr(), true).unwrap();
println!("The dll is loaded at base address 0x{:x}", m.1);
let dll_exported_function = dinvoke::get_function_address(m.1, "run");
let run: unsafe extern "Rust" fn (usize) = std::mem::transmute(dll_exported_function);
run(m.1 as usize);
مثال على الحمولة:
#[no_mangle]
fn run(base_address: usize)
{
...
let time_to_sleep = Some(10); // النوم لمدة 10 ثوانٍ
let _ = shelter::fluctuate_from_address(time_to_sleep, None, base_address); // تشفير الـ PE بالكامل من عنوان القاعدة هذا
...
}
تتوقع الدالة المعاملات التالية:
None، فستكون المهلة غير محدودة، مما يعني أن التنفيذ لن يعود حتى يتم الإشارة إلى الحدث المرسل إلى NtWaitForSingleObject.None. سيتوقف البرنامج إذا قمت بتعيين هذا المعامل والمهلة كلاهما إلى None.[u8;2] تحتوي على بايتات سحرية مخصصة للبحث عنها للحصول على عنوان قاعدة الـ PE.الهدف من إنشاء هذه الدالة هو السماح للمحمل بإزالة رأس الـ PE والتوقيعات الأخرى، بما في ذلك بايتات MZ التقليدية. بهذه الطريقة، يمكن استبدال تلك البايتات بنمط مخصص سيبحث عنه Shelter لاسترداد عنوان قاعدة الـ PE.
let time_to_sleep = Some(10); // النوم لمدة 10 ثوانٍ
let pattern = [0x29,0x07];
let _ = shelter::fluctuate_from_pattern(time_to_sleep, None, pattern); // تشفير الـ PE بالكامل باستخدام نمط مخصص كبايتات سحرية
لاختبار تنفيذ التقنية، تم استخدام PE-sieve بشكل أساسي. بشكل افتراضي، يبحث PE-sieve عن الغرسات (implants) داخل مناطق الذاكرة القابلة للتنفيذ، مما يعني أنه حتى إخفاء منطقة الذاكرة الحالية فقط (.text) يكفي لتجنب الاكتشافات:

لاحظ أنه بما أننا نستخدم Unwinder، فإن سلسلة الاستدعاءات مزيفة وبالتالي لا يكتشف العلم /threads الـ DLL المعين أيضًا.
الآن، يسمح PE-sieve بفحص مناطق الذاكرة غير القابلة للتنفيذ أيضًا باستخدام العلم /data. وفقًا للتوثيق الرسمي للأداة، فإن تعيين هذا العلم على always يمكن أن "يولد الكثير من الضوضاء / الإيجابيات الخاطئة". على الرغم من ذلك، قررنا استخدامه للتحقق من فعالية قدرة تشفير الـ PE بالكامل، حيث إنه يسمح بإخفاء مناطق بيانات الـ PE التي قد تحتوي على مؤشرات على وجود غرسة في الذاكرة.

كما يمكن رؤيته، في الصورة الأولى يظهر كيف أن إخفاء قسم .text فقط غير كافٍ عندما يقوم PE-sieve بفحص صفحات الذاكرة غير القابلة للتنفيذ، حيث قد تحتوي بعض المناطق على سلاسل نصية تكشف عن وجود DLL (MZ، رأس DOS، أسماء الأقسام، إلخ). من ناحية أخرى، تُظهر الصورة الثانية كيف يمكن حل هذه المشكلة باستخدام آلية إخفاء الـ PE بالكامل من Shelter. في أي حال، وكما هو مذكور في ويكي PE-sieve، يؤدي هذا الخيار إلى الكثير من الإيجابيات الخاطئة نظرًا لأن مجرد وجود سلاسل مثل ".data" أو "rdata" في الكومة يحذر بالفعل من وجود PE مزروع، على الرغم من أنه غير قادر على تفريغ أي شيء من الذاكرة (نظرًا لعدم وجود محتوى PE حقيقي في تلك المنطقة).
أخيرًا، لدى PE-sieve خيار جديد نسبيًا لاكتشاف وجود غرسات مخفية من خلال البحث عن مناطق ذاكرة عالية الإنتروبيا. هذا الخيار (/obfusc) بالاشتراك مع /data قادر على اكتشاف وجود الحمولة بسبب الإنتروبيا العالية لمنطقة الذاكرة التي تحتويها (على الرغم من أنه لا يمكنه استعادة الـ PE لأنه مشفر بالكامل):
على الرغم من أن Shelter جاهز للاستخدام وتم تطويره مع مراعاة OPSEC، إلا أنه لا تزال هناك بعض التحسينات التي سيتم إضافتها في المستقبل القريب:
BCryptEncrypt/BCryptDecrypt بوظيفة Nt المقابلة.