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
ThreadStackSpoofer — Thread Stack Spoofing - PoC pour une technique avancée d'évasion en mémoire permettant de mieux cacher l'allocation mémoire du shellcode injecté aux scanners et analystes. | Kitploit
Outils/GitHubGitHub/mgeeky/threadstackspoofer
Frameworks de Tests d'IntrusionFrameworks d'ExploitationCriminalistique MémoireShellcodeAnalyse de MalwareRed TeamingDéveloppement de Charges Utiles
GitHubmgeeky/threadstackspoofer

ThreadStackSpoofer

Thread Stack Spoofing - PoC pour une technique avancée d'évasion en mémoire permettant de mieux cacher l'allocation mémoire du shellcode injecté aux scanners et analystes.

Voir le dépôt
1.2k192il y a 4 ansVé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

Preuve de concept de Thread Stack Spoofing / Call Stack Spoofing

Une implémentation de preuve de concept pour une technique avancée d'évasion en mémoire qui usurpe la pile d'appels du thread (Thread Call Stack). Cette technique permet de contourner les règles d'examen de la mémoire basées sur les threads et de mieux cacher les shellcodes en mémoire intra-processus.

Introduction

Ceci est un exemple d'implémentation pour la technique Thread Stack Spoofing visant à contourner les analystes de malwares, les AV et les EDR qui recherchent des références aux frames du shellcode dans la pile d'appels d'un thread examiné. L'idée est de masquer les références au shellcode sur la pile d'appels du thread, dissimulant ainsi les allocations contenant le code du malware.

Cette implémentation, associée à mon projet ShellcodeFluctuation, apporte à la communauté de la sécurité offensive des implémentations d'exemples pour rattraper l'offre faite par les produits C2 commerciaux, afin que nous ne fassions pas moins bien dans nos outils Red Team. 💪

L'implémentation a changé

L'implémentation actuelle diffère considérablement de ce qui a été publié initialement. Cela est dû au fait que j'ai réalisé qu'il existe une approche bien plus simple pour terminer le traitement de la pile d'appels du thread et masquer les frames liées au shellcode en écrivant simplement 0 dans l'adresse de retour de la première frame que nous contrôlons :

root@kitploit:~
void WINAPI MySleep(DWORD _dwMilliseconds)
{
    [...]
    auto overwrite = (PULONG_PTR)_AddressOfReturnAddress();
    const auto origReturnAddress = *overwrite;
    *overwrite = 0;

    [...]
    *overwrite = origReturnAddress;
}

L'implémentation précédente, utilisant StackWalk64, est accessible dans ce commit c250724.

Cette implémentation est beaucoup plus stable et fonctionne parfaitement à la fois en Debug et Release sous les deux architectures - x64 et x86.

Demo

Voici à quoi peut ressembler une pile d'appels lorsqu'elle N'EST PAS usurpée :

not-spoofed

Ceci, en revanche, lorsque l'usurpation de la pile de thread est activée :

spoofed

Ci-dessus, nous pouvons voir que la dernière frame de notre pile d'appels est notre callback MySleep. On peut se demander si cela apporte immédiatement de nouveaux IOCs ? Les règles de chasse peuvent rechercher des threads dont la pile d'appels ne se déroule pas jusqu'aux points d'entrée de thread attendus suivants, situés dans les bibliothèques système :

root@kitploit:~
kernel32!BaseThreadInitThunk+0x14
ntdll!RtlUserThreadStart+0x21

Cependant, la pile d'appels du thread usurpé peut sembler plutôt étrange au premier abord ; un bref examen de mon système a montré qu'il existe d'autres threads qui ne se déroulent pas non plus jusqu'aux points d'entrée ci-dessus :

legit call stack

La capture d'écran ci-dessus montre un thread de Total Commander x64 non modifié. Comme nous pouvons le voir, sa pile d'appels ressemble beaucoup à la nôtre en termes de frames initiales de la pile.

Pourquoi devrions-nous nous soucier de falsifier soigneusement notre pile d'appels alors qu'il existe des processus présentant des caractéristiques que nous pouvons simplement imiter ?

Comment ça fonctionne ?

L'algorithme approximatif est le suivant :

  1. Lire le contenu du shellcode depuis un fichier.
  2. Acquérir tous les pointeurs de fonction nécessaires depuis dbghelp.dll, appeler SymInitialize
  3. Hooker kernel32!Sleep en pointant vers notre callback.
  4. Injecter et lancer le shellcode via VirtualAlloc + memcpy + CreateThread. Le thread doit démarrer à partir de notre fonction runShellcode pour éviter que l'adresse de démarrage du thread pointe vers un endroit inattendu et anormal (comme ntdll!RtlUserThreadStart+0x21)
  5. Dès que Beacon tente de dormir, notre callback MySleep est invoquée.
  6. Nous écrasons ensuite la dernière adresse de retour sur la pile avec 0, ce qui devrait effectivement terminer la pile d'appels.
  7. Enfin, un appel à ::SleepEx est effectué pour laisser Beacon dormir en attendant une communication ultérieure.
  8. Une fois le sommeil terminé, nous restaurons les adresses de retour originales des fonctions précédemment sauvegardées et l'exécution reprend.

