Une série de programmes de plus en plus complexes démontrant l'accrochage de fonctions sur Windows 64 bits.
J'ai écrit ces programmes tout en m'apprenant comment fonctionne le hooking de fonctions. Ils peuvent être utiles à d'autres personnes qui essaient de faire la même chose (ou à mon avenir après avoir oublié comment tout cela fonctionne à nouveau). Ils sont destinés à être examinés dans l’ordre. La première fois qu'une fonction est utilisée dans un programme, elle sera incluse dans le fichier .cpp de cet exemple de programme spécifique, avec son nom préfixé par un trait de soulignement. Tous les exemples ultérieurs qui utilisent la même fonction utiliseront la copie de cette fonction incluse dans hooking_common.h, pour minimiser la duplication de code et garder les exemples de programmes ultérieurs suffisamment petits pour être toujours facilement lisibles.
J'ai fait un peu de travail pour les nettoyer, mais les exemples suivants sont encore un peu brouillons. Il est important de noter que le résultat final de ce dépôt n'est pas suffisant pour écrire une bibliothèque de hooking complète, mais il est suffisant pour se lancer dans cette voie.
Au moment de l'exécution, certains projets peuvent s'appuyer sur d'autres projets en cours de construction (l'injection d'une DLL nécessite que la DLL ait été construite) ou en cours d'exécution en même temps (l'accrochage d'un programme cible nécessite qu'il soit déjà en cours d'exécution).
Tous les exemples ont été créés à l'aide de Visual Studio 2019 (v142) avec le SDK Windows 10.0.17763.0. Je ne pense pas qu'il y ait quoi que ce soit ici qui dépende de la version du VS ou du SDK, mais je l'énumère ici juste au cas où. Il y a presque certainement certaines choses qui sont spécifiques à MSVC.
Enfin, le dernier exemple de trampoline installe un crochet dans mspaint. Je suppose qu'à un moment donné dans le futur, une mise à jour de mspaint entraînera la rupture de cet exemple. Au moment de la rédaction de cet article, la version actuelle de mspaint était 1909 (OS Build 18363.1016).
Les exemples sont divisés en deux catégories : ceux qui utilisent des trampolines et ceux qui n'en utilisent pas. Les exemples non-trampoline existent uniquement pour démontrer la redirection du flux de programme d'une fonction à une autre dans différentes situations. Construire des trampolines est compliqué, et lorsque j'essayais de comprendre comment fonctionnait l'accrochage de fonctions, il a été extrêmement utile de commencer par construire d'abord les exemples non-trampolines. De plus, il existe 4 "programmes cibles" qui sont utilisés par des exemples qui souhaitent montrer comment installer des hooks dans différents processus (déjà en cours d'exécution).
La plupart de ces exemples fuient la mémoire liée aux hooks. Je m'en fiche, à la fois parce que ces exemples visent simplement à démontrer un concept de hooking et parce que ces allocations "fuites" doivent de toute façon exister jusqu'à la fin du programme.
Bien qu'il ne semble pas y avoir beaucoup de terminologie standard pour les techniques d'accrochage de fonctions, le code (et les fichiers Lisez-moi) de ce référentiel utilisent les termes suivants :
Étant donné que ces exemples ne créent pas de trampolines lors de l'installation de leurs crochets, je considère ces fonctions comme démontrant un accrochage "destructeur", dans le sens où la fonction d'origine est complètement inutilisable après avoir été accrochée.
Un petit exemple d'écrasement des octets de départ d'une fonction avec une instruction de saut qui redirige le flux du programme vers une fonction différente au sein du même programme. Puisqu’aucun trampoline n’est en construction, cette opération est destructrice et la fonction d’origine n’est plus appelable. C'est le seul exemple 32 bits du référentiel.
La version 64 bits de l'exemple précédent. Dans les applications 64 bits, les fonctions peuvent être situées suffisamment loin dans la mémoire pour ne pas être accessibles via une instruction de saut relatif 32 bits. Puisqu'il n'y a pas d'instruction de saut relatif de 64 bits, ce programme crée d'abord une fonction "relais", qui contient des octets pour une instruction jmp absolue pouvant atteindre n'importe où dans la mémoire (et passe à la fonction de charge utile). Le saut de 32 bits installé dans la fonction cible passe à cette fonction de relais, au lieu d'accéder immédiatement à la charge utile.
Fournit un exemple d’utilisation des techniques du projet précédent pour accrocher une fonction membre, plutôt qu’une fonction libre.
Légèrement différent des exemples précédents, ce programme montre comment installer un hook dans une fonction membre virtuelle en obtenant l'adresse de cette fonction via la table virtuelle d'un objet. Aucun autre exemple ne traite des fonctions virtuelles, mais j'ai pensé que c'était suffisamment intéressant pour être inclus ici.
L'exemple le plus simple d'installation d'un hook dans un autre processus en cours d'exécution. Cet exemple utilise la bibliothèque DbgHelp pour localiser une fonction dans un processus cible (A - Target With Free Function) par nom de chaîne. Cela n'est possible que parce que le programme cible est construit avec les symboles de débogage activés. Bien que simple, cet exemple est un peu plus long que les programmes précédents en raison du grand nombre de nouvelles fonctions qu'il introduit (pour localiser et manipuler un processus distant).
Cet exemple montre comment accrocher une fonction qu'un autre processus a importée à partir d'une DLL. Il existe quelques nuances dans la façon d'obtenir l'adresse d'une fonction dll dans un processus distant en raison du fonctionnement d'ASLR, qui est démontré ici. Sinon, cet exemple est quasiment identique au précédent.
Cet exemple montre comment installer un hook dans une fonction qui n'est pas importée par une DLL et qui n'est pas dans la table des symboles (probablement parce que le processus distant n'a pas de symboles de débogage). Cela signifie qu'il n'y a pas de moyen (simple) de trouver la fonction cible par nom de chaîne. Au lieu de cela, cet exemple suppose que vous avez utilisé un désassembleur tel que x64dbg pour obtenir l'adresse virtuelle relative (RVA) de la fonction que vous souhaitez accrocher. Ce programme utilise ce RVA pour installer un hook.
Semblable à celui ci-dessus, sauf que cet exemple utilise l'injection de DLL pour installer la fonction de charge utile plutôt que d'écrire des octets de code machine brut. C'est beaucoup plus facile à utiliser, puisque vos charges utiles peuvent à nouveau être écrites en C++. La charge utile de cet exemple est contenue dans le projet 08B-DLL-Payload.
Les exemples suivants installent des trampolines lors du hooking, ce qui signifie que le programme peut toujours exécuter la logique dans la fonction cible après l'installation d'un hook. Puisque l'installation d'un hook écrase au moins les 5 premiers octets de la fonction cible, les instructions contenues dans ces 5 octets sont déplacées vers la fonction trampoline. Ainsi, l’appel de la fonction trampoline exécute efficacement la logique originale de la fonction cible.
L'équivalent d'installation de trampoline de l'exemple n°2. Cet exemple est un peu bizarre car je voulais démontrer la création d'un trampoline sans avoir besoin d'utiliser un moteur de démontage. Dans ce cas, la fonction cible a été créée pour avoir une instruction connue de 5 octets au début, nous pouvons donc simplement copier les cinq premiers octets de cette fonction dans la fonction trampoline. Cela signifie que la création du trampoline est vraiment simple, puisque nous connaissons sa taille exacte et qu'il n'utilise aucun adressage relatif qui doit être corrigé. Si vous écriviez un trampoline pour un cas d'utilisation très spécifique, vous pourriez probablement vous contenter de faire une variation à ce sujet.
Cet exemple montre un scénario similaire au précédent, sauf que cette fois j'utilise un désassembleur (capstone) pour obtenir les octets dont nous avons besoin pour voler la fonction cible. Cela permet au code de hooking d'être utilisé sur n'importe quelle fonction, pas seulement sur celles dont nous savons qu'elles seront des cas faciles. Il se passe en fait beaucoup de choses dans cet exemple, car il s'agit de passer d'un hook ciblé (comme le précédent) à la construction d'une fonction de hooking générique. Le trampoline doit convertir les appels/sauts relatifs en instructions qui utilisent des adresses absolues, ce qui complique encore les choses. Ce n'est pas non plus un exemple 100 % raffiné de hooking générique, il échouera avec les instructions de boucle, et si vous essayez de hooker des fonctions avec moins de 5 octets d'instructions.
Fondamentalement identique à celui ci-dessus, sauf que cet exemple inclut du code pour suspendre tous les threads en cours d'exécution pendant qu'il installe un hook. Il n'est pas garanti que cela soit thread-safe dans tous les cas, mais c'est certainement beaucoup plus sûr que de ne rien faire.
Cela développe le code de hooking/trampoline utilisé dans les deux exemples précédents pour prendre en charge la redirection de plusieurs fonctions vers la même charge utile et pour permettre aux fonctions de charge utile d'appeler d'autres fonctions avec des hooks installés.
Il s'agit du premier exemple de trampoline qui installe un crochet dans un processus différent (dans ce cas, l'application cible B - Target with Free Functions From DLL). Toute la logique d’accrochage est contenue dans une charge utile DLL 13B - Trampoline Imported Func DLL Payload. Il n'y a pas grand chose de nouveau ici, cet exemple combine simplement les éléments de hooking sur trampoline déjà réalisés avec les techniques présentées précédemment pour hooker une fonction importée depuis une DLL.
Le joyau de la couronne du repo. Cet exemple injecte une charge utile DLL (14B - Trampoline Hook MSPaint Payload) dans une instance en cours d'exécution de mspaint (vous devez lancer mspaint vous-même avant de l'exécuter). Le crochet installé fait que les pinceaux sont dessinés en rouge, quelle que soit la couleur que vous avez réellement sélectionnée dans MSPaint. Honnêtement, il n'y a rien ici qui n'ait pas été montré dans l'exemple précédent, c'est juste cool de voir cela fonctionner sur un programme non artificiel.
Application cible simple qui appelle une fonction gratuite en boucle. Compilé avec des informations de débogage incluses.
Application cible qui appelle une fonction gratuite importée depuis une DLL (B2 - GetNum-DLL) en boucle.
Application cible qui appelle une fonction membre non virtuelle dans une boucle.
Application cible qui appelle une fonction membre virtuelle dans une boucle.