Plus sur les Loaders

Nouveaux concepts :

Utilisation du contexte du thread
Autorisations d'accès à la mémoire
Loaders de débogueur

Nouvelles fonctions API :

SusupendThread
GetThreadContext
SetThreadContext
VirtualProtectEx
IsBadStringPtr
IsBadCodePtr
IsBadReadPtr
IsBadWritePtr
VirtualQueryEx
WaitForInputIdle
EnumDesktopWindows
GetClassName
GetWindowText
OpenProcess
DebugActiveProcess
WaitForDebugEvent
DebugActiveProcessStop

 

La structure CONTEXT du thread

Comme nous l'avons mentionné dans le dernier tutoriel, chaque thread maintient une structure CONTEXT qui comprend les registres, la pile du noyau, un bloc d'environnement de thread et une pile utilisateur. Nous pouvons lire et modifier les valeurs des registres à partir d'un thread en cours d'exécution dans un processus en utilisant les API GetThreadContext et SetThreadContext pour lire ou écrire dans la structure CONTEXT.

Cette structure de données est complexe et seules certaines parties sont utiles, mais nous pouvons spécifier ce que nous voulons obtenir/définir à travers le champ ContextFlags. La valeur de ContextFlags spécifie quelles parties du contexte d'un thread sont récupérées. La documentation sur cela se trouve dans WINNT.H et non pas dans MSDN :

context.ContextFlags = CONTEXT_FULL | CONTEXT_FLOATING_POINT | CONTEXT_DEBUG_REGISTERS | CONTEXT_EXTENDED_REGISTERS.

Essayer d'obtenir/définir le contexte d'un thread en cours d'exécution donne des résultats imprévisibles. Utilisez d'abord la fonction SuspendThread pour suspendre le thread.

Un problème important lors de l'écriture de loaders est la gestion des autorisations de lecture/écriture de la mémoire. Généralement, lors de l'accès aux sections exécutables d'un processus, ces sections sont déjà lisibles et inscriptibles. Cependant, si vous essayez d'écrire dans d'autres sections qui ne devraient normalement être que des sections en lecture, telles que les ressources du programme, vous échouerez.

Écrire un loader pour accéder à des pages mémoire protégées implique donc de modifier les autorisations d'accès de l'adresse modifiée en PAGE_EXECUTE_READWRITE, de lire et d'écrire ce dont nous avons besoin, puis de restaurer la protection précédente. La fonction VirtualProtectEx nous permet de faire cela. Les paramètres suivants sont intéressants :



hProcess - Gestionnaire du processus cible - doit avoir un accès PROCESS_VM_OPERATION, mais généralement l'API CreateProcess le fait pour nous.

lpAddress - Pointeur vers l'adresse de base de la région de pages dont les autorisations d'accès doivent être modifiées. Toutes les pages de la région spécifiée doivent se trouver dans la même région réservée allouée lors de l'appel de la fonction VirtualAlloc ou VirtualAllocEx avec MEM_RESERVE. Les pages ne peuvent pas chevaucher des régions réservées adjacentes qui ont été allouées par des appels distincts à VirtualAlloc ou VirtualAllocEx en utilisant MEM_RESERVE.

dwSize - la taille en octets de ce que nous voulons modifier. Comprend toutes les pages contenant un ou plusieurs octets dans la plage allant du paramètre lpAddress à (lpAddress+dwSize). Cela signifie qu'une plage de 2 octets chevauchant une limite de page entraîne la modification des attributs de protection des deux pages.

flNewProtect - Protection de la mémoire - nous avons besoin de PAGE_EXECUTE_READWRITE.

lpflOldProtect - Pointeur vers une variable qui reçoit la protection d'accès précédente de la première page de la région spécifiée de pages. Si ce paramètre est NULL ou ne pointe pas vers une variable valide, la fonction échoue. N'oubliez pas que l'API échoue si vous définissez ce paramètre sur NULL, nous aurons donc besoin d'une variable pour stocker cette valeur.

L'ensemble d'API utilisées pour tester les droits de mémoire (tester si une adresse mémoire spécifiée peut être lue ou écrite) comprend IsBadStringPtr, IsBadCodePtr, IsBadReadPtr et IsBadWritePtr. Toutes ces fonctions ont une utilisation similaire ; juste avant d'utiliser ReadProcessMemory ou WriteProcessMemory, utilisez la fonction appropriée pour tester, puis utilisez VirtualProtectEx pour définir si nécessaire.