Les adresses de retour des fonctions sont dispersées dans toute la zone mémoire de la pile du thread, pointées par le registre RBP/EBP. Pour les trouver sur la pile, nous devons d'abord collecter les pointeurs de frame, puis les déréférencer pour les écraser :

stack frame

(l'image ci-dessus est empruntée au billet d'Eli Bendersky intitulé Stack frame layout on x86-64)

root@kitploit:~
	*(PULONG_PTR)(frameAddr + sizeof(void*)) = Fake_Return_Address;

L'implémentation initiale de ThreadStackSpoofer faisait cela dans les fonctions walkCallStack et spoofCallStack, mais l'implémentation actuelle montre que ces efforts ne sont pas nécessaires pour maintenir une pile d'appels furtive.

Exemple d'exécution

Utilisation :

root@kitploit:~
C:\> ThreadStackSpoofer.exe <shellcode> <spoof>

Où :

  • <shellcode> est un chemin vers le fichier shellcode
  • <spoof> quand 1 ou true active l'usurpation de la pile de thread, toute autre valeur la désactive.

Exemple d'exécution qui usurpe la pile d'appels du thread de beacon :

root@kitploit:~
PS D:\dev2\ThreadStackSpoofer> .\x64\Release\ThreadStackSpoofer.exe .\tests\beacon64.bin 1
[.] Reading shellcode bytes...
[.] Hooking kernel32!Sleep...
[.] Injecting shellcode...
[+] Shellcode is now running.
[>] Original return address: 0x1926747bd51. Finishing call stack...

===> MySleep(5000)

[<] Restoring original return address...
[>] Original return address: 0x1926747bd51. Finishing call stack...

===> MySleep(5000)

[<] Restoring original return address...
[>] Original return address: 0x1926747bd51. Finishing call stack...

Comment l'utiliser ?

Regardez le code et son implémentation, comprenez le concept et réimplémentez-le dans vos propres chargeurs de shellcode que vous utilisez pour mener vos engagements Red Team. C'est une technique supplémentaire d'évasion avancée en mémoire qui augmente les chances de vos équipes de ne pas être détectées par les antivirus, les EDR et les analystes de malwares qui examinent vos implants.

Lors du développement de votre chargeur de shellcode avancé, vous pourriez également vouloir implémenter :

  • Chiffrement du tas du processus - inspirez-vous de cet article de blog : Hook Heaps and Live Free - qui peut vous permettre d'éviter les extracteurs de configuration Beacon comme BeaconEye
  • Modifier la protection des pages mémoire de votre Beacon en RW (depuis RX/RWX) et chiffrer leur contenu - en utilisant la technique Shellcode Fluctuation - juste avant de dormir (cela peut contourner des scanners comme Moneta ou pe-sieve)
  • Nettoyer tous les résidus du chargeur réflexif pour éviter les détections basées sur des signatures en mémoire
  • Déhooker tout ce que vous avez pu hooker (comme AMSI, ETW, WLDP) avant de dormir, puis re-hooker ensuite.

En réalité, ce n'est (pas encore) une véritable usurpation de pile

Comme on me l'a fait remarquer, la technique présentée ici n'est pas encore vraiment à la hauteur de son nom pour être un usurpateur de pile. Puisque nous nous contentons d'écraser les adresses de retour sur la pile du thread, nous n'usurpons pas les autres zones de la pile elle-même. De plus, nous laissons notre pile d'appels déroulable, ce qui la rend anormale car le système ne pourra pas parcourir correctement toute la chaîne des frames de la pile d'appels.

Cependant, je suis conscient de ces limitations ; pour l'instant, je l'ai laissée telle quelle car je me souciais principalement de contourner les scanners automatisés qui pourraient itérer sur les processus, énumérer leurs threads, parcourir les piles de ces threads et repérer toute adresse de retour pointant vers une mémoire non-image (comme SEC_PRIVATE - celle allouée dynamiquement par VirtualAlloc et compagnie). Un analyste de malwares concentré repérerait immédiatement l'anomalie et considérerait le thread comme plutôt inhabituel, traquant notre implant. J'en suis plus que certain. Pourtant, je ne crois pas que les scanners automatisés actuels tels que les AV/EDR aient des heuristiques implémentées qui parcourraient réellement la pile de chaque thread pour vérifier si elle est déroulable ¯\_(ツ)_/¯.

