Au cœur des stratégies de défense des terminaux, les EDR se positionnent comme des compléments, parfois même remplaçants, efficaces des solutions antivirales classiques. Cet article étudie les fondamentaux des mécanismes permettant aux EDR de superviser les opérations réalisées sur un système Windows, tels que l’userland hooking, l’utilisation des kernel callbacks, et des évènements de Threat Intelligence.
Une première partie de l’article visera à décrire certains éléments de base de l’architecture logicielle de Windows, nécessaires pour comprendre les différents mécanismes employés par les EDR pour surveiller les actions des processus en exécution sur le système. Ces différents mécanismes seront étudiés et décrits dans en second temps.
Afin d’illustrer les mécanismes de détection des EDR, nous nous appuierons parfois sur l’exemple d’une tentative de création d’un dump mémoire du processus LSASS. Ce processus, contenant des secrets d’authentification des utilisateurs sous Windows, représente de fait une cible de choix lors de scénarios de post exploitation.
1. Fondamentaux de l’architecture logicielle Windows
1.1 Appels système
Sur la plupart des systèmes d’exploitation modernes, et en particulier sous Windows, lorsqu’un processus souhaite interagir avec un élément du système (tel qu’un fichier, une socket réseau, un autre processus, etc.), celui-ci réalise cette interaction par le biais d’un appel système (ou system call / syscall en anglais). Un appel système est une passerelle entre le mode utilisateur (user mode) et le mode noyau (kernel mode) : il permet au processus de demander au système d’exploitation d’effectuer une opération qu’il ne peut réaliser lui-même, notamment pour des raisons de sécurité (vérification côté noyau des privilèges du processus) et fonctionnelles dues à la possible complexité des opérations réalisées (ex. : l'écriture d’un fichier cache une partie complexe de parcours et d’analyse du système de fichiers, de mise à jour des métadonnées, de fragmentation du contenu sur le disque, etc.).
1.2 API de plus haut niveau
Sur Windows, les fonctions déclenchant ces appels système sont principalement situées dans ntdll.dll. Le code de la fonction NtOpenProcess, servant à préparer l’accès à un processus « étranger », est présent ci-dessous :
Comme observé, cette fonction a pour principal effet de stocker la constante 0x26 dans le registre rax, correspondant au numéro de l’appel système NtOpenProcess, et de déclencher l’appel à l’aide de l’instruction syscall ou int 0x2E.
Ces fonctions de ntdll.dll, rarement appelées directement par les programmes standards (car non documentées officiellement), sont généralement appelées depuis des bibliothèques telles que kernel32.dll, kernelbase.dll, user32.dll, etc. Les fonctions présentes dans ces dernières sont pour la plupart documentées officiellement par Microsoft et fournissent un niveau d’abstraction supplémentaire.
Certaines fonctions de kernel32.dll, telles qu’OpenProcess, sont des fonctions enveloppant simplement leurs homologues de ntdll.dll, tandis que d’autres embarquent une réelle complexité et peuvent appeler plusieurs appels système distincts ; c’est le cas par exemple de la fonction WriteProcessMemory, qui fait notamment usage des appels système NtQueryVirtualMemory, NtProtectVirtualMemory et NtWriteVirtualMemory, afin de rendre temporairement une zone mémoire accessible en écriture avant d’y écrire le contenu passé en paramètre.
1.3 Exemple du dump mémoire d’un processus
Dans notre cas d’étude, nous souhaitons faire usage de la fonction MiniDumpWriteDump, permettant de réaliser un dump mémoire d’un processus en cours d’exécution. Le schéma ci-dessous présente une partie de la pile d’appels effectués par cette fonction, dans une vue représentant l’architecture simplifiée des bibliothèques et modules sous Windows :
Sous Windows 10 20H2 (a minima), lors d’un appel à MiniDumpWriteDump (située dans la bibliothèque dbgcore.dll), la principale fonction réalisant la lecture de l’espace mémoire du processus ciblé est la fonction ReadProcessMemory, importée depuis kernel32.dll. Comme énoncé précédemment, la fonction se repose elle-même sur NtReadVirtualMemory de ntdll.dll, qui lève l’appel système correspondant. C’est alors que le noyau prend la main, et redirige l’appel vers la fonction correspondante dans ntoskrnl.exe (i.e. le module correspondant au noyau sous Windows), également nommée NtReadVirtualMemory. Cette fonction fait alors appel à une fonction interne plus générique, MiReadWriteVirtualMemory (également appelée lors de l’utilisation de la fonction NtWriteVirtualMemory). MiReadWriteVirtualMemory fait enfin appel à 2 fonctions principales :
- MmCopyVirtualMemory, dont le rôle est d’effectuer la « lecture » ou l’« écriture » mémoire (opérations en pratique identiques pour le noyau, qui copie une donnée de l’espace mémoire virtuel d’un processus vers un autre) ;
- EtwTiLogReadWriteVm, dont le rôle est d’enregistrer l’opération à des fins d’analyse par un produit de sécurité (voir section « Le fournisseur d’évènements « Windows-Threat-Intelligence » »).
Afin de détecter un dump mémoire potentiellement malveillant (par exemple, celui du processus LSASS), un produit de sécurité comme un EDR devra donc être en mesure d’être notifié à un moment de cette chaîne d’exécution.
2. Mécanismes de surveillance des EDR
« Kernel Patch Protection » / « PatchGuard »
La fonctionnalité Kernel Patch Protection (KPP), aussi connue sous le nom de « PatchGuard », est un mécanisme de protection du noyau Windows (chargé en mémoire et non du fichier ntoskrnl.exe). Uniquement présent sur les systèmes d’exploitation Windows 64-bits, KPP protège les structures critiques et l’intégrité du code du noyau contre des modifications illégitimes. En cas de détection d’une anomalie, KPP génère un « Bug Check » (aussi surnommé « Écran bleu de la mort » / « Blue Screen of Death ») avec le code d’erreur « CRITICAL_STRUCTURE_CORRUPTION » (code d’erreur 0x00000109).
Depuis l’introduction de KPP, les développeurs de produits de sécurité (et de rootkits) ont dû revoir leur méthode de supervision des opérations réalisées sous Windows 64-bits. De fait, cette supervision était historiquement réalisée au travers d’interceptions des appels système via des modifications en mémoire noyau du tableau System Service Descriptor Table (SSDT), contenant les pointeurs des fonctions traitant les différents appels système. Désormais protégé par KPP, le tableau SSDT ne peut plus être modifié par des solutions tierces, et les produits de sécurité, tels que les EDR, ne peuvent plus introduire de code dans le noyau Windows.
Les EDR disposent donc de deux principales solutions, pouvant être utilisées de manière complémentaire, pour effectuer la supervision de processus sur une machine Windows sans recourir à du patching de code noyau :
- insérer du code permettant cette supervision côté utilisateur plutôt que côté noyau ;
- se reposer sur des mécanismes offerts par le système d’exploitation pour être averti des opérations ayant lieu sur le système, tels que les kernel callbacks, ou les événements de type ETW Threat Intelligence.
Ces différentes techniques seront décrites dans les parties suivantes.
2.1 Userland hooking
Un produit de sécurité souhaitant intercepter une action d’un processus pour l’analyser peut par exemple injecter du code sur la chaîne d’exécution présentée en Fig. 1, dans la partie dite userland. Ceci est généralement réalisé en utilisant la technique du hooking ou detour. Le schéma ci-dessous, emprunté à la documentation du framework Detours de Microsoft [DETOURS], résume le fonctionnement du hooking :
- Les premières instructions (que nous appellerons « prologue » par la suite) de la fonction à intercepter (« Target function ») sont remplacées par un « hook », une instruction de saut conçue pour détourner le flot d’exécution vers une fonction d’analyse (« Detour function »), implémentée par le produit de sécurité. Cette fonction pourra enregistrer les paramètres passés lors de l’appel, déterminer s’ils correspondent à un comportement malveillant et potentiellement bloquer l’exécution.
- Une fois cette fonction d’analyse exécutée, une copie du prologue initial de la fonction surveillée (écrasé par le hook) est exécutée (« Trampoline function »).
- Enfin, par le biais d’un autre saut, le flot d’exécution est repassé au code qui suivait initialement ce prologue afin de laisser s’exécuter la fonction surveillée dans son intégralité.
- La fonction d’analyse peut également reprendre la main une fois la fonction surveillée terminée, avant de passer le relai à la fonction ayant appelée la fonction surveillée (« Source function »). Cela permet notamment à l’EDR d’enregistrer les valeurs de retour de la fonction.
En observant le schéma présent en Fig. 1, il est aisé de se rendre compte qu’un EDR adoptant la technique du hooking pour monitorer les appels système d’un processus a tout intérêt à réaliser cette supervision au plus proche de la frontière « user-land / kernel-land ». Intercepter la fonction ReadProcessMemory de kernel32.dll serait en effet trivialement contournable par un programme malveillant, puisqu’il lui suffirait d’appeler manuellement la fonction sous-jacente NtReadVirtualMemory, non surveillée, pour réaliser une action semblable et échapper à l’EDR. Les hooks sont donc majoritairement placés par les EDR au sein de ntdll.dll.
Afin de pouvoir intercepter les appels de l’ensemble des processus sur le système, l’EDR a théoriquement deux choix : placer ces hooks dans le fichier constituant ntdll.dll, ou modifier en mémoire le code de cette bibliothèque une fois chargée par chaque processus. La première solution n’est en pratique jamais utilisée par les EDR du marché, notamment parce que celle-ci invaliderait la signature de la DLL. À la place, les EDR sont notifiés lors du démarrage de chaque processus via l’utilisation de callbacks noyau (cf. section « Kernel Callbacks »), viennent charger une DLL implémentant les fonctions d’analyse, et installer l’ensemble des hooks sur les fonctions à surveiller, tel que décrit ci-dessus.
Le code de la fonction NtOpenProcess, modifié par un produit EDR lors de l’exécution d’un programme, est présenté ci-dessous à titre d’exemple :
2.2 Kernel Callbacks
Les kernel callbacks sont un mécanisme fourni par le noyau Windows permettant à des logiciels tiers d’être notifiés lors de la réalisation de certaines opérations sur le système supervisé. Ces callbacks sont enregistrés auprès du noyau au travers de diverses API pour pilotes Windows, dont les principales sont recensées dans le tableau ci-dessous :
Opération |
API Windows |
Tableau des callbacks |
Création de processus |
nt!PsSetCreateProcess nt!PsSetCreateProcess |
PspCreateProcessNotify |
Création de threads |
nt!PsSetCreateThread nt!PsSetCreateThread |
PspCreateThreadNotify |
Chargement de fichiers PE (exécutables, bibliothèques, modules) |
nt!PsSetLoadImageNotify nt!PsSetLoadImageNotify |
PspLoadImageNotify |
Opérations liées au registre |
nt!CmRegisterCallbackEx |
Liste chaînée dynamique ayant pour premier élément CallbackListHead |
Opérations effectuées sur des handles de processus, threads et desktops |
nt!ObRegisterCallbacks |
Liste chaînée dynamique basée sur des structures non documentées [REGISTER_CALLBACKS] |
Lecture et écriture sur le système de fichiers via un pilote Mini-Filter |
FltRegisterFilter |
[PRJTZERO] |
De légères différences existent entre les API, et notamment entre les API nt!PsSetCreateProcessNotifyRoutine et nt!PsSetCreateProcessNotifyRoutineEx : la seconde permet notamment au pilote de stopper le processus avant son exécution [NOTIFICATIONS].
Les Kernel callbacks configurés sur un système peuvent être énumérés via le débuggeur Windows WinDBG :
Les 4 bits de poids faibles sont sans incidence et peuvent être retirés pour obtenir des adresses de structures _EX_CALLBACK_ROUTINE_BLOCK [CALLBACK_STRUCT] :
La commande WinDBG suivante, ignorant la structure de 8 octets EX_RUNDOWN_REF RundownProtect, permet alors de retrouver le pointeur vers la fonction de callback (conservée dans le champ PEX_CALLBACK_FUNCTION Function) :
Dans le cas de la supervision des créations de processus, par exemple, les pointeurs des callbacks sont enregistrés dans le tableau global nt!PspCreateProcessNotifyRoutine. Ces callbacks seront appelés par le noyau via la routine nt!PspCallProcessNotifyRoutines à chaque lancement (ou terminaison) de processus [CALLBACK1] [CALLBACK2].
Les callbacks pour les créations de processus doivent implémenter le prototype suivant [PROCESSNOTIFYROUTINE] :
Dans l’exemple d’un dump du processus LSASS, plusieurs callbacks peuvent fournir de l’information au produit de sécurité. L’utilisation d’un exécutable ou d’une bibliothèque pour réaliser le dump provoquera la création de processus, thread(s) et le chargement de PE en mémoire, actions étant surveillées par les Kernel Callbacks *CreateProcessNotifyRoutine*, *CreateThreadNotifyRoutine* et *LoadImageNotifyRoutine* respectivement. « L’ouverture » du processus LSASS provoquera également la création d’un handle sur ce processus, observable par les callbacks enregistrés avec ObRegisterCallbacks. Enfin, l’écriture du dump sur le disque déclenchera les callbacks enregistrés avec FltRegisterFilter. On notera toutefois que les actions observables par les callbacks n’apportent qu’une information partielle sur l’évènement, puisqu’elles n’incluent pas l’action de lecture de la mémoire du processus LSASS en tant que telle.
Les informations fournies par le noyau Windows au travers des Kernel Callbacks sont ainsi très limitées, et toute l’intelligence d’analyse doit de fait être implémentée par les EDR eux-mêmes. Cette lacune des Kernel Callbacks est sans doute l’une des raisons ayant poussé Microsoft à introduire le fournisseur d’évènements « Windows-Threat-Intelligence », présenté ci-dessous.
2.3 Le fournisseur d’évènements « Windows-Threat-Intelligence »
Un dernier mécanisme pouvant être utilisé par les EDR pour superviser les systèmes Windows est le fournisseur d’évènements pour Windows (« Event Tracing for Windows (ETW) ») « Windows-Threat-Intelligence (TI) », abrégé ETW TI dans la suite de l’article. Les EDR peuvent s’enregistrer auprès du fournisseur d’évènements ETW TI afin de recevoir des informations sur les utilisations de certaines API Windows (identifiées comme communément utilisées à des fins malveillantes par Microsoft). Ces différentes API vont effectuer des appels aux fonctions ETW TI, dont le rôle sera de générer des évènements sur l’opération réalisée au travers de l’API [ETWTI].
Néanmoins, l’accès à ce flux d’évènements n’est pas autorisé à tous les processus. Seuls les services ou processus exécutés en tant que, respectivement, SERVICE_ LAUNCH_ PROTECTED_ ANTIMALWARE_ LIGHT ou PS_ PROTECTED_ ANTIMALWARE_ LIGHT et associés à un pilote Windows « Early Launch Antimalware (ELAM) » [ELAM] peuvent recevoir ces événements de Threat Intelligence.
La fonction MiReadWriteVirtualMemory, appelée par NtWriteVirtualMemory dans le cadre de la création d’un dump mémoire d’un processus comme vu précédemment, fait ainsi appel à la fonction ETW TI EtwTiLogReadWriteVm (introduite dans la version 1809 de Windows 10) :
Ces fonctions ETW TI permettent notamment la surveillance des accès mémoires interprocessus, de l’allocation de mémoire exécutable, de la manipulation de l’état d’un processus tiers ou du chargement de pilotes. Le tableau ci-dessous référence les fonctions ETW TI implémentées, et les API Windows les appelant (extraites à l’aide d’IDA) pour Windows 10 20H2 build 19042.985 (mai 2021) :
Fonction ETW Threat Intelligence |
API Windows |
EtwTiLogAllocExecVm |
MiAllocateVirtualMemory |
EtwTiLogDeviceObjectLoadUnload |
IoCreateDevice IoDeleteDevice |
EtwTiLogDriverObjectLoad |
IoCreateDriver IopLoadDriver |
EtwTiLogDriverObjectUnLoad |
MiAllocateVirtualMemory |
EtwTiLogInsertQueueUserApc |
IopfCompleteRequest KeInsertQueueApc |
EtwTiLogMapExecView |
NtMapViewOfSection MiMapViewOfSectionExCommon |
EtwTiLogProtectExecVm |
NtProtectVirtualMemory |
EtwTiLogReadWriteVm |
MiReadWriteVirtualMemory |
EtwTiLogSetContextThread |
PspSetContextThreadInternal PspWow64SetContextThread |
EtwTiLogSuspendResumeProcess |
PsFreezeProcess PsResumeProcess PsSuspendProcess PsThawProcess |
EtwTiLogSuspendResumeThread |
PsResumeThread PsSuspendThread |
Les champs des évènements générés par la fonction ETW TI EtwTiLogReadWriteVm pour les évènements de lecture de mémoire virtuelle, extraits à l’aide de l’outil EtwExplorer [ETWEXPLORER] sont les suivants :
Les champs CallingProcessId, CallingProcessSignatureLevel, TargetProcessId et TargetProcessSignatureLevel permettent notamment d’effectuer des vérifications sur le processus réalisant l’action et le processus ciblé par la lecture de mémoire virtuelle.
Il est à noter que la génération effective d’un évènement dépend à la fois de l’activation du fournisseur d’évènement associé (dans notre cas, le provider ETW TI), et de l’activation du type d’évènement observé (lecture de mémoire dans un processus distant, allocation de mémoire exécutable dans le processus local, etc.) au sein de ce provider. Pour chaque type d’évènement, un EVENT_DESCRIPTOR constant (THREATINT_READVM_REMOTE, THREATINT_ALLOCVM_LOCAL, etc.) est associé. Cette double vérification est portée par les API EtwProviderEnabled et EtwEventEnabled, directement appelées par les fonctions ETW TI et permet notamment d’éviter la génération ou l’analyse de certains types d’évènements pouvant être considérés comme non pertinents pour la surveillance de sécurité (par exemple les lectures réalisées localement par un processus sur lui-même, par opposition avec des opérations réalisées sur un processus distant).
Bien qu’offrant des capacités de détection natives plus fines, le mécanisme ETW TI ne permet toutefois pas nativement de bloquer la réalisation d’actions potentiellement malveillantes et souffre d’un délai de l’ordre de quelques secondes dans les émissions d’événements.
Conclusion
Cet article avait pour vocation d’apporter une vue d’ensemble des principales techniques de surveillance employées par les EDR et d’illustrer comment ces techniques pouvaient être utilisées pour détecter une action malveillante telle que le dump du processus LSASS. La connaissance de ces techniques de base permet à la fois de mieux comprendre ce qu’un produit de sécurité est en mesure d’observer ou non sur un système Windows, mais également d’envisager des méthodes de contournement pour chacune des techniques décrites. Ce dernier sujet fera notamment l’objet d’un article futur.
Il est nécessaire de garder à l’esprit que si les produits de sécurité sont en mesure techniquement de surveiller les types d’évènements décrits, tous les produits du marché n’implémentent pas nécessairement l’ensemble des techniques à disposition. De plus, la corrélation des données collectées pour détecter les « incidents de sécurité » reste à la charge des EDR, qui doivent constamment trouver un équilibre entre l’exhaustivité de la détection et la réduction du nombre de faux-positifs.
Remerciements
Remerciements à @brsn76945860, @fdiskyou, Benjamin DELPY (@gentilkiwi), Filip Olszak (@_lpvoid) et l’ensemble des auteurs ayant réalisé les travaux de recherche sur lesquels se base cet article.
Références
[DETOURS] Microsoft, Documentation du framework Detours : https://github.com/microsoft/Detours
[REGISTER_CALLBACKS] douggem, « OBREGISTERCALLBACKS AND COUNTERMEASURES » : https://douggemhax.wordpress.com/2015/05/27/obregistercallbacks-and-countermeasures/
[PRJTZERO] James Forshaw (@tiraniddo), « Hunting for Bugs in Windows Mini-Filter Drivers » : https://googleprojectzero.blogspot.com/2021/01/hunting-for-bugs-in-windows-mini-filter.html
[NOTIFICATIONS] Benjamin DELPY (@gentilkiwi), « WinDBG : notifications Kernel » : https://blog.gentilkiwi.com/retro-ingenierie/windbg-notifications-kernel
[CALLBACK_STRUCT] Matt Hand (@matterpreter), « Mimidrv In Depth: Exploring Mimikatz’s Kernel Driver » : https://posts.specterops.io/mimidrv-in-depth-4d273d19e148
[CALLBACK1] rui (@fdiskyou), « Windows Kernel Ps Callbacks Experiments » : http://blog.deniable.org/posts/windows-callbacks/
[CALLBACK2] brsn (@brsn76945860), « Removing Kernel Callbacks Using Signed Drivers » :
https://br-sn.github.io/Removing-Kernel-Callbacks-Using-Signed-Drivers/
[PROCESSNOTIFYROUTINE] Prototype de la fonction PcreateProcessNotifyRoutine : https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/ntddk/nc-ntddk-pcreate_process_notify_routine
[ETWTI] Filip Olszak (@_lpvoid), « Detecting process injection with ETW » : https://blog.redbluepurple.io/windows-security-research/kernel-tracing-injection-detection
[ELAM] Microsoft, « Overview of Early Launch AntiMalware » : https://docs.microsoft.com/en-us/windows-hardware/drivers/install/early-launch-antimalware
[ETWEXPLORER] Pavel YOSIFOVICH (@zodiacon), EtwExplorer : https://github.com/zodiacon/EtwExplorer