Introduction
Lors d’une analyse de réponse à un incident sur site, CERT-Sekoia a été contacté afin de répondre à une attaque de ransomware Spook.
Après avoir rassemblé les preuves, nous avons identifié que des acteurs malveillants utilisaient un compte VPN légitime pour initier la première connexion. Le compte était également administrateur de domaine dans la société Active Directory. Tous les mouvements latéraux ont utilisé ce compte via RDP ou SMB. Nous soupçonnons que ce compte valide a été acquis plus tôt en 2021, et potentiellement auprès d’un Access Broker initial, car :
- Les fichiers et artefacts des ordinateurs locaux montrent la présence de 2 exécutables contenant Mimikatz et netscanner avec une dernière modification datant de juillet 2021 ;
- À partir des quelques journaux résiduels disponibles sur l’appliance VPN, peu de temps avant l’établissement de la session VPN, l’utilisateur a été ajouté au groupe VPN autorisé. Nous avons identifié une interface d’administration web exposée à internet ;
- La version de l’appliance logicielle était en retard de plusieurs mois dans les mises à jour de sécurité, donc exposée à des CVE critiques ;
- La durée de l’attaque (temps écoulé entre la 1ère connexion VPN et la fin de la routine de chiffrement) était inférieure à une heure ;
- Les échecs d’authentification RDP et SMB affichent des erreurs avec des domaines incorrects, ces domaines faisant référence à des victimes connues de Spook, suivis d’autres erreurs de frappe. Cela pourrait indiquer un opérateur effectuant des actions de copier/coller.
Enfin, trois binaires ont été exécutés à distance via PSExec, ce qui a conduit au chiffrement de tous les fichiers des entreprises. Cela ne s’est produit que sur les quelques systèmes en place au moment de l’opération (4 heures du matin un dimanche matin). Les 3 binaires avaient un préfixe « Worker-« , mais aucun d’entre eux n’a été récupéré. Nous supposons que chacun d’eux a un objectif spécifique, le dernier étant chargé de couvrir les actions de l’acteur menaçant. Une note de rançon a été déposée sur les ordinateurs de la victime détaillant le paiement et les coordonnées pour payer la rançon. Le site Web indiqué était le site Web Spook leak, hébergé sur une adresse tor onion.
Lors de la tentative de récupération des fichiers du client, il est apparu que volume (VSC) n’avaient pas été supprimés sur le serveur de partage de fichiers principal. Les intervenants en cas d’incident ont ainsi testé une récupération VSC sur un échantillon de fichiers, ce qui s’est passé avec succès.
Source : CERT-SEKOIA
Comprendre un exemple de Spook
Suite à la réponse à l’incident précédent, nous avons choisi de nous concentrer sur le rançongiciel Spook. Comme pour de nombreux autres ransomwares, Spook a été conçu à l’aide du constructeur Thanos. Le constructeur de Thanos a été annoncé pour la première fois sur le forum XSS en février 2020 par l’acteur Nosophoros. Il était vendu sous forme d’abonnement, ce qui expliquait son intégration dans d’autres ransomwares considérés comme des variantes, comme Spook. L’échantillon le plus courant trouvé dans différentes ressources a le hachage SHA-256 : 8dad29bd09870ab9cacfdea9e7ab100d217ff128aea64fa4cac752362459991c.
L’exécutable est un binaire .NET obfusqué. ExeInfo PE indique l’outil commercial d’obfuscateur SmartAssembly dans la version 6.5.2–612.X.
Source : CERT-SEKOIA
Avec ces informations, nous lançons de4dot à partir du référentiel archivé pour obtenir une version lisible de notre échantillon. Malheureusement, l’outil n’a pas réussi à désobscurcir les noms de fonctions, les noms de classes et la section des ressources qui stocke toutes les chaînes, même s’il détecte un obfuscateur. Un article de Fortinet datant de juillet 2020 mentionnait le même comportement, mais ne fournissait pas le processus pour obtenir un échantillon lisible.
Source : CERT-SEKOIA
Après quelques recherches sur Google, nous avons trouvé cet article très intéressant de Jason Reaves, détaillant le fonctionnement de l’obfuscation Haron (un ancien ransomware également construit avec Thanos) basé sur SmartAssembly. Il n’a pas fourni la version SmartAssembly dans l’article, mais d’après son échantillon, il semble que ce soit 7.5.1.4370. En ouvrant notre exemple dans DNSpy, nous obtenons la v8.0.2.4779.
Source : CERT-SEKOIA
Même si les versions sont différentes, nous avons essayé de suivre son processus. On récupère le même type de constructeur pour récupérer une chaîne. Chaque classe différente déclare un premier attribut de type GetString. Cela nous amène au module SmartAssembly.Delegates dans notre navigateur .NET.
Source : CERT-SEKOIA
Le deuxième point est l’utilisation de Strings.CreateGetStringDelegate (du SmartAssembly.HouseofCards ) basé sur la classe Strings (de SmartAssembly.StringsEncoding) avec le paramètre typeof(NameOfTheClass).
Source : CERT-SEKOIA
Cette fonction récupère l’attribut GetType du paramètre de classe (de type GetString), et définit une DynamicMethod appartenant au module du paramètre de classe, prenant un entier en entrée et renvoyant une chaîne. Ensuite, il récupère la première méthode de la Strings qui renvoie une chaîne : la Get comme le montre la capture d’écran ci-dessous.
Source : CERT-SEKOIA
Examinons maintenant la classe Strings, où se produisent tous les mécanismes d’obscurcissement. Cette classe stocke les informations de base concernant l’emplacement des chaînes associées à différentes constantes :
• Une valeur de décalage, codée en dur avec 75 comme attribut de classe ;
• Une valeur XOR initiale avec le paramètre entier défini sur 107396847 (en hexa 0x666BEEF).
Source : CERT-SEKOIA
Attention, une fonctionnalité de cache pour éviter de décrypter plusieurs fois une chaîne, contrôlée par un attribut de classe cacheStrings défini à l’initialisation de la classe avec une valeur par défaut à True.
Source : CERT-SEKOIA
Nous apprenons que les chaînes sont situées dans une ressource manifeste nommée {56258a19–7489–468b-86ee-e7899203d67c} et décompressées avec une Unzip . Cette fonction est définie dans le Zip module
Source : CERT-SEKOIA
Cette fonction de décompression commence par créer un ZipStream qui est une classe enfant de MemoryStream avec deux méthodes supplémentaires :
• ReadShort qui renvoie le résultat de la méthode de classe parent ReadByte, qui lit et convertit un octet en Int32. Si la méthode du parent renvoie -1 (fin de flux), la fonction renvoie 0 ;
• ReadInt, même fonction mais pour un mot (16 octets) en utilisant la ReadShort .
L’étape suivante vérifie un en-tête à l’intérieur du fichier de ressources par rapport à la valeur ‘{z}\x00’ (le code affiche 8223355, qui est la valeur 32 bits non signée). De la ressource, nous obtenons ‘{z}\x03’.
Source : CERT-SEKOIA
Pour obtenir la valeur sur laquelle est basée la prochaine instruction switch, un décalage de 24 bits vers la droite est effectué sur l’entier 32 bits non signé initial lu (n’oubliez pas que nous sommes en petit boutien) conduisant à la valeur 0x03 . Le commutateur n’a que deux cas :
• 1 = décompresser le contenu
• 3 = décrypter avec l’algorithme symétrique AES et des valeurs codées en dur pour la clé et le vecteur initial
Dans ce cas, le flux est décrypté. Dans la capture d’écran ci-dessous, array2 représente la clé codée en dur et array3 le vecteur initial codé en dur pour l’algorithme de chiffrement AES symétrique. Ensuite, le nouveau flux est à nouveau passé à la Unzip .
Source : CERT-SEKOIA
En utilisant le même processus que décrit ci-dessus, nous avons réussi à décrypter le premier flux. Celui qui vient d’être déchiffré a l’en-tête suivant ‘{z}\x01’ : le cas du commutateur ira dans la première option où les données sont simplement décompressées.
Source : CERT-SEKOIA
Enfin, nous obtenons une chaîne de chaînes préfixées avec une valeur d’un octet. D’après l’article de Jason Reaves, nous pensions que le préfixe entier correspondait à la longueur de la chaîne. Nous avons essayé d’utiliser sa preuve de concept : cela fonctionne pour une petite partie des données avant de lever une exception concernant la valeur à décoder en UTF-8.
En regardant dans la GetFromResource méthode Strings , il semble que différentes opérations soient exécutées avant de renvoyer la chaîne finale. Le premier octet de la chaîne est en fait lié à sa longueur mais ne représente pas la valeur elle-même pour un cas spécifique : si la valeur après le ET logique avec 0x80 n’est pas 0. Dans ce cas, la longueur de la chaîne est évaluée à travers un long lambda fonction. Nous pouvons le diviser en 3 parties :
- La condition est à nouveau une opération ET avec une valeur 0x40
- si vrai, puis
(num & 0x1F) << 24) + (octets[index++] << 16) + (octets[index++] << 8 ) + bytes[index++]
- if false
((num & 0x3F) << 8) + bytes[index++]
Les valeurs 0x80 (10000000 en binaire) et 0x40 (01000000 en binaire) sont liées au flux UTF-8 d’une chaîne comme expliqué dans ce thread de débordement de pile.
Source : CERT-SEKOIA En compilant
toutes ces connaissances dans un petit script Python, nous avons pu décoder l’intégralité de la ressource. Certaines de ces chaînes affichent des schémas d’encodage base64 et base64 inversé supplémentaires : nous ajustons le code pour obtenir un texte en clair complet.
Source : CERT-SEKOIA
Mais une question demeure : comment parcourir le code original et récupérer la chaîne décodée depuis la ressource qu’utilise la fonction que nous analysons actuellement (en associant un entier à sa valeur lisible) ?
Source : CERT-SEKOIA
Comment récupérer la méthode FTP STOR à partir de la valeur entière 107364636 ?
Source : CERT-SEKOIA
Découper en parties le code python et passer au peigne fin la fonction de décodage ne fonctionne pas. Nous avons raté quelque chose.
Sur la fonction déléguée dans le module HouseofCards et la classe Strings, on remarque que 4 appels à Opcodes sont utilisés :
- OpCodes.Ldarg_0 qui pousse l’argument à l’index 0 représentant l’entier utilisé par la Get dans la classe Strings ;
- OpCodes.Ldc_I4 qui pousse un entier 32 bits sur la pile, dans ce cas le MetadataToken de la classe d’entrée (l’opération AND avec 0xFFFFFF = 16777215 n’a pas de sens) ;
- OpCodes.Sub qui soustrait la dernière valeur à la valeur précédente poussée sur la pile ;
- OpCodes.Call qui appelle simplement la fonction Get de la Strings avec le résultat de la soustraction précédente.
Source : CERT-SEKOIA
Le décodeur à une chaîne nécessite l’ajout d’une soustraction avec la valeur du MetadaToken de la fonction, qui est imprimée en tant que RID dans DNSpy.
Nous avons publié le code source sur le référentiel CERT GitHub, mais Jiří Vinopal a créé un moyen plus simple de gérer SmartAssembly v8+ sur son tweet de fin 2021. En utilisant la dernière version (2015) de Simple Assembly Explorer et son désobfuscateur intégré utilisant le profil Name, String et Flow et en cochant la case Delegate Call, associée au fameux ancien de4dot, vous obtenez l’échantillon désobscurci et plus lisible. Il suffit de passer du temps sur les chaînes base64.
Notre script est capable de décoder :
- L’intégralité du contenu des ressources précédemment extraites du malware ;
- Un entier unique associé à sa fonction RID à une chaîne de texte en clair ;
- Plusieurs chaînes associées à un rid.
Toutes les commandes nécessitent le fichier de ressources (extrait de l’exemple), la clé et le vecteur initial codés en dur à l’intérieur du malware (Module SmartAssembly.Zip — Méthode Unzip). L’outil a été testé sur différents échantillons de rançongiciels Spook, mais aussi sur d’autres rançongiciels connus pour utiliser le constructeur Thanos (Hackbit, Haron, RecoveryGroup) ou encore sur les acteurs de la menace les plus récents comme Midas. Celui-ci a le même processus, ils changent simplement la valeur codée en dur pour le XOR et le décalage dans la Get méthode Strings . Nous ajustons notre code pour être plus personnalisable concernant cette modification. La dernière version de SmartAssembly testée (rançongiciel Midas) était la v8.0.3.4821.
Conclusion
Pour conclure, en partant d’une réponse à un incident impliquant un acteur de menace opportuniste, nous avons réussi à fournir de nouvelles des renseignements sur la façon dont l’obscurcissement est mis en œuvre par le constructeur Thanos. Considérant que des acteurs malveillants peuvent acquérir ce builder pour quelques dollars, nous verrons dans la deuxième partie, rédigée par la Threat and Detection Team (TDR) de SEKOIA.IO, comment l’intelligence susmentionnée peut être étendue à l’ensemble de l’écosystème des ransomware.
Vous pouvez également lire l’article sur la vulnérabilité log4shell.
Sources
https://stackoverflow.com/questions/3911536/utf-8-unicode-whats-with-0xc0-and-0x80
https://www.recordedfuture.com/thanos-ransomware-builder/
https://github.com/SekoiaLab/CERT-Services/tree/main/202202-thanos_story_of_a_ransomware
Lire notre article sur :
- L’histoire d’un constructeur de ransomware : de Thanos à Spook et au-delà (Partie 2)
- Lapsus$ : quand les enfants jouent dans la cour des grands
- Le paysage des menaces ransomware observé par SEKOIA.IO durant le 1ᵉʳ semestre 2022
- The story of a ransomware builder: from Thanos to Spook and beyond (Part 1)
- The story of a ransomware builder: from Thanos to Spook and beyond (Part 2)
- Raccoon Stealer v2 – Partie 2 : Analyse approfondie.