
Moteur de hooking par points d'arrêt matériels pour Windows qui utilise les registres de débogage pour hooker des fonctions, contourner ETW/AMSI et échapper à la surveillance EDR en espace utilisateur.
Cet article était à l’origine destiné au VX-Underground Black Mass Halloween Edition 2022.
Moteurs de hooking :
Technique générique d’évasion en user-land x64 utilisant les registres de débogage :
Exemples de hooks ETW/AMSI disponibles
Notre tâche consiste à hooker trivialement des fonctions et à détourner le flux du code selon les besoins, puis à supprimer le hook une fois qu’il n’est plus nécessaire.
Nous ne pouvons pas envisager d’appliquer des hooks IAT car ils ne sont pas toujours appelés et donc peu fiables. Le hooking inline est une technique puissante ; cependant, elle nécessite de patcher la mémoire là où se trouve le code. C’est une technique puissante, mais des outils tels que PE-Sieve et Moneta peuvent distinguer la différence entre la copie résidente en mémoire et celle sur disque d’un module et signaler cela. Cela nous laisse avec l’outil parfait pour le travail : les registres de débogage, bien qu’ils soient assez sous-estimés par les auteurs de malwares !
Sur Windows, en résumé, un processus est essentiellement une encapsulation de threads, et chacun de ces threads maintient un contexte qui est l’état du thread : les registres et la pile, etc. Les registres de débogage sont une ressource privilégiée, tout comme leur configuration ; cependant, Windows expose divers syscalls qui nous permettent de demander au noyau d’effectuer une action privilégiée en notre nom ; cela inclut la configuration des registres de débogage qui sont parfaits pour nous. NtSetThreadContext et NtGetThreadContext exposent des fonctionnalités permettant de modifier tout contexte de thread pour lequel nous pouvons ouvrir un handle avec le privilège nécessaire. Nous pouvons voir comment définir les registres de débogage avec l’API Win32.```c CONTEXT context = { .ContextFlags = CONTEXT_DEBUG_REGISTERS }; GetThreadContext(thd, &context);
// set our debug information in the Dr registers
SetThreadContext(thd, &context);
Il y a 8 registres de débogage, de Dr0 à Dr7. Ceux qui nous intéressent sont uniquement
Dr0-3, dans lesquels nous stockons les adresses sur lesquelles nous souhaitons placer des points d'arrêt, et Dr6 n'est que le statut de débogage.
Le plus important est Dr7, qui décrit les conditions des points d'arrêt dans lesquelles le processeur lèvera une exception. L'utilisation des registres de débogage comporte diverses limitations, comme un nombre limité (4) et le fait qu'ils ne s'appliquent pas à tous les threads/nouveaux threads créés. Nous allons chercher à remédier à certaines de ces limitations !
Lorsque l'exception est levée, le processeur recherche un gestionnaire d'exceptions que nous pouvons définir et enregistrer dans notre programme [1]. Dans notre gestionnaire d'exceptions défini, nous voulons que notre code associé (différents flux de code) s'exécute lorsque le point d'arrêt correspondant est déclenché.```c
LONG WINAPI ExceptionHandler(PEXCEPTION_POINTERS ExceptionInfo)
{
if (ExceptionInfo->ExceptionRecord->ExceptionCode == STATUS_SINGLE_STEP)
{
// Look for our associated code flow relative to our RIP
if (HWBP_ADDRESS_MAP.contains(ExceptionInfo->ContextRecord->Rip)) {
HWBP_ADDRESS_MAP.at(ExceptionInfo->ContextRecord->Rip).func(ExceptionInfo);
return EXCEPTION_CONTINUE_EXECUTION;
}
}
return EXCEPTION_CONTINUE_SEARCH;
}
Ceci est réalisé par une fonction constructeur qui définit la correspondance entre une fonction lambda "callback" et une adresse. using EXCEPTION_FUNC = std::function <void(PEXCEPTION_POINTERS)>;```c typedef struct { UINT pos; EXCEPTION_FUNC func; } HWBP_CALLBACK;
// Global std::unordered_map<uintptr_t, HWBP_CALLBACK> HWBP_ADDRESS_MAP{ 0 };
// Create our mapping HWBP_ADDRESS_MAP[address].func = function; HWBP_ADDRESS_MAP[address].pos = pos;
Nous devons parcourir tous les threads de nos processus et définir les ajustements correspondants dans le contexte pour chacun d'eux. Cela peut être réalisé à l'aide des fonctions d'assistance ToolHelp32 : CreateToolhelp32Snapshot et Thread32Next. Ce n'est rien d'extraordinaire, mais cela répond à l'une de nos limites : ne pas nous attacher à tous les threads.```c
VOID SetHWBPS(const uintptr_t address, const UINT pos, const bool init = true)
{
DWORD pid{ GetCurrentProcessId() };
HANDLE h{ CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0) };
if (h != INVALID_HANDLE_VALUE) {
THREADENTRY32 te{ .dwSize = sizeof(THREADENTRY32) };
if (Thread32First(h, &te)) {
do {
if ((te.dwSize >= FIELD_OFFSET(THREADENTRY32, th32OwnerProcessID) +
sizeof(te.th32OwnerProcessID)) && te.th32OwnerProcessID == pid) {
HANDLE thd = OpenThread(THREAD_ALL_ACCESS, FALSE, te.th32ThreadID);
if (thd != INVALID_HANDLE_VALUE) {
SetHWBP(thd, address, pos, init);
CloseHandle(thd);
}
}
te.dwSize = sizeof(te);
} while (Thread32Next(h, &te));
}
CloseHandle(h);
}
}
Le fait d'avoir des points d'arrêt matériels définis est sans doute suspect, car cela peut indiquer une activité malveillante (bien qu'aucun EDR, à ma connaissance, ne les recherche activement). Ils peuvent être utilisés contre nous comme un IoC potentiel, nous devons donc supprimer leurs traces une fois que nous avons fini de les utiliser.
Nous pouvons implémenter cela dans notre fonction de destructeur !! Cela itérera sur tous les threads et vérifiera si le registre (&context.Dr0)[pos] pointe vers l'adresse à laquelle nous avons initialement défini le point d'arrêt matériel (pos est simplement un index % 4 nous donnant accès aux context.Dr0-Dr3). Nous pouvons également supprimer les conditions nécessaires dans le registre Dr7. Nous devons aussi penser à supprimer notre entrée de mappage. Par conséquent, notre point d'arrêt matériel ne sera présent que pendant la durée requise !```c SetHWBPS(address, pos, false); HWBP_ADDRESS_MAP.erase(address);
Un exemple de point d'arrêt matériel serait Sleep, où l'on remplace simplement la durée du sommeil
par 0.```c
HWBP HWBPSleep{ (uintptr_t)&Sleep, 0, // Set Dr 0
([&](PEXCEPTION_POINTERS ExceptionInfo) {
ExceptionInfo->ContextRecord->Rcx = 0;
ExceptionInfo->ContextRecord->EFlags |= (1 << 16); // continue execution
}) };
Nous savons qu'il faut définir RCX en raison de la convention d'appel rapide à quatre registres de Windows x64[1]. Le premier argument du constructeur est l'adresse sur laquelle placer le point d'arrêt, le second est le registre Dr0- 3 dans lequel stocker (notez que nous ne pouvons avoir que 4 adresses de point d'arrêt à la fois), et le troisième est une fonction lambda qui capturera par référence PEXCEPTION_POINTERS, qui est l'information qu'un gestionnaire d'exceptions recevra. Cela nous permettra en fin de compte de contrôler le déroulement d'un programme différemment selon le point d'arrêt qui a été déclenché.