Les EDRs, mode d’emploi
Le but de cet article est d’exposer l’ensemble des sources de détection employées aujourd’hui par les solutions existantes d’EDR ainsi que leurs limitations, et, dans un second temps, de mettre en comparaison ces éléments avec les détections pouvant survenir lors d’un test d’intrusion typique, ainsi que de mettre en contraste avec les techniques fréquemment observées chez des acteurs malveillants, typiquement lors d’une intrusion précédent le déploiement d’un ransomware.
L’article va se concentrer sur les détections sur un environnement Windows, de fichiers compilés malveillants. Cela n’inclut donc pas des sujets comme la détection de scripts PowerShell ou Python malveillants, par exemple.
Enfin, cela ne concerne que partiellement les applications en code managé (.NET), car ces applications ne sont pas directement compilées en code natif, seulement en code intermédiaire (en général).
L’analyse statique
La fonctionnalité d’analyse statique est la fonctionnalité de détection la plus connue des solutions d’antivirus et d’EDR. Il s’agit de la reconnaissance de patterns connus comme appartenant à du code malveillant, suite à une analyse manuelle. Ces patterns sont principalement de deux types différents, soit des chaînes de caractères ou des données bien spécifiques. Par exemple, le motif présent au sein de mimikatz dispose de nombreuses signatures :
.#####. mimikatz 2.0 .## ^ ##. ## / \ ## /* * * ## \ / ## Benjamin DELPY `gentilkiwi` ( [email protected] ) '## v ##' https://blog.gentilkiwi.com/mimikatz (oe.eo) '#####' with 13 modules * * */
L’autre type de patterns est une suite d’instructions assembleur bien spécifique à ce programme malveillant.
Un format universel permettant de se partager ce genre de règle des détection a été créé pour cela : le format Yara.
rule XorExample2 { strings: $xor_string_00 = "This program cannot" $xor_string_01 = "Uihr!qsnfs`l!b`oonu" $xor_string_02 = "Vjkq\"rpmepco\"acllmv" // Repeat for every single byte XOR condition: any of them }
La principale limitation de cette analyse statique, est qu’elle est contournée par des reflective loaders. Ce sont de petits programmes, dont le rôle est d’exécuter en mémoire un plus gros programme malveillant pour lequel il existe des signatures connues. Comme c’est le reflective loader qui charge le programme final, il est possible de chiffrer ou d’encoder au préalable le programme malveillant final, cassant ainsi les signatures et rendant les règles de détection inefficaces, le reflective loader se chargeant de déchiffrer ou décoder le programme final juste avant son exécution.
Il est possible tenter de détecter statistiquement des anomalies. Par exemple, si l’exécutable contient une grande zone mémoire ayant une forte entropie de Shannon, cela pourrait indiquer la présence d’un contenu chiffré, mais il est simple de baisser cette entropie, soit en ajoutant des données redondantes (par exemple, de nombreux zéros), soit en séparant le contenu chiffré dans un fichier séparé.
L’analyse dynamique
Sandboxing
Afin de contourner le problème que posent les reflective loaders, une première solution qui a été mise en place est de récupérer l’exécutable en question, et de l’exécuter sous un hyperviseur sur une machine tierce, généralement hébergée sur un cloud. Cela permet d’observer le comportement, notamment le trafic réseau peu après le lancement de l’application.
Cela a une grosse limitation : ce sandboxing se déroulant avant l’exécution réelle du programme, cette analyse ne peut pas être très longue. Ainsi, pour un auteur de malware, il est possible d’abuser de ces différences de comportements. Par exemple, il est possible de vérifier que le nom de domaine de la machine sur laquelle le code correspond bien à celui attendu, ou encore d’effectuer un Sleep de quelques minutes avant d’exécuter le code malveillant.
Certaines plateformes tentent de modifier l’environnement pour que les fonctions telles que Sleep retournent immédiatement. Cela n’est toutefois que peu efficace, car il suffit de ne pas appeler de bibliothèque tierce pour Sleep, et de simplement effectuer des opérations prenant du temps à s’exécuter, par exemple, un algorithme mathématique de calcul (nombre premiers, multiplications matricielles, etc.) mal optimisés.
Userland hooking
La première méthode d’analyse du comportement dynamique à être apparue est celle du hooking.
Le principe est de remplacer la première instruction d’une fonction par un saut inconditionnel vers une fonction d’analyse par l’EDR, puis, si l’exécution est autorisée, un second saut qui retourne vers la fonction originale, après avoir exécuté les instructions qui ont été réécrites par le saut.
Certaines implémentations d’EDR réécrivent la seconde instruction plutôt que la première, ce qui est plus difficile à détecter.
Par exemple, la fonction NtOpenProcess
pourrait initialement ressembler à ceci :
0:000> u ntdll!NtOpenProcess ntdll!NtOpenProcess: 00007ffc`74d6fe00 4c8bd1 mov r10,rcx 00007ffc`74d6fe03 b826000000 mov eax,26h 00007ffc`74d6fe08 f604250803fe7f01 test byte ptr [SharedUserData+0x308 (00000000`7ffe0308)],1 00007ffc`74d6fe10 7503 jne ntdll!NtOpenProcess+0x15 (00007ffc`74d6fe15) 00007ffc`74d6fe12 0f05 syscall 00007ffc`74d6fe14 c3 ret 00007ffc`74d6fe15 cd2e int 2Eh 00007ffc`74d6fe17 c3 ret
En comparaison, la fonction une fois hookée ressemble à ceci :
0:017> u ntdll!NtOpenProcess L10 ntdll!NtOpenProcess: 00007ffc`74d6fe00 e97b339aa8 jmp [redacted.dll]+0x63180 (00007ffc`1d713180) 00007ffc`74d6fe05 0000 add byte ptr [rax],al 00007ffc`74d6fe07 00f6 add dh,dh 00007ffc`74d6fe09 0425 add al,25h 00007ffc`74d6fe0b 0803 or byte ptr [rbx],al 00007ffc`74d6fe0d fe ??? 00007ffc`74d6fe0e 7f01 jg ntdll!NtOpenProcess+0x11 (00007ffc`74d6fe11) 00007ffc`74d6fe10 7503 jne ntdll!NtOpenProcess+0x15 (00007ffc`74d6fe15) 00007ffc`74d6fe12 0f05 syscall 00007ffc`74d6fe14 c3 ret 00007ffc`74d6fe15 cd2e int 2Eh 00007ffc`74d6fe17 c3 ret
Il faut uniquement prêter attention à la première instruction, les autres ne sont pas désassemblées correctement car le jmp
a réécrit une partie des instructions suivantes.
Cette première instruction a été remplacée par une instruction jmp
, un saut inconditionnel, qui permettra de journaliser l’appel qui est fait à la fonction, les arguments qui sont utilisés, le thread appellant, etc. Cela permet d’observer le comportement de l’application lors de son exécution.
Bien que l’on entende très fréquemment parler de hooks pour les appels systèmes, comme NtOpenProcess
dans l’exemple ci-dessus, ce ne sont pas les seules fonctions à se faire hooker. En particulier, il est intéressant pour un EDR de hooker les fonctions de bibliothèques Windows dont le comportement est non-trivial à reproduire.
On retrouve fréquemment des hooks sur les fonctions liées au chargement dynamique de code (LoadLibrary
, LdrLoadDll
, GetProcAddress
, LdrGetProcedureAddress
, etc), ou encore les fonctions liées aux communications HTTP (les différentes fonctions de wininet.dll
, winhttp.dll
)
Idéalement, les solutions EDR préfereraient pouvoir hooker les appels système du côté kernel, car un programme utilisateur standard malveillant ne pourrait pas interférer avec ces hooks.
Cela n’est toutefois pas employé par la majorité des solutions, car Windows tente d’interdire aux drivers de faire cela (bien que ça n’empêche pas certaines solutions à y recourir via des contournements connus, mais instables).
Un malware tentant d’échapper à la visibilité de ces hooks tentera alors d’interférer ceux-ci. Il est intéressant de procéder différemment pour les hooks sur les syscalls et pour les fonctions des autres bibliothèques.
Tout d’abord, concernant les syscalls, il est à noter que toutes les fonctions servant à les appeler ont le même préfixe, à savoir :
mov r10,rcx mov eax,<numéro syscall> test byte ptr [SharedUserData+0x308],1 jne rip+0x5 syscall
Une subtilité est que le numéro du syscall pour un syscall donné n’est pas constant entre les versions de Windows.
Par conséquent, un malware voulant éviter le hook, doit retrouver le numéro du syscall de la fonction qui l’intéresse, refaire les premières instructions, puis sauter sur l’instruction syscall directement.
Il existe différentes techniques pour récupérer dynamiquement les numéros des appels système, dont certains sont recensées dans cet article.
Pour que les hooks s’appliquent à des fonctions de bibliothèque n’étant pas des appels systèmes, le préfixe de ces fonctions n’est pas constant, il n’est donc pas aussi simple de le retrouver. Cependant, une fois qu’il est possible d’exécuter des syscalls sans passer par les hooks, il est possible d’effectuer des syscalls pour lire les fichiers DLL sur disque, puis de les parser, pour récupérer les premiers octets des fonctions, qui ont été écrasés par le `jmp`.
Certaines implémentations de unhooking vont ensuite réécrire le code contenant le jmp
par les instructions originales pour enlever le hook placé par la solution d’EDR, mais cela peut être détecté par une vérification de l’intégrité du hook.
Memory Scanning
Afin de tenter de pallier aux limites des détections statiques, les solutions d’EDR peuvent mettre en place des « scans mémoire » pour essayer de repérer un programme malveillant connu une fois qu’il a été chargé en mémoire par le reflective loader, puisque le programme malveillant ne peut pas être chiffré ou encodé lors de son exécution.
Cela peut avoir lieu soit périodiquement (par exemple, toutes les quelques heures sur l’ensemble des processus d’une machine), ou bien sur un processus spécifique, suite à des actions suspectes.
La limitation principale est qu’effectuer ce genre de scan de mémoire sur l’ensemble de la mémoire d’un processus peut être très consommateur en performances, cela ne peut donc être fait en permanence. Les solutions d’EDR tentent de limiter la mémoire d’un processus qui est scanné, par exemple en ne ciblant que la mémoire exécutable du processus allouée dynamiquement.
Seuls les malwares de type Command And Control (C2) sont affectés par ce genre de détection, car ce sont les seuls à demeurer suffisamment longtemps en mémoire pour être affectés.
Pour contourner ce vecteur de détections, puisque la majorité du temps ces programmes sleepent en attendant le prochain intervalle de temps afin de vérifier s’il y a des nouvelles commandes à exécuter : ils peuvent simplement se re-chiffrer en mémoire lors de leurs périodes de sleep puis se déchiffrer avant de reprendre leur exécution. Lorsque les scans mémoire surviendront, les EDR ne verront alors que des données chiffrées – qui ne correspondent pas à des patterns connus.
Callbacks Kernel
Process Notification Callbacks
Puisque les drivers des solutions d’EDR ne sont pas en mesure de mettre en place des hooks côté kernel, Microsoft a mis en place des callbacks, permettant d’informer les drivers de certains événements. Les plus connus sont les callbacks liés aux processus, enregistrés via la famille des API PsSetCreateProcessNotifyRoutine
.
Ces callbacks servent fréquemment à injecter au sein du processus une DLL qui sera en charge de mettre en place le hooking userland, comme vu précédemment, et ce avant l’exécution du début du processus.
Ces callbacks servent également pour des politiques que certains EDR peuvent avoir, notamment, empêcher des processus inconnus de créer des processus fils.
Cela sert notamment à détecter le comportement de commandes de type shell
sur la plupart des C2 existants (comme par exemple Cobalt Strike) qui sert à exécuter des commandes directement en créant un processus fils, ou bien de la post-exploitation utilisant le principe de fork & run, dans lequel un processus fils légitime est créé, le contenu de l’exécutable est remplacé en mémoire par un module de post exploitation, et l’exécution est reprise.
Ces callbacks ne sont pas contournables par un malware. Toutefois, la solution pour éviter ces politiques est de simplement tout exécuter au sein du même processus. En pratique, cela se traduit par le développement de Beacon Object Files, qui est un moyen spécifique de développer des modules de post-exploitation qui seront exécutés en mémoire au sein du même processus.
Image Load Notification callback
Ces callbacks sont invoqués lors de l’appel en userland à la fonction NtMapViewOfSection
, qui sert à mapper en mémoire l’objet Section correspondant à la DLL qui est en train d’être chargée.
En soit, il est possible de ne pas invoquer ces callbacks, en réimplémentant l’ensemble du mapping d’une DLL en mémoire. Toutefois, cela est d’une complexité importante à mettre en place, et implique d’avoir des zones mémoires non associées à des fichiers sur disque. Il s’agit donc d’une décision à prendre pour un malware.
Il existe un outil tel quel DarkLoadLibrary, qui réimplémente manuellement la mise en mémoire de la DLL, toutefois il ne s’agit que d’une implémentation partielle.
En effet, la DLL est mise en mémoire manuellement, mais ses dépendances sont chargées via un appel à LoadLibraryA
, qui va déclencher des appels à NtMapViewOfSection
.
Cela peut toutefois servir pour échapper à certaines détections qui se basent sur ces éléments, comme par exemple, certaines règles de détection de post-exploitation.
Object callback
Les Object Callback sont des callbacks enregistrés auprès de l’Object Manager via l’API ObRegisterCallbacks
, qui sont invoqués avant ou après une opération sur un HANDLE
d’un type spécifié.
En particulier, les solutions d’EDR mettent souvent en place ce genre de callback pour les objets de type Process et Thread. Cela permet, entre autres, d’observer la demande de création d’un HANDLE
vers le processus lsass.exe
: si la demande contient un droit d’accès tel que PROCESS_VM_READ
, soit tuer le processus ayant fait la demande, soit simplement retirer ce droit – afin d’empêcher les dumps de lsass.exe
.
Opérations IO (Filesystem, Network)
Par ailleurs, les drivers de solution d’EDR comportent souvent un driver de type minifilter. Ce sont des drivers qui vont s’enregistrer auprès du filter manager, pour, lorsqu’un requête IO (création ou modification de fichiers sur disque, entre autres) est effectuée, obtenir la visibilité dessus et éventuellement prendre des actions.
Cela est utilisé pour de nombreux usages, par exemple, pour détecter qu’un nouveau fichier ayant une extension .exe
est créé et qu’il faut donc effectuer une analyse statique. Cela peut aussi servir à bloquer un exécutable dont l’analyse a révélé qu’il était malveillant, avant que le fichier ne soit supprimé.
Enfin les drivers incluent des Callout Drivers, qui permettent d’observer le contenu du trafic réseau.
Cela peut également permettre de détecter certains patterns connus au sein d’opérations réseau, par exemple, un meterpreter n’utilisant pas de chiffrement peut se faire détecter de cette façon, car il a un protocole bien défini et repérable.
ETW
ETW, l’acronyme de Event Tracing For Windows, est le mécanisme de Windows permettant de collecter informations sur l’exécution de différents composants de l’OS.
Bien qu’initialement conçu plus pour pouvoir identifier la source de problèmes dans les applications Windows, son usage peut également servir à des solutions d’EDR.
ETW est architecturé avec des providers, qui sont différentes sources qui fournissent des types différents d’informations, par exemple, sur un type d’opérations réseau spécifique (HTTP, LDAP, etc.) ou encore sur les authentifications Windows.
Des consumers peuvent ensuite venir s’attacher à plusieurs providers dans une Tracing Session, qui permettra de collecter les événements générés par les différentes sources, soit en instantané, soit en différé.
Un exemple parlant de consumer ETW est l’application Event Viewer sur Windows, qui affiche dans une interface graphique les événements de certains providers.
Ces événements sont générés par différentes sources. Certaines sont situées en userland, et d’autres en kernelland.
Par exemple, si un processus fait des requêtes LDAP, via la bibliothèque Wldap32.dll
, cela génèrera des événements ETW. Toutefois, ceux-ci seront générés en userland, via les appels système NtTraceEvent
et NtTraceControl
. Comme ceux-ci sont générés en userland, un malware pourrait modifier l’une de ces fonctions ou l’une des fonctions appelantes, pour ne pas faire l’appel à ces fonctions, ce qui permettrait d’occulter cette télémétrie.
Toutefois, d’autres événements sont générés au niveau du kernelland, qui eux ne peuvent pas être occultés par un malware.
En particulier, parmi ces providers, on peut citer Microsoft-Windows-Threat-Intelligence
, qui fournit des événements liés à la gestion de la mémoire virtuelle et des threads d’un processus. Entre autres, cela inclut les modifications des permissions des pages mémoires.
Les événements sont divisés en deux catégories : ceux locaux, et ceux cross-processus. Les événements cross-processus peuvent aider à identifier les injections de processus, alors que les événements locaux peuvent aider à identifier les reflective loaders, entre autres.
Toutefois, la difficulté principale liée à cette télémétrie est le nombre d’événements, et le taux de faux positifs : par exemple sur Windows, toutes les applications en .NET utilisent du code dynamique, et donc des allocations de code RWX. Similairement, les injections de code sont également relativement fréquentes sur des environnements Windows.
Cet article donne le détail de ces événements, ainsi que les limitations et assouplissements qui sont en place du fait du nombre trop important de faux positifs.
L’analyse de la callstack
Les événements ETW sont également intéressants, puisqu’ils contiennent les informations de la callstack du thread lorsque l’événement a été enregistré.
Par conséquent, si un reflective loader a été utilisé, une zone mémoire a été allouée dynamiquement pour contenir le payload final, celui-ci a été copié dedans, puis la zone mémoire a été rendue exécutable.
Cette zone mémoire n’est donc pas associée à un fichier sur disque, car elle a été allouée dynamiquement. Ainsi, si une telle zone mémoire apparaît dans la callstack d’un processus lors d’un événement ETW, cela peut être considéré comme probablement malveillant.
Il existe trois grands types de méthodes pour avoir une callstack ne faisant pas apparaître de zone mémoire non associée à un fichier sur disque.
La première consiste à utiliser des threads différents, qui ne contiennent pas de zone exécutable non associée à un fichier sur disque dans leur callstack. En particulier, sur Windows, il existe le Thread Pool, qui consiste en un ensemble de threads managés, qui sont présents dans l’ensemble des processus, et qui permettent d’effectuer des actions en différé. Il est donc possible de demander à ces threads d’effectuer des actions qui nous intéressent, par exemple des appels systèmes générant de la télémétrie ETW. Comme ce n’est pas dans ces threads que s’exécute le payload principal, la zone non associée à un fichier sur disque n’apparaît pas dans la télémetrie ETW.
Cette technique a une limitation principale : les appels systèmes ne sont pas faits dans le même thread. Pour un payload de type C2, il n’est donc pas possible de faire de cette façon l’appel système pour faire sleep l’agent. De plus, des opérations spécifiques à un thread ne peuvent pas fonctionner, notamment voler un token Windows.
La seconde technique est plus difficile à implémenter : elle implique de complètement modifier sa callstack, pour quelle fasse uniquement apparaître des modules légitimes. Fondamentalement, la callstack est complètement sous le contrôle du programme en mode utilisateur, la difficulté n’est pas de faire l’appel d’une fonction avec une callstack arbitraire, mais de récupérer le flux d’exécution une fois que la fonction retourne.
Un des exemples d’implémentation existant utilise des gadgets, qui sont des suites d’instructions assembleur dans des modules Windows légitimes, qui ensemble forment une callstack complète, mais qui, lorsqu’ils sont exécutés, vont modifier la stack, ce qui va désynchroniser les retours de fonctions successives, pour pouvoir récupérer le flux de l’exécution du programme.
La dernière catégorie consiste à mettre en mémoire une DLL légitime, associée à un fichier sur disque, puis de modifier le contenu en mémoire de cette DLL (module stomping). Toutefois, cela peut mener à des détections spécifiques, notamment des événements ETW « locaux » au processus, et cela modifie un attribut de la page mémoire spécifique lié au copy-on-write.
Des pièges en mémoire
Enfin, différentes solutions emploient différentes techniques peu documentées pour tenter d’obtenir des éléments de télémétrie supplémentaires.
On peut citer différents mécanismes, tels que :
– L’utilisation des Intrumentation Callback, permettant à une solution d’EDR de récupérer le flux d’exécution d’un programme après chaque instruction syscall
.
– La présence de fausses entrées au sein des listes chaînées des modules dans le PEB. Si un programme tente d’accéder à un zone mémoire associée à ces fausses entrées, une exception est levée, car la mémoire est protégée avec l’attribut PAGE_GUARD
. L’exception est alors attrapée par l’EDR, qui pourra noter ce comportement anormal.
– L’enregistrement de fonctions de callback lors du chargement dynamique de DLL via LdrRegisterDllNotification du côté userland.
– Des vérifications d’intégrité, pour s’assurer que certaines fonctions, comme NtTraceEvent
n’ont pas été modifiées en mémoire.
Bien que ces mécanismes puissent générer de la télémétrie supplémentaire, ces différentes techniques ne sont que peu robustes : un malware étant au courant de l’existence de ces mécanismes pourra simplement les retirer avant d’exécuter son programme malveillant.
Par rapport à un pentest « typique »
Aujourd’hui, il n’est plus du tout aussi trivial de s’affranchir des mécanismes de détection et de blocage des EDR.
Aussi, afin de contourner ce problème, en test d’intrusion notamment, des méthodes alternatives sont employées. Depuis un poste physiquement présent au sein du réseau (ou une Raspberry Pi, par exemple), l’auditeur va lancer la majorité de ses attaques depuis son propre poste, qui n’est pas monitoré. De plus, les actions menées vont tenter au maximum de ne pas exécuter de programme directement sur la machine cible, et utiliser uniquement des appels de procédures à distance (RPC) existants sur l’ensemble des machines Windows.
Il est difficile pour un EDR de correctement bloquer ces attaques, puisqu’il n’y a pas vraiment de processus sur la machine victime à bloquer. Au mieux il est possible de le détecter, car le processus malveillant est sur une machine distante non monitorée.
Certains EDR tentent tout de même de bloquer certains appels RPC en tuant le processus sur la machine victime, mais en général ces détections ne sont pas très robustes. Par exemple, l’utilitaire secretsdump.py
de la suite Impacket
permet de faire un dump de la base SAM à distance. Pour ce faire, les ruches de registre correspondantes sont d’abord écrites sur disque, puis récupérées via le protocole SMB. Certaines solutions d’EDR vont tenter de détecter ce dump, pour essayer de rapidement le supprimer, mais cela laisse une fenêtre de temps durant laquelle le fichier peut être lu.
Bien que cela puisse représenter certains scénarios de compromission (accès VPN compromis, appliance non patchée, application Web compromise, accès physique), cela ne couvre pas l’ensemble des scénarios existants (phishing, accès RDP exposé, etc)
Par rapport à de vrais attaquants
Tous les attaquants n’ont pas forcément les compétences ou les moyens de contourner ces solutions. Pour arriver à leurs fins, il est très fréquent de voir l’utilisation d’utilitaires légitimes d’accès à distance (TeamViewer, AnyDesk, etc). Bien qu’il soit simple de détecter l’usage de ces utilitaires, il n’est pas possible de les bloquer par défaut, car ils sont également très fréquemment utilisés pour de l’administration système.
Cela est également vrai pour les différentes phases d’une attaque, où les attaquants vont abuser d’utilitaires légitimes pour ne pas être bloqués par des solutions EDR (par exemple, utilisation d’ADExplorer pour de la reconnaissance, de PsExec pour du mouvement latéral, etc).
Enfin, les attaquants tentent aussi de désactiver l’EDR en place. Pour ce faire, un accès administrateur est nécessaire. Avec celui-ci, les attaquants vont tenter d’altérer le driver de l’EDR, chargé dans le kernel.
Pour pouvoir interagir avec la mémoire du kernel, ils vont charger un driver connu pour avoir une vulnérabilité, puis exploiter cette vulnérabilité pour retirer les différents callbacks et points de visibilité de l’EDR, qui ne sera alors plus en mesure de détecter des programmes malveillants, même connus statiquement. Il existe différents exemples open-source, comme EDRSandBlast.
Différents vendeurs de solution d’EDR ont déjà signalé avoir observé ce genre de comportement lors de vraies intrusions.
Pour contrer cela, depuis Windows 11, une blocklist de drivers connus comme vulnérables existe, mais la liste n’est pas forcément à jour, et tous les drivers vulnérables ne sont pas forcément connus.