Sûrement ce projet (et l'implémentation commerciale trouvée dans les frameworks C2) donne aux fournisseurs d'AV et d'EDR des arguments pour envisager d'implémenter des heuristiques appropriées couvrant une telle technique d'évasion novatrice.

Pour améliorer cette technique, on peut viser un véritable Thread Stack Spoofer en insérant des frames de pile factices soigneusement conçues, établies dans un processus de déroulement inversé. Lisez-en plus sur cette idée ci-dessous.

Implémenter un véritable Thread Stack Spoofer

Une conversation de plusieurs heures avec namazso m'a appris que pour viser un véritable usurpateur de pile de thread, nous devrions inverser le processus de déroulement de la pile d'appels x64. Premièrement, il faut prendre en compte attentivement le processus de déroulement de la pile expliqué dans (a) lié ci-dessous. Le système, lorsqu'il parcourt la pile d'appels de thread sur l'architecture x64, ne se fie pas simplement aux adresses de retour dispersées sur la pile du thread, mais plutôt il :

  1. prend l'adresse de retour
  2. tente d'identifier la fonction contenant cette adresse (avec RtlLookupFunctionEntry)
  3. Cette fonction retourne les structures RUNTIME_FUNCTION, UNWIND_INFO et UNWIND_CODE. Ces structures décrivent où se trouvent l'adresse de début et de fin de la fonction, ainsi que toutes les séquences de code qui modifient RBP ou RSP.
  4. Le système a besoin de connaître toutes les modifications des pointeurs de pile et de frame survenues dans chaque fonction à travers la pile d'appels pour ensuite annuler virtuellement ces changements et restaurer virtuellement les pointeurs de la pile d'appels lorsqu'un appel à la frame traitée de la pile d'appels a eu lieu (ceci est implémenté dans RtlVirtualUnwind)
  5. Le système traite tous les UNWIND_CODE que la fonction examinée présente pour calculer précisément l'emplacement de l'adresse de retour et la valeur du pointeur de pile de cette frame.
  6. Grâce à cette émulation, le système est capable de descendre la chaîne des piles d'appels et de 'dérouler' efficacement la pile d'appels.

Pour interférer avec ce processus, nous aurions besoin de l'inverser en ayant notre forme inversée de RtlVirtualUnwind. Nous devrions itérer sur les fonctions définies dans un module (par exemple kernel32), scanner les codes UNWIND_CODE de chaque fonction et l'émuler soigneusement en arrière (par rapport à RtlVirtualUnwind et précisément RtlpUnwindPrologue) afin de trouver les emplacements sur la pile où mettre nos adresses de retour factices.

namazso mentionne la nécessité d'introduire 3 frames de pile factices pour bien recoudre la pile d'appels :

  • Une frame 'desync' (considérez-la comme une gadget-frame) qui se déroule différemment par rapport à l'appelant de notre MySleep (ayant un UWOP - code d'opération de déroulement - différent). Nous faisons cela en examinant toutes les fonctions d'un module, en regardant leurs UWOP, en calculant la taille de la frame factice. Cette frame doit avoir des UWOP différents de ceux de l'appelant de notre MySleep.
  • La frame suivante que nous voulons trouver est une fonction qui se déroule en dépilant dans RBP depuis la pile - essentiellement via le code UWOP_PUSH_NONVOL.
  • La troisième frame dont nous avons besoin est une fonction qui restaure RSP depuis RBP via le code UWOP_SET_FPREG

Le RSP restauré doit être défini avec le RSP pris depuis l'endroit où le flux de contrôle est entré dans notre MySleep, de sorte que toutes nos frames deviennent cachées, suite au déroulement du troisième gadget là-bas.

Pour commencer le processus, on peut itérer sur le .pdata de l'exécutable en déréférençant l'entrée du répertoire de données IMAGE_DIRECTORY_ENTRY_EXCEPTION. Considérez l'exemple ci-dessous :

