
Un toolkit CobaltStrike pour écrire les fichiers produits par Beacon en mémoire plutôt que sur le disque.
MemFiles est une boîte à outils pour CobaltStrike qui permet aux opérateurs d'écrire en mémoire les fichiers produits par le processus Beacon, plutôt que de les écrire sur le disque du système cible. Il a été testé avec succès sur Windows 7, 10 et 11 ; les versions serveur correspondantes devraient fonctionner sans problème. MemFiles est limité aux Beacons x64.
Pour ce faire, il hooke plusieurs NtAPI différentes dans NTDLL.dll et redirige les appels vers ces API vers des fonctions injectées dans l'espace mémoire du processus Beacon.
MemFiles suppose une copie propre/non hookée de NTDLL dans le processus Beacon. Aucune garantie n'est fournie quant à la viabilité de MemFiles dans un processus Beacon où des hooks EDR sont toujours en place. Réparez/actualisez NTDLL avant d'utiliser MemFiles !
Un répertoire « spécial », inexistant, est défini dans la boîte à outils MemFiles ; tous les fichiers écrits dans ce répertoire spécial sont capturés par MemFiles et écrits en mémoire, où ils peuvent ensuite être téléchargés vers le Teamserver.
MemFiles est compatible avec la plupart (pas tous) des outils qui s'exécutent dans le processus Beacon et qui peuvent être configurés pour écrire leur sortie dans un répertoire spécifique. Il ne nécessite PAS de privilèges élevés pour fonctionner.
Cela inclut :
-Les BOF
-Les assemblys .NET exécutés en ligne via quelque chose comme inline-executeAssembly
-Les PE exécutés en ligne via quelque chose comme Inline-Execute-PE
Tous ces éléments sont compatibles car ils s'exécutent dans le processus Beacon, où les NtAPI concernées ont été hookées.
MemFiles ne fonctionne PAS avec des éléments comme :
-execute-assembly
-shell
-run
Aucun de ces éléments n'est compatible car ils lancent tous d'autres processus dont les NtAPI n'ont PAS été hookées.
MemFiles a été testé avec succès avec des outils comme Rubeus, SharpHound, Procdump et PowerShell lorsqu'ils sont exécutés dans le processus Beacon.

Clonez le dépôt et modifiez éventuellement la variable hookdir définie à la ligne 56 dans /PIC/Source/NtCreateFile.c et /PIC/Source/NtOpenFile.c. Cette variable est le répertoire « spécial » qui indique à MemFiles qu'il doit intercepter le fichier en cours de création. La variable hookdir est définie sur « redteam » par défaut. Assurez-vous que cette variable ne correspond pas à un répertoire réel sur le système cible et qu'elle est identique dans les deux fichiers !

Exécutez 'make all' pour compiler à la fois les BOF nécessaires et les fonctions PIC.
Chargez MemFiles.cna dans le client CobaltStrike. Assurez-vous que le répertoire à partir duquel CobaltStrike est exécuté est accessible en écriture par votre utilisateur ; MemFiles y crée un fichier texte (memfiles.txt) afin de garantir la disponibilité des données nécessaires au fonctionnement de MemFiles.
MemFiles peut être configuré pour s'installer dans chaque nouveau Beacon qui se connecte au Teamserver ; cela s'effectue via l'élément de menu MemFiles->Config. Par défaut, MemFiles ne s'installe PAS automatiquement dans les nouveaux Beacons. Notez qu'il s'agit d'un paramètre global ; si deux Clients sont connectés au Teamserver et que les deux ont chargé MemFiles.cna, si le Client A active le paramètre « Install on beacon initial », la modification s'appliquera également au Client B !

MemFiles comprend 4 commandes orientées cible qui exécutent des BOF et 1 commande interne qui manipule la structure de données du projet.
Côté cible :
Structure de données interne :
meminit est responsable de l'installation de MemFiles dans le processus Beacon.
La liste des NtAPI hookées par MemFiles est la suivante :
meminit effectue les actions principales suivantes :
memlist est utilisé pour afficher tous les fichiers actuellement stockés en mémoire par MemFiles pour un Beacon donné.

Plusieurs champs sont affichés, les plus pertinents et intéressants pour l'utilisateur étant le nom du fichier et la longueur des données stockées.
memfetch est utilisé pour récupérer effectivement les fichiers stockés en mémoire par MemFiles pour un Beacon donné.
Par défaut, memfetch récupère tous les fichiers stockés par MemFiles dont le « handle » a été fermé. Ce choix de conception a été fait pour éviter tout problème lié au téléchargement d'un fichier qu'un programme ou une application n'a pas fini d'écrire.
Cela signifie que si un programme ou une application ne ferme pas le handle qu'il ouvre sur le fichier, le fichier ne sera pas téléchargé par memfetch.
Cela peut être atténué en utilisant l'argument « force » avec memfetch, c.-à-d. 'memfetch force', afin de récupérer tous les fichiers depuis la mémoire, quel que soit l'état de leur handle.
Les fichiers que memfetch récupère depuis la mémoire sont renvoyés au Teamserver sous forme de téléchargement et peuvent être synchronisés du Teamserver vers le Client via l'onglet Downloads dans CobaltStrike.
Une fois qu'un fichier a été téléchargé par le Teamserver, il est effacé de la mémoire du processus Beacon et son entrée, telle qu'affichée via memlist, est supprimée.
memclean est responsable du nettoyage et de la suppression de MemFiles d'un processus Beacon.
Le cas d'usage standard de MemFiles consiste à l'installer et à le laisser installé pendant toute la durée de vie du Beacon ; cependant, si l'on souhaite utiliser MemFiles en conjonction avec un outil pour capturer et récupérer la sortie de fichiers, puis désinstaller MemFiles afin que ses artefacts ne soient pas en mémoire, memclean peut être utilisé pour ramener le processus Beacon à son état d'origine avant l'exécution de meminit.
Cela implique :
Notez qu'avant d'effectuer ces actions, memclean téléchargera de force tous les fichiers stockés en mémoire par MemFiles. Si l'on a l'intention d'utiliser MemFiles avec un seul outil puis de le retirer, on peut sauter memfetch et utiliser simplement memclean pour à la fois récupérer les fichiers ET supprimer MemFiles du processus Beacon en une seule opération.
memtable est utilisé pour afficher et suivre les informations concernant les Beacons dans lesquels MemFiles est actuellement installé. Il affiche également des informations de configuration globales.
Chaque client CobaltStrike a son propre memtable ; MemFiles fait de gros efforts pour garantir la synchronisation de ses données entre tous les clients CobaltStrike connectés, afin que MemFiles puisse être utilisé par tous les opérateurs dans tous les Beacons. Pour en savoir plus, voir « Design Considerations and Commentary ».

