Hook d'api, injection de dll, table d'import

Description

#### Prérequis :

Lorsque l'on lance un exécutable, le loader de Windows mappe le fichier .exe (Ou plus précisément les sections qu'il contient) en RAM. Les lieurs sont généralement configurés pour produire des .exe qui ne peuvent fonctionner correctement qu'à une adresse fixe (Adresse de base de l'image dans les options de Delphi).

Comme le .exe est le premier à être chargé, le loader peut le mettre sans problème à cette adresse.

Puis le loader regarde la table d'importation du fichier .exe pour déterminer les dlls qu'il doit charger.

Cette fois, les dlls risquent de ne pas être chargées à leurs adresses de base préférées : elles peuvent être déjà prises par le .exe ou par un une autre dll. Heureusement, les dlls contiennent une table de relocation. Cette table est traitée par le loader de Windows qui va ainsi corriger des adresses de manière à ce que la dll fonctionne à l'adresse où elle est effectivement chargée.

La table d'importation du .exe contient aussi la liste des fonctions utilisées par le programme, pour chaque dll. Le loader de Windows va déterminer ces adresses et les mettre en place à des emplacements prévus à cet effet dans le mappage du .exe. Les adresses de ces futurs adresses sont donc connues au moment du linkage.

Les appels des fonctions importées ne sont donc pas implémentées sous forme de "exécute la fonction à cette adresse", adresse qui peut théoriquement être aléatoire, mais sous la forme "exécute la fonction dont l'adresse à été mise en place par le loader à cette adresse".

En assembleur on ne fait pas :
call XXXXXXX
car on ne connaît pas XXXXXXX au linkage, mais :
call dword ptr [YYYYYYYY]
où YYYYYYYY est connues au linkage, et le loader mettra "XXXXXXXX" à cette adresse car le .exe l'aura demandé via sa table d'importation.

#### Objectif :

Le hook d'API consiste à remplacer l'adresse mise en place par le loader par une autre. Ainsi, tout appel à la fonction hookée est redirigé vers la fonction de notre choix.

Cela permet deux choses :
1 Connaître les arguments passés à la fonction, pour par exemple connaître tous les fichiers ouverts par l'exécutable.
2 Modifier le comportement d'une application, pour par exemple forcer l'application à demander l'autorisation de l'utilisateur avant que l'application ne lance un exécutable avec CreateProcess ou ShellExecute.

#### Limitations :

Seules les fonctions importées par l'exe peuvent être hookées (Par cette technique...). Donc :

1 Tout emploi de LoadLibrary/GetProcAddress n'est pas filtré. Evidemment, rien n'empêche de hooker ces fonctions.
2 Les fonctions importées par les dlls utilisées par l'exe ne sont pas filtrées.

Le deuxième problème est sans doute peu courant dans les applications en Delphi qui s'appuient généralement uniquement sur des tables d'importations bien fournies avec des fonctions très classiques de l'API Win32.

Par contre, dans le cas d'une application C utilisant une runtime sous forme de dll, CreateFile peut tout à fait être absent de la table d'import par exemple, car le développeur peut n'avoir utilisé que fopen... Il suffit donc de hooker fopen aussi, mais il faut y penser.

Exemple encore plus flagrant : le VB6, où aucune fonction classique n'est importée. Les .exe de VB6 compilés en natif n'importent que depuis msvbvm60.dll, et encore, pas grand chose.

#### Préparation :

Il faut déduire le nom des fonctions hookées de l'objectif final que l'on souhaite atteindre. La doc du SDK Win32 peut nous aider à faire notre choix.

Une fois que l'on a les noms, il faut s'assurer que ces fonctions sont bien importées par le .exe cible.

Pour cela, on peut utiliser l'utilitaire Import.exe inclus en source et en binaire (Renommer le .exec en .exe) dans le zip. C'est une appli console qui prend en argument un fichier image (.exe, .dll... Tant que le fichier est au format PE, c'est ok), et qui affiche toutes les fonctions de la table d'import.

Hooker une fonction ne nous servira pas à grand chose si on ne connaît pas son prototype, c'est à dire les arguments qu'elle attend et sa convention d'appel. On ne peut pas facilement connaître ces informations sans avoir accès à la documentation de la dll.

#### Injection de la procédure dans le processus :

On souhaite remplacer une fonction importée par notre propre fonction. On sait que l'on va remplacer une adresse par une autre dans la table d'import. Il faut donc que l'on commence par avoir l'adresse de notre fonction, et ce, dans le processus distant.

Certains seraient peut être tentés de mettre leur procédure dans le processus qui installe le hook, puis de mettre en place l'adresse de cette procédure dans le processus hooker. C'est absolument impossible : deux processus ne partagent pas de mémoire, les adresses valables dans un processus pointent sur n'importe quoi dans l'autre. L'adressage est tout à fait virtuel.

Il faut donc charger notre procédure dans le processus à hooker. Le plus logique est de faire une injection de dll.

On va donc commencer par réaliser une dll qui exporte une procédure avec le même prototype que la fonction que l'on souhaite hooker.

Un exemple est dans le répertoire InjectedDll.

Cette dll exporte ShellAboutW, la même procédure que celle exportée par la dll shell32.dll.
Cette fonction est utilisée par de nombreux programmes sous windows (calc, mspaint, notepad...) lorsque l'on demande leur "A propos de ..."

On va afficher une boîte de dialogue juste avant d'afficher la boîte standard.

La méthode d'injection de la dll utilisée est détaillée au chapitre 22 de "Programming Applications for Microsoft Windows", de Richter.

#### Remplacement de la fonction :

Le remplacement de la fonction peut par exemple se faire dans la fonction d'initialisation de la dll injectée. La table d'import est parcourue à la recherche de la fonction à remplacer et est modifiée quand elle est trouvée.

La structure de la table d'import est détaillée la spécification pe/coff de M$.

#### Application finale :

L'application finale lance une instance de la calculatrice de Windows et injecte la dll dedans.

La principale difficulté lors de la réalisation de cette appli a été de lancer l'injection suffisement tard. Par exemple, avec CREATE_SUSPENDED, le loader de Windows ne charge même pas kernel32, ce qui conduit à l'échec de l'injection.

L'emploi d'un Sleep, bien que possible, est vraiment pas propre (Tiens y a un jeu de mot là. Vivi je sors.).

La meilleur solution que j'ai trouvé a d'utiliser DEBUG_PROCESS. Bien pratique : on est informé des chargement de modules.

J'ai cependant dû rajouter un thread à cause d'un deadlock : le processus débogué attendais que le débogueur acquitte la création du thread, tandis que le débogueur attendais la fin du thread d'injection...

Conclusion :


pecoff :
http://www.google.fr/...

Richter :
http://brunews.free.fr/brunews/download/JR4.zip

Codes Sources

A voir également

Vous n'êtes pas encore membre ?

inscrivez-vous, c'est gratuit et ça prend moins d'une minute !

Les membres obtiennent plus de réponses que les utilisateurs anonymes.

Le fait d'être membre vous permet d'avoir un suivi détaillé de vos demandes et codes sources.

Le fait d'être membre vous permet d'avoir des options supplémentaires.