Points d'arrêt et décision de l'emplacement de la modification

Dans les cas simples (comme dans le dernier tutoriel), nous pouvons peut-être modifier une cible en mémoire dès que son processus est créé, mais avant l'exécution du thread principal. Dans d'autres cas, nous devons laisser la cible s'exécuter et l'arrêter à un moment précis (par exemple, après que l'application compressée ait été décompressée en mémoire ou après une vérification CRC). Notre loader doit donc être capable d'arrêter l'application à un point spécifié, de modifier le code, puis de laisser l'application se dérouler normalement. Les problèmes liés au timing incluent :

1. attendre que l'emplacement mémoire que vous souhaitez modifier soit décompressé par le décompresseur du protecteur
2. modifier l'emplacement mémoire uniquement après la vérification d'intégrité
3. modifier l'emplacement mémoire avant que les instructions ne soient exécutées ou utilisées par la cible

Il existe plusieurs façons (certaines plus robustes que d'autres) d'arrêter la cible, notamment :

1. utiliser l'API sleep pour attendre pendant une durée définie que l'application compressée soit décompressée en mémoire (les durées pour différents décompresseurs varient et doivent être trouvées par essais et erreurs).

2. utiliser l'API WaitForInputIdle pour déterminer quand la cible a été exécutée, a terminé son initialisation et est inactive (attend que le processus spécifié attende une entrée utilisateur sans entrée en attente, ou jusqu'à ce que l'intervalle de temps d'attente soit écoulé).

3. trouver quand la fenêtre principale de l'application a été affichée. Dans ce cas, nous supposons que si la fenêtre principale de l'application est déjà sur le bureau (visible ou non), alors l'application est en cours d'exécution et prête à être modifiée. Cela se fait à l'aide de l'API EnumDesktopWindows. Cette fonction énumère toutes les fenêtres associées au bureau spécifié. Elle transmet le gestionnaire de chaque fenêtre ouverte à une fonction de rappel définie par l'utilisateur. En d'autres termes, elle appelle notre fonction de rappel de manière répétée jusqu'à ce que la dernière fenêtre de niveau supérieur soit énumérée ou que la fonction de rappel renvoie FALSE. Notre fonction de rappel se contenterait d'appeler GetClassName et GetWindowText pour chaque fenêtre afin de vérifier si la classe et le titre de la fenêtre correspondent à ceux de notre cible. La classe de la fenêtre principale de la cible peut être facilement trouvée en utilisant l'un des nombreux outils d'observation de fenêtres, tels que Winspector.

4. utiliser la méthode yAtEs pour injecter un point d'arrêt à l'emplacement souhaité dans le code (qui doit d'abord être trouvé en déboguant l'application). Cela implique de lire et de stocker 2 octets à notre point d'arrêt souhaité, puis d'injecter les 2 octets hexadécimaux EB FE (jmp -2) au même endroit pour interrompre l'exécution en créant une boucle infinie. Ensuite, nous devons continuellement interroger le contexte du thread (en utilisant GetThreadContext) pour la valeur dans le registre EIP. EIP est le pointeur d'instruction et contient l'adresse de l'instruction suivante à exécuter. Lorsque la valeur ici == notre adresse d'arrêt, nous pouvons suspendre le thread et effectuer notre modification. Une fois que nous avons terminé la modification, nous restaurons les 2 octets écrasés par EBFE et reprenons l'exécution. Cela présente l'avantage d'agir comme un "point d'arrêt", mais sans utiliser aucune API de débogage et donc sans avoir à se soucier d'un éventuel mécanisme anti-débogage dans l'application cible. Évidemment, il y a certaines hypothèses avec un loader aussi simple :

• la victime est un processus à thread unique

• une fois que l'application est décompressée en mémoire, il n'y a pas de vérifications d'intégrité

• la mémoire du processus cible peut être modifiée

• le contexte de sécurité de la victime nous permet d'opérer sur celui-ci

• la victime ne dispose pas de protections complexes contre les modifications

Loaders de débogueurs

Les loaders standard décrits ci-dessus ont l'avantage considérable d'être simples et de ne pas être soumis à des astuces anti-débogueurs, mais il arrive parfois que le loader doive déboguer l'application cible afin de trouver un point d'arrêt. Un exemple typique en est la création d'un loader pour Asprotect. Nous sommes probablement tous familiers avec l'utilisation de la méthode LAST EXCEPTION pour arriver à l'OEP (Entry Point d'Origine), mais que faire si nous voulons que notre loader fasse la même chose ?

