Skip to content
KitploitKITPLOIT
FerramentasBlog
Enviar
FerramentasBlog
Enviar

Ferramentas de Hacking, PenTest e Cibersegurança para o seu Arsenal de Segurança!

Kitploit é um diretório de ferramentas de hacking, cibersegurança e pentesting. Descubra as últimas atualizações de projetos para encontrar vulnerabilidades, analisar sistemas, automatizar testes e fortalecer sua segurança.

··Feeds·Contato·Privacidade·© 2026 Kitploit

Diretório de Ferramentas

Categorias

Ver todas as categorias
Loading categories
Unwinder — Call stack spoofing for Rust | Kitploit
Ferramentas/GitHubGitHub/kudaes/unwinder
IDS/IPS EvasionPost-ExploitationRed TeamingPayload DevelopmentAdversarial Attack
GitHubkudaes/unwinder

Unwinder

Call stack spoofing for Rust

Ver Repositório
38436há 1 anoRevisado pelo Kitploit

Mais Populares

Ver todos →

Descubra as ferramentas mais usadas pela nossa comunidade.

Explore todas as ferramentas

Navegue pela nossa coleção de ferramentas

Ver todas as ferramentas →
Compartilhar

Conteúdo

  • SilentMoonWalk
    • Descrição
    • Créditos
    • Uso
      • Macro call_function!()
      • Macro indirect_syscall!()
      • Passagem de parâmetros
    • Exemplos
      • Chamando kernel32.dll!Sleep()
      • Chamando kernel32.dll!OpenProcess()
      • Chamando NtDelayExecution() como syscall indireto
      • Concatenar chamadas de macro
    • Considerações
      • Frame inicial
      • PoC
  • Substituição de pilha
    • Descrição
    • Como usar
    • Exemplo prático
    • Observações

SilentMoonWalk

Descrição

O Unwinder fornece uma weaponização completa da técnica SilentMoonWalk, permitindo obter spoofing de pilha de chamadas completo e estável em Rust.

Esta técnica possui as seguintes características:

  • Suporte para executar qualquer função arbitrária com até 11 parâmetros.
  • Suporte para executar syscalls indiretos (sem alocações adicionais de heap) com até 11 parâmetros.
  • A crate permite recuperar o valor retornado pelas funções chamadas por meio dela.
  • O processo de spoofing pode ser concatenado qualquer número de vezes sem aumentar o tamanho da pilha de chamadas.
  • TLS é usado para aumentar a eficiência durante o processo de spoofing.
  • O dinvoke_rs é usado para realizar qualquer chamada à API do Windows necessária pela crate.

Créditos

Créditos aos criadores da técnica SilentMoonWalk:

  • KlezVirus
  • Waldo-IRC
  • Trickster0

E, claro, um enorme agradecimento a namazso pelo thread no Twitter que inspirou todo este projeto.

Uso

Importe esta crate para o seu projeto adicionando a seguinte linha ao seu cargo.toml e compile no modo release:

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

A funcionalidade principal desta crate foi encapsulada em duas macros:

  • A macro call_function!() permite executar qualquer função arbitrária com uma pilha de chamadas limpa.
  • A macro indirect_syscall!() executa o syscall indireto especificado com uma pilha de chamadas limpa.

Para usar qualquer uma destas macros, é necessário importar o tipo de dados std::ffi::c_void.

Ambas as macros retornam um *mut c_void que pode ser usado para recuperar o valor retornado pela função executada. Informações mais detalhadas na seção de exemplos.

Macro call_function

Esta macro é usada para chamar qualquer função desejada com uma pilha de chamadas limpa. A macro espera os seguintes parâmetros:

  • O primeiro parâmetro é o endereço de memória a ser chamado após o spoofing da pilha de chamadas. Este parâmetro deve ser passado como usize, isize ou um ponteiro.
  • O segundo parâmetro é um bool que indica se deve manter ou não o frame da função inicial. Se você não tiver certeza sobre isso, defina-o como false, o que sempre garante uma boa pilha de chamadas.
  • Os parâmetros seguintes são os argumentos a serem enviados para a função depois que a pilha de chamadas foi alvo de spoofing.

Macro indirect_syscall

Esta macro é usada para realizar qualquer syscall indireto desejado com uma pilha de chamadas limpa. A macro espera os seguintes parâmetros:

  • O primeiro parâmetro é uma string que contém o nome da função NT cujo syscall você deseja executar.
  • O segundo parâmetro é um bool que indica se deve manter ou não o frame da função inicial. Se você não tiver certeza sobre isso, defina-o como false, o que sempre garante uma boa pilha de chamadas.
  • Os parâmetros seguintes são os argumentos a serem enviados para a função NT.

