François a effectué un stage chez nous entre avril et août. Il a notamment travaillé au développement d’un outil qui extrait les symboles d’un programme écrit en Go. Voici un article qui présente une application de son travail pour l’analyse et la comparaison de ransomware.

Analyse d'EKANS

EKANS (SNAKE à l’envers) est un nouveau ransomware programmé en langage Go apparu en décembre 2019, il a notamment affecté Honda et Enel. La particularité de ce dernier, celle qui a suscité de l’intérêt, ne réside pas dans sa méthode de propagation mais plutôt dans sa volonté de « tuer » des systèmes de contrôle industriel. Cette particularité a été notamment étudiée et expliquée par Dragos.

La technologie GLIMPS permet de détecter des similarités entre binaires partageant du code source, même compilés différemment ou pour des architectures différentes. De plus, j’ai, au cours de mon stage dans l’entreprise, développé un outil permettant d’extraire les symboles du runtime d’exécutable Go.

Deux malware avec des similarités identifiées, et écrits en Go… C’est donc un bon cas d’étude pour vérifier la pertinence des résultats et pour tester l’outil de récupération des symboles en situation réelle !

Cette analyse nous a ainsi permis de vérifier l’origine commune indiscutable des virus utilisés dans les attaques Honda et Enel.

Application de la technologie de conceptualisation de code de GLIMPS

GLIMPS a mis au point une technologie de conceptualisation de code permettant de comparer très efficacement des logiciels compilés entre eux, même s’ils ont subi des transformations lors des étapes de compilation, ou qu’ils ciblent des architectures différentes.
2 applications de cette technologie ont été développées : GLIMPS-Malware compare un fichier à une base de millions d’exemplaires de malware classés, de manière à identifier si un fichier est bienveillant ou malveillant, et s’il est malveillant, à caractériser l’origine probable de l’attaque.
GLIMPS-Audit facilite l’analyse fine d’un logiciel, légitime ou malware, en identifiant et documentant le code connu (bibliothèques sur étagères…) le constituant. Il permet également la comparaison logiciel à logiciel, ce que nous avons utilisé dans le cadre de cette analyse.

En passant les exemplaires provenant des attaques Honda et Enel dans GLIMPS-Malware, ils sont immédiatement détectés comme des fichiers malveillants et caractérisés comme des variantes du ransomware SNAKE. A priori, nous avons récupéré les bons fichiers !

Grâce à GLIMPS-Audit, nous avons ensuite comparé les 2 ransomware entre eux. Nous pouvons ainsi confirmer que, malgré les méthodes d’offuscation, les ransomware ayant ciblé Enel et Honda sont quasiment identiques et issus de la même origine : sur 4369 fonctions, 4230 sont ainsi identifiés de manière certaine, et 88 matchs supplémentaires ont une bonne probabilité d’être identiques. Il reste ainsi, sur les 4369 fonctions des 2 ransomware moins de 50 fonctions pour lesquelles la corrélation est moins immédiate.

La récupération des symboles runtime

Le langage Go est un langage de haut niveau possédant un garbage collector et un runtime capturant les erreurs d’exécutions (division par 0, buffers overflow, etc.). Le runtime de Go lorsqu’il détecte une erreur provoque une « panic » et affiche le détail de l’erreur. Par exemple :

panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x460c1f]

goroutine 1 [running]:
main.main()
/tmp/sandbox311401280/prog.go:18 +0x1f
exit status 2

Ce détail contient le chemin des fichiers sources (/tmp/sandbox311401280/prog.go), le nom des fonctions (main.main), les numéros de lignes (18), l’adresse des fonctions (pc=0x460c1f et +0x1f donc l’adresse de la fonction est 0x460c00), etc… Ce sont ces mêmes symboles que nous récupérons.

Une fois les symboles des EKANS appliqués au code désassemblé, d’importantes similitudes sautent aux yeux. En effet, presque toute les fonctions du paquet main (paquet contenant la fonction main en Go) ont vu leur nom ainsi que le chemin des fichiers les contenants offusqué, transformé en une chaîne aléatoire. Notons, que le nom de la fonction main du paquet main, main.main (main_main dans IDA) n’a pas pu être changée puisqu’elle est nécessaire à la compilation (c’est le point d’entrée du programme) et que leurs modifications ont lieu avant cette dernière. De plus, le numéro de ligne de plusieurs fonctions correspond, par exemple la fonction main est à la ligne 339 dans les deux ransomwares. Mais encore, l’offuscation du chemin des fichiers est partielle et montre d’importants points communs, comme nous pouvons le voir dans l’exemple ci-dessous :

« Enel » : [{
« Name »: »main.cempcamllplldijidcni »,
« File »: »C:/Users/Admin3/go/src/nmhgfmidblhkfacdajkc/pbopnijdlnfbnimbiham.go »,
« Line »:254
}]

« Honda » : [{
« Name »: »main.jfplojgoheneeggeidee »,
« File »: »C:/Users/Admin3/go/src/cgillndadlcobliljlfo/nknejkdfponfiknchiad.go »,
« Line »:254
}]

Les fonctions sont situées à la même ligne et sont dans le même paquet (main), sont situées dans des fichiers dont les chemins commencent de la même façon et sont offusqués de la même façon. Nous pouvons dès lors considérer sans prendre trop de risque que ce sont les mêmes fonctions. Ce qui était déjà confirmé par les résultats de GLIMPS-Audit et qu’on voit également par une analyse plus précise avec un logiciel de rétro-ingénierie (IDA). Ces observations permettent ainsi dans de nombreux cas de venir confirmer la pertinence des résultats obtenus avec GLIMPS-Audit.