root@kitploit:~
    ULONG_PTR imageBase = (ULONG_PTR)GetModuleHandleA("kernel32");
    PIMAGE_NT_HEADERS64 pNthdrs = PIMAGE_NT_HEADERS64(imageBase + PIMAGE_DOS_HEADER(imageBase)->e_lfanew);

    auto excdir = pNthdrs->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXCEPTION];
    if (excdir.Size == 0 || excdir.VirtualAddress == 0)
        return;

    auto begin = PRUNTIME_FUNCTION(excdir.VirtualAddress + imageBase);
    auto end = PRUNTIME_FUNCTION(excdir.VirtualAddress + imageBase + excdir.Size);

    UNWIND_HISTORY_TABLE mshist = { 0 };
    DWORD64 imageBase2 = 0;

    PRUNTIME_FUNCTION currFrame = RtlLookupFunctionEntry(
        (DWORD64)caller,
        &imageBase2,
        &mshist
    );

    UNWIND_INFO *mySleep = (UNWIND_INFO*)(currFrame->UnwindData + imageBase);
    UNWIND_CODE myFrameUwop = (UNWIND_CODE)(mySleep->UnwindCodes[0]);

    log("1. MySleep RIP UWOP: ", myFrameUwop.UnwindOpcode);

    for (PRUNTIME_FUNCTION it = begin; it < end; ++it)
    {
        UNWIND_INFO* unwindData = (UNWIND_INFO*)(it->UnwindData + imageBase);
        UNWIND_CODE frameUwop = (UNWIND_CODE)(unwindData->UnwindCodes[0]);

        if (frameUwop.UnwindOpcode != myFrameUwop.UnwindOpcode)
        {
            // Found candidate function for a desynch gadget frame

        }
    }

Le processus est un peu complexe, mais se résume à inverser le processus de déroulement de la pile d'appels du thread en substituant des frames de pile arbitraires par d'autres soigneusement sélectionnées, dans une approche similaire au ROP.

Cette preuve de concept ne suit pas la réplication de cet algorithme, car ma compréhension actuelle me permet d'accepter que la pile d'appels se termine sur une frame de pile basée sur un EXE et je ne veux pas surcharger ni mes chargeurs de shellcode ni cette preuve de concept. Je laisse l'exercice d'implémenter cela et de le partager publiquement à un lecteur intéressé. Ou peut-être que je vais m'asseoir et essayer de le faire moi-même si j'ai plus de temps libre :)

Plus d'informations :

  • a) x64 exception handling - Processus de déroulement de la pile expliqué
  • b) Exemple d'implémentation de RtlpUnwindPrologue et RtlVirtualUnwind
  • c) Section .pdata
  • d) un autre exemple d'implémentation de RtlpUnwindPrologue

Mot d'avertissement

Si vous prévoyez d'ajouter cette fonctionnalité à vos propres chargeurs de shellcode / outils, assurez-vous d'ÉVITER de déhooker kernel32.dll. Une tentative de déhooker kernel32 restaurera la fonctionnalité originale de Sleep, empêchant notre callback d'être appelée. Si notre callback n'est pas appelée, le thread ne pourra pas usurper sa propre pile d'appels tout seul.

Si c'est ce que vous voulez, alors vous pourriez avoir besoin d'exécuter un autre thread, un thread de surveillance, garantissant que le thread de Beacon sera usurpé chaque fois qu'il dort.

Si vous utilisez Cobalt Strike et un BOF unhook-bof de Raphael Mudge, assurez-vous de consulter ma Pull Request qui ajoute un paramètre optionnel au BOF spécifiant les bibliothèques qui ne doivent pas être déhookées.

De cette façon, vous pouvez maintenir vos hooks dans kernel32 :

root@kitploit:~
beacon> unhook kernel32
[*] Running unhook.
    Will skip these modules: wmp.dll, kernel32.dll
[+] host called home, sent: 9475 bytes
[+] received output:
ntdll.dll            <.text>
Unhook is done.

unhook-bof modifié avec option pour ignorer des modules spécifiés


Remarque finale

Cette preuve de concept a été conçue pour fonctionner avec les shellcodes de Beacon de Cobalt Strike. Le Beacon est connu pour appeler kernel32!Sleep pour attendre d'autres instructions de son C2. Ce chargeur tire parti de ce fait en hookant Sleep afin d'effectuer ses tâches de maintenance.

Cette implémentation pourrait ne pas fonctionner avec d'autres shellcodes sur le marché (comme Meterpreter) s'ils n'utilisent pas Sleep pour se mettre en veille. Comme il s'agit simplement d'une preuve de concept montrant la technique, je n'ai pas l'intention d'ajouter le support pour tout autre framework C2.

Lorsque vous comprendrez le concept, vous serez sûrement capable de le traduire dans vos besoins spécifiques de shellcode et d'adapter la solution à votre avantage.

Veuillez ne pas ouvrir de problèmes Github liés à "ce code ne fonctionne pas avec le shellcode XYZ", ils seront fermés immédiatement.


☕ Montrez votre soutien ☕

Ce projet et d'autres sont le résultat de nuits blanches et de beaucoup de travail acharné. Si vous aimez ce que je fais et appréciez que je donne toujours en retour à la communauté, Envisagez de m'offrir un café (ou mieux une bière) juste pour me remercier ! 💪


Auteur

root@kitploit:~
   Mariusz Banach / mgeeky, 21
   <mb [at] binary-offensive.com>
   (https://github.com/mgeeky)
Télécharger l’outil