Skip to content
KitploitKITPLOIT
OutilsBlog
Soumettre
OutilsBlog
Soumettre

Outils de Hacking, PenTest et Cybersécurité pour votre Arsenal de Sécurité !

Kitploit est un répertoire d'outils de hacking, de cybersécurité et de pentesting. Découvrez les dernières mises à jour des projets pour trouver des vulnérabilités, analyser des systèmes, automatiser les tests et renforcer votre sécurité.

··Flux·Contact·Confidentialité·© 2026 Kitploit

Répertoire d'outils

Catégories

Voir toutes les catégories
Loading categories
Unwinder — Call stack spoofing for Rust | Kitploit
Outils/GitHubGitHub/kudaes/unwinder
IDS/IPS EvasionPost-ExploitationRed TeamingPayload DevelopmentAdversarial Attack
GitHubkudaes/unwinder

Unwinder

Call stack spoofing for Rust

Voir le dépôt
38436il y a 1 anVérifié par Kitploit

Populaires

Voir tout →

Découvrez les outils les plus utilisés par notre communauté.

Explorer tous les outils

Parcourez notre collection d'outils

Voir tous les outils →
Partager

Contenu

  • SilentMoonWalk
    • Description
    • Crédits
    • Utilisation
      • Macro call_function!()
      • Macro indirect_syscall!()
      • Passage des paramètres
    • Exemples
      • Appel de kernel32.dll!Sleep()
      • Appel de kernel32.dll!OpenProcess()
      • Appel de NtDelayExecution() en syscall indirect
      • Concaténation des appels de macros
    • Considérations
      • Cadre initial
      • PoC
  • Remplacement de pile
    • Description de la technique
    • Comment l'utiliser
    • Exemple pratique
    • Remarques

SilentMoonWalk

Description

Unwinder fournit une weaponization complète de la technique SilentMoonWalk, permettant d'obtenir un spoofing complet et stable de la pile d'appels en Rust.

Cette technique présente les caractéristiques suivantes :

  • Prise en charge de l'exécution de toute fonction arbitraire avec jusqu'à 11 paramètres.
  • Prise en charge des syscalls indirects (sans allocations de tas supplémentaires) avec jusqu'à 11 paramètres.
  • La crate permet de récupérer la valeur retournée par les fonctions appelées via elle.
  • Le processus de spoofing peut être concaténé un nombre illimité de fois sans augmenter la taille de la pile d'appels.
  • TLS est utilisé pour accroître l'efficacité du processus de spoofing.
  • dinvoke_rs est utilisé pour effectuer tout appel API Windows requis par la crate.

Crédits

Merci aux créateurs de la technique SilentMoonWalk :

  • KlezVirus
  • Waldo-IRC
  • Trickster0

Et bien sûr, un grand merci à namazso pour le fil Twitter qui a inspiré tout ce projet.

Utilisation

Importez cette crate dans votre projet en ajoutant la ligne suivante à votre cargo.toml et compilez en mode release :

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

La fonctionnalité principale de cette crate est encapsulée dans deux macros :

  • La macro call_function!() permet d'exécuter toute fonction arbitraire avec une pile d'appels propre.
  • La macro indirect_syscall!() exécute le syscall (indirect) spécifié avec une pile d'appels propre.

Pour utiliser l'une de ces macros, il est nécessaire d'importer le type std::ffi::c_void.

Les deux macros retournent un *mut c_void qui peut être utilisé pour récupérer la valeur retournée par la fonction exécutée. Des informations plus détaillées se trouvent dans la section des exemples.

Macro call_function!()

Cette macro est utilisée pour appeler n'importe quelle fonction souhaitée avec une pile d'appels propre. La macro attend les paramètres suivants :

  • Le premier paramètre est l'adresse mémoire à appeler après avoir usurpé la pile d'appels. Ce paramètre doit être passé comme usize, isize ou un pointeur.
  • Le deuxième paramètre est un booléen indiquant s'il faut conserver ou non le cadre de la fonction de départ. Si vous n'êtes pas sûr, définissez-le sur false, ce qui garantit toujours une pile d'appels correcte.
  • Les paramètres suivants sont les arguments à envoyer à la fonction une fois la pile d'appels usurpée.

Macro indirect_syscall!()

Cette macro est utilisée pour effectuer n'importe quel syscall indirect souhaité avec une pile d'appels propre. La macro attend les paramètres suivants :

  • Le premier paramètre est une chaîne contenant le nom de la fonction NT dont vous souhaitez exécuter le syscall.
  • Le deuxième paramètre est un booléen indiquant s'il faut conserver ou non le cadre de la fonction de départ. Si vous n'êtes pas sûr, définissez-le sur false, ce qui garantit toujours une pile d'appels correcte.
  • Les paramètres suivants sont les arguments à envoyer à la fonction NT.

Passage des paramètres