Une autre importante similitude visible depuis les symboles concerne les noms des fonctions importés. En effet, pour les deux ransomwares, les mêmes fonctions des mêmes paquets sont importées.

La recherche des fonctions pour tuer les processus

Une fois les symboles précédemment récupérés chargés dans un logiciel de rétro-ingénierie nous avons décidé de chercher le mécanisme utilisé par les ransomwares pour tuer des processus.

Pour cela nous avons recherché les différentes fonctions Go permettant de fermer un processus. Malheureusement, sa bibliothèque standard ne fournit pas de fonction effectuant une telle tâche indépendamment de l’OS. Nous avons donc regardé du côté des bibliothèques Windows (puisque les ransomwares sont des PE permettant de tuer un processus dont l’on connaît le nom ainsi que des chaînes de caractères des noms des exécutables habituellement visés par les ransomwares. Ces exécutables sont, entre autres, les antivirus (par exemple, kavshell.exe) et les navigateurs (par exemple, firefox.exe). Seul la fonction TerminateProcess a été trouvé. En remontant l’origine de ses arguments nous avons compris que les autres fonctions, notamment celles permettant de lister les processus, étaient importées dynamiquement. Une fois la fonction permettant d’effectuer des importations dynamiques trouvées NewLazyDll nous avons réalisé que si nous ne trouvions pas de noms de fonction ou de processus c’était parce que toutes les chaînes utilisées par les deux ransomwares sont chiffrées. En fait, chacune des assignations de chaîne a été remplacé par un appel vers une fonction littérale qui utilise deux autres chaines afin d’obtenir et de retourner la chaîne d’origine. Par exemple,

print(« Hello world\n »);
//devient
print(func () string {

var string1 = « \x21\x0d\xc3\xb5\x79\x7c\x2d\x9e\x38\x92\xb2\x99 »
var string2 = « \x69\x6a\xab\xd7\xee\xa6\x4e\xc3\x3a\xc8\xa2\xa5 »

var slice1 = []byte(string1)
var slice2 = []byte(string2)

var size = len(slice1)

var sliceRes = make([]byte, size)
for i := 0; i < size; i++ {
sliceRes[i] = byte((int(slice1[i]) + i*2) ^ int(slice2[i]))
}
return string(sliceRes)/*la valeur de retour est « Hello world\n »*/

}())

Cette méthode d’offuscation cause la création de plus de 1 000 fonctions littérales presque identiques et double l’espace pris par les chaînes de caractères dans la section data. Le chiffrement étant relativement simple la fonction en langage Go suivante permet de déchiffrer la chaîne d’origine à partir des deux nouvelles chaines correspondantes :

func decode(a, b string) string{
var slice1 = []byte(a)
var slice2 = []byte(b)

var size = len(slice1)

var sliceRes = make([]byte, size)
for i := 0; i < size; i++ {
sliceRes[i] = byte((int(slice1[i]) + i*2) ^ int(slice2[i]))
}
return string(sliceRes)
}

Une fois les chaînes déchiffrées nous avons trouvé, pour chacun des ransomwares les noms des fonctions importées dynamiquement et le nom des processus tués par le ransomwares. La totalité des chaînes déchiffrées est par exemple présente ici pour Enel. Nous observons aussi que la très grande majorité des chaînes déchiffrées obtenues sont présentes dans chacun des deux ransomwares.

La procédure de chiffrement

Nous avons ensuite cherché à comprendre le mécanisme de chiffrement du programme. Pour cela nous sommes partis des bibliothèques de chiffrement Go et avons regardé lesquelles sont utilisées. La première trouvée fut l’AES-256. Après avoir étudié le cheminement des entrées et des sorties de la fonction de chiffrement AES et des appels de fonctions documentées environnant nous avons pu déterminer la procédure de chiffrement, encore une fois la même pour les deux ransomwares. Tout d’abord un couple de clé publique, clé privée qui servira pour effectuer un chiffrement (et plus tard un déchiffrement) RSA est obtenue. La clé publique est écrite en dure dans les sources du programme tandis que la clé privée est conservée par les concepteurs du ransomware. Ainsi, seule la clé publique est présente dans l’exécutable. Précisons, que les ransomwares ayant ciblé Honda et Enel ont des clés publiques différentes. La seconde étape est le listage des fichiers. Les ransomwares parallélisent leur calcul afin de chiffrer plusieurs fichiers simultanément. Ensuite, pour chacun des fichiers trouvés, une clé de 256-bits est générée aléatoirement. Cette dernière est utilisée afin d’effectuer un chiffrement AES-256 sur le fichier. Elle est ensuite chiffrée avec un RSA utilisant la clé publique précédemment évoquée. Enfin, le résultat du RSA (la clé de l’AES chiffrée) est formaté avant d’être écrite à la fin du fichier chiffré. Rappelons, que puisque le RSA est un chiffrement asymétrique, c’est avec la clé privée que possède le concepteur du ransomware uniquement que l’on pourra déchiffrer la clé de l’AES présente à la fin du fichier et donc déchiffrer le fichier. Vous trouverez ci-dessous un schéma détaillant la procédure de chiffrement et de déchiffrement du ransomware.

Conclusion

Ainsi, nous pouvons dire que les ransomwares sont bien deux versions du même programme. Glimps Audit a détecté avec succès leurs importantes similitudes. De plus, l’outil de récupération des symboles Go s’est avéré efficace et a fortement facilité le travail de rétro-conception.

Categories: Blog