Skip to content
KitploitKITPLOIT
ИнструментыБлог
Отправить
ИнструментыБлог
Отправить

Инструменты для хакинга, пентеста и кибербезопасности — ваш арсенал защиты!

Kitploit — это каталог инструментов для хакинга, кибербезопасности и пентестинга. Находите последние обновления проектов для поиска уязвимостей, анализа систем, автоматизации тестирования и усиления вашей безопасности.

··Ленты·Контакты·Конфиденциальность·© 2026 Kitploit

Каталог инструментов

Категории

Все категории
Loading categories
Инструменты/GitHubGitHub/kudaes/unwinder
Обход IDS/IPSПост-эксплуатацияRed TeamingРазработка Полезной НагрузкиСостязательная Атака
GitHubkudaes/unwinder

Unwinder

Трассировка стека вызовов для Rust

Репозиторий
3843641 год назадПроверено Kitploit

Популярное

Смотреть все →

Откройте для себя самые используемые инструменты нашего сообщества.

Изучить все инструменты

Просмотрите нашу коллекцию инструментов

Смотреть все инструменты →
Поделиться

Содержание

  • SilentMoonWalk
    • Описание
    • Благодарности
    • Использование
      • Макрос call_function!()
      • Макрос indirect_syscall!()
      • Передача параметров
    • Примеры
      • Вызов kernel32.dll!Sleep()
      • Вызов kernel32.dll!OpenProcess()
      • Вызов NtDelayExecution() в качестве непрямого системного вызова
      • Объединение вызовов макросов
    • Важные замечания
      • Начальный кадр
      • PoC
  • Подмена стека
    • Описание техники
    • Как использовать
    • Пример
    • Примечания

SilentMoonWalk

Описание

Unwinder предоставляет полноценную реализацию техники SilentMoonWalk, позволяющую добиться полного и стабильного спуфинга стека вызовов в Rust.

Данная техника обладает следующими особенностями:

  • Поддержка выполнения любой произвольной функции с количеством параметров до 11.
  • Поддержка выполнения непрямых системных вызовов (без дополнительных выделений памяти в куче) с количеством параметров до 11.
  • Крейт позволяет получить значение, возвращаемое вызванными через него функциями.
  • Процесс спуфинга может быть выстроен в цепочку любое количество раз без увеличения размера стека вызовов.
  • Для повышения эффективности процесса спуфинга используется TLS.
  • dinvoke_rs используется для выполнения любых необходимых крейту вызовов Windows API.
  • Благодарности

    Благодарность создателям техники SilentMoonWalk:

    • KlezVirus
    • Waldo-IRC
    • Trickster0

    И, конечно, огромная благодарность namazso за тред в Twitter, который вдохновил весь этот проект.

    Использование

    Чтобы подключить этот крейт к вашему проекту, добавьте следующую строку в ваш cargo.toml и выполните сборку в режиме release:

    root@kitploit:~
    [dependencies]
    unwinder = "=0.1.4"
    

    Основная функциональность этого крейта обёрнута в два макроса:

    • Макрос call_function!() позволяет выполнять любую произвольную функцию с чистым стеком вызовов.
    • Макрос indirect_syscall!() выполняет указанный (непрямой) системный вызов с чистым стеком вызовов.

    Для использования любого из этих макросов необходимо импортировать тип данных std::ffi::c_void.

    Оба макроса возвращают *mut c_void, который можно использовать для получения значения, возвращённого выполненной функцией. Более подробная информация приведена в разделе с примерами.

    Макрос call_function

    Этот макрос используется для вызова любой нужной функции с чистым стеком вызовов. Макрос ожидает следующие параметры:

    • Первый параметр — адрес памяти, по которому выполняется вызов после спуфинга стека вызовов. Этот параметр должен передаваться как usize, isize или указатель.
    • Второй параметр — логическое значение (bool), указывающее, нужно ли сохранять кадр стартовой функции. Если вы не уверены в этом, установите его в false — это всегда гарантирует корректный стек вызовов.
    • Следующие параметры — это аргументы, которые будут переданы функции после спуфинга стека вызовов.

    Макрос indirect_syscall

    Этот макрос используется для выполнения любого нужного непрямого системного вызова с чистым стеком вызовов. Макрос ожидает следующие параметры:

    • Первый параметр — строка, содержащая имя NT-функции, системный вызов которой вы хотите выполнить.
    • Второй параметр — логическое значение (bool), указывающее, нужно ли сохранять кадр стартовой функции. Если вы не уверены в этом, установите его в false — это всегда гарантирует корректный стек вызовов.
    • Следующие параметры — это аргументы, которые будут переданы NT-функции.

    Передача параметров

    Чтобы передавать этим двум макросам аргументы различных типов, необходимо учитывать следующее:

    • Любой базовый тип данных, который может быть преобразован в usize (u8-u64, i8-i64, bool и т.д.), может передаваться макросам напрямую.
    • Структуры и объединения размером 8, 16, 32 или 64 бита передаются так, как если бы они были целыми числами того же размера.
    • Структуры и объединения размером более 64 бит должны передаваться как указатель.
    • Строки (&str и String) должны передаваться как указатель.
    • Нулевые указатели (ptr::null(), ptr::null_mut() и т.д.) передаются как 0 (независимо от того, u8 это, u16, i32 или любой другой тип).
    • Параметры с плавающей запятой и двойной точностью в настоящее время не поддерживаются.
    • Любой другой тип данных должен передаваться как указатель.

    Примеры

    Вызов kernel32.dll!Sleep()

    root@kitploit:~
    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);
    

    Вызов kernel32.dll!OpenProcess()

    root@kitploit:~
    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, — новый дескриптор целевого процесса.

    Вызов NtDelayExecution() в качестве непрямого системного вызова

    root@kitploit:~
    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, который можно использовать для получения NTSTATUS, возвращённого функцией NtDelayExecution.

    Объединение вызовов макросов

    Процесс спуфинга может быть выстроен в цепочку любое количество раз без аномального увеличения размера стека вызовов. Поток выполнения при этом также сохраняется. Ниже приведён пример:

    root@kitploit:~
    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.

    Спуфинг стека вызовов без основного модуля.

    PoC

    Для тестирования реализации этой техники использовался PE-sieve с флагом /threads. Результаты теста показывают, что проверка стека вызовов не выявляет присутствия полезной нагрузки при использовании функциональности этого крейта. Как видно на втором изображении, нагрузка детектируется, когда unwinder не используется.

    Результаты PE-sieve при использовании unwinder. Результаты PE-sieve без использования unwinder.

    Подмена стека

    Описание техники

    Это альтернативный SilentMoonWalk метод спуфинга стека вызовов, который позволяет сохранять чистый стек вызовов во время выполнения вашей программы. Основная идея этой техники заключается в том, что каждая вызываемая внутри вашего модуля функция берёт на себя заботу о ранее помещённом в стек обратном адресе, находя во время выполнения легитимную функцию с таким же размером кадра, что и у подменяемого обратного адреса. Как только такая функция с тем же размером кадра найдена, вычисляется смещение внутри неё, и итоговый адрес используется для замены последнего обратного адреса, скрывая любые аномальные записи в стеке вызовов и сохраняя его раскручиваемым. Исходный обратный адрес сохраняется unwinder и возвращается на нужную позицию в стеке перед выполнением инструкции возврата, что позволяет продолжить нормальный поток выполнения программы.

    Подмена стека

    Это экспериментальная функция: несмотря на полную работоспособность, она всё ещё находится в стадии разработки и исследований, поэтому обязательно тестируйте свой код, если решите интегрировать в него эту технику.

    Как использовать

    Чтобы использовать функциональность подмены стека, добавьте следующую строку в ваш cargo.toml и выполните сборку в режиме release:

    root@kitploit:~
    [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, содержащий значение, возвращённое вызванной таким образом функцией (то есть они работают так же, как описано для макросов call_function и indirect_syscall, используемых для выполнения SilentMoonWalk).

    Для использования этих макросов необходимо импортировать тип данных std::ffi::c_void. Все функции, использующие любой из этих макросов, должны быть помечены атрибутами #[no_mangle] или #[inline(never)], чтобы компилятор Rust не встраивал их во время оптимизации.

    Прежде чем перейти к практическому примеру, демонстрирующему использование всего этого, кратко рассмотрим пару макросов replace_and_call/replace_and_syscall и то, как передавать им ожидаемые аргументы.

    replace_and_call

    Этот макрос используется для вызова любой нужной функции за пределами текущего модуля с чистым стеком вызовов при использовании подмены стека. Макрос ожидает следующие параметры:

    • Первый параметр — адрес памяти вызываемой функции. Этот параметр должен передаваться как usize, isize или указатель.
    • Следующие параметры — это аргументы, которые будут переданы указанной функции. Они подчиняются тем же правилам, что указаны в разделе Передача параметров.

    replace_and_syscall

    Этот макрос используется для выполнения любого нужного непрямого системного вызова с чистым стеком вызовов при использовании подмены стека. Макрос ожидает следующие параметры:

    • Первый параметр — строка, содержащая имя NT-функции, системный вызов которой вы хотите выполнить.
    • Следующие параметры — это аргументы, которые будут переданы NT-функции. Они подчиняются тем же правилам, что указаны в разделе Передача параметров.

    Пример

    Думаю, лучше всего показать, как используются эти макросы, на практическом примере. Предположим, мы создаём DLL, которая будет рефлексивно внедрена в память. Эта DLL будет экспортировать две функции: ExportA и ExportB, поэтому мы будем считать эти две функции точками входа модуля. Обе они должны вызывать макрос start_stack_replacement в самом начале, а перед возвратом также вызывать противоположный макрос end_replacement. Макрос start_stack_replacement ожидает в качестве аргумента базовый адрес модуля; также можно передать 0, если вы не знаете этот адрес во время выполнения, — макрос попытается определить его самостоятельно.

    root@kitploit:~
    #[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:

    root@kitploit:~
    #[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 (непрямой системный вызов).

    root@kitploit:~
    #[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
    } 
    

    Примечания

    Поскольку это функция, находящаяся в разработке, следует учитывать некоторые моменты:

    • Если в процессе загрузки вы удаляете заголовки вашего PE-файла, вы должны передать макросу start_stack_replace базовый адрес модуля. Сейчас он не сможет найти его самостоятельно (это будет исправлено в следующем обновлении).
    • Если вам интересно: подмена стека использует ту же комбинацию jmp rbx + маскирующий кадр, что и техника SilentMoonWalk. Это происходит только при использовании макросов 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 аргументов.

    Пожалуйста, сообщайте мне о любых ошибках, которые могут возникнуть при использовании этой функции.

    Скачать инструмент