Afin de passer des arguments de différents types à ces deux macros, les considérations suivantes doivent être prises en compte :

  • Tout type de données de base pouvant être converti en usize (u8-u64, i8-i64, bool, etc.) peut être passé directement aux macros.
  • Les structures et unions de taille 8, 16, 32 ou 64 bits sont passées comme s'il s'agissait d'entiers de même taille.
  • Les structures et unions dont la taille dépasse 64 bits doivent être passées sous forme de pointeur.
  • Les chaînes (&str et String) doivent être passées sous forme de pointeur.
  • Les pointeurs nuls (ptr::null(), ptr::null_mut(), etc.) sont passés comme 0 (peu importe qu'il s'agisse d'un u8, u16, i32 ou autre).
  • Les paramètres à virgule flottante et en double précision ne sont actuellement pas pris en charge.
  • Tout autre type de données doit être passé sous forme de pointeur.

Exemples

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

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

Remarquez que la macro retourne un *mut c_void qui peut être directement converti en HANDLE, car les deux types ont la même taille. Cela permet d'accéder à la valeur retournée par OpenProcess, qui est le nouveau handle du processus cible.

Appel de NtDelayExecution() en syscall indirect

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

Remarquez que la macro retourne un *mut c_void qui peut être utilisé pour récupérer le NTSTATUS retourné par NtDelayExecution.

Concaténation des appels de macros

Le processus de spoofing peut être concaténé un nombre illimité de fois sans augmentation anormale de la taille de la pile d'appels. Le flux d'exécution est également préservé. Le code suivant en est un exemple :

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

Considérations

Cadre initial

Si vous définissez le deuxième paramètre sur true (pour les deux macros), le processus de spoofing tentera de conserver le cadre de l'adresse de démarrage du thread dans la pile d'appels afin d'accroître la légitimité.

Pile d'appels usurpée en conservant le module principal.

Parfois, la fonction de démarrage du thread n'effectue pas d'appel vers une fonction ultérieure (par exemple, une instruction jmp est exécutée à la place), ce qui signifie qu'aucune adresse de retour n'est poussée sur la pile. Dans ce scénario (et également si vous définissez ce deuxième paramètre sur false), la pile d'appels usurpée commencera au cadre de BaseThreadInitThunk.

Pile d'appels usurpée sans le module principal.

PoC

Afin de tester l'implémentation de la technique, PE-sieve a été utilisé avec le flag /threads. Les résultats du test montrent comment l'inspection de la pile d'appels ne révèle pas la présence de la charge utile lorsque les fonctionnalités de cette crate sont utilisées. Comme on peut le voir sur la deuxième image, la charge utile est détectée lorsque unwinder n'est pas utilisé.

Résultats de PE-sieve lorsque unwinder est utilisé. Résultats de PE-sieve lorsque unwinder n'est pas utilisé.

Remplacement de pile

Description de la technique

Il s'agit d'une alternative à SilentMoonWalk pour l'usurpation de la pile d'appels, qui permet de conserver une pile d'appels propre pendant l'exécution de votre programme. L'idée principale derrière cette technique est que chaque fonction appelée à l'intérieur de votre module prend en charge l'adresse de retour précédemment poussée, en trouvant au runtime une fonction légitime ayant la même taille de cadre que celle de l'adresse de retour à usurper. Une fois qu'une fonction légitime avec la même taille de cadre a été localisée, un décalage interne est calculé et l'adresse finale est utilisée pour remplacer la dernière adresse de retour, masquant toute entrée anormale dans la pile d'appels et la maintenant déroulable (unwindable). L'adresse de retour d'origine est stockée par unwinder et est remise à la bonne position dans la pile avant l'exécution d'une instruction de retour, ce qui permet de poursuivre le flux normal du programme.

Remplacement de pile

Il s'agit d'une fonctionnalité expérimentale qui, bien que pleinement fonctionnelle, est encore en cours de développement et de recherche. Assurez-vous donc de tester votre code si vous décidez d'intégrer cette technique.

Comment l'utiliser

Pour utiliser la fonctionnalité de remplacement de pile, vous devez ajouter la ligne suivante à votre cargo.toml et compiler en mode release :

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

La fonctionnalité principale de cette option est encapsulée dans les macros suivantes :

  • La paire de macros start_stack_replacement!()/end_replacement!() indique à unwinder de démarrer/arrêter le processus de remplacement de pile. Ces deux macros doivent être appelées au point d'entrée de votre code (par exemple, dans les fonctions exportées de votre dll).
  • La paire de macros replace_and_continue!()/restore!() effectue le remplacement/la restauration de la dernière adresse de retour.
  • Enfin, la paire de macros replace_and_call!()/replace_and_syscall!() est utilisée pour effectuer le remplacement de pile lorsque vous souhaitez appeler des fonctions en dehors du module actuel (par exemple, lors de l'utilisation de l'API Windows ou de l'appel du code d'une autre dll). Ces deux macros retournent un *mut c_void contenant la valeur retournée par la fonction appelée de cette manière (c'est-à-dire qu'elles fonctionnent de la même manière que les macros call_function et indirect_syscall utilisées pour exécuter SilentMoonWalk).

Pour utiliser ces macros, il est nécessaire d'importer le type std::ffi::c_void. Toutes les fonctions utilisant l'une de ces macros doivent être annotées avec les attributs #[no_mangle] ou #[inline(never)] pour empêcher le compilateur Rust de les inliner pendant le processus d'optimisation.

Avant de plonger dans un exemple pratique montrant comment utiliser tout cela, jetons un coup d'œil rapide à la paire de macros replace_and_call/replace_and_syscall et à la manière de leur passer les arguments attendus.

replace_and_call

Cette macro est utilisée pour appeler n'importe quelle fonction souhaitée en dehors du module actuel, avec une pile d'appels propre, tout en utilisant le remplacement de pile. La macro attend les paramètres suivants :

  • Le premier paramètre est l'adresse mémoire de la fonction à appeler. Ce paramètre doit être passé comme usize, isize ou un pointeur.
  • Les paramètres suivants sont les arguments à envoyer à la fonction spécifiée. Ils suivent les mêmes règles que celles décrites dans la section Passage des paramètres.

replace_and_syscall

Cette macro est utilisée pour effectuer n'importe quel syscall indirect souhaité avec une pile d'appels propre, tout en utilisant le remplacement de pile. La macro attend les paramètres suivants :

  • Le premier paramètre est une chaîne contenant le nom de la fonction NT dont vous souhaitez exécuter le syscall.
  • Les paramètres suivants sont les arguments à envoyer à la fonction NT. Ils suivent les mêmes règles que celles décrites dans la section Passage des paramètres.

Exemple pratique

Je pense que la meilleure façon de montrer comment ces macros sont utilisées est de passer par un exemple pratique. Supposons que nous créons une dll qui sera injectée par réflexion en mémoire. Cette dll exportera deux fonctions ExportA et ExportB; nous considérerons donc ces deux fonctions comme les points d'entrée du module. Toutes deux doivent appeler la macro start_stack_replacement au tout début et également appeler la macro inverse end_replacement avant de retourner. La macro start_stack_replacement attend comme argument l'adresse de base du module, ou vous pouvez passer 0 si vous ne connaissez pas cette adresse au runtime; la macro essaiera de la déterminer par elle-même.

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
}

