
Résolveur et déhooker d'API Windows dynamique qui détecte et restaure les fonctions hookées (IAT, EAT, correctifs inline) afin d'invoquer des appels système non surveillés depuis des malwares Red Team.
À l'ère des AV et EDR intrusifs qui introduisent des correctifs à chaud dans les processus en cours pour leurs besoins d'optique renforcée, les adversaires modernes doivent disposer d'un outil robuste pour contourner ces gardiens. L'implémentation proposée d'un résolveur d'imports dynamique capable de désaccrocher les fonctions utilisées à la volée constitue une étape supplémentaire vers le renforcement des efforts de résilience des adversaires.
La solution que je propose ici est de passer de l'utilisation d'imports WinAPI résolus par l'éditeur de liens, restant visibles dans les en-têtes PE de l'exécutable compilé (notamment la table d'adresses d'import), à une approche entièrement dynamique insistant sur la résolution des imports uniquement de manière dynamique. Un tel résolveur dynamique peut être équipé d'une logique de désaccrochage se déroulant en arrière-plan, sans aucune guidance de la part de l'opérateur.
Voici comment vous pouvez garantir l'appel de MessageBoxW désaccroché et non surveillé :
RESOLVE(user32, MessageBoxW);
_MessageBoxW(0, L"Look Ma! I'm unhooked!", L"Third - Unhooked", 0);
Toute la magie opère dans la macro-définition RESOLVE, qui construit un objet ImportResolver<T> nommé _MessageBoxW.