Initialisez MemFiles dans un Beacon en utilisant la commande meminit. Cela peut être configuré pour se produire automatiquement en activant l'option dans le menu MemFiles->Config.

Une fois MemFiles initialisé, vous pouvez désormais utiliser vos outils favoris pour écrire des fichiers en mémoire ! La manière de procéder dépendra de l'outil spécifique ; certains permettent de spécifier un répertoire dans lequel écrire plusieurs fichiers, tandis que d'autres permettent de spécifier un chemin absolu pour un fichier unique créé par l'outil. Quelques exemples sont présentés ci-dessous :
Ici, nous spécifions que SharpHound doit écrire tous les fichiers produits dans le répertoire c:\redteam\ (notre répertoire spécial MemFiles) et qu'il ne doit pas compresser les fichiers ; MemFiles ne prend pas en charge la lecture de fichiers depuis la mémoire par les programmes, uniquement leur écriture. La fonctionnalité de compression zip de SharpHound ne fonctionne donc pas.

La commande « dump » est utilisée avec Rubeus et nous lui demandons d'envoyer toute la sortie console dans un fichier (situé dans notre répertoire spécial)

Dans cet exemple, Inline-Execute-PE est utilisé pour charger powershell.exe dans le processus Beacon et exécuter « Get-ADUser » afin de récupérer une liste des utilisateurs du domaine. En utilisant un pipe et « out-file », les données peuvent être écrites en mémoire puis récupérées.

Lorsque vous souhaitez récupérer vos fichiers, exécutez memfetch :

Lorsque vous avez terminé avec MemFiles et/ou que vous ne souhaitez pas le laisser installé dans un processus Beacon, exécutez memclean :

Notez que dans l'exemple ci-dessus, un fichier n'avait pas encore été téléchargé ; memclean télécharge ce fichier et l'efface de la mémoire avant de désinstaller MemFiles.
Interrogez le statut et la configuration de MemFiles à l'aide de memtable. Au cours d'opérations longues, supprimez les entrées des beacons morts/anciens de memtable pour éviter l'encombrement.