Le démarrage du processus de remplacement de pile implique la construction manuelle d'une nouvelle pile qui sera utilisée jusqu'à ce que la macro end_replacement soit appelée. L'image suivante illustre ce qui se passe sous le capot :

Remplacement de pile

Bien que théoriquement il ne soit pas nécessaire de démarrer une nouvelle pile de zéro, j'ai décidé d'implémenter le processus de cette façon pour assurer la stabilité et éviter que quoi que ce soit ne casse.

Maintenant, supposons que notre fonction ExportedA effectue plusieurs appels vers deux autres fonctions internes. Ces deux fonctions internes sont responsables du remplacement/restauration de l'adresse de retour d'origine qui pointera vers un endroit dans ExportedA, cassant la pile d'appels si nous n'y prenons pas garde. Ce processus de remplacement consiste à encapsuler le code de notre fonction interne entre les macros replace_and_continue et 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
} 

Enfin, les deux fonctions internal_a et internal_b utilisent certaines fonctionnalités de l'API Windows. Pour conserver une pile d'appels déroulable, ces appels doivent être effectués via les macros replace_and_call (appel normal) ou replace_and_syscall (syscall indirect).

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
} 

Remarques

Comme il s'agit d'une fonctionnalité en cours de développement, certaines choses doivent être prises en compte :

  • Si vous supprimez les en-têtes de votre PE pendant le processus de chargement, vous devez passer à la macro start_stack_replace l'adresse de base du module. Actuellement, elle ne sera pas capable de la trouver par elle-même (à résoudre dans la prochaine mise à jour).
  • Au cas où vous vous poseriez la question, le remplacement de pile utilise la même combinaison de jmp rbx + cadre de dissimulation que la technique SilentMoonWalk. Cela ne se produit que lors de l'utilisation des macros replace_and_call et replace_and_syscall et il est prévu de le modifier dans la prochaine mise à jour.
  • Les macros replace_and_call et replace_and_syscall retournent toutes deux un *mut c_void qui peut être utilisé pour récupérer la valeur retournée par la fonction exécutée via elles. C'est le même comportement que celui décrit pour les macros call_function et indirect_syscall.
  • Les macros replace_and_call et replace_and_syscall acceptent jusqu'à 11 arguments.

Veuillez me signaler tout bug qui pourrait survenir lors de l'utilisation de cette fonctionnalité.

Télécharger l’outil