
Une implémentation de preuve de concept pour une technique d'évasion visant à terminer le thread actuel et à le restaurer avant de reprendre l'exécution, tout en appliquant des modifications de protection de page pendant l'absence d'exécution.
██████╗ ███████╗ █████╗ ████████╗██╗ ██╗███████╗██╗ ███████╗███████╗██████╗
██╔══██╗██╔════╝██╔══██╗╚══██╔══╝██║ ██║██╔════╝██║ ██╔════╝██╔════╝██╔══██╗
██║ ██║█████╗ ███████║ ██║ ███████║███████╗██║ █████╗ █████╗ ██████╔╝
██║ ██║██╔══╝ ██╔══██║ ██║ ██╔══██║╚════██║██║ ██╔══╝ ██╔══╝ ██╔═══╝
██████╔╝███████╗██║ ██║ ██║ ██║ ██║███████║███████╗███████╗███████╗██║
╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚══════╝╚══════╝╚══════╝╚══════╝╚═╝
Une implémentation POC d'une technique d'évasion pour terminer le thread actuel et le restaurer avant de reprendre l'exécution, tout en modifiant les protections de pages pendant l'absence d'exécution.

Les méthodes de sommeil et d'obfuscation sont bien connues dans la communauté maldev, avec différentes implémentations, elles ont pour objectif de se cacher des scanners mémoire pendant le sommeil, en changeant généralement les protections de pages et même en ajoutant des fonctionnalités intéressantes comme le chiffrement du shellcode, mais il y a un autre point important pour cacher notre shellcode : cacher le thread d'exécution actuel. Spoofer la stack est cool, mais après y avoir réfléchi un peu, je me suis dit qu'il n'est pas nécessaire de spoof la stack… s'il n'y a pas de stack :)
L'utilité de cette technique est laissée à l'appréciation du lecteur, mais dans tous les cas, je pense que c'est une façon intéressante de revoir certains sujets et d'apprendre un peu de maldev pour ceux qui, comme moi, débutent dans ce monde.
L'implémentation principale présentée ici contient tout ce que nous devons sortir de la stack dans la section data, sous forme de variables globales, mais une implémentation déplaçant tout vers le heap sera publiée prochainement. Elle vise à montrer quelques modifications clés nécessaires pour rendre ce code pic et injectable.
Ce dépôt est dupliqué entre GitHub et GitLab.
Tout ce qui est indiqué ici provient de ma compréhension des différents sujets abordés, que ce soit par la lecture ou l'expérience lors du développement. Je suis conscient que je ne suis pas un expert et la dernière chose que je souhaite est de propager des informations erronées, donc si vous pensez que quelque chose n'est pas correct, je serais ravi que vous m'en fassiez part, vous pouvez me contacter sur twitter, ou ouvrir des issues dans ce repo. Merci beaucoup pour votre compréhension. :)
L'objectif principal de cette technique est clair : terminer le thread actuel et le restaurer avant de reprendre l'exécution, mais qu'est-ce que cela signifie exactement, et quelles nouvelles contraintes cela impose-t-il ?
Pour pouvoir restaurer l'exécution, nous devons sauvegarder deux choses avant de terminer le thread : d'abord l'état du CPU, et ensuite la stack, et les remettre en place efficacement après le lancement du nouveau thread.
J'ai parlé de nouvelles contraintes qui apparaîtront dans cette technique, et il y en a deux principales : premièrement, nous devons stocker en dehors de la stack tout ce qui est nécessaire depuis le moment où le thread se termine jusqu'à ce que la stack soit restaurée, et comme vous le verrez, cela crée de nouveaux défis.
Deuxièmement, nous avons toujours besoin d'au moins un autre thread en cours d'exécution dans notre processus, car nous terminons notre thread ; s'il n'y a pas d'autres threads, le processus se terminera. Je ne pense pas que ce soit un gros problème, car la plupart des agents sont injectés dans d'autres processus, nous pouvons supposer que ce processus conservera au moins un thread en cours d'exécution.
Nous pouvons voir dans cette POC 4 fonctions principales :
Lorsque nous sommes sur le point de sauvegarder la stack, une question se pose : quelle quantité de la stack doit être sauvegardée ?
Revoyons d'abord ce qui se trouve dans la stack après avoir appelé la fonction DeathSleep (c'est la fonction qui sauvegarde le contexte, la stack et prépare tout pour l'obfuscation et la restauration)

Comme nous pouvons le voir, chaque fonction a trois parties :
La partie minimale de la stack que nous devons évidemment sauvegarder est tout ce qui se trouve à l'intérieur de notre programme principal, c'est-à-dire son shadow space, son adresse de retour et tout jusqu'à la fonction DeathSleep. Tout ce qui précède n'est pas vraiment nécessaire (sauvegarder la stack utilisée par la fonction d'entrée a ses avantages, mais nous en discuterons plus tard), car c'est la stack utilisée par les routines Windows pour lancer notre nouveau thread. En plus de cela, j'ai décidé de stocker également le shadow space de la fonction DeathSleep (pas vraiment nécessaire, mais cela facilite le calcul du Rsp au moment du réveil).
Donc au final, nous sauvegardons ceci :

Chaque fonction dans une compilation standard devrait être composée de 3 parties : le prologue, le code de la fonction, et l'épilogue.
Le Rsp (pointeur de stack) ne devrait être modifié que dans le prologue et l'épilogue de la fonction. Le prologue augmente le pointeur de stack (rappelez-vous qu'augmenter la stack signifie réduire les adresses, car elles vont dans des directions opposées), pour sauvegarder les registres, contenir toutes ses variables locales, puis contenir le shadow space, et l'épilogue fait exactement le contraire.