Comme souligné dans l'introduction, MemFiles nécessite une copie propre de NTDLL dans le processus Beacon pour fonctionner. Cela est nécessaire car il lit les octets d'origine de la NtFunction et en copie certains vers le trampoline, qui est ensuite utilisé pour compléter les appels normaux à la NtFunction avec laquelle MemFiles ne doit pas interférer. Ce sujet est abordé plus en détail dans la section « Technical Details, Design Considerations, and Commentary ».
MemFiles effectue une allocation initiale de 1 048 576 octets pour chaque fichier ; à mesure que des données sont écrites en mémoire, il peut et va étendre cette allocation si nécessaire pour contenir des fichiers plus volumineux.
Le nom de fichier stocké dans la structure MemFiles est extrait d'un argument passé à la fonction de remplacement NtCreateFile. MemFiles procède de manière assez simpliste : il localise le répertoire « spécial » dans l'argument du chemin de fichier, se déplace jusqu'à la fin de celui-ci, puis incrémente le pointeur de 1 pour tenir compte du caractère '\' qui sépare le répertoire « spécial » du nom de fichier. Par exemple, dans le chemin 'C:\users\tom\redteam\myfile.txt', MemFiles localise 'redteam', tient compte du caractère antislash et sélectionne 'myfile.txt' comme nom de fichier.
MemFiles ne se soucie pas des répertoires précédents dans le chemin du fichier ; 'C:\redteam\myfile.txt' et 'c:\users\tom\appdata\local\redteam\myfile.txt' sont des chemins également valides en ce qui concerne MemFiles.
Étant donné la façon dont MemFiles extrait les noms de fichiers des chemins, il convient de noter que MemFiles ne prend pas en charge la création de sous-répertoires ; cela signifie que MemFiles ne fonctionnera pas correctement avec des outils qui, par exemple, tentent de créer c:\redteam\mynewdir\file1.txt, c:\redteam\mynewdir\file2.txt, c:\redteam\mysecondir\file3.txt, etc.
Les NtAPI hookées par MemFiles ont été identifiées comme étant celles utilisées par divers programmes pour les opérations d'E/S. Comme mentionné précédemment, les outils/capacités testés avec succès avec MemFiles/cet ensemble de NtAPI hookées sont : SharpHound, Rubeus, Powershell, Procdump, les BOF, les programmes C génériques qui effectuent des opérations d'écriture de fichiers, et la commande bupload_raw de CobaltStrike (qui permet à l'opérateur de spécifier l'emplacement distant du fichier). Il existe certainement d'autres outils qui fonctionneront avec MemFiles sans modification ; d'autres seront incompatibles.
À première vue, le processus de création de fichiers sous Windows semble simple : NtCreateFile->NtWriteFile->NtClose. Rapidement après avoir creusé ce projet, j'ai constaté qu'un certain nombre d'autres API étaient impliquées et, pour encore plus de plaisir, ces autres API diffèrent selon les programmes. Certains programmes, dans le cadre de leur processus d'E/S, appellent l'API Win32 SetFilePointerEx, qui appelle à son tour la NtAPI NtSetInformationFile. D'autres, comme les programmes .NET, notamment SharpHound, finissent par appeler NtFlushBuffersFile.
L'absence d'une chaîne commune unique d'appels API pour créer et écrire des fichiers laisse place à des incompatibilités selon l'outil avec lequel MemFiles est utilisé. Un programme peut appeler une autre NtAPI que MemFiles n'a pas hookée, en passant le faux handle créé par MemFiles dans la fonction de remplacement NtCreateFile, ce qui entraîne une erreur « Invalid Handle » qui interrompt l'exécution. D'autres programmes effectuent des actions plus complexes que MemFiles ne sait pas correctement imiter/remplacer. Une incompatibilité déjà identifiée est ADExplorer.exe.
ADExplorer.exe est un binaire signé par Microsoft utilisé pour l'énumération d'Active Directory. Pendant son exécution, ADExplorer écrit des données dans le fichier de sortie spécifié, puis y revient et le référence avant d'écrire finalement la sortie finale dans le fichier à la fin de l'exécution. Parce qu'il utilise le fichier de sortie comme une sorte de cache, il doit être capable de lire les données qu'il a déjà écrites dans le fichier, et il y a très peu de chances qu'il le fasse de manière simple et prévisible.
MemFiles ne prend actuellement pas en charge la lecture de fichiers en mémoire par des programmes ou applications, mais cela devrait être possible en développant davantage la fonction NtReadFile personnalisée et en ajoutant quelques variables/suivi de données supplémentaires à la structure MemFiles.
ADExplorer présente un autre défi avec la taille des fichiers qu'il produit. Dans les grands environnements d'entreprise, le fichier de sortie peut dépasser 1 Go ; bien que MemFiles devrait être capable de le gérer programmatiquement, il n'est certainement pas destiné à de tels cas d'usage.
Sans aucun doute, la communauté découvrira des outils avec lesquels MemFiles ne fonctionne pas correctement ; je vous encourage à ouvrir une issue détaillant le programme/outil incompatible et les circonstances dans lesquelles vous l'avez exécuté, par exemple s'il s'agit d'un BOF, via inline-executeAssembly, Inline-Execute-PE, etc., afin que je puisse voir si je peux étendre MemFiles et le faire fonctionner.
Les IOC associés à MemFiles incluent, sans s'y limiter :
MemFiles n'a pas été développé contre un EDR approprié ni testé contre un tel EDR ; Microsoft Defender était ce qui était disponible. Cela étant dit, je dirais que l'outil/programme que Beacon exécute pour produire un fichier est plus susceptible d'être signalé que MemFiles capturant ou stockant ce fichier en mémoire. L'écrasement de mémoire dans NTDLL/le hook des NtAPI me semble être quelque chose que certains produits pourraient désapprouver, mais je n'ai aucune preuve pour corroborer cela. Pour les appels aux NtFunctions hookées qui ne concernent pas des fichiers qui sont/doivent être capturés par MemFiles, l'appel système est toujours émis depuis l'espace d'adressage de NTDLL.dll, car les produits de sécurité détectent et signalent les appels système effectués depuis l'extérieur de cette zone.
Il convient de noter que les fichiers stockés en mémoire par MemFiles ne sont NI encodés NI chiffrés ; cette fonctionnalité pourrait être ajoutée si un cas d'usage réel/une instance où l'AV/EDR signale un fichier produit en mémoire est identifié.
J'ai découvert le concept de système de fichiers en mémoire lors d'une conférence il y a plusieurs mois au Tradecraftcon de KFiveFour, où un intervenant (@DexterGerig) a démontré une POC qui créait un système de fichiers en mémoire à l'aide d'un modèle client-serveur. La moitié de la fonctionnalité que j'envisageais pour un tel projet était couverte par ma dernière grande version, Inline-Execute-PE. L'autre moitié, l'idée de pouvoir capturer les fichiers produits par les outils et de les stocker en mémoire plutôt que sur disque, n'était pas remplie par ce projet et restait une capacité extrêmement souhaitable pour des raisons évidentes.
MemFiles a été un projet incroyablement difficile pour moi, car avant ce projet, j'avais passé très peu de temps dans un débogueur, je ne comprenais pas l'assembleur et ne comprenais pas le hook d'API. J'ai rencontré plusieurs blocages de 10 à 20 heures au cours de ce projet que j'ai heureusement pu surmonter grâce à ma persévérance. Bien que ce ne soit probablement pas la manière la plus efficace, j'ai acquis une grande familiarité avec les débogueurs et une meilleure compréhension du fonctionnement interne des ordinateurs en ce qui concerne l'assembleur, les registres, la pile et les conventions d'appel.
Ce qui suit est une plongée technique dans certains des détails techniques et des considérations de conception les plus importants qui ont été intégrés à MemFiles.
La création de fichiers sous Windows commence par NtCreateFile, auquel le chemin du fichier souhaité est fourni ; en retour, Windows crée un fichier à cet emplacement et fournit un handle vers celui-ci. Le handle retourné est utilisé dans tous les appels ultérieurs impliquant le fichier, par exemple NtWriteFile et NtClose.
En réfléchissant à la façon de séparer les appels à toutes ces API entre ceux que nous voulons intercepter et modifier et ceux que nous voulons laisser tranquilles, j'ai opté pour la recherche d'un mot-clé dans l'appel NtCreateFile. Cela a été réalisé en spécifiant un répertoire unique et inexistant comme partie du chemin de fichier dans l'appel NtCreateFile. Lorsque notre hook redirige l'exécution vers la fonction de remplacement NtCreateFile, le chemin de fichier passé comme argument à NtCreateFile est examiné pour détecter la présence de ce « mot-clé » unique ; s'il le trouve, MemFiles sait que cet appel NtCreateFile concerne un fichier qui doit être placé en mémoire plutôt que sur disque. Lorsque cela se produit, MemFiles initialise plusieurs variables et alloue une mémoire initiale de 1 Mo pour le fichier, mais surtout, il associe un faux handle au nom de fichier spécifié dans la structure MemFiles et retourne ce faux handle à l'appelant.
Pour toutes les autres NtAPI hookées par MemFiles, les NtFunction de remplacement correspondantes examinent le handle passé en argument et vérifient s'il existe dans la structure MemFiles ; si le handle existe (les faux handles produits par MemFiles sont suffisamment faux pour ne jamais croiser un vrai), MemFiles identifie cet appel comme concernant un fichier en mémoire et agit en conséquence.### Théorie du hooking et fonctions de remplacement Afin d'écrire en mémoire des fichiers destinés au disque, MemFiles doit intercepter les appels à certaines API effectués par les programmes lorsqu'ils tentent de créer un fichier. Le hooking d'API existe depuis très longtemps et est activement utilisé par de nombreux produits EDR comme élément central de leurs fonctionnalités ; les appels à certaines API sont redirigés vers l'espace d'adressage de l'EDR, où une analyse est effectuée sur l'appel API et les variables qui lui sont passées. Si l'EDR détermine que l'appel est malveillant, par exemple s'il fait partie d'un outil d'attaque ou d'une kill chain, il empêche l'appel de se terminer et déclenche une alerte. Si l'EDR décide que l'appel est bénin, il rétablit l'exécution à l'endroit d'où elle a été redirigée et laisse l'appel API se terminer comme prévu initialement. Une analogie simpliste consisterait à dire que vous envoyez une lettre à un ami, mais qu'avant que votre ami ne la reçoive, un tiers l'ouvre, la lit, puis décide si la lettre contient quelque chose d'illégal ; dans ce cas, votre ami ne reçoit jamais la lettre et la police est alertée.
MemFiles suit la même théorie, sans les alertes (ni l'implication théorique de la police). Le hooking d'API est généralement implémenté au niveau le plus bas possible en espace utilisateur : les NtFunctions de NTDLL.dll. Examinons NtCreateFile, avant qu'aucun hook n'ait été posé :
Toutes les NtFunctions sont identiques, à l'exception du numéro de syscall, qui est 55 dans cet exemple. Le numéro de syscall change d'une NtFunction à l'autre, et il convient également de noter que ce numéro peut changer entre les versions de Windows ; le numéro de syscall de NtCreateFile sur ce système d'exploitation (Windows 11) est 55, mais il pourrait être différent sur Windows 10 (et certainement sur Windows 7).
Il convient de noter les instructions TEST et JNE. Elles existent pour déterminer si la NtFunction doit utiliser l'instruction syscall normale ou l'instruction héritée INT 2E. Je citerai l'article de klezvirus SysWhispers is dead, long live SysWhispers! :
Maintenant, la partie intéressante : la fonction vérifie si SharedUserData[0x308] (BYTE PTR DS:[7FFE0308]) est défini sur 1. SharedUserData est un symbole qui fait référence à la structure du mode noyau KUSER_SHARED_DATA.
La structure KUSER_SHARED_DATA définit un espace mémoire fixe (ou prédéfini) utilisé pour partager des informations avec les logiciels en mode utilisateur. Cela a bien sûr été fait pour rendre certaines informations système globales disponibles au code en mode utilisateur sans avoir à basculer à chaque fois entre l'exécution en mode utilisateur et en mode noyau.
La valeur à l'index 0x308 représente l'instruction syscall, qui est prise en charge dans toutes les versions de Windows à partir de la version 1511. Comme vous pouvez l'imaginer, dans toutes les versions de Windows antérieures à 1511, la manière standard d'exécuter un syscall était d'appeler l'interruption int 2Eh.
...
Si vous vous demandez pourquoi cet int 2Eh est toujours présent, même si Windows est désormais bien au-delà de la version 1511, c'est parce que cette instruction est toujours utilisée. En effet, lorsque HVCI (Hypervisor-protected Code Integrity) est activé, SharedUserData[0x308] est défini sur 0, et int 2Eh est utilisé à la place de l'instruction syscall. Cela est principalement fait pour des raisons de performances, en raison de la manière dont le passage de Ring3 à Ring0 est effectué à l'aide de l'une ou l'autre instruction.
J'ai demandé des précisions supplémentaires sur ce sujet sur Twitter, et @yarden_shafir a répondu ceci :

En bref, chaque instruction de la NtFunction peut être nécessaire à un moment donné (à l'exception peut-être du NOP multi-octets situé à la fin qui ne semble pas accessible), et si nous écrasons des instructions dans la NtFunction, nous devons nous assurer de les sauvegarder et de les exécuter à un moment donné avant d'effectuer le syscall final (ou INT 2E selon le cas).
Il convient de noter qu'avant d'effectuer le syscall, le numéro de syscall est déplacé dans RAX (affiché comme EAX dans la capture d'écran). Comme nous ne voyons pas RAX poussé sur la pile avant cela, j'ai supposé (peut-être naïvement) que la valeur contenue dans RAX avant que le numéro de syscall n'y soit déplacé n'est pas importante ni requise après l'émission du syscall. C'est une bonne nouvelle, car cela signifie que nous pouvons utiliser librement le registre RAX tant que nous veillons à ce qu'il contienne le numéro de syscall avant l'émission du syscall.
Afin de rediriger l'exécution vers notre code personnalisé / notre NtFunction de remplacement, nous allons écraser une partie de la NtAPI d'origine, déplacer l'adresse de notre NtFunction de remplacement dans RAX, puis utiliser une instruction JMP pour aller vers ce code :
12 octets sont nécessaires pour les instructions MOV et JMP ; comme nous modifions d'autres instructions en écrasant les 12 premiers octets de la NtAPI, ces instructions ont été remplacées par des NOP afin de conserver l'espacement et l'alignement corrects de la NtAPI.
Lorsque le programme appelle désormais NtCreateFile, il fait exécuter un saut vers notre NtFunction de remplacement.
MemFiles s'écarte de la manière dont les EDR effectuent le hooking en ce qui concerne l'emplacement où résident les fonctions de remplacement vers lesquelles les API hookées sont redirigées. De nombreux EDR chargent leur propre DLL dans un processus. Les API hookées sont redirigées vers l'espace d'adressage de cette DLL chargée, où l'analyse peut avoir lieu. Étant donné que l'objectif même de ce projet était d'éviter de déposer des fichiers sur le disque, placer une DLL sur le disque et demander à notre processus Beacon de la charger afin d'accéder à nos NtFunctions de remplacement semblait être une mauvaise approche. Il existe un POC vieux de plusieurs années permettant de charger des DLL depuis la mémoire, ce qui est une stratégie viable pour nos besoins, mais le projet n'est pas maintenu et semble présenter plusieurs problèmes. De plus, il fait 1200 lignes de code et serait un véritable défi à convertir au format BOF.
Au passage, nos NtFunctions de remplacement ne peuvent pas résider dans un BOF ; CobaltStrike charge, exécute puis efface les BOF de la mémoire du processus lorsqu'ils ont terminé. Étant donné que nous avons besoin de fonctions persistantes en mémoire qui peuvent être appelées à chaque fois que le processus appelle l'une des NtAPI hookées, les BOF ne fonctionneront pas.
La réponse à laquelle je suis arrivé est le code indépendant de la position (PIC, Position Independent Code). Comme son nom l'indique, contrairement aux exécutables normaux qui doivent être chargés à un endroit précis / avec certaines parties dans une certaine relation les unes par rapport aux autres, le PIC peut être placé et exécuté n'importe où en mémoire. Cela ouvre la possibilité d'écrire nos NtFunctions de remplacement sous forme d'exécutables PIC, de les injecter dans le processus Beacon, et de faire en sorte que nos hooks redirigent l'exécution vers elles lorsque des appels à nos NTAPI hookées sont effectués.
Le modèle de ces NtFunctions PIC provient du projet ShellcodeTemplate de Cracked5pider.
Une déviation notable par rapport au projet de base est que le projet de base est conçu pour créer un exe PIC complet ; c'est-à-dire qu'il inclut de l'ASM pour sauvegarder le pointeur de pile, créer de l'espace sur la pile, appeler la NtFunction de remplacement désignée contenue dans l'exe, puis restaurer le pointeur de pile une fois que cette fonction est retournée. L'instruction call effectuée par l'exe PIC pose un problème, car elle pousse l'adresse de retour de l'endroit où l'appel a été effectué (dans l'ASM de l'exe PIC) sur la pile, ce qui conduit l'instruction ret rencontrée plus tard à ramener l'exécution dans l'exe PIC, plutôt qu'à l'appelant de la NtAPI d'origine.
Pour atténuer ce problème, le fichier ASM du projet ShellcodeTemplate a été modifié afin de supprimer l'ASM lié à la configuration de la pile, à l'appel de la fonction, puis à la restauration du pointeur de pile après l'exécution de la fonction. Le résultat est que le hook placé dans la NtAPI fait désormais exécuter un saut directement dans la NtFunction de remplacement, avec la pile et les registres configurés comme ils l'étaient lorsque la NtAPI d'origine a été appelée par le programme (à l'exception de RAX qui est utilisé pour notre JMP).
ASM du ShellcodeTemplate d'origine :

ASM de MemFiles :

Chaque NtAPI hookée possède sa propre NtFunction PIC contenant la logique requise pour soit :
A. Effectuer des actions spécifiques à MemFiles, telles que créer un faux handle, écrire des données en mémoire, modifier des variables dans la struct MemFiles, etc.
ou
B. Diriger l'exécution vers un trampoline qui remettra l'appel API sur les rails et le rapiécera dans NTDLL où le syscall pourra être effectué.
Certaines NtFunctions de remplacement sont plus complexes que d'autres ; lorsqu'un appel à une NtAPI hookée concerne MemFiles, certaines, comme NtCreateFile et NtQueryVolumeInformationFile, modifient les variables passées comme arguments à la NtAPI conformément à la documentation MSDN, aux résultats de tests et à quelques suppositions / au bon sens. D'autres, comme NtClose et NtReadFile, renvoient simplement STATUS_SUCCESS à l'appelant d'origine afin d'éviter l'inévitable erreur « Invalid Handle » qui surviendrait autrement lors du passage d'un faux handle créé par MemFiles.
Lorsqu'un appel à une NtAPI hookée ne concerne PAS MemFiles, nous devons diriger l'exécution vers un trampoline afin de remettre les choses sur les rails :

Le trampoline est chargé d'exécuter toute instruction / toutes les instructions qui n'ont pas été exécutées dans la NtAPI d'origine en raison du hook de cette API ; cela inclut toutes les instructions partiellement ou totalement écrasées par le hook d'origine. Le hooking d'API peut rapidement conduire à des situations où nous ne disposons pas de suffisamment d'espace pour effectuer toutes les instructions nécessaires. Les trampolines peuvent également aider à atténuer ce problème, car nous pouvons effectuer un nombre quelconque d'actions pour configurer nos registres et/ou notre pile avant de revenir à la NtAPI d'origine. Le trampoline de NtCreateFile est visible ci-dessous :

Le plus évident est que les trois instructions écrasées par notre hook initial peuvent être vues comme les trois premières instructions du trampoline :
MOV R10, RCX
MOV EAX, 55
TEST BYTE PTR DS:[7FFE0308], 1
Comme mentionné précédemment, l'exigence majeure dans toute cette manipulation est que le numéro de syscall (55 dans l'exemple ci-dessus) réside dans RAX(EAX) avant l'émission du syscall. Nous avons toutefois un problème : nous devons encore utiliser une instruction JMP pour diriger l'exécution vers la NtAPI d'origine. Bien qu'il puisse exister un autre registre qui ne contient pas d'informations importantes et qui pourrait être utilisé à cette fin, je n'ai pas trouvé d'option systématiquement sûre étant donné le nombre de NtAPI que nous hookons, chacune pouvant utiliser les registres différemment. L'option sûre consiste à continuer d'utiliser RAX ; nous pouvons le faire en poussant le numéro de syscall stocké dans RAX sur la pile. Nous pouvons ensuite déplacer l'adresse vers laquelle nous souhaitons revenir dans la NtAPI d'origine dans RAX et utiliser une instruction JMP pour revenir dans NTDLL :

Le hook installé dans la NtAPI inclut une instruction POP RAX, et c'est vers elle que nous sautons à l'aide de notre trampoline. L'exécution de cette instruction restaure le numéro de syscall dans RAX depuis le sommet de la pile et nous prépare à émettre le syscall. Notez que l'instruction JNE de la NtAPI d'origine non hookée est toujours là ; l'instruction TEST correspondante, qui définit le drapeau ZF et détermine si un JNE est pris (ce qui nous ferait sauter par-dessus le syscall vers le INT 2E), a été effectuée dans le trampoline. Agencer les choses de cette manière nous permet à la fois de hooker avec succès la NtAPI et de rediriger l'exécution vers notre NtFunction PIC de remplacement, tout en garantissant que nous ne sautons rien et ne perdons aucune fonctionnalité à cause de notre hooking.
Pour résumer brièvement ce qui a été couvert jusqu'ici : lorsque MemFiles est initialisé, une struct est créée dans la mémoire du processus Beacon, contenant des informations importantes pour le fonctionnement de MemFiles. Les informations de cette struct sont continuellement référencées tout au long du cycle de vie de MemFiles, y compris par chacune des NtFunction PIC de remplacement ainsi que par les BOF utilisés pour interroger et récupérer les fichiers stockés en mémoire par MemFiles. À cette fin, lorsque la struct est créée, l'adresse mémoire à laquelle elle réside est communiquée au Teamserver :

Après avoir stocké cette adresse dans memtable, les commandes MemFiles ultérieures (memlist, memfetch, memclean) envoient cette adresse comme argument au BOF afin que la struct puisse être localisée et référencée. Mais comment les NtFunction PIC localisent-elles la struct ?
Le problème évident est que les NtFunction PIC nécessitent l'adresse mémoire de la struct MemFiles, mais elles sont déjà compilées au moment où la struct est créée. Une implémentation précoce de MemFiles a abordé ce problème en divisant l'initialisation de MemFiles en deux BOF distincts. Le premier créait la struct et renvoyait l'adresse au Teamserver, qui, grâce à une certaine magie de script Aggressor, analysait l'adresse, l'insérait dans le fichier de code source de chaque NtFunction, puis les recompilait dans la NtFunction PIC finale. Le second BOF transmettait ensuite les NtFunctions PIC terminées et effectuait l'injection et le hooking réels des NtAPI.
En dehors du fait que c'était moche et prenait du temps, de véritables problèmes pouvaient survenir dans le cas où plusieurs beacons tentaient d'initialiser MemFiles en même temps. Si le Beacon 2 rappelait avec l'adresse de sa struct MemFiles pendant que le Beacon 1 était en train de patcher et de recompiler les fichiers de code source des NtFunction, les choses pouvaient devenir compliquées.
La solution élégante à ce problème consiste à effectuer un patch binaire sur la NtFunction PIC, dans lequel l'adresse de la struct MemFiles est patchée dans la NtFunction compilée et accessible pendant l'exécution. Pour faciliter cela, une chaîne de caractères de substitution est écrite dans chaque NtFunction :

Cette variable est visible dans le code compilé à l'aide d'un outil comme xxd :

Lorsque la commande meminit est exécutée, chacune des NtFunctions PIC est envoyée au Beacon avec le BOF InstallHooks. Ce BOF est chargé de créer la struct MemFiles ; après l'avoir créée, il appelle la fonction patchAddr sur chacune des NtFunctions PIC. patchAddr est chargée de localiser la chaîne de A et de la remplacer par la représentation sous forme de chaîne de l'adresse de la struct. En effet, en utilisant l'adresse mémoire de la capture d'écran précédente, la variable pFileInfoStr ressemble désormais à :
char* pFileInfoStr = GET_SYMBOL( "00000178BC106860" );
Cette représentation sous forme de chaîne peut ensuite être transformée en la valeur hexadécimale réelle, qui est notre adresse mémoire. Les BOF accomplissent cela assez simplement à l'aide de l'API _strtoi64 :

Bien sûr, les choses ne pouvaient pas être aussi simples pour les NtFunctions PIC.
Ce bref extrait d'assembleur montre la fin de l'une des NtFunctions PIC. Notez l'instruction JMP RAX, qui est la NtFunction PIC appelant le trampoline (ce qui signifie qu'il s'agit d'un appel auquel MemFiles n'a PAS fait de spoof / dans lequel il n'est pas intervenu) :

