Certains d’entre vous l’aiment et d’autres le détestent, mais à ce stade, il n’est pas surprenant que le .NET soit là pour durer un peu plus longtemps que prévu. Le cadre des exigences .NET fait partie intégrante du système d’exploitation de Microsoft, la version la plus récente de .NET étant .NET Core. Core est le successeur multiplateforme du cadre des exigences qui apporte .NET sur Linux et macOS. Le .NET n’a donc jamais été aussi populaire qu’aujourd’hui pour les échanges post-exploitation, aussi bien chez les adversaires que chez les red teams. Cet article s’intéressera à un nouveau fichier d’objets de balise (BOF) qui permet aux opérateurs d’exécuter des assemblages .NET en cours via Cobalt Strike, au lieu du module d’exécution et d’assemblage intégré traditionnel, qui utilise la technique fork and run.
Cobalt Strike, un logiciel populaire de simulation d’adversaires, a reconnu la tendance des red teams à s’éloigner des outils PowerShell au profit de C# en raison de l’augmentation de la capacité de détection pour PowerShell. D’ailleurs, en 2018, la version 3.11 de Cobalt Strike a introduit le module execute-assembly. Cela a permis aux opérateurs de tirer parti de la puissance des assemblages .NET post-exploitation en les exécutant en mémoire sans risquer de laisser ces outils sur le disque. Bien que la capacité de charger des assemblages .NET en mémoire via du code non géré n’était ni nouvelle ni inconnue au moment de sa sortie, je dirais que Cobalt Strike a porté cette capacité au grand public et a contribué à continuer de renforcer la popularité de .NET pour les tâches de post-exploitation.
Le module execute-assembly de Cobalt Strike utilise la technique fork and run, qui consiste à créer un nouveau processus sacrificiel, à y injecter votre code malveillant de post-exploitation, à exécuter ce code et, une fois terminé, à tuer le nouveau processus. Cela présente à la fois ses avantages et ses inconvénients. L’avantage de la méthode fork and run est que l’exécution intervient en dehors de notre processus d’implantation d’une balise : ainsi, si un problème survient ou est détecté lors de notre tâche de post-exploitation, notre implantation a beaucoup plus de chances de survivre. Pour simplifier, cela contribue vraiment à la stabilité globale de l’implantation. Cependant, comme les fournisseurs de sécurité se sont aperçus de ce comportement de fork and run, cette technique a ajouté ce que Cobalt Strike admet être un modèle d’OPSEC coûteux.
Depuis la version 4.1 publiée en juin 2020, Cobalt Strike a introduit une nouvelle fonctionnalité pour tenter de résoudre ce problème : les fichiers d’objets de balise (BOF). Les BOF permettent aux opérateurs d’éviter les schémas d’exécution bien connus décrits ci-dessus ou d’autres défaillances OPSEC telles que l’utilisation de cmd.exe/powershell.exe en exécutant des fichiers d’objets en mémoire au sein du même processus que notre implantation de balise. Je ne vais pas présenter le fonctionnement interne des BOF, mais voici quelques articles de blog que j’ai trouvés intéressants :
Si vous lisez les articles ci-dessus, nous devrions comprendre que les BOF n’étaient pas vraiment la solution salvatrice que nous espérions. Si vous rêviez de réécrire tous ces outils .NET géniaux et de les transformer en BOF, vos rêves sont désormais anéantis. Désolé. Tout espoir n’est cependant pas perdu, car à mon avis, les BOF peuvent offrir de superbes résultats. D’ailleurs, je me suis récemment amusé (et un peu tiré les cheveux) à repousser les limites de ce qui peut être fait avec eux. Tout d’abord, j’ai créé CredBandit qui effectue un vidage complet de la mémoire d’un processus tel que LSASS et le renvoie via votre canal de communication de balise existant. Aujourd’hui, je publie InlineExecute-Assembly, qui peut être utilisé pour exécuter des assemblages .NET dans votre processus de balise sans modification de votre outil .NET favori. Voyons pourquoi j’ai écrit le BOF, certaines de ses principales fonctionnalités, ses mises en garde et comment il peut être utile lors de simulations d’adversaires et de red teams.
Newsletter sectorielle
Restez au fait des tendances les plus étonnantes du secteur dans le domaine de l’IA, de l’automatisation, des données et bien d’autres avec la newsletter Think. Consultez la Déclaration de confidentialité d’IBM.
Vous recevrez votre abonnement en anglais. Vous trouverez un lien de désabonnement dans chaque newsletter. Vous pouvez gérer vos abonnements ou vous désabonner ici. Consultez la Déclaration de confidentialité d’IBM pour plus d’informations.
La raison derrière la création d’InlineExecute-Assembly est assez simple. Je voulais trouver un moyen pour notre équipe de simulation d’adversaires d’exécuter des assemblages .NET en cours de traitement afin d’éviter certains des pièges OPSEC évoqués plus haut lors de l’utilisation de Cobalt Strike dans des environnements matures. J’avais également besoin de cet outil pour ne pas imposer à notre équipe plus de temps de développement en apportant des modifications à la plupart de nos outils .NET actuels. Il devait aussi être stable, aussi stable que puisse être un BOF complexe, car la dernière chose que nous voulons, c’est perdre l’une de nos rares balises dans l’environnement. Il devait donc fonctionner aussi facilement que possible pour l’opérateur et pour le module execute-assembly de Cobalt Strike.
Je sais, c’est assez évident. Nous n’irions pas très loin sans elle. Blague à part, les subtilités du fonctionnement du CLR et de ce qui se passe en coulisses peuvent faire l’objet d’un article de blog. Nous allons donc passer en revue ce que le BOF utilise à un très haut niveau lorsqu’il charge le CLR via du code non géré.
Chargement du CLR
Comme le montre la capture d’écran simplifiée ci-dessus, les principales étapes que le BOF va suivre pour charger le CLR sont les suivantes :
Le CLR est donc initialisé, mais il reste encore quelques étapes à franchir avant de pouvoir exécuter nos assemblages .NET préférés. Nous devons créer notre instance AppDomain, ce que Microsoft décrit comme « un environnement isolé dans lequel les applications s’exécutent ». En d’autres termes, ils seront utilisés pour charger et exécuter nos assemblages .NET post-exploitation.
Création d’AppDomain et chargement/exécution de l’assemblage
Comme le montre la capture d’écran simplifiée ci-dessus, les principales étapes du BOF pour charger et invoquer notre assemblage .NET sont les suivantes :
J’espère que vous comprenez désormais l’exécution de .NET avec du code non géré. Toutefois, cela ne nous permet toujours pas d’avoir un outil opérationnellement performant. Nous allons donc examiner quelques fonctionnalités qui ont été implémentées dans le BOF pour le faire passer de moyen à fantastique.
Vous vous demandez probablement pourquoi c’est important. Si vous êtes comme moi et que votre temps vous importe, vous ne devriez pas le passer à modifier presque tous les assemblages .NET pour que son point d’entrée renvoie une chaîne avec toutes vos données qui seraient normalement acheminées vers une sortie standard de console, pas vrai ? C’est bien ce que je pensais. Pour éviter cela, nous devons rediriger notre sortie standard vers un pipeline nommé ou un emplacement de messagerie, lire la sortie après qu’elle ait été écrite, puis la ramener à son état d’origine. De cette manière, nous pouvons exécuter nos assemblages non modifiés comme nous le ferions depuis cmd.exe ou powershell.exe. Avant de passer en revue le code, je dois remercier @N4k3dTurtl3 et son article de blog sur l’assemblage en cours d’exécution et les emplacements de messagerie. C’est ce qui m’a poussé à mettre en œuvre cette technique dans ma propre implantation C privée lorsqu’elle est sortie pour la première fois et, plusieurs mois plus tard, j’ai porté cette même fonctionnalité sur un BOF. Maintenant que les propriétés ont été attribuées, observons un exemple simplifié de la manière dont cela serait réalisé en redirigeant le stdout vers un pipeline nommé ci-dessous :
Rediriger la sortie standard de la console vers le pipeline nommé et revenir en arrière
Rappelez-vous que lorsque nous avons chargé le CLR via ICLRMetaHost -> GetRuntime, nous devions spécifier la version du cadre des exigences .NET dont nous avions besoin. Vous vous souvenez que cela dépend de la version avec laquelle notre assemblage .NET a été compilé ? Ce ne serait pas vraiment amusant de devoir spécifier manuellement la version requise à chaque fois. Heureusement pour nous, @b4rtik a implémenté une fonction intéressante pour gérer cela dans son module execute-assembly pour cadre des exigences Metasploit, que nous pouvons facilement intégrer dans nos propres outils présentés ci-dessous :
Fonction qui lit notre assemblage .NET et aide à déterminer quelle version .NET nous devons utiliser lors du chargement du CLR
Essentiellement, cette fonction fait que lorsqu’elle est transmise à nos octets d’assemblage, elle lira ces octets et recherchera les valeurs hexagone de 76 34 2E 30 2E 33 30 33 31 39, qui lorsqu’elle est convertie en ASCII est v4.0.30319. J’espère que cela vous semble familier. Si cette valeur est trouvée lors de la lecture de l’assemblage, la fonction renvoie 1 ou true, et si elle n’est pas trouvée, elle renvoie 0 ou false. Nous pouvons l’utiliser pour déterminer facilement la version à charger selon que 1/true ou 0/false revient, comme le montre l’exemple de code ci-dessous :
Argument if/else pour définir la variable de version .NET
Nous ne pouvons pas parler de l’ingénierie offensive .NET sans parler de l’AMSI. Même si nous n’entrerons pas dans les détails de ce qu’est l’AMSI et toutes les façons dont elle peut être contournée, car cela a été abordé à maintes reprises, nous allons expliquer pourquoi il peut être nécessaire de corriger l’AMSI selon ce que vous décidez d’exécuter via le BOF. Par exemple, si vous décidez d’exécuter Seatbelt sans aucune obfuscation, vous remarquerez rapidement que vous n’avez reçu aucune sortie en retour et que votre balise est morte. Oui, morte morte. En effet, l’AMSI a détecté votre assemblage, a déterminé qu’il était malveillant et vous a stoppé net, comme une soirée un peu trop bruyante. Ce n’est pas l’idéal. Nous avons deux bonnes options pour AMSI : soit nous pouvons obscurcir nos outils .NET via un élément comme ConfuserX ou Invisibility Cloak, soit désactiver AMSI en utilisant différentes techniques. Dans notre cas, nous utiliserons un élément de RastaMouse, qui sert à corriger le fichier amsi.dll en mémoire pour qu’il renvoie E_INVALIDARG et renvoie un résultat d’analyse de 0. Comme indiqué dans l’article de blog, il est généralement interprété comme AMSI_RESULT_CLEAN. Examinons ci-dessous une version simplifiée du code pour un processus x64 :
Correction d’AmsiScanBuffer
Comme vous pouvez le voir dans la capture d’écran ci-dessus, nous procédons simplement comme suit :
En l’implémentant dans notre outil, nous devrions désormais être en mesure d’exécuter la version par défaut de Seatbelt.exe en utilisant l’indicateur –amsi pour contourner la détection AMSI, comme indiqué ci-dessous :
Exemple de contournement AMSI InlineExecute-Assembly
Heureusement pour les défenseurs, il n’y a pas qu’AMSI pour détecter des tâches .NET malveillantes en utilisant ETW. Malheureusement, comme AMSI, il est assez facile pour les adversaires de le contourner. @xpn a d’ailleurs fait des recherches vraiment impressionnantes sur la façon dont cela pourrait être fait. Voici un exemple simplifié de la manière dont vous pourriez appliquer des correctifs ETW pour le désactiver complètement :
Correction d’EtwEventWrite
Comme vous pouvez le voir sur la capture d’écran ci-dessus, les étapes sont quasiment identiques à celles que nous avons suivies pour corriger l’AMSI. Je ne vais donc pas revenir sur les étapes pour celui-ci. Vous pouvez voir une capture d’écran avant-après montrant l’utilisation de l’indicateur –etw ci-dessous :
Utiliser Process Hacker pour consulter les propriétés de PowerShell.exe avant de lancer InlineExecute-Assembly avec l’indicateur –etw
Exécuter InlineExecute-Assembly à l’aide de l’indicateur –etw
Utiliser Process Hacker pour afficher les mêmes propriétés de PowerShell.exe après avoir exécuté InlineExecute-Assembly
Par défaut, l’AppDomain, le Named Pipe ou le Mail Slot créés utilisent la valeur par défaut « totesLegit ». Ces valeurs peuvent être modifiées pour mieux s’intégrer dans l’environnement que vous testez, soit en modifiant le script d’agression fourni, soit via des indicateurs de ligne de commande à la volée. Vous trouverez ci-dessous un exemple de modification via la ligne de commande :
Exemple de InlineExecute-Assembly utilisant un nom unique AppDomain et un nom unique de pipeline nommé
Exemple unique de nom AppDomain ChangedMe
Exemple de pipeline LookAtMe nommé unique
Exemple de suppression d’AppDomain une fois l’exécution terminée
Exemple de suppression du pipeline nommé après l’exécution terminée
Cette section sera en quelque sorte une répétition de ce que j’explique dans le référentiel GitHub, mais j’ai pensé qu’il était important de rappeler quelques éléments à garder à l’esprit lorsque vous utilisez cet outil :
Voici quelques considérations défensives :