Les fondamentaux du Loader

Nouveaux concepts:

Processus et threads
Gestion de la mémoire virtuelle

Nouvelles fonctions API:

CreateProcess
WriteProcessMemory
ResumeThread
TerminateProcess

 

Un loader est un programme autonome utilisé pour charger et exécuter un autre programme. Il est utilisé lorsque nous voulons patcher une cible en mémoire, par exemple après une vérification CRC, ou lorsque l'on veut patcher quelque chose et restaurer les octets d'origine plus tard dans le programme. Un exemple de loader est le trainer généralement utilisé pour tricher dans les jeux.

Dans ce tutoriel, nous allons coder un loader très simple et comparer son action avec le patcheur que nous avons créé dans le dernier tutoriel. Tout d'abord, nous devons aborder quelques notions théoriques générales sur les processus, les threads et la gestion de la mémoire.

Processus et Threads

Un processus fournit les ressources nécessaires pour exécuter un programme. Chaque processus est une collection d'espace d'adresse mémoire virtuelle, de code exécutable, de données, de handles ouverts vers des objets système, d'un contexte de sécurité, d'un identifiant de processus unique, de variables d'environnement, d'une priorité de base, de tailles minimale et maximale de l'ensemble de travail, et au moins un thread d'exécution.

Un thread est du code qui doit être exécuté au sein d'un processus. Un processeur exécute des threads, pas des processus, donc chaque application a au moins un processus, et un processus a toujours un thread en cours d'exécution, appelé thread principal. Un processus peut également avoir plusieurs threads.

Tous les threads d'un processus partagent son espace d'adresse virtuelle, ses variables globales et ses ressources. De plus, chaque thread possède des gestionnaires d'exceptions, une priorité d'ordonnancement, un stockage local au thread (par exemple, des variables locales), un identifiant de thread unique et un ensemble de structures que le système utilisera pour sauvegarder le contexte du thread jusqu'à ce qu'il soit planifié. Le contexte du thread comprend l'ensemble des registres de la machine du thread, la pile du noyau, un bloc d'environnement du thread et une pile utilisateur dans l'espace d'adressage du processus du thread.

L'utilisation de threads confère à Windows sa capacité apparente de multitâche, c'est-à-dire la capacité d'exécuter plusieurs programmes simultanément. Au lieu de continuer à exécuter un seul morceau de code jusqu'à son achèvement, Windows peut décider d'interrompre un thread en cours d'exécution à n'importe quel moment et passer à un autre thread.

Un thread est comme un petit processeur virtuel qui possède son propre contexte et sa propre pile. Le processeur physique réel bascule entre plusieurs processeurs virtuels et démarre toujours l'exécution à partir des informations de contexte actuelles du thread et en utilisant sa pile.

Les composants qui gèrent les threads dans Windows sont le planificateur et le gestionnaire de files d'attente, qui sont responsables ensemble de décider quel thread peut s'exécuter pendant combien de temps, et d'effectuer le véritable changement de contexte lorsqu'il est temps de changer de thread.

Les threads peuvent volontairement céder le processeur, par exemple s'ils sont inactifs en attendant une entrée utilisateur. Cependant, le système d'exploitation utilise une planification préalable au cas où un thread commencerait une tâche prolongée qui occuperait le processeur pendant un certain temps. Les threads se voient accorder une quantité limitée de temps (quantum ou tranche de temps) pour s'exécuter avant d'être interrompus.

Pendant l'exécution d'un thread, le système d'exploitation utilise une interruption matérielle de minuterie de bas niveau pour surveiller la durée de son exécution. Une fois que le quantum du thread est écoulé, il est temporairement interrompu et le système permet à d'autres threads de s'exécuter. Si aucun autre thread n'a besoin du processeur, le thread est immédiatement repris. Le processus de suspension et de reprise du thread est complètement transparent pour ce dernier. Le noyau stocke l'état de tous les registres du CPU dans une structure CONTEXT avant de suspendre le thread, puis restaure cet état lorsque le thread est repris. De cette manière, le thread ne sait même pas qu'il a été interrompu.

La séquence d'initialisation du processus

Il est important de comprendre ce qui se passe lorsqu'un processus est créé. Ce qui suit fournit une brève description des étapes prises par le système dans la séquence d'initialisation moyenne d'un processus.

1. L'API CreateProcess crée un nouvel objet de processus et alloue un espace d'adressage virtuel pour celui-ci (voir le paragraphe suivant).

2. CreateProcess mappe ensuite NTDLL.DLL et le fichier exécutable du programme (.exe) dans l'espace d'adressage nouvellement alloué.

3. CreateProcess crée le premier thread du processus et alloue un espace de pile.
4. Le premier thread du processus commence à s'exécuter dans la fonction LdrpInitialize à l'intérieur de NTDLL.DLL.

5. LdrpInitialize traverse récursivement les tables d'importation de l'exécutable et mappe en mémoire chaque exécutable requis.

6. À ce stade, LdrpRunInitializeRoutines est appelée et est responsable de l'initialisation de toutes les DLL liées statiquement actuellement chargées dans l'espace d'adressage. Le processus d'initialisation consiste à appeler le point d'entrée de chaque DLL avec la constante DLL_PROCESS_ATTACH (voir Bibliothèques de liens dynamiques).

7. Une fois que toutes les DLL sont initialisées, LdrpInitialize appelle la routine d'initialisation réelle du thread, qui est la fonction BaseProcessStart de KERNEL32.DLL. Cette fonction appelle à son tour le point d'entrée WinMain de l'exécutable, à partir duquel le processus a terminé sa séquence d'initialisation.

