
Un projet PoC pour l'intégration de shellcode dans la table Hint/Name
HintInject est un outil d'encapsulation et de chargement de shellcode que j'ai développé en jouant avec les structures du fichier PE et de la table d'annuaire des importations. Il prend un fichier shellcode brut et place le shellcode par morceaux dans les entrées de la table Hint/Name, accessibles via la table de recherche d'importations d'une entrée de DLL importée fictive dans l'exécutable du chargeur. Ce chargeur fusionne ensuite ces morceaux pour exécuter le shellcode. Je ne sais pas s'il s'agit d'une technique nouvelle ou non, mais j'ai voulu la partager pour le plaisir.
Avant d'expliquer le fonctionnement de HintInject, je souhaite mentionner brièvement le processus de chargement des DLL en m'appuyant sur un post de Stack Overflow.
Pour obtenir les informations sur les DLL requises et les fonctions/imports, il faut d'abord consulter la Table d'annuaire des importations. La table d'annuaire des importations est une table d'entrées, une entrée pour chaque DLL importée. Ces entrées contiennent un pointeur vers le nom de la DLL importée, un pointeur vers la Table de recherche d'importations, un pointeur vers la Table d'adresses d'importations, ainsi que d'autres champs pour diverses informations.
En résumé, la table de recherche d'importations mène aux informations sur les imports, et la table d'adresses d'importations mène aux adresses des imports. Cependant, lorsque l'exécutable est sur le disque, ou juste avant le processus de chargement des DLL, la table d'adresses d'importations est identique à celle de la table de recherche d'importations. Le contenu de la table d'adresses d'importations est écrasé avec les adresses des imports pendant le processus de chargement des DLL.
Sans entrer dans les détails, nous pouvons examiner le diagramme ci-dessous pour voir la relation entre ces trois tables.
Si l'on entre un peu plus dans les détails, la table de recherche d'importations ne contient pas directement les noms des imports. Pour les fonctions importées par nom, elle contient des RVA (Relative Virtual Address) d'entrées de la Table Hint/Name. Ces entrées stockent les noms des fonctions sous forme de chaînes ASCII terminées par un caractère nul. Par conséquent, la définition de structure d'une entrée de la table Hint/Name est la suivante :
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint;
CHAR Name[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
Le champ Hint ici est en fait un index dans la table des pointeurs de noms d'exportation de la DLL. Il est utilisé pour accélérer la recherche de la position de cet import. Ainsi, nous pouvons résumer la relation entre la table de recherche d'importations et la table Hint/Name à l'aide du diagramme ci-dessous.
Pour conclure, afin de trouver une DLL et ses imports, il faut suivre les étapes suivantes :
Récemment, en examinant la technique d'injection de DLL dans les imports (ImportDLLInjection) partagée par x86matthew, j'ai vu comment ajouter une entrée fictive à l'annuaire d'importations des DLL chargées en mémoire. Ensuite, j'ai voulu développer un petit projet qui ajoute une entrée fictive directement à la table d'importations d'un binaire sur le disque, à la fois pour le plaisir et pour rafraîchir mes connaissances. En passant en revue le processus de chargement des DLL pour développer ce projet, le champ Hint dans les entrées de la table Hint/Name utilisées dans ce processus a attiré mon attention.
D'après la documentation MSDN, j'ai appris que ce champ est utilisé par le chargeur Windows pour trouver l'adresse de cet import, importé par son nom, directement à partir de la table des noms d'exportation de la DLL dans laquelle il se trouve. Cependant, dans le même document, il était indiqué que si la fonction ne peut pas être trouvée en utilisant ce champ, cette fonction sera recherchée via une opération de recherche binaire dans la table des noms d'exportation de la DLL. Sur la base de cette phrase, j'ai pensé que mettre une valeur incorrecte pour ce champ ne perturberait pas le processus de chargement des DLL.
Comme je l'ai mentionné ci-dessus, il y a une entrée dans la table Hint/Name correspondant à chaque fonction à importer. En d'autres termes, nous avons 2 octets à utiliser pour chaque import. En combinant plusieurs imports, on peut obtenir suffisamment de champs Hint pour stocker des shellcodes malveillants. En conséquence, on peut utiliser un binaire chargeur pour intégrer le shellcode dans les entrées Hint/Name de l'entrée fictive de DLL importée. Ce binaire chargeur peut accéder à ces entrées pour fusionner le shellcode à exécuter pendant l'exécution.
HintInject peut être utilisé pour créer un tel chargeur qui contient le shellcode dans sa table Hint/Name. Il crée d'abord une nouvelle section nommée .rrdata et copie l'annuaire d'importations actuel dans cette section. Ensuite, il ajoute une nouvelle entrée fictive dont les imports serviront à contenir le shellcode d'entrée. Les octets restants de la section sont utilisés pour stocker la table de recherche d'importations, la table d'adresses d'importations, le nom de la DLL et la table Hint/Name de la nouvelle entrée fictive. En dernière étape, HintInject utilise les champs Hint des imports pour placer des morceaux du shellcode d'entrée.
Exemple d'imports d'un binaire chargeur :
HintInject.exe avec votre shellcode au format brut et un chemin de sortie pour créer un chargeur de shellcode : HintInject.exe <Fichier Shellcode> <Nom de sortie>Loader.exe <PID>InjectShellcode dans le fichier HintInjectLoader/Main.cpp.Testé sur Windows 10 19044 avec Visual Studio 2019 :
dllNames dans le fichier DllNamesForFakeImports.h.dllNames dans le fichier DllNamesForFakeImports.h. Par défaut, HintInject choisira les fonctions des DLL dans l'ordre du tableau. En d'autres termes, l'outil sélectionne d'abord les fonctions exportées de user32.dll ; si la taille du shellcode est supérieure à 2 * le nombre d'exportations de user32.dll, il commence également à utiliser les exportations de advapi32.dll et des autres.static LPCSTR dllNames[] = {"user32.dll","advapi32.dll","gdi32.dll","wininet.dll","comctl32.dll","shell32.dll","wsock32.dll","oleaut32.dll","ws2_32.dll","urlmon.dll"};
Pour des tests de sécurité autorisés uniquement. Toute utilisation abusive de cet outil contre des systèmes sans autorisation explicite est illégale.