Voici comment fonctionne l'exemple UnhookMe :
MessageBoxW qui n'est pas sujette à un crochet.MessageBoxW pour qu'elle retourne toujours 0 sans afficher son message.MessageBoxW dynamiquement en utilisant le résolveur UnhookingImportResolver, qui détectera les correctifs de prologue appliqués et restaurera les octets originaux, désaccrochant ainsi effectivement la fonctionnalité de MessageBoxW.Pendant l'affichage des boîtes de message, voici les lignes de journal imprimées sur la sortie standard de la console :
[~] Symbole résolu kernel32.dll!CreateFileA
[~] Symbole résolu kernel32.dll!ReadProcessMemory
[~] Symbole résolu kernel32.dll!MapViewOfFile
[~] Symbole résolu kernel32.dll!VirtualProtectEx
[#] Crochet de trampoline trouvé dans le symbole : MessageBoxW . Octets originaux restaurés depuis le fichier.
[~] Symbole résolu user32.dll!MessageBoxW
Il y a au total 5 fichiers source/en-tête C++ que votre solution doit inclure. Cependant, votre fichier de programme principal n'a besoin d'inclure que deux en-têtes requis, comme détaillé ci-dessous.
resolver.h - en-tête contenant la majeure partie de l'implémentation de UnhookingImportResolver et des macro-définitions pratiques.resolver.cpp - code source avec les options globales définies.usings.h - un gros et méchant fichier d'en-tête contenant des dizaines de définitions de type using pour les API WinAPI couramment utilisées.PE.cpp - fichier de code source de l'analyseur PE personnalisé.PE.h - fichier d'en-tête de l'analyseur PE personnalisé.Votre programme n'aura besoin d'inclure que deux en-têtes :
#include "usings.h"
#include "resolver.h"
Il existe quelques options globales qui peuvent être modifiées pour affecter la manière dont le résolveur fonctionne ou rapporte son activité. Elles sont définies au tout début du fichier resolver.cpp :
Options globales du résolveur :
globalQuietOption - mettre à true si vous ne voulez aucune sortie.globalVerboseOption - mettre à true si vous voulez une sortie verbeuse détaillée.globalAntiSplicingOption - désaccrocher les fonctions résolues si elles sont accrochées.globalLogFilePath - où rediriger les lignes de journal de sortie. Si vide, utiliser stdout.bool globalQuietOption = false;
bool globalVerboseOption = true;
bool globalAntiSplicingOption = true;
wchar_t globalLogFilePath[MAX_PATH] = L"";
Pour utiliser le résolveur, un type de pointeur de fonction doit d'abord être déclaré avec une instruction using de forme stricte :
using fn_FunctionName = ReturnType WINAPI (
ParamType1 paramName1,
...,
ParamTypeN paramNameN,
);
Ce dépôt est fourni avec le fichier d'en-tête usings.h contenant des types prédéfinis pour des dizaines d'API Windows populaires.
Le FunctionName correspondra à l'API WinAPI que nous voulons que ImportResolver résolve, et ce pointeur de fonction doit être marqué comme ayant la convention d'appel WINAPI ( __stdcall sur x86 et __fastcall sur x64). Le ReturnType doit précéder le modificateur de type WINAPI.
Une fois le type de pointeur de fonction défini comme spécifié ci-dessus, nous pourrons l'utiliser de la manière suivante :
RESOLVE(libraryName, FunctionName);
ReturnType output = _FunctionName(param1, ..., paramN);
La macro RESOLVE se charge d'instancier l'objet template ImportResolver et d'ajuster le nom de la bibliothèque spécifiée.
Le résolveur introduit plusieurs autres macro-définitions offrant une invocation de constructeur facile à utiliser dans diverses circonstances :
#define RESOLVE(mod, func) RESOLVE_PARAMETERIZED(mod, func, ::globalVerboseOption, ::globalAntiSplicingOption)
#define RESOLVE_NO_UNHOOK(mod, func) RESOLVE_PARAMETERIZED(mod, func, ::globalVerboseOption, false)
#define RESOLVE_VERBOSE_UNHOOK(mod, func) RESOLVE_PARAMETERIZED(mod, func, true, true)
#define RESOLVE_VERBOSE_NOUNHOOK(mod, func) RESOLVE_PARAMETERIZED(mod, func, true, false)
#define RESOLVE_NOVERBOSE_UNHOOK(mod, func) RESOLVE_PARAMETERIZED(mod, func, false, true)
#define RESOLVE_NOVERBOSE_NOUNHOOK(mod, func) RESOLVE_PARAMETERIZED(mod, func, false, false)
Constructeur du résolveur :
template<typename Ret, typename ...Args>
ImportResolver<Ret WINAPI(Args...)>(
std::string dllName,
std::string funcName,
bool _verbose = false,
bool _unhook = false,
bool *_wasItHooked = nullptr
)
Le résolveur sous-jacent exploite un analyseur d'en-têtes PE personnalisé qui traite chaque module DLL référencé pour mapper ses exportations et vérifier l'intégrité des en-têtes PE du module ainsi que l'intégrité des octets du stub de la fonction référencée.
L'idée est la suivante :
Nous émettons d'abord LoadLibrary pour charger la bibliothèque référencée par l'utilisateur (celle spécifiée comme premier paramètre de la macro RESOLVE) si elle ne peut pas être atteinte via GetModuleHandle.
Ensuite, nous traitons les en-têtes PE de la bibliothèque chargée/référencée, mappons ses exportations, récupérons le tableau des adresses d'exportation et calculons également ces adresses nous-mêmes pour une vérification croisée.
Si l'adresse d'une routine définie dans la table des adresses d'exportation (EAT) de la DLL ne correspond pas à ce que nous attendons, l'exportation est considérée comme accrochée via EAT. Il en va de même si l'entrée de notre table d'adresses d'import (IAT) de l'exécutable pour cette fonction a été modifiée et ne pointe plus vers l'emplacement correct dans la section de code de la DLL - alors la fonction est considérée comme accrochée via IAT.
En supposant qu'aucun crochet n'ait été trouvé jusqu'à présent, nous récupérons les N premiers octets du prologue de la fonction et les comparons à ce qui est stocké dans le fichier DLL sur le disque. S'il y a une différence entre les octets récupérés en mémoire et ceux du fichier - nous considérons que la fonction a été patchée inline (correctif à chaud).
Si la fonction est considérée comme accrochée - nous retournons l'adresse d'exportation originale (celle que nous avons calculée nous-mêmes) et/ou désaccrochons l'entrée. S'il y avait des octets de correctif en place, nous les restaurons.
Enfin, pour optimiser l'impact sur les performances du résolveur - nous mettons en cache toutes les bases d'image des modules chargés et les adresses des fonctions résolues, et les retournons à partir d'un cache (étant std::map) lors des requêtes suivantes.
Parmi les problèmes auxquels un tel résolveur de désaccrochage dynamique est confronté, on trouve les difficultés liées au parcours des API redirigées (une DLL peut contenir un thunk d'exportation indiquant que cette fonction n'est pas implémentée dans ce module, mais dans un autre) - bien que cette implémentation en ait le support, elle casse parfois sa logique de parcours.
Ce projet et d'autres sont le fruit de nuits blanches et de beaucoup de travail acharné. Si vous aimez ce que je fais et que vous appréciez que je donne toujours en retour à la communauté, Envisagez de m'offrir un café (ou plutôt une bière) juste pour dire merci ! 💪
Mariusz Banach / mgeeky, 21
<mb [at] binary-offensive.com>
(https://github.com/mgeeky)