
مكتبة Rust لتزييف مكدس الاستدعاءات ghost-frame call-stack spoofing، واستدعاءات النظام غير المباشرة runtime indirect syscalls، وحقن APC على Windows x64. توفر حل SSN resolution، وإصدار كعب JIT stub emission، وتشفير المعلمات القائم على VEH-based parameter encryption لتجنب EDR evasion.
انتحال سلسلة الاستدعاء بإطار الشبح + استدعاءات النظام غير المباشرة وقت التشغيل لنظام Windows x64 — منقول إلى Rust.
منقول من LACUNA Chain (lacuna_chain.c) بواسطة Mohamed Alzhrani (0xmaz).
lacuna-rs هي مكتبة Rust قابلة لإعادة الاستخدام توفر نفس البدائيات الموجودة في TTP الأصلي المكتوب بلغة C، ومهيكلة بحيث يمكن إسقاطها في أي مشروع Rust — مثل wsyscall-rs أو syscalls-rs، ولكن مع استخراج SSN وقت التشغيل، واستهداف syscall;ret لكل دالة على حدة، وانتحال مكدس إطار الشبح.
ميزة
stack-spoofستفشل بصمت إذا لم يتم تجميع المكتبة المستهلكة مع مؤشرات الإطارات.
تحدد البدائيات الخاصة بتحطيم المكدس في chain.rs خانة عنوان الإرجاع الخاص بالمستدعي عبر mov rbp, {x} في التجميع المضمّن. يتطلب ذلك أن تكون سلسلة RBP سليمة. تحذف Rust (ومعظم المترجمات في وضع الإصدار) مؤشرات الإطارات افتراضيًا.
يضبط build.rs الخيار force-frame-pointers=yes لتوليد الشيفرة الخاص بهذه المكتبة، لكن Cargo لا يستطيع تمرير أعلام المترجم إلى المكتبات التابعة. يجب عليك إضافة هذا إلى مشروعك الخاص:```toml
[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` |
| إصدار كعب syscall غير مباشر 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` |
---
## أعلام الميزات```toml
[dependencies]
lacuna-rs = { version = "0.4", features = ["inject", "stack-spoof", "veh"] }
| الميزة | الوصف | يتطلب مؤشرات الإطار؟ |
|---|---|---|
syscalls (افتراضي) | حل SSN + إصدار JIT stub | لا |
inject (افتراضي) | حقن APC القائم على الأقسام (inject::inject_sapc) | لا |
veh | تشفير معاملات VEH + نقطة التوقف العتادية | لا |
stack-spoof | سلسلة الإطار الشبحي LACUNA + stack stomp | نعم |
no-std | وضع no_std (تجريبي) | لا |
عند عدم تمكين أي ميزات، تتوفر فقط طبقات الفحص/PE/NT.
cargo run --example scan
### بناء سلسلة الإطار الشبح + التحقق منها```sh
cargo run --example verify --features stack-spoof
إثبات الحقن -- تم تنفيذ الحمولة داخل العملية الهدف.```sh
cargo run --example inject --features inject,stack-spoof,veh -- <sc.bin>
أضف `--verbose` لتمكين مخرجات تشخيص VEH (تفريغ المكدس، طباعة السجلات):```sh
cargo run --example inject --features inject,stack-spoof,veh -- <pid> <sc.bin> --verbose
يقوم inject_sapc بترتيب خيوط الهدف. أقوى إشارة ترتيب هي ما إذا كان الخيط يمتلك نافذة:
EnumWindows +
GetWindowThreadProcessId. خيط واجهة رسومية ينتظر في انتظار قابل للتنبيه في حلقة
رسائله، لذا يُنفَّذ APC المُصطف هناك أولاً. +1000.NtQueryInformationThread(ThreadCycleTime)؛ يتراجع إلى
ThreadTimes عندما تكون الفئة غير متاحة على البنية قيد التشغيل (انظر
examples/tprobe.rs). يُطبَّع ويُضاف.NtQueryInformationThread(ThreadTimes) — وقت النواة + المستخدم.NtQueryInformationThread(ThreadSuspendCount). الخيط المعلّق
لا يمكنه تشغيل APC: −2000 لكل عدد تعليق.NtQueryInformationThread(ThreadBasicInformation). الأولوية
8–10 تحصل على +150؛ الأولوية خارج النطاق تحصل على −300.التصفية. الخيوط المعلّقة والخيوط المنتظرة في انتظار نواة غير قابل للتنبيه —
WrDelayExecution (Sleep/NtDelayExecution) أو WrSuspended، مقروءة من
NtQuerySystemInformation(SystemProcessInformation) — تُستبعد كلياً (لا يمكنها
أبداً استقبال APC). سبب الانتظار بخلاف ذلك تشخيصي فقط: يُسجَّل لكل مرشح
كـ State: / Reason: / Alert:، لكنه لا يؤثر على النقاط، لأن KWAIT_REASON
لا يرمّز قابلية التنبيه (انتظار كائن قابل للتنبيه وآخر غير قابل للتنبيه يُبلّغان
بنفس القيمة — تم التحقق باستخدام فحص على نمط examples/tprobe.rs).
تُرتَّب المرشحات (الأعلى نقاطاً، مع كسر التعادل لصالح واجهة الرسومية ثم الدورات)
وتُقتطع إلى MAX_APC_CANDIDATES (10) للتعداد/التسجيل.
التعداد ≠ الحقن. عدد الخيوط المُدرجة منفصل عن العدد المحقون.
MAX_APC_CANDIDATES(10) يتحكم في عدد ما يُرتَّب ويُسجَّل؛APC_INJECT_THREADS(5) يتحكم في عدد ما يستقبل APC فعلياً.لماذا الحقن في عدة خيوط ثم إعادة المحاولة: وضع المستخدم لا يمكنه تحديد أي خيط في انتظار قابل للتنبيه، وغالباً ما لا يدخل خيط واحد مختار أبداً في مثل هذا الانتظار، لذا لن يُنفَّذ APC أبداً. لذلك يصطف
inject_sapcالـ APC إلى عدة خيوط ثم يعيد الاصطفاف في حلقة قصيرة (~80 مللي ثانية) حتى تتحول كلمة البوابة المشتركة من 0 إلى 1 — أي حتى ينفّذ أحد خيوط الهدف الحمولة فعلياً. يسجّل ما إذا حدث ذلك:payload claimed the gate(نُفِّذ) مقابلgate NOT claimed.لماذا التشغيل مرة واحدة: نفس الشل كود/السياق يُصطف لكل خيط، لذا بدون حماية ستُنفَّذ حمولة غير قابلة لإعادة الدخول بالتزامن وتُسقط المضيف (تم القياس: 3 تنفيذات متزامنة). بوابة إعادة الدخول (
USE_REENTRANCY_GATE، مفعّلة افتراضياً) تجعل جسم الحمولة يُنفَّذ مرة واحدة بالضبط بغض النظر عن عدد الخيوط (أو عدد جولات إعادة المحاولة) التي تستقبل APC (تم القياس: 1 بالضبط). اترك البوابة مفعّلة إلا إذا كانت حمولتك آمنة للتزامن صراحةً.
use lacuna::{scan, nt, win::get_module};
let ntdll = get_module(b"ntdll.dll\0");
// Scan for ghost regions let mut ghosts = [scan::Ghost::default(); 512]; let n = scan::scan_ghosts(ntdll, &[b"NtAllocateVirtualMemory\0"], &mut ghosts); println!("{} ghost regions in ntdll", n);
// Resolve SSN + syscall;ret for a specific function let (ssn, syscall_ret) = nt::resolve(ntdll, b"NtOpenProcess\0"); println!("NtOpenProcess: ssn={:#x}, syscall;ret={:#x}", ssn, syscall_ret);
### إصدار stub لاستدعاء نظام غير مباشر لأي NT API```rust
use lacuna::{nt, stub, win::{get_module, HMODULE}};
let ntdll: HMODULE = get_module(b"ntdll.dll\0");
// Resolve SSN + the function's own syscall;ret address
let (ssn, syscall_ret) = nt::resolve(ntdll, b"NtAllocateVirtualMemory\0");
assert!(ssn != nt::SSN_INVALID && syscall_ret != 0);
// JIT-emit a stub: mov r10,rcx; mov eax,SSN; jmp [syscall;ret]
// (or jmp [ghost_gadget] -> JMP [RBX] -> syscall;ret if build_chain was called)
let stub = stub::make_stub(ssn, syscall_ret).expect("stub alloc failed");