Démantèlement de KillDisk : rétro-ingénierie du composant destructeur de BlackEnergy
Passons l’introduction longue sur la menace BlackEnergy et allons directement à l’étude du composant malveillant appelé « ololo.exe », également connu du public sous le nom de KillDisk. KillDisk est un module du cadre BlackEnergy visant à la destruction de données et à créer le chaos/distraction pendant les opérations APT.
https://www.virustotal.com/en/file/11b7b8a7965b52ebb213b023b6772dd2c76c66893fc96a18a9a33c8cf125af80/analysis/Les principaux outils utilisés dans notre analyse aujourd’hui sont Process Monitor qui fait partie des utilitaires SysInternals de Mark Russinovich et IDA Pro Disassembler. Toutes les manipulations seront effectuées dans un environnement virtuel basé sur le système d’exploitation Windows XP. Nous commençons par une configuration initiale rapide de la VM de test, nous allumons la machine et créons un instantané appelé « Avant l’infection ». Commençons !
Pour suivre tous les événements liés à notre objet d’étude nous lançons Process Monitor et nous assurons qu’il reste visible :Et qu’il suit le processus nécessaire :
Ensuite, nous chargeons le virus dans IDA Pro Disassembler et voyons l’image suivante.C’est la fonction WinMain, c’est-à-dire la fonction principale, donc nous commençons son analyse et voyons que la première (et seule) chose qu’elle fait est un appel de procédure nommé
sub_40E070
:
Allons donc directement à cette procédure. Ici, nous pouvons trouver des procédures responsables de l’extraction des extensions de fichiers en mémoire et la première procédure juste après celles-ci, qui appelle une chose intéressante appelée sub_40E080
:
Examinons de plus près son contenu :
Son intérieur contient un appel système intéressant
CreateFile
et quelques procédures sub_40C390
and sub_40C400
. Il est donc temps de prendre un instantané de notre laboratoire virtuel et de continuer notre voyage !
Examinons l’appel de fonction CreateFile, définissez le Point d’arrêt sur celui-ci et exécutez notre échantillon d’étude :
Un petit indice concernant cette fonction :
Comme nous le savons, la plupart des compilateurs passent les arguments à une fonction via une pile et puisque nous traitons d’un échantillon initialement écrit en C, et selon les directives C, les arguments sont poussés sur la pile de droite à gauche de manière à ce que le premier argument de la fonction soit poussé en dernier sur la pile et devienne le premier élément en haut. [1].
There is a special register for working with the stack – ESP (Extended Stack Pointer). The ESP, by definition, always points to the top of the stack:
Un aperçu de la pile :
Le dernier argument qui a été poussé dans la pile (celui en haut) est le premier argument accepté par la fonction (selon la syntaxe de
CreateFile
fonction) et il contient le nom de fichier dans notre cas \.PHYSICALDRIVE0
, ainsi la cible est le premier disque physique.Nous analysons maintenant d’autres arguments envoyés à la fonction en prenant leurs valeurs de la pile :
Quelques conclusions :
- lpFileName – pointeur vers une chaîne ASCIIZ contenant le nom (chemin) du fichier ouvert ou créé (comme nous l’avons déjà découvert) ;
- dwDesiredAccess – type d’accès au fichier : dans notre cas, la valeur est
0C0000000h
ce qui signifieGENERIC_READ+GENERIC_WRITE
oraccès en lecture-écriture
; - DwShareMode – un mode qui permet le partage des fichiers avec différents processus et ce paramètre peut avoir différentes valeurs, dans notre cas la valeur de 00000003b se traduit par
FILE_SHARE_READ+FILE_SHARE_WRITE
oud'autres processus peuvent ouvrir le fichier en lecture/écriture
; - IpSecurityAttributes – pointeur vers une structure SecurityAttributes (fichier winbase.h) qui définit les paramètres de sécurité liés à l’objet fichier noyau, si la sécurité n’est pas définie une valeur
NULL
est utilisée ; - dwCreationDistribution – est responsable de décider des actions lorsque le fichier existe ou non, dans notre cas une valeur de
3
signifieOPEN_EXISTING
orouvrir le fichier s'il existe, et retourner une erreur s'il n'existe pas
; - DwFlagsAndAttributes – flags et attributs; ce paramètre est utilisé pour définir les caractéristiques du fichier créé et est ignoré en mode lecture 0.
- hTemplateFile – le paramètre n’est utilisé que lors de la création d’un nouveau fichier
0
. - En cas de succès, la fonction retourne le nouveau gestionnaire de fichier à
ЕАХ
Et si la fonction échoue unNULL
est écrit dans leЕАХ
registre.
Bien, donc après avoir appelé CreateFile
nous obtenons une valeur non nulle pour EAX ce qui signifie qu’il contient le gestionnaire du fichier demandé par ex. \.PHYSICALDRIVE0
:
Nous avons notre gestionnaire alors passons à l’appel suivant,
sub_40C390
:
Comme précédemment, nous regardons à l’intérieur de la procédure et voyons un autre appel intéressant :
Indice de la fonction :
Un coup d’œil à la pile juste avant d’appeler cette fonction :Conclusions :
- hDevice – descripteur de périphérique (nous reconnaissons un vieil ami ici
\.PHYSICALDRIVE0
); - dwIoControlCode – code d’opération. Dans notre cas, la valeur est
0x70000h
ce qui signifieIOCTL_DISK_GET_DRIVE_GEOMETRY
ou récupération des informations concernant la géométrie des disques (nombre de cylindres, type de média, pistes sur cylindre, secteurs par piste, octets par secteur) ; - lpInBuffer – tampon avec des données d’entrée. Nous n’avons pas besoin de données d’entrée donc
NULL
il est ; - nInBufferSize – taille du tampon d’entrée en octets égale à
0
dans notre cas ; - lpOutBuffer – pointeur vers le tampon de sortie. Son type est défini par le paramètre dwIoControlCode
0x0011FCA8h
; - nOutBufferSize – taille du tampon de sortie en octets
0x18h
(24) ; - lpBytesReturned – un pointeur vers une variable qui contient la quantité d’octets écrits dans la sortie
0x0011FCA4h
; - lpOverlapped – pointeur vers une structure OVERLAPPED ;
- Lorsque l’appel est traité nous regardons à l’intérieur du tampon (à l’adresse qui a été envoyée comme argument à lpOutBuffer précisément
0x0011FCA8h
):
- À l’adresse de
0x0011FCA4h
nous avons un retour de la quantité d’octets écrits dans le tampon de sortie comme prévu par la fonction (marqué en vert) nous avons autant que demandé 24 symboles. - À l’adresse de
0x0011FCA8h
nous avons des informations sur la géométrie du premier disque physique (\.PHYSICALDRIVE0
):- nombre de cylindres –
0x519h
(1305) - type de média –
0x0Ch
(12) signifiant disque dur fixe. - pistes par cylindre –
0x0FFh
(255) - secteurs par piste –
0x3Fh
(63) - octets par secteur –
0x200
(512)
- nombre de cylindres –
Nous nous référons maintenant à la procédure trois, sub_40C400
:
À l’intérieur de la procédure deux appels ont immédiatement attiré notre attention, en particulier
SetFilePointerEx
etWriteFile
:
Comme d’habitude, nous regardons dans la pile d’appels de la fonction :
- hfile – notre descripteur de périphérique familier
\.PHYSICALDRIVE0
; - liDustanceToMove – position initiale pour commencer une écriture
0
; - lpNewFilePointer – pointeur vers une variable qui récupère un nouveau pointeur de position depuis le fichier
0x0011FCA8h
. Dans le cas où le paramètre est VIDE (NULL) un nouveau pointeur n’est pas renvoyé dans le fichier ; - dwMoveMethod – taille du tampon d’entrée en octets. Dans notre cas
0x200h
(512) – octets dans le secteur ;
Maintenant notre « bête » utilise la WriteFile
fonction et pour remplir le fichier de zéros auquel il a déjà établi un accès direct et fiable.
Indice de fonction :
Une WriteFile fonction écrit des données dans le fichier ou périphérique d’entrée/sortie spécifié à partir de la position spécifiée dans le fichier. Cette fonction est conçue pour une opération à la fois synchrone et asynchrone.
Liste des arguments de la fonction :Bien, pour nous assurer de ne rien manquer d’intéressant, nous regardons le tableau de bord et parmi toutes les choses nous prêtons attention à la pile :
Tout est prêt pour l’action :
- 1 – resté sur l’appel de fonction
WriteFile
- 2 – Process Monitor anticipe anxieusement le moindre mouvement de notre « bête »
- 3 – arguments poussés des registres vers la pile
- 4 – et bien, la pile elle-même
- Un carré dédié dans la fenêtre Hex View-EBP nous avons marqué une zone qui est adressée par le deuxième argument de la fonction (tampon de données) et vous pouvez me croire sur parole – elle contient exactement
0x200h
(512
) de zéros.
Nous exécutons la commande en pressant la touche F8 et observons le changement :Comme prévu, l’œil attentif de Process Monitor est en plein dans le mille et fixe l’écriture de 512 octets à partir de la position 0.
Ensuite, la procédure se termine et est appelée à nouveau mais maintenant avec des valeurs différentes :
Maintenant nous prenons les 512 octets suivants ce qui est particulièrement visible à 0x200 (contrairement aux arguments de pile précédents que nous avons marqués par un cadre bleu)Et ils subissent le même sort que les précédents :
Ce manège continue jusqu’à ce que la vérification de
0x40C865h
adresse renvoie la valeur de EBX
registre égale à 255 fois
(Pour s'assurer que cela s'est produit, nous plaçons un point d'arrêt sur l'instruction qui suit notre cycle vicieux de « destruction » :
):
d’opérations de réécriture :
256
C’est à peu près tout, notre « bête » a fait son sale travail et ferme le gestionnaire de fichier en appelant la
That’s basically it, our “beast” has done its dirty work and closes the file handler by calling the CloseHandle
fonction.
Indice de fonction :
Arguments de la fonction :
Notre vue traditionnelle de la préparation de la pile :
Et qu’avons-nous dans le registre ESI ?
Comme prévu notre descripteur familier
0x44h
est passé à la pile comme argument de la fonction.
Nous voyons les valeurs retournées par la fonction dans le registre EAX : Si la fonction se termine avec succès la valeur retournée
non nulle
.
Si la fonction existe avec une erreur la valeur retournée est nulle.
Cette action n’a pas échappé à l’œil vigilant de notre Process Monitor:
Tout a fonctionné normalement (si vous pouvez appeler la destruction de la partie la plus importante du lecteur système normale) et le fichier est fermé.
Continuons :Pour une perception plus confortable, j’ai renommé la fonction en
Effaceur
. Après avoir détruit avec succès les données sur PhysicalDrive0
, le compteur situé dans ESI
registre est augmenté de 1
, vérifié avec la valeur de 0x0Ah
(10
) et lance une opération de réécriture avec une nouvelle valeur :et ainsi de suite jusqu’à ce que 10 soit atteint.
De cette façon, la bête continue de parcourir tous les disques physiques et d’effacer les premiers 512 * 255 = 128ko
(cela dépend du nombre d’octets dans un secteur que la « bête » a appris en connaissant la géométrie du disque).
Cette fois cependant, le résultat de la fonction CreateFileW
sera une valeur suivante : Ce qui signifie que dans notre « laboratoire improvisé », il n’y a pas de deuxième disque physique (ou bien qu’il n’existe pas de disque avec un numéro de séquence supérieur à 0) et que le cycle n’a rien à réécrire.
Maintenant que nous sommes armés de connaissances fraîchement acquises, essayons de désactiver la fonction destructrice que nous venons d’apprendre en modifiant le code de manière à ce que notre modification puisse permettre de contourner la réécriture des données :
Souvenons-nous de la procédure
sub_40E080
qui contient l’appel de fonction CreateFileW
. Puisque nous savons que la valeur de 0xFFFFFFFFh
apparaîtrait après l’achèvement de la fonction CreateFileW
uniquement si le disque physique n’est pas présent et dans un tel scénario, la fonction « destruction de données » effectue une sortie, changeons une instruction de branchement conditionnel située à l’adresse 0x0040C7E4h
à une instruction inconditionnelle :
IDA Pro est un outil intéressant, alors nous devons juste écrire une commande.
La seule chose que nous devons considérer est la taille de la commande avant et après, donc vérifions combien d’octets sont utilisés par la séquence conditionnelle :
Comme nous pouvons le voir, une instruction de branchement conditionnelle équivaut à
6
octets, tandis que la séquence inconditionnelle ne prendra que 5
et donc nous devons ajouter une autre commande – NOP, afin de garder l’équilibre :Nous faisons un NOP sur la commande suivante, peu importe ce qu’elle contenait avant (même si nous voyons un 0 là) :
Nous vérifions deux fois que les adresses des commandes sont maintenant égalisées en fonction de l’adresse de la commande suivante :
et appuyons sur annuler. Terminé.
Après ces manipulations, la procédure semblera simple : entrer et sortir, en faisant une boucle vide (puisque nous ne savons pas quelles autres parties du programme peuvent s’y accrocher, nous la laissons telle quelle).La preuve finale trouvée par Process Monitor indique qu’aucun disque physique n’a été endommagé suite à l’exécution disséquée de KillDisk :
Le malware a lu les informations sur le disque, les a étudiées et a continué ses affaires que nous analyserons en profondeur dans nos prochains articles.
Sources :
[1] – Hacker Disassembling Uncovered. Kris Kaspersky, 2009.
Traduit de l’original par Andrii Bezverkhyi | CEO SOC Prime