Dans ce cas, nous utilisons l'API de débogage pour créer un loader de débogueur basé sur les événements, qui peut répondre aux événements de débogage. Les événements de débogage incluent les exceptions générées par le packer de la même manière que nous les voyons dans Olly, donc notre loader de débogueur utilise en réalité le SEH (gestion des exceptions structurée).

Je vais décrire brièvement ce type de loader, mais une discussion détaillée peut être trouvée dans la série d'articles « Cracking with Loaders » de Shub & TunderPwr.

Tout comme avec un loader standard, nous essayons de trouver un moment approprié pour patcher notre cible. Dans ce cas, cependant, notre point d'arrêt sera une exception générée par le packer de la cible. Nous devons d'abord déboguer la cible (avec Olly configuré pour NE PAS gérer les exceptions). Lorsque nous arrivons à l'endroit correct (par exemple, la dernière exception dans Asprotect), nous devons examiner l'exception pour ses caractéristiques uniques (par exemple, l'instruction PUSH 0C). Notre loader doit donc vérifier chaque exception générée par la cible jusqu'à ce qu'il trouve le schéma unique recherché.

Un loader de débogueur peut soit créer un nouveau processus pour le débogage, soit se « rattacher » à un processus existant avant d'attendre les événements de débogage. Dans le premier cas, l'API CreateProcess est utilisée avec DEBUG_ONLY_THIS_PROCESS ou DEBUG_PROCESS (qui débogue également tous les processus enfants) dans dwCreationFlags.

Dans le deuxième cas, OpenProcess est utilisé pour obtenir l'ID de processus d'un processus existant, et DebugActiveProcess est utilisé avec cet ID de processus pour attacher le débogueur au processus. Généralement, les débogueurs ouvrent un processus avec les indicateurs PROCESS_VM_READ et PROCESS_VM_WRITE afin de lire et d'écrire dans la mémoire virtuelle du processus en utilisant ReadProcessMemory et WriteProcessMemory.

Le loader doit ensuite utiliser une boucle appropriée avec WaitForDebugEvent pour attendre les événements de débogage de la cible. Chaque exception générée par la cible et transmise à notre loader de débogueur par le système d'exploitation doit être examinée pour voir si elle correspond aux caractéristiques uniques que nous recherchons, avant de patcher en mémoire en utilisant WriteProcessMemory.

Enfin, le loader peut soit transférer le contrôle à la cible (en restant attaché), se détacher de la cible et la laisser s'exécuter librement, soit fermer la cible. Le détachement semble être la méthode la plus propre et est réalisé avec DebugActiveProcessStop, mais cela n'est pas disponible dans les systèmes d'exploitation antérieurs à Windows XP.

L'approche de test de la mémoire basée sur les fonctions isBad*Ptr dont nous avons discuté dans le dernier tutoriel fonctionne bien lorsque le loader lance le processus cible via l'API CreateProcess. Cependant, si vous avez utilisé OpenProcess avec un processus déjà en cours d'exécution, vous devriez plutôt utiliser VirtualQueryEx qui fournit des informations sur une plage de pages dans l'espace d'adressage virtuel d'un processus spécifié.

Bien que l'ajout de capacités de débogage apporte de la flexibilité, cela ajoute également de la complexité et la nécessité d'éviter les différents mécanismes anti-débogage utilisés par les protecteurs.

 

Comme exemple pratique, nous allons compresser SuperCleaner et essayer notre loader à partir du dernier tutoriel. Pour simplifier les choses, nous utiliserons upx v1.25. La ligne de commande habituelle "upx SuperCleaner.exe" a généré une erreur, mais en tapant "upx SuperCleaner.exe --force", cela a fonctionné :



1



Remarquez que notre cible a réduit sa taille de 508 Ko à 209 Ko. Maintenant, il est évident que nos anciens patcheurs ne fonctionneront pas sur la cible compressée et notre premier loader non plus. Essayez le loader et voyez ce qui se passe :

1



Notre loader crée le processus en mode suspendu et essaie immédiatement de patcher la mémoire, mais l'adresse correcte n'existe pas encore car le stub upx n'a pas décompressé la cible en mémoire. Lorsque le thread est repris, la cible est décompressée et exécutée, mais notre patch n'a pas été appliqué, donc nous obtenons l'invite ennuyeuse.

Pour résoudre cela, nous allons coder un loader en utilisant l'une des techniques ci-dessus. Attendre une entrée en mode veille ou la fenêtre principale n'évitera évidemment pas l'invite ennuyeuse, et essayer de deviner un temps de pause sera trop imprécis, donc nous utiliserons la méthode yAtEs.

Tout d'abord, ouvrez la cible compressée dans Olly et trouvez le saut vers le point d'entrée original (OEP). Dans les applications compressées avec upx, c'est un point idéal pour extraire ou patcher l'application car nous n'avons pas à nous soucier des vérifications CRC ou anti-altération, et trouver le point d'entrée original est très facile. Il suffit de faire défiler jusqu'à la fin du code pour trouver l'instruction POPAD suivie d'un JMP vers le OEP :



1



Dans mon cas, le OEP est 0042AADA comme vous pouvez le voir. Nous savons déjà que l'adresse virtuelle à patcher est 0042374F. Une bonne façon de contourner cela serait d'utiliser la méthode de yAtEs. Nous créons le processus en mode suspendu et remplaçons les 2 premiers octets du saut vers le OEP (E9h E9h) par EBh FEh. Notez que s'arrêter au OEP lui-même n'est pas une option car il n'a pas encore été décompressé en mémoire. Nous devons donc effectuer notre patch en mémoire, restaurer le saut final vers le OEP et reprendre le thread. Nous améliorerons également notre loader en vérifiant les octets corrects à la fois au niveau du OEP et de l'emplacement du patch pour vérifier la version correcte du fichier cible.

Créez un nouveau projet dans WinASM et collez le code loader2.asm, puis ajoutez loader.rc dans la section source :

1

Ce loader lance un processus pour notre cible et lit 2 octets à partir de l'adresse que nous pensons être le saut vers le OEP. Si les octets sont corrects (version correcte du fichier cible), il les remplace par le code d'arrêt EBFE avant de reprendre le thread principal et de faire une pause de 10 millisecondes pour permettre à la cible de s'exécuter . La cible s'exécutera jusqu'à ce qu'elle atteigne finalement l'instruction EBFE, ce qui la fera rester bloquée dans une boucle infinie et EIP ne changera pas. Le stub upx a maintenant décompressé l'application en mémoire et est sur le point de sauter vers le OEP.

Ensuite, nous préchargeons le champ des indicateurs de notre structure CONTEXT vide avec CONTEXT_FULL pour permettre à GetThreadContext de lire toutes les valeurs des registres et de comparer dans eax la valeur actuelle d'EIP (ThreadContext.regEip) avec l'adresse de notre point d'arrêt (c'est-à-dire si nous sommes bloqués au point d'arrêt). Si nous ne sommes pas encore au point d'arrêt, nous faisons simplement une pause de 10 millisecondes et réessayons GetThreadContext.

Si nous sommes au point d'arrêt, nous suspendons le thread et lisons les octets à notre adresse de patch cible (qui a maintenant été décompressée en mémoire) pour vérifier à nouveau la version correcte du fichier. Si nous avons la version correcte, nous restaurons le saut original vers le OEP, écrivons notre octet de patch et reprenons le thread avant de quitter, laissant notre cible patchée s'exécuter heureusement. Si nous avons une version incorrecte de la cible, nous le signalons, terminons le thread suspendu avant de quitter.

Notez que la définition de la structure CONTEXT doit être placée dans la section de données non initialisées (.data?) sinon GetThreadContext renverra une erreur, tandis que les structures StartupInfo et ProcessInfo ne sont pas pointilleuses et peuvent aller dans .data ou .data?

 


Copyright (C)- xtx Team (2021)

XHTML valide 1.1 CSS Valide !