
रस्ट के लिए कॉल स्टैक स्पूफिंग
Unwinder, SilentMoonWalk तकनीक का पूर्ण शस्त्रीकरण प्रदान करता है, जो Rust में पूर्ण और स्थिर कॉल स्टैक स्पूफिंग प्राप्त करने की अनुमति देता है।
इस तकनीक में निम्नलिखित विशेषताएँ हैं:
SilentMoonWalk तकनीक के निर्माताओं को धन्यवाद:
और निश्चित रूप से namazso को उस Twitter थ्रेड के लिए एक बड़ा धन्यवाद, जिसने इस पूरे प्रोजेक्ट को प्रेरित किया।
इस क्रेट को अपने प्रोजेक्ट में आयात करने के लिए अपने cargo.toml में निम्नलिखित पंक्ति जोड़ें और release मोड पर कंपाइल करें:
[dependencies]
unwinder = "=0.1.4"
इस क्रेट की मुख्य कार्यक्षमता को दो मैक्रोज़ में लपेटा गया है:
call_function!() मैक्रो एक साफ कॉल स्टैक के साथ किसी भी मनमाने फ़ंक्शन को चलाने की अनुमति देता है।indirect_syscall!() मैक्रो एक साफ कॉल स्टैक के साथ निर्दिष्ट (अप्रत्यक्ष) syscall निष्पादित करता है।इनमें से किसी भी मैक्रो का उपयोग करने के लिए std::ffi::c_void डेटा प्रकार को आयात करना आवश्यक है।
दोनों मैक्रोज़ एक *mut c_void लौटाते हैं जिसका उपयोग निष्पादित फ़ंक्शन द्वारा लौटाए गए मान को प्राप्त करने के लिए किया जा सकता है। अधिक विस्तृत जानकारी उदाहरण अनुभाग में दी गई है।
इस मैक्रो का उपयोग एक साफ कॉल स्टैक के साथ किसी भी वांछित फ़ंक्शन को कॉल करने के लिए किया जाता है। मैक्रो निम्नलिखित पैरामीटर की अपेक्षा करता है:
usize, isize या पॉइंटर के रूप में पास किया जाना चाहिए।इस मैक्रो का उपयोग एक साफ कॉल स्टैक के साथ किसी भी वांछित अप्रत्यक्ष syscall को करने के लिए किया जाता है। मैक्रो निम्नलिखित पैरामीटर की अपेक्षा करता है:
इन दोनों मैक्रोज़ में विभिन्न प्रकार के तर्क पास करने के लिए, निम्नलिखित बातों को ध्यान में रखा जाना चाहिए:
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 लौटाता है जिसका उपयोग NtDelayExecution द्वारा लौटाए गए NTSTATUS को प्राप्त करने के लिए किया जा सकता है।
स्पूफिंग प्रक्रिया को कॉल स्टैक आकार में असामान्य वृद्धि के बिना कितनी भी बार जोड़ा जा सकता है। निष्पादन प्रवाह भी संरक्षित रहेगा। निम्नलिखित कोड इसका एक उदाहरण है:
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 का एक कॉल स्टैक स्पूफिंग विकल्प है जो आपके प्रोग्राम के निष्पादन के दौरान एक साफ कॉल स्टैक बनाए रखने की अनुमति देता है। इस तकनीक के पीछे मुख्य विचार यह है कि आपके मॉड्यूल के अंदर प्रत्येक बुलाया गया फ़ंक्शन पहले से पुश किए गए रिटर्न एड्रेस का ध्यान रखता है, और रनटाइम पर एक वैध फ़ंक्शन ढूंढता है जिसका फ्रेम आकार स्पूफ किए जाने वाले रिटर्न एड्रेस के समान होता है। एक बार समान फ्रेम आकार वाला वैध फ़ंक्शन मिल जाने पर, उसके भीतर एक ऑफसेट की गणना की जाती है और अंतिम एड्रेस का उपयोग अंतिम रिटर्न एड्रेस को प्रतिस्थापित करने के लिए किया जाता है, जिससे कॉल स्टैक में किसी भी असामान्य प्रविष्टि को छिपाया जा सके और इसे unwindable रखा जा सके। मूल रिटर्न एड्रेस unwinder द्वारा संग्रहीत किया जाता है और रिटर्न निर्देश निष्पादित होने से पहले इसे स्टैक में सही स्थान पर वापस ले जाया जाता है, जिससे प्रोग्राम के सामान्य प्रवाह को जारी रखा जा सकता है।
यह एक प्रयोगात्मक सुविधा है जो पूरी तरह कार्यात्मक होने के बावजूद अभी भी विकास और शोध के अधीन है, इसलिए यदि आप इस तकनीक को अपने कोड में एकीकृत करने का निर्णय लेते हैं, तो अपने कोड का परीक्षण करना सुनिश्चित करें।
स्टैक रिप्लेसमेंट कार्यक्षमता का उपयोग करने के लिए आपको अपने 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 लौटाएंगे जिसमें इस तरह से बुलाए गए फ़ंक्शन द्वारा लौटाया गया मान होगा (अर्थात ये उसी तरह काम करते हैं जैसा कि SilentMoonWalk को निष्पादित करने के लिए उपयोग किए जाने वाले call_function और indirect_syscall मैक्रोज़ के लिए वर्णित है)।इन मैक्रोज़ का उपयोग करने के लिए std::ffi::c_void डेटा प्रकार को आयात करना आवश्यक है।
इनमें से किसी भी मैक्रो का उपयोग करने वाले सभी फ़ंक्शनों को #[no_mangle] या #[inline(never)] एट्रिब्यूट से चिह्नित किया जाना चाहिए ताकि ऑप्टिमाइज़ेशन प्रक्रिया के दौरान rust कंपाइलर उन्हें इनलाइन न कर सके।
यह सब उपयोग करने के तरीके को दर्शाने वाले व्यावहारिक उदाहरण में जाने से पहले, replace_and_call/replace_and_syscall मैक्रोज़ की जोड़ी और उन्हें अपेक्षित तर्क कैसे पास करें, इसका एक संक्षिप्त अवलोकन करते हैं।
इस मैक्रो का उपयोग स्टैक रिप्लेसमेंट का उपयोग करते हुए एक साफ कॉल स्टैक के साथ वर्तमान मॉड्यूल के बाहर किसी भी वांछित फ़ंक्शन को कॉल करने के लिए किया जाता है। मैक्रो निम्नलिखित पैरामीटर की अपेक्षा करता है:
usize, isize या पॉइंटर के रूप में पास किया जाना चाहिए।इस मैक्रो का उपयोग स्टैक रिप्लेसमेंट का उपयोग करते हुए एक साफ कॉल स्टैक के साथ किसी भी वांछित अप्रत्यक्ष syscall को करने के लिए किया जाता है। मैक्रो निम्नलिखित पैरामीटर की अपेक्षा करता है:
मुझे लगता है कि इन मैक्रोज़ के उपयोग को दिखाने का सबसे अच्छा तरीका एक व्यावहारिक उदाहरण है। मान लीजिए कि हम एक dll बना रहे हैं जिसे मेमोरी में reflectively inject किया जाएगा। यह 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 (अप्रत्यक्ष 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 फ्रेम के संयोजन का उपयोग करता है। यह केवल 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 तर्कों की अनुमति देते हैं।कृपया इस सुविधा का उपयोग करते समय उत्पन्न होने वाले किसी भी बग की रिपोर्ट करें।