Passagem de parâmetros

Para passar argumentos de diferentes tipos para estas duas macros, as seguintes considerações devem ser levadas em conta:

  • Qualquer tipo de dado básico que possa ser convertido para usize (u8-u64, i8-i64, bool, etc.) pode ser passado diretamente para as macros.
  • Structs e unions de tamanho 8, 16, 32 ou 64 bits são passados como se fossem inteiros do mesmo tamanho.
  • Estruturas e unions com tamanho maior que 64 bits devem ser passadas como um ponteiro.
  • Strings (&str e String) devem ser passadas como um ponteiro.
  • Ponteiros nulos (ptr::null(), ptr::null_mut(), etc.) são passados como 0 (não importa se é u8, u16, i32 ou qualquer outro).
  • Parâmetros de ponto flutuante e dupla precisão não são suportados atualmente.
  • Qualquer outro tipo de dado deve ser passado como um ponteiro.

Exemplos

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

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

Observe que a macro retorna um *mut c_void que pode ser convertido diretamente para um HANDLE, já que ambos os tipos de dados têm o mesmo tamanho. Isso permite acessar o valor retornado por OpenProcess, que é o novo handle para o processo alvo.

Chamando NtDelayExecution como syscall indireto

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

Observe que a macro retorna um *mut c_void que pode ser usado para recuperar o NTSTATUS retornado por NtDelayExecution.

Concatenar chamadas de macro

O processo de spoofing pode ser concatenado qualquer número de vezes sem um incremento anormal no tamanho da pilha de chamadas. O fluxo de execução também será preservado. O código a seguir é um exemplo disso:

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

Considerações

Frame inicial

Se você definir o segundo parâmetro como true (em ambas as macros), o processo de spoofing tentará manter o frame do endereço de início da thread na pilha de chamadas para aumentar a legitimidade.

Pilha de chamadas com spoofing mantendo o módulo principal.

Às vezes, a função de início da thread não executa um call para uma função subsequente (por exemplo, uma instrução jmp é executada em vez disso), o que significa que não há endereço de retorno empurrado para a pilha. Nesse cenário (e também se você definir o segundo parâmetro como false), a pilha de chamadas com spoofing começará no frame de BaseThreadInitThunk.

Pilha de chamadas com spoofing sem o módulo principal.

PoC

Para testar a implementação da técnica, o PE-sieve foi usado com a flag /threads. Os resultados do teste mostram como a inspeção da pilha de chamadas não revela a presença do payload quando as funcionalidades desta crate são usadas. Como pode ser visto na segunda imagem, o payload é detectado quando o unwinder não é usado.

Resultados do PE-sieve quando o unwinder é usado. Resultados do PE-sieve quando o unwinder não é usado.

Substituição de pilha

Descrição da técnica

Esta é uma alternativa de spoofing de pilha de chamadas ao SilentMoonWalk que permite manter uma pilha de chamadas limpa durante a execução do seu programa. A ideia principal por trás desta técnica é que cada função chamada dentro do seu módulo cuida do endereço de retorno previamente empurrado, encontrando em tempo de execução uma função legítima com o mesmo tamanho de frame do endereço de retorno a ser falsificado. Uma vez localizada uma função legítima com o mesmo tamanho de frame, um offset dentro dela é calculado e o endereço final é usado para substituir o último endereço de retorno, ocultando qualquer entrada anômala na pilha de chamadas e mantendo-a passível de desenrolamento (unwindable). O endereço de retorno original é armazenado por unwinder e é movido de volta para a posição correta na pilha antes que uma instrução de retorno seja executada, permitindo continuar o fluxo normal do programa.

Substituição de pilha

Este é um recurso experimental que, apesar de totalmente funcional, ainda está em desenvolvimento e pesquisa, portanto, certifique-se de testar seu código se decidir integrar esta técnica nele.

Como usar

Para usar a funcionalidade de substituição de pilha, você deve adicionar a seguinte linha ao seu cargo.toml e compilar no modo release:

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

A funcionalidade principal deste recurso foi encapsulada nas seguintes macros:

  • O par de macros start_stack_replacement!()/end_replacement!() indica ao unwinder para iniciar/encerrar o processo de substituição de pilha. Essas duas macros devem ser chamadas no ponto de entrada do seu código (por exemplo, nas funções exportadas da sua dll).
  • O par de macros replace_and_continue!()/restore!() realiza a substituição/restauração do último endereço de retorno.
  • Por fim, o par de macros replace_and_call!()/replace_and_syscall!() é usado para realizar a substituição de pilha quando queremos chamar funções fora do módulo atual (por exemplo, ao usar a API do Windows ou chamar código de qualquer outra dll). Ambas as macros retornam um *mut c_void contendo o valor retornado pela função chamada dessa forma (ou seja, elas operam da mesma maneira que foi descrita para as macros call_function e indirect_syscall usadas para executar o SilentMoonWalk).