Gestion de la mémoire virtuelle de Windows

Chaque processus a son propre espace d'adressage virtuel qui permet d'adresser jusqu'à 4 Go de mémoire. Tous les threads d'un processus peuvent accéder à son espace d'adressage virtuel, mais pas à la mémoire appartenant à un autre processus, ce qui protège un processus contre toute corruption par un autre processus.

Les adresses virtuelles utilisées par un processus ne représentent pas l'emplacement physique réel d'un objet en mémoire. Au lieu de cela, le système maintient une table de pagination pour chaque processus - une structure de données interne utilisée pour traduire les adresses virtuelles en adresses physiques correspondantes. Chaque fois qu'un thread référence une adresse, le système traduit l'adresse virtuelle en adresse physique. Les pages de l'espace d'adressage virtuel d'un processus peuvent se trouver dans l'un des états suivants :

Libre - La page n'est ni allouée ni réservée. La page n'est pas accessible par le processus. Elle est disponible pour être allouée, réservée ou simultanément réservée et allouée. Tenter de lire depuis ou écrire sur une page libre entraîne une exception d'accès violation.

Réservée - La page a été réservée pour une utilisation future. La plage d'adresses ne peut pas être utilisée par d'autres fonctions d'allocation. La page n'est pas accessible et n'a pas de stockage physique associé. Elle est disponible pour être allouée.

Allouée - Un espace de stockage physique est alloué pour la page, et l'accès est contrôlé par une option de protection de la mémoire. Le système initialise et charge chaque page allouée dans la mémoire physique uniquement lors de la première tentative de lecture ou d'écriture sur cette page. Lorsque le processus se termine, le système libère l'espace de stockage des pages allouées.

La fonction API VirtualAlloc est utilisée pour réserver et allouer de la mémoire, et spécifier les permissions d'accès à la mémoire pour les pages allouées :

1

Un processus utilise d'abord VirtualAlloc pour réserver un bloc de pages dans son espace d'adressage. Plus tard, VirtualAlloc est appelé à nouveau chaque fois qu'il est nécessaire d'allouer une page à partir de cette région réservée. Allouer l'ensemble de la région au lieu de la réserver uniquement consomme un espace de stockage physique qui pourrait ne pas être nécessaire, le rendant indisponible pour une utilisation par d'autres processus. Le processus utilise VirtualFree pour libérer les pages réservées et allouées lorsqu'il en a terminé avec elles.

VirtualAllocEx est une fonction importante qui peut réserver ou allouer une région de mémoire dans l'espace d'adressage virtuel d'un autre processus spécifié.

Considérations lors de l'écriture d'un loader

Un loader doit créer un nouveau processus et charger la cible, mais nous voulons mettre en pause le thread dès sa création afin de pouvoir modifier ce que nous voulons. Nous utilisons l'API CreateProcess à cet effet, avec la valeur CREATE_SUSPENDED dans dwCreationFlags.

CreateProcess prend des pointeurs vers 2 structures importantes : ProcessInformation et StartupInfo. La structure ProcessInformation est remplie avec le handle du processus, le handle du thread et l'ID du processus/thread (consultez win32.hlp pour plus d'informations).

REMARQUE : L'utilisation du handle du processus vous donne un accès PROCESS_ALL_ACCESS (accès en lecture/écriture) à l'ensemble du processus. Lors de l'utilisation du handle du thread, vous devrez activer manuellement l'accès en écriture.

Lorsque la cible est chargée en mode suspendu, nous pouvons laisser le thread s'exécuter avec ResumeThread, puis le mettre en pause à nouveau avec SuspendThread. Ces API prennent le handle hThread trouvé dans la structure LPPROCESS_INFORMATION. Enfin, nous pouvons lire et écrire dans le processus en utilisant ReadProcessMemory et WriteProcessMemory.

 


Notre loader se contente de créer un processus et de charger SuperCleaner avec le thread principal suspendu. Ensuite, il patchera le fichier en mémoire, reprendra l'exécution du thread principal et se terminera.

Lancez WinAsm comme d'habitude et collez le code loader1.asm. Bien que ce loader n'ait pas vraiment d'interface graphique, il est préférable qu'il ait au moins une icône. Ajoutez un script de ressources au projet et ajoutez simplement une icône. Cela donne le script suivant :

1

Le code d'assemblage est court mais efficace. Remarquez qu'il n'y a pas d'interface graphique (pas d'appel à DialogBoxParam), donc nous n'avons pas besoin de récupérer et de sauvegarder le handle du module. De plus, en cas d'erreur, nous devons terminer le processus cible suspendu avant de quitter :

1

Une critique possible de ce code est que nous avons spécifié l'adresse virtuelle (VA) à patcher. Cela repose sur le fait que la cible est chargée à l'ImageBase préféré pour que le patch fonctionne. Une façon plus robuste serait de spécifier l'adresse relative (RVA) puis d'obtenir l'adresse de base de la cible et de les ajouter pour obtenir l'adresse virtuelle (VA) à patcher. Ainsi, si la cible était chargée à une adresse de base différente, le loader patcherait toujours les octets corrects, bien que cela s'applique principalement aux DLL. Une autre critique est que c'est un loader aveugle - il ne vérifie pas. Nous aborderons des concepts plus avancés dans le prochain tutoriel.

 


Copyright (C)- xtx Team (2021)

XHTML valide 1.1 CSS Valide !