Pour des raisons inconnues, lorsque j'ai tenté d'utiliser _strtoi64 (ou l'une de ses API cousines comme stroull ou atoll), cette instruction JMP RAX est devenue une instruction CALL RAX. Je suis sûr qu'il existe une raison valable impliquant un niveau profond de « comment fonctionnent les ordinateurs », mais ce changement en apparence insignifiant casse pas mal de choses. Après avoir passé plus de 15 heures à essayer de trouver un moyen de transformer la représentation sous forme de chaîne de l'adresse de la struct MemFiles en la valeur hexadécimale réelle afin de pouvoir utiliser les valeurs qui y sont stockées, je suis tombé sur ce post StackOverflow dans lequel un commentateur a fourni une routine personnalisée conçue pour les microcontrôleurs afin de convertir une chaîne en uint32 ; heureusement, elle a également fonctionné pour un uint64 sans modification, et surtout elle a préservé l'instruction JMP RAX plus loin dans la NtFunction PIC sans la transformer en CALL. L'extrait final :

Par souci de simplicité, cela n'a pas été mentionné plus tôt, mais le format des NtAPI a changé au fil des ans. Les versions antérieures, comme celles utilisées par Windows 7, sont beaucoup plus courtes que leurs homologues modernes, ne faisant que 16 octets au lieu de 32 :

Cela nécessite des modifications dans la manière dont les NtAPI sont hookées par MemFiles ainsi que dans la construction du trampoline. Afin que MemFiles puisse identifier la version des NtAPI auxquelles il a affaire, le BOF InstallHooks résout d'abord l'adresse de la NtAPI concernée, puis lit 32 octets à partir de cet emplacement. Le format différent fait que l'instruction syscall est située à des endroits différents entre les versions de NtAPI ; en vérifiant la présence ou l'absence du syscall à un certain décalage d'octet, MemFiles est capable de déterminer s'il a affaire à l'implémentation moderne ou héritée de la NtAPI et d'agir en conséquence :

Après le hooking, l'API NtCreateFile héritée ressemble à ceci :

Et le trampoline utilisé pour configurer les registres puis revenir à NtCreateFile :

Dans l'ensemble, la technique est très similaire, mais les choses sont beaucoup plus serrées et sans grande marge de manœuvre. Il convient de noter que pour que tout tienne et fonctionne correctement, l'instruction syscall a dû être déplacée dans NtCreateFile ; elle réside toujours dans l'espace mémoire de l'API NtCreateFile dans NTDLL, mais elle est passée des 9e et 10e octets de l'API aux 14e et 15e octets de l'API, le NOP multi-octets ayant été sacrifié pour que tout puisse être casé correctement.
Certaines des API liées aux opérations d'E/S sur les fichiers étaient intuitives et donc faciles à identifier et à hooker ; d'autres étaient beaucoup plus insaisissables, nécessitant des heures et des heures passées dans WinDbg et x64dbg à parcourir l'assembleur pas à pas pour essayer d'identifier quelles API étaient appelées. Je dois croire qu'il existait un moyen plus efficace d'accomplir cette tâche, mais j'apprenais au fur et à mesure.Pour faire bonne mesure, j'ai pensé détailler le processus (qui, rétrospectivement, aurait dû être très rapide) pour identifier la NtAPI qui empêchait SharpHound de fonctionner pendant 20 heures. En tant que programme .NET, SharpHound a généré une pile d'appels très moche, toute liée au fait qu'il avait déterminé que "The handle is invalid". Bien que .NET soit un langage convivial pour les programmeurs, la plupart (toutes ?) des fonctionnalités sont traduites et passent en fin de compte par l'API Win32 (et par conséquent la NtAPI) où nous pouvons l'observer et la hooker comme n'importe quoi d'autre.

L'erreur "Invalid Handle" était un indice révélateur qu'il y avait une NtAPI utilisée par SharpHound que je ne hookais pas (par opposition à l'une de mes fonctions PIC NtFunctions existantes qui ne fonctionnerait pas correctement), je me suis donc attelé à essayer de comprendre de quoi il s'agissait. La technique que j'avais utilisée avec d'autres outils jusqu'à présent consistait d'abord à placer un point d'arrêt sur NtCreateFile et à localiser l'appel qui concernait mon répertoire "special". De là, j'observais le programme pas à pas (généralement plusieurs fois parce que je m'égarais), et je voyais quelles fonctions le programme appelait ensuite. C'est en suivant cette méthodologie que j'ai découvert que NtQueryVolumeInformationFile, NtQueryInformationFile et NtSetInformationFile étaient appelées et devaient être hookées.
SharpHound ajoute des difficultés supplémentaires en effectuant une grande partie de ses tâches de manière asynchrone. Cela rend beaucoup plus difficile de retracer les étapes linéaires qu'un seul fichier suit à travers les appels API, car plusieurs fichiers passent par le processus simultanément. De plus, en parcourant le programme pas à pas après l'appel NtCreateFile, j'ai constaté que le thread dans lequel l'appel NtCreateFile avait été effectué finit par se terminer ; les appels NtWriteFile ultérieurs (et tous les autres appels API inconnus qui font l'objet de cette recherche) se produisent dans un thread différent, ce qui complique encore davantage le processus de recherche.
N'ayant eu qu'un bref contact avec .NET, je ne maîtrisais pas bien l'analyse des piles d'appels produites par les erreurs, et encore moins celles que l'utilisation de l'async rend deux fois plus laides. Au cours de ce problème de 20 heures, en l'absence de progrès avec ma stratégie précédente, je n'arrêtais pas d'y revenir et, lentement mais sûrement, j'y voyais de plus en plus clair. À peu près au troisième « bloc » en partant du haut, tel que séparé par les lignes "--- End of stack trace...", on peut voir une ligne indiquant "at Sharphound.Writers.JsonDataWriter...". Cela m'a donné un point de repère pour commencer dans le code réel de SharpHound, qui est open source et disponible sur Github. Comme son nom l'indiquait, la fonction SharpHound était chargée d'écrire la sortie JSON dans un fichier ; je savais déjà que mes données ne s'écrivaient pas correctement dans un fichier, ce n'était donc pas une nouveauté. En remontant d'un niveau dans la pile d'appels, la ligne pertinente suivante était "at System.IO.Streamwriter.". Le préfixe System.IO m'indiquait qu'il s'agissait d'une fonction inhérente à .NET, par opposition à une fonction spécifique à SharpHound. En regardant la toute première section de la pile d'appels, la ligne qui m'a sauté aux yeux était "at System.IO.FileStream.FlushOSBuffer()". J'ai décidé de googler FlushOSBuffer pour voir ce que je pouvais trouver.
Ce faisant, je suis arrivé à la documentation .NET de Microsoft pour filestream.cs. J'y ai trouvé la définition de FlushOSBuffer :

Il semble appeler une API Win32, FlushFileBuffers. La définition de Win32Native.FlushFileBuffers peut être trouvée en consultant la documentation Win32Native :

Quiconque a travaillé en .NET avec P/Invoke reconnaîtra le format. J'avais maintenant une API Win32 que je savais être appelée par System.IO.Filestream.FlushOSBuffer(), ma fonction .NET problématique. Mettre un point d'arrêt sur KERNEL32!FlushFileBuffers et exécuter SharpHound l'a confirmé, et en parcourant pas à pas, j'ai rapidement vu que sous le capot FlushFileBuffers appelle NtFlushBuffersFile. Le hook de cette API a atténué les problèmes que SharpHound rencontrait et lui a permis de s'exécuter avec succès, écrivant ses fichiers de sortie en mémoire.
Un élément critique de ce projet est la capacité de télécharger réellement les fichiers vers le Teamserver CobaltStrike une fois qu'ils sont en mémoire. Comme prévu, la commande download normale de CobaltStrike ne fonctionne pas avec un chemin de fichier qui n'existe pas réellement. Avec le recul, une solution consisterait probablement à développer le code de remplacement PIC NtReadFile pour permettre aux fichiers en mémoire d'être lus au lieu d'être simplement écrits. Ne disposant pas de cette connaissance à l'époque, pouvoir réellement récupérer les fichiers était un obstacle majeur.
Par accident, je suis tombé sur un BOF écrit par EspressoCake qui contenait une fonction qui m'a sauté aux yeux :

En parcourant le code, il semble utiliser une option CALLBACK Beacon non documentée :

Cette fonction permet à un BOF d'initier le téléchargement d'un fichier vers le Teamserver depuis la cible, et non depuis le Client. Cette capacité (dont j'ai appris plus tard qu'elle était le fruit d'un effort combiné de plusieurs autres personnes, dont @Cr0Eax, @EthicalChaos et @anthemtotheego) a supprimé le principal obstacle qui existait auparavant, car j'avais désormais un moyen d'initier un transfert de fichiers depuis le système cible pour le fichier en mémoire. Un grand merci à tous les participants pour cet extrait de code, qui, je le prévois, sera également utile à l'avenir.
L'une des parties difficiles de ce projet consistait à garantir la disponibilité des fonctionnalités MemFiles pour tous les Clients CobaltStrike connectés au Team Server. Les données de MemFiles sont stockées dans des structures créées par MemFiles.cna, qui doivent être chargées dans chaque Client souhaitant utiliser l'outil ; par conséquent, ces structures de données vivent dans chaque Client, et non sur le Team Server. Si ces données vivaient dans un emplacement central unique (TS), il serait trivial de les récupérer depuis chaque Client et tout cela ne poserait aucun problème ; si l'équipe CobaltStrike intégrait formellement une capacité comme MemFiles dans CobaltStrike, je suis convaincu que c'est la direction qu'elle prendrait. Mais comme il s'agit d'un ajout communautaire, nous faisons avec ce que nous avons.
Il y a quelques scénarios différents dont il faut se préoccuper pour garantir que chaque Client CobaltStrike dispose des données les plus récentes et précises concernant l'état de MemFiles dans les Beacons et la configuration :
Les nouveaux Clients qui se connectent au TS et ont besoin de la memtable actuelle
Les cas où un seul Client est connecté au TS et redémarre CobaltStrike (perdant ainsi la memtable stockée dans la mémoire du Client)
Le Client A qui apporte une modification aux données MemFiles, modification qui doit être communiquée au Client B
Une approche à plusieurs volets a été adoptée pour répondre à ces scénarios. Pour gérer le cas où un seul Client CobaltStrike est connecté au TS (et est donc la seule entité à détenir les données de la memtable), chaque fois que le Client modifie la memtable (meminit, memclean), il écrit également le contenu de sa memtable dans un fichier texte local situé dans le répertoire CobaltStrike. Si le Client se ferme/redémarre, ou lorsque MemFiles.cna est rechargé, il tentera d'abord de lire le fichier local memtable.txt afin de remplir sa memtable en mémoire.
Lorsque plusieurs Clients sont connectés à un TS et qu'un nouveau Client se joint (selon le journal des événements), chaque Client récupère la liste de tous les utilisateurs connectés au TS et la trie par ordre alphabétique. Le Client qui figure en premier sur cette liste est sélectionné comme Client "Broadcast", et après avoir attendu 5 secondes (pour permettre au nouveau Client de s'initialiser et de lire son memtable.txt local), il enverra des messages (Actions) dans le journal des événements pour chaque entrée de sa memtable. Tous les Clients (hormis celui qui diffuse) liront ces messages et mettront à jour leur memtable avec les informations diffusées ; cela inclut la mise à jour des entrées existantes ainsi que l'ajout de toute entrée supplémentaire que leur memtable respective ne contient pas.
Les opérations normales impliquant MemFiles reposent également sur l'envoi de messages dans le journal des événements. Lorsque le Client A exécute meminit, un message contenant toutes les informations pertinentes de la memtable est diffusé ; TOUS les Clients mettent à jour leur memtable respective en analysant ces messages diffusés dans le journal des événements à l'aide du hook "on Event_Action". Des modifications sont également apportées aux données MemFiles lorsque meminit termine l'exécution de son BOF ; ces modifications sont renvoyées par Beacon (par exemple, après l'exécution de meminit, Beacon rappelle avec l'emplacement mémoire de la structure pMemAddrs) et sont donc visibles pour tous les Clients connectés, qui mettent à jour leur memtable respective à l'aide du hook "on Beacon_Output".
La combinaison de ces différents efforts permet à MemFiles de synchroniser efficacement et de manière fiable les données critiques entre plusieurs Clients.
Ce projet ne serait pas possible sans les contributions des personnes et projets suivants :