Skip to content
KitploitKITPLOIT
HerramientasBlog
Enviar
HerramientasBlog
Enviar

¡Herramientas de Hacking, PenTest y Ciberseguridad para tu Arsenal de Seguridad!

Kitploit es un directorio de herramientas de hacking, ciberseguridad y pentesting. Descubre las últimas actualizaciones de proyectos para encontrar vulnerabilidades, analizar sistemas, automatizar pruebas y fortalecer tu seguridad.

··Feeds·Contacto·Privacidad·© 2026 Kitploit

Directorio de Herramientas

Categorías

Ver todas las categorías
Loading categories
Unwinder — Spoofing de pila de llamadas para Rust | Kitploit
Herramientas/GitHubGitHub/kudaes/unwinder
Evasión de IDS/IPSPost-ExplotaciónRed TeamingDesarrollo de PayloadsAtaque Adversario
GitHubkudaes/unwinder

Unwinder

Spoofing de pila de llamadas para Rust

Ver Repositorio
384365hace 1 añoRevisado por Kitploit

Más Populares

Ver todos →

Descubre las herramientas más usadas por nuestra comunidad.

Explora todas las herramientas

Explora nuestra colección de herramientas

Ver todas las herramientas →
Compartir

Contenido

  • SilentMoonWalk
    • Descripción
    • Créditos
    • Uso
      • Macro call_function!()
      • Macro indirect_syscall!()
      • Paso de parámetros
    • Ejemplos
      • Llamando a kernel32.dll!Sleep()
      • Llamando a kernel32.dll!OpenProcess()
      • Llamando a NtDelayExecution() como syscall indirecto
      • Concatenar llamadas a macros
    • Consideraciones
      • Frame inicial
      • PoC
  • Reemplazo de pila
    • Descripción de la técnica
    • Uso
    • Ejemplo práctico
    • Observaciones

SilentMoonWalk

Descripción

Unwinder proporciona una weaponización completa de la técnica SilentMoonWalk, permitiendo obtener un spoofing completo y estable de la pila de llamadas en Rust.

Esta técnica presenta las siguientes características:

  • Permite ejecutar cualquier función arbitraria con hasta 11 parámetros.
  • Permite ejecutar syscalls indirectos (sin asignaciones adicionales en el heap) con hasta 11 parámetros.
  • El crate permite recuperar el valor devuelto por las funciones invocadas a través de él.
  • El proceso de spoofing puede concatenarse cualquier número de veces sin aumentar el tamaño de la pila de llamadas.
Descargar herramienta
  • Se usa TLS para aumentar la eficiencia durante el proceso de spoofing.
  • dinvoke_rs se utiliza para realizar cualquier llamada a la API de Windows requerida por el crate.
  • Créditos

    Reconocimientos a los creadores de la técnica SilentMoonWalk:

    • KlezVirus
    • Waldo-IRC
    • Trickster0

    Y por supuesto, un enorme agradecimiento a namazso por el hilo de Twitter que inspiró todo este proyecto.

    Uso

    Importa este crate en tu proyecto añadiendo la siguiente línea a tu cargo.toml y compila en modo release:

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

    La funcionalidad principal de este crate se ha envuelto en dos macros:

    • La macro call_function!() permite ejecutar cualquier función arbitraria con una pila de llamadas limpia.
    • La macro indirect_syscall!() ejecuta el syscall (indirecto) especificado con una pila de llamadas limpia.

    Para usar cualquiera de estas macros es necesario importar el tipo de datos std::ffi::c_void.

    Ambas macros devuelven un *mut c_void que puede usarse para recuperar el valor devuelto por la función ejecutada. En la sección de ejemplos se proporciona información más detallada.

    Macro call_function

    Esta macro se utiliza para llamar a cualquier función deseada con una pila de llamadas limpia. La macro espera los siguientes parámetros:

    • El primer parámetro es la dirección de memoria a la que llamar después de falsear la pila de llamadas. Este parámetro debe pasarse como usize, isize o un puntero.
    • El segundo parámetro es un bool que indica si se debe mantener o no el frame de la función de inicio. Si no estás seguro de esto, establécelo en false, lo que siempre garantiza una buena pila de llamadas.
    • Los siguientes parámetros son los argumentos que se enviarán a la función una vez que la pila de llamadas haya sido falseada.

    Macro indirect_syscall

    Esta macro se utiliza para realizar cualquier syscall indirecto deseado con una pila de llamadas limpia. La macro espera los siguientes parámetros:

    • El primer parámetro es una cadena que contiene el nombre de la función NT cuyo syscall se desea ejecutar.
    • El segundo parámetro es un bool que indica si se debe mantener o no el frame de la función de inicio. Si no estás seguro de esto, establécelo en false, lo que siempre garantiza una buena pila de llamadas.
    • Los siguientes parámetros son los argumentos que se enviarán a la función NT.

    Paso de parámetros

    Para pasar argumentos de diferentes tipos a estas dos macros, se deben tener en cuenta las siguientes consideraciones:

    • Cualquier tipo de dato básico que pueda convertirse a usize (u8-u64, i8-i64, bool, etc.) puede pasarse directamente a las macros.
    • Las estructuras y uniones de tamaño 8, 16, 32 o 64 bits se pasan como si fueran enteros del mismo tamaño.
    • Las estructuras y uniones con un tamaño superior a 64 bits deben pasarse como un puntero.
    • Las cadenas (&str y String) deben pasarse como un puntero.
    • Los punteros nulos (ptr::null(), ptr::null_mut(), etc.) se pasan como un 0 (sin importar si es u8, u16, i32 o cualquier otro).
    • Los parámetros de coma flotante y doble precisión no están soportados actualmente.
    • Cualquier otro tipo de dato debe pasarse como un puntero.

    Ejemplos

    Llamando a 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);
    

    Llamando a 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);
    

    Observa que la macro devuelve un *mut c_void que puede convertirse directamente a un HANDLE ya que ambos tipos de datos tienen el mismo tamaño. Esto permite acceder al valor devuelto por OpenProcess, que es el nuevo handle al proceso objetivo.

    Llamando a NtDelayExecution como syscall indirecto

    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);
    

    Observa que la macro devuelve un *mut c_void que puede utilizarse para recuperar el NTSTATUS devuelto por NtDelayExecution.

    Concatenar llamadas a macros

    El proceso de spoofing puede concatenarse cualquier número de veces sin un incremento anómalo del tamaño de la pila de llamadas. El flujo de ejecución también se conservará. El siguiente código es un ejemplo de ello:

    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.
    	}
    }
    

    Consideraciones

    Frame inicial

    Si estableces el segundo parámetro en true (en ambas macros), el proceso de spoofing intentará mantener el frame de la dirección de inicio del hilo en la pila de llamadas para aumentar la legitimidad.

    Pila de llamadas falseada manteniendo el módulo principal.

    A veces, la función de inicio del hilo no realiza una call a una función posterior (por ejemplo, se ejecuta una instrucción jmp en su lugar), lo que significa que no se ha introducido una dirección de retorno en la pila. En ese escenario (y también si estableces ese segundo parámetro en false), la pila de llamadas falseada comenzará en el frame de BaseThreadInitThunk.

    Pila de llamadas falseada sin el módulo principal.

    PoC

    Para probar la implementación de la técnica, se ha utilizado PE-sieve con la flag /threads. Los resultados de la prueba muestran cómo la inspección de la pila de llamadas no revela la presencia del payload cuando se utilizan las funcionalidades de este crate. Como se puede ver en la segunda imagen, el payload se detecta cuando no se utiliza unwinder.

    Resultados de PE-sieve cuando se usa unwinder. Resultados de PE-sieve cuando no se usa unwinder.

    Reemplazo de pila

    Descripción de la técnica

    Esta es una alternativa a SilentMoonWalk para el spoofing de la pila de llamadas que permite mantener una pila de llamadas limpia durante la ejecución de tu programa. La idea principal detrás de esta técnica es que cada función llamada dentro de tu módulo se encarga de la dirección de retorno previamente introducida en la pila, encontrando en tiempo de ejecución una función legítima con el mismo tamaño de frame que el de la dirección de retorno que se va a falsear. Una vez localizada una función legítima con el mismo tamaño de frame, se calcula un offset dentro de ella y la dirección final se utiliza para reemplazar la última dirección de retorno, ocultando cualquier entrada anómala en la pila de llamadas y manteniéndola desenrollable. La dirección de retorno original es almacenada por unwinder y se vuelve a colocar en la posición correcta de la pila antes de que se ejecute una instrucción de retorno, lo que permite continuar con el flujo normal del programa.

    Reemplazo de pila

    Esta es una característica experimental que, a pesar de ser totalmente funcional, aún está en desarrollo e investigación, así que asegúrate de probar tu código si decides integrar esta técnica en él.

    Cómo usarlo

    Para usar la funcionalidad de reemplazo de pila, debes añadir la siguiente línea a tu cargo.toml y compilar en modo release:

    root@kitploit:~
    [dependencies]
    unwinder = {version = "0.1.4", features = ["Experimental"]}
    

    La funcionalidad principal de esta característica se ha envuelto en las siguientes macros:

    • El par de macros start_stack_replacement!()/end_replacement!() indica a unwinder que inicie/finalice el proceso de reemplazo de pila. Estas dos macros deben llamarse en el punto de entrada de tu código (por ejemplo, en las funciones exportadas de tu dll).
    • El par de macros replace_and_continue!()/restore!() realiza el reemplazo/restauración de la última dirección de retorno.
    • Finalmente, el par de macros replace_and_call!()/replace_and_syscall!() se utiliza para realizar el reemplazo de pila cuando queremos llamar a funciones fuera del módulo actual (por ejemplo, al usar la API de Windows o al llamar a código de cualquier otra dll). Ambas macros devuelven un *mut c_void que contiene el valor devuelto por la función llamada de esta manera (es decir, funcionan de la misma forma que se describe para las macros call_function e indirect_syscall utilizadas para ejecutar SilentMoonWalk).

    Para usar estas macros es necesario importar el tipo de datos std::ffi::c_void. Todas las funciones que utilicen cualquiera de estas macros deben etiquetarse con los atributos #[no_mangle] o #[inline(never)] para evitar que el compilador de Rust las inlinee durante el proceso de optimización.

    Antes de sumergirnos en un ejemplo práctico que muestre cómo usar todo esto, hagamos una rápida inspección del par de macros replace_and_call/replace_and_syscall y de cómo pasarles los argumentos esperados.

    replace_and_call

    Esta macro se utiliza para llamar a cualquier función deseada fuera del módulo actual con una pila de llamadas limpia mientras se usa el reemplazo de pila. La macro espera los siguientes parámetros:

    • El primer parámetro es la dirección de memoria de la función a llamar. Este parámetro debe pasarse como usize, isize o un puntero.
    • Los siguientes parámetros son los argumentos que se enviarán a la función especificada. Siguen las mismas reglas especificadas en la sección Paso de parámetros.

    replace_and_syscall

    Esta macro se utiliza para realizar cualquier syscall indirecto deseado con una pila de llamadas limpia mientras se usa el reemplazo de pila. La macro espera los siguientes parámetros:

    • El primer parámetro es una cadena que contiene el nombre de la función NT cuyo syscall se desea ejecutar.
    • Los siguientes parámetros son los argumentos que se enviarán a la función NT. Siguen las mismas reglas especificadas en la sección Paso de parámetros.

    Ejemplo

    Creo que la mejor manera de mostrar cómo se usan estas macros es mediante un ejemplo práctico. Supongamos que estamos creando una dll que será inyectada reflectivamente en memoria. Esta dll exportará dos funciones ExportA y ExportB, por lo que consideraremos estas dos funciones como los puntos de entrada del módulo. Ambas deben llamar a la macro start_stack_replacement justo al principio y también deben llamar a la macro inversa end_replacement antes de retornar. La macro start_stack_replacement espera como argumento la dirección base del módulo, o puedes pasar 0 si no conoces esa dirección en tiempo de ejecución; la macro intentará averiguarla por sí misma.

    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
    }
    

    Iniciar el proceso de reemplazo de pila implica la construcción manual de una nueva pila que se utilizará hasta que se llame a la macro end_replacement. La siguiente imagen ilustra lo que sucede internamente:

    Reemplazo de pila

    Aunque teóricamente no sería necesario comenzar una nueva pila desde cero, he decidido implementar el proceso de esta manera para garantizar la estabilidad y evitar que algo se rompa.

    Ahora, supongamos que nuestra función ExportedA realiza varias llamadas a otras dos funciones internas. Estas dos funciones internas se encargan de reemplazar/restaurar la dirección de retorno original que apuntará a algún lugar dentro de ExportedA, rompiendo la pila de llamadas a menos que nos encarguemos de ello. Este proceso de reemplazo implica envolver el código de nuestra función interna entre las macros replace_and_continue y 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
    } 
    

    Finalmente, tanto internal_a como internal_b hacen uso de algunas funcionalidades de la API de Windows. Para mantener la pila de llamadas desenrollable, estas llamadas deben realizarse a través de las macros replace_and_call (llamada normal) o replace_and_syscall (syscall indirecto).

    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
    } 
    

    Observaciones

    Dado que esta es una característica en desarrollo, se deben tener en cuenta algunas cosas:

    • Si estás eliminando los encabezados de tu PE durante el proceso de carga, debes pasar a la macro start_stack_replace la dirección base del módulo. Ahora mismo no podrá encontrarla por sí sola (se resolverá en la próxima actualización).
    • Por si te lo preguntas, el reemplazo de pila utiliza la misma combinación de jmp rbx + frame de ocultación que la técnica SilentMoonWalk. Esto ocurre solo cuando se usan las macros replace_and_call y replace_and_syscall, y está previsto cambiarlo en la próxima actualización.
    • Tanto replace_and_call como replace_and_syscall devuelven un *mut c_void que puede utilizarse para recuperar el valor devuelto por la función ejecutada a través de ellas. Este es el mismo comportamiento descrito para las macros call_function e indirect_syscall.
    • Las macros replace_and_call y replace_and_syscall permiten hasta 11 argumentos.

    Por favor, infórmame de cualquier bug que pueda surgir al usar esta característica.