Para usar essas macros, é necessário importar o tipo de dados std::ffi::c_void. Todas as funções que usam qualquer uma dessas macros devem ser rotuladas com os atributos #[no_mangle] ou #[inline(never)] para impedir que o compilador Rust as inline durante o processo de otimização.

Antes de mergulhar em um exemplo prático mostrando como usar tudo isso, apenas uma rápida inspeção do par de macros replace_and_call/replace_and_syscall e de como passar a eles os argumentos esperados.

replace_and_call

Esta macro é usada para chamar qualquer função desejada fora do módulo atual com uma pilha de chamadas limpa enquanto usa a substituição de pilha. A macro espera os seguintes parâmetros:

  • O primeiro parâmetro é o endereço de memória da função a ser chamada. Este parâmetro deve ser passado como usize, isize ou um ponteiro.
  • Os parâmetros seguintes são os argumentos a serem enviados para a função especificada. Eles seguem as mesmas regras especificadas na seção Passagem de parâmetros.

replace_and_syscall

Esta macro é usada para realizar qualquer syscall indireto desejado com uma pilha de chamadas limpa enquanto usa a substituição de pilha. A macro espera os seguintes parâmetros:

  • O primeiro parâmetro é uma string que contém o nome da função NT cujo syscall você deseja executar.
  • Os parâmetros seguintes são os argumentos a serem enviados para a função NT. Eles seguem as mesmas regras especificadas na seção Passagem de parâmetros.

Exemplo

Eu acho que a melhor maneira de mostrar como essas macros são usadas é por meio de um exemplo prático. Vamos supor que estamos criando uma dll que será injetada reflexivamente na memória. Essa dll exportará duas funções ExportA e ExportB, então consideraremos essas duas funções como os pontos de entrada do módulo. Ambas devem chamar a macro start_stack_replacement logo no início e também devem chamar a macro reversa end_replacement antes de retornar. A macro start_stack_replacement espera como argumento o endereço base do módulo, ou você pode passar 0 se não souber esse endereço em tempo de execução; a macro tentará descobri-lo sozinha.

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 o processo de substituição de pilha envolve a criação manual de uma nova pilha que será usada até que a macro end_replacement seja chamada. A imagem a seguir ilustra o que está acontecendo nos bastidores:

Substituição de pilha

Embora teoricamente não fosse necessário iniciar uma nova pilha do zero, decidi implementar o processo dessa forma para garantir estabilidade e evitar que algo quebre.

Agora, vamos supor que nossa função ExportedA faça várias chamadas a outras duas funções internas. Essas duas funções internas são responsáveis por substituir/restaurar o endereço de retorno original que apontará para algum lugar dentro de ExportedA, quebrando a pilha de chamadas a menos que cuidemos disso. Esse processo de substituição envolve envolver o código da nossa função interna entre as macros replace_and_continue e 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 a função internal_a quanto a internal_b fazem uso de alguma funcionalidade da API do Windows. Para manter a pilha de chamadas passível de desenrolamento, essas chamadas devem ser realizadas por meio das macros replace_and_call (chamada normal) ou replace_and_syscall (syscall indireto).

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
} 

Observações

Como este é um recurso em desenvolvimento, algumas coisas devem ser levadas em conta:

  • Se você estiver removendo os cabeçalhos do seu PE durante o processo de carregamento, você deve passar para a macro start_stack_replace o endereço base do módulo. No momento, ela não conseguirá encontrá-lo sozinha (a ser resolvido na próxima atualização).
  • Caso você esteja se perguntando, a substituição de pilha usa a mesma combinação de jmp rbx + quadro de ocultação (concealment frame) da técnica SilentMoonWalk. Isso acontece apenas quando se usa as macros replace_and_call e replace_and_syscall e está planejado para ser alterado na próxima atualização.
  • Ambas as macros replace_and_call e replace_and_syscall retornam um *mut c_void que pode ser usado para recuperar o valor retornado pela função executada através delas. Este é o mesmo comportamento descrito para as macros call_function e indirect_syscall.
  • As macros replace_and_call e replace_and_syscall permitem até 11 argumentos.

Por favor, reporte-me qualquer bug que possa surgir ao usar este recurso.

Baixar ferramenta