Demontage von KillDisk: Reverse Engineering der destruktiven Komponente von BlackEnergy
Lassen Sie uns die lange Einführung in die BlackEnergy-Bedrohung überspringen und direkt zur Untersuchung der Malware-Komponente namens „ololo.exe“ gehen, auch bekannt als KillDisk. KillDisk ist ein Modul des BlackEnergy-Frameworks, das auf die Zerstörung von Daten abzielt und während der APT-Operationen Chaos und Ablenkung schafft.
https://www.virustotal.com/en/file/11b7b8a7965b52ebb213b023b6772dd2c76c66893fc96a18a9a33c8cf125af80/analysis/Die Hauptwerkzeuge, die wir heute in unserer Analyse verwenden, sind Process Monitor das Teil der SysInternals-Utilities von Mark Russinovich ist und IDA Pro Disassembler. Alle Manipulationen werden in einer virtuellen Umgebung basierend auf dem Betriebssystem Windows XP durchgeführt. Wir beginnen mit der schnellen Grundkonfiguration der Test-VM, schalten die Maschine ein und erstellen einen Snapshot namens „Vor der Infektion“. Lasst uns beginnen!
Um alle Ereignisse im Zusammenhang mit unserem Untersuchungsgegenstand zu verfolgen, starten wir Process Monitor und stellen sicher, dass es sichtbar bleibt:Und dass es den benötigten Prozess verfolgt:
Als nächstes laden wir das Virus in IDA Pro Disassembler und sehen folgendes Bild.Dies ist die WinMain-Funktion, d.h. die Hauptfunktion, also beginnen wir mit ihrer Analyse und sehen, dass das erste (und einzige) was sie tut, ein Prozeduraufruf namens
sub_40E070
:
ist. Lassen Sie uns direkt zu dieser Prozedur gehen. Hier finden wir Prozeduren, die für die Extraktion von Dateierweiterungen in den Speicher verantwortlich sind, und direkt danach wird eine interessante Sache namens sub_40E080
:
aufgerufen. Werfen wir einen genaueren Blick auf deren Inhalt:
Ihre Innenseiten enthalten einen interessanten Systemaufruf
CreateFile
und ein paar Prozeduren sub_40C390
and sub_40C400
. Es ist also an der Zeit, einen Snapshot unseres virtuellen Labors zu machen und unsere Reise fortzusetzen!
Untersuchen wir den Funktionsaufruf CreateFile, setzen wir den BreakPoint darauf und führen unser Studienobjekt aus:
Ein kleiner Hinweis zu dieser Funktion:
Wie wir wissen, geben die meisten Compiler Argumente über einen Stack an eine Funktion weiter, und da wir es mit einem Beispiel zu tun haben, das ursprünglich in C geschrieben wurde, und gemäß den C-Direktiven werden Argumente von Stack von rechts nach links übergeben, sodass das erste Funktionsargument als letztes auf den Stack gepusht wird und schließlich als erstes Element oben endet. [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:
Ein Blick in den Stack:
Das letzte Argument, das in den Stack gepusht wurde (eines oben drauf), ist das erste Argument, das von der Funktion akzeptiert wird (gemäß der Syntax der
CreateFile
Funktion) und in unserem Fall den Dateinamen enthält \.PHYSICALDRIVE0
, somit ist das Ziel die erste physische Festplatte.Nun analysieren wir andere Argumente, die an die Funktion gesendet werden, indem wir ihre Werte aus dem Stack entnehmen:
Einige Schlussfolgerungen:
- lpFileName – Zeiger auf eine ASCIIZ-Zeichenkette, die den Namen (Pfad) der geöffneten oder erstellten Datei enthält (wie wir bereits herausgefunden haben);
- dwDesiredAccess – Zugriffsart auf die Datei: in unserem Fall ist der Wert
0C0000000h
was bedeutetGENERIC_READ+GENERIC_WRITE
orLese-Schreib-Zugriff
; - DwShareMode – ein Modus, der das Teilen der Datei mit verschiedenen Prozessen ermöglicht und dieser Parameter kann unterschiedliche Werte haben; in unserem Fall übersetzt der Wert pf 00000003b in
FILE_SHARE_READ+FILE_SHARE_WRITE
oderandere Prozesse können die Datei zum Lesen/Schreiben öffnen
; - IpSecurityAttributes – Zeiger auf die SecurityAttributes-Struktur (Datei winbase.h), die Sicherheitseinstellungen im Zusammenhang mit dem Kernel-Dateiobjekt definiert; wenn die Sicherheit nicht festgelegt ist, wird ein
NULL
Wert verwendet; - dwCreationDistribution – entscheidet über Aktionen, wenn eine Datei existiert oder nicht existiert, in unserem Fall hat der Wert von
3
die BedeutungOPEN_EXISTING
oröffnen Sie die Datei, wenn sie existiert, und geben Sie einen Fehler zurück, wenn sie nicht existiert
; - DwFlagsAndAttributes – Flags und Attribute; dieser Parameter wird verwendet, um die Merkmale der erstellten Datei festzulegen und wird im Lesezugriffsmodus 0 ignoriert.
- hTemplateFile – Parameter wird nur verwendet, wenn eine neue Datei erstellt wird
0
. - Bei Erfolg gibt die Funktion den Handler der neuen Datei an
ЕАХ
zurück, und wenn die Funktion fehlschlägt, wird einNULL
in das Register geschrieben.ЕАХ
Alright, nachdem die Funktion
aufgerufen wurde, erhalten wir einen Wert ungleich Null für EAX, was bedeutet, dass er den Handler der angeforderten Datei enthält, z. B. CreateFile
we get a non-zero value for EAX which means that it contains the handler of requested file e.g. \.PHYSICALDRIVE0
:
Wir haben unseren Handler, fahren wir fort zum nächsten Aufruf,
sub_40C390
:
Wie zuvor schauen wir in die Prozedur und sehen einen weiteren interessanten Aufruf:
Funktionshinweis:
Ein Blick in den Stack direkt vor dem Aufruf dieser Funktion:Schlussfolgerungen:
- hDevice – Gerätedescriptor (wir erkennen hier einen bekannten Wert
\.PHYSICALDRIVE0
); - dwIoControlCode – Operationscode. In unserem Fall ist der Wert
0x70000h
was bedeutetIOCTL_DISK_GET_DRIVE_GEOMETRY
oder Abruf der Informationen zur Plattengeometrie (Anzahl der Zylinder, Medientyp, Spuren pro Zylinder, Sektoren pro Spur, Bytes pro Sektor); - lpInBuffer – Puffer mit Eingabedaten. Wir benötigen keine Eingabedaten, daher
NULL
ist es; - nInBufferSize – Größe des Eingabepuffers in Bytes gleich
0
in unserem Fall; - lpOutBuffer – Zeiger auf den Ausgabepuffer. Sein Typ wird durch den Parameter dwIoControlCode festgelegt
0x0011FCA8h
; - nOutBufferSize – Größe des Ausgabepuffers in Bytes
0x18h
(24); - lpBytesReturned – ein Zeiger auf eine Variable, die die Anzahl der Bytes enthält, die in den Ausgang geschrieben wurden
0x0011FCA4h
; - lpOverlapped – Zeiger auf eine OVERLAPPED-Struktur;
- Sobald der Aufruf verarbeitet ist, schauen wir in den Puffer (an der Adresse, die als Argument an lpOutBuffer gesendet wurde)
0x0011FCA8h
):
um die Antwort zu interpretieren:
- An der Adresse von
0x0011FCA4h
haben wir eine Rückgabekodierung der Anzahl der in den Ausgangspuffer geschriebenen Bytes, wie von der Funktion beabsichtigt (grün markiert), genau so viele, wie wir angefordert haben, nämlich 24 Symbole. - An der Adresse von
0x0011FCA8h
wir haben Informationen über die Geometrie der ersten physischen Festplatte erhalten (\.PHYSICALDRIVE0
):- Anzahl der Zylinder –
0x519h
(1305) - Medientyp –
0x0Ch
(12), feststehende Festplatte. - Spuren pro Zylinder –
0x0FFh
(255) - Sektoren pro Spur –
0x3Fh
(63) - Bytes pro Sektor –
0x200
(512)
- Anzahl der Zylinder –
Jetzt beziehen wir uns auf die dritte Prozedur, sub_40C400
:
Im Inneren der Prozedur zogen zwei Aufrufe sofort unsere Aufmerksamkeit auf sich, speziell
SetFilePointerEx
undWriteFile
:
Wie üblich werfen wir einen Blick in den Funktionsaufruf-Stack:
- hfile – unser bekannter Gerätedeskriptor
\.PHYSICALDRIVE0
; - liDustanceToMove – anfängliche Position zum Beginn eines Schreibens
0
; - lpNewFilePointer – ein Zeiger auf eine Variable, die einen neuen Positionszeiger aus der Datei abruft
0x0011FCA8h
. Falls der Parameter LEER (NULL) ist, wird kein neuer Zeiger in der Datei zurückgegeben; - dwMoveMethod – Größe des Eingabepuffers in Bytes. In unserem Fall
0x200h
(512) – Bytes im Sektor;
Jetzt verwendet unser „Viech“ die WriteFile
Funktion und um die Datei mit Nullen zu füllen, auf die es bereits direkten und zuverlässigen Zugriff hat.
Ein Funktionstipp:
Eine WriteFile Funktion schreibt Daten in die angegebene Datei oder das Eingabe-/Ausgabegerät (I/O-Gerät), beginnend mit der in der Datei angegebenen Position. Diese Funktion ist für sowohl synchrone als auch asynchrone Operationen ausgelegt.
Liste der Funktionsargumente:In Ordnung, damit wir nichts Interessantes verpassen, werfen wir einen Blick ins Dashboard und achten dabei auf den Stack:
Alles bereit für den Einsatz:
- 1 – Bleibend beim Funktionsaufruf
WriteFile
- 2 – Process Monitor wartet gespannt auf den kleinsten Zucken der „Muskeln“ unseres „Viech“
- 3 – Argumente von Registern in den Stack gedrückt
- 4 – und nun, der Stack selbst
- Ein spezielles Quadrat im Fenster Hex View-EBP markierten wir ein Gebiet, das von dem zweiten Funktionsargument (Datenpuffer) adressiert wird und Sie können mir glauben – es enthält genau
0x200h
(512
) Nullen.
Wir führen den Befehl durch Drücken der F8-Taste aus und beobachten die Änderung:Wie erwartet ist das scharfe Auge von Process Monitor spot on und fixiert das Schreiben von 512 Bytes beginnend von Position 0.
Dann verlässt die Prozedur und wird erneut aufgerufen, jedoch nun mit unterschiedlichen Werten:
Nun nehmen wir die nächsten 512 Bytes, was insbesondere bei 0x200 sichtbar ist (im Gegensatz zu vorherigen Argumenten in dem wir den Stack durch einen blauen Rahmen markierten)Und das gleiche Schicksal ereilt sie:
Diese Karussellfahrt geht weiter, bis die Überprüfung von
0x40C865h
Adresse den Wert von EBX
Registrierung gleich 0x100h
(255 Mal
):
Um sicherzugehen, dass dies geschehen ist, setzen wir einen Stoppunkt an der Anweisung, die unserem destruktiven Zyklus folgt:
Insgesamt erhalten wir
256
von Überschreiboperationen:Das ist im Wesentlichen alles, unser „Viech“ hat seine dreckige Arbeit erledigt und schließt den Dateihandler, indem es die
CloseHandle
Funktion aufruft.
Funktionshinweis:
Funktionsargumente:
Unser traditioneller Stack Vorbereitungsblick:
Und was haben wir im ESI-Register?
Wie erwartet wird unser bekannter Deskriptor
0x44h
an den Stack als Funktionsargument übergeben.
Wir sehen die von der Funktion zurückgegebenen Werte im EAX-Register: Wenn die Funktion erfolgreich endet, ist der zurückgegebene Wert
nicht-null
.
Wenn die Funktion mit einem Fehler endet, ist der zurückgegebene Wert null.
Diese Aktion entging nicht dem wachsamen Auge unseres Process Monitor:
Alles verlief normal (wenn man die Zerstörung des wichtigsten Teils des Systemlaufwerks normal nennen kann) und die Datei wird geschlossen.
Lass uns weitermachen:Für eine bequemere Wahrnehmung benannte ich die Funktion um in
Eraser
. Nachdem es erfolgreich die Daten auf PhysicalDrive0
zerstört hat, wird der Zähler im ESI
Register um 1
erhöht, mit dem Wert von 0x0Ah
(10
) verglichen und startet den Überschreibungsvorgang mit einem neuen Wert:und so weiter, bis die 10 erreicht ist.
Auf diese Weise wandert das Viech weiter über alle physischen Laufwerke und löscht die ersten 512 * 255 = 128kb
(dies hängt von der Anzahl der Bytes in einem Sektor ab, die „Viech“ aufgrund der Kenntnis der Plattengeometrie gelernt hat).
Dieses Mal wird jedoch das Ergebnis der Funktion CreateFileW
ein folgender Wert sein: Was bedeutet, dass in unserem „improvisierten Labor“ kein zweites physisches Laufwerk (oder nicht irgendeine Art von Laufwerk mit einer Sequenznummer größer als 0) existiert und der Zyklus nichts zu überschreiben hat.
Jetzt, da wir mit frisch erworbenem Wissen bewaffnet sind, lassen Sie uns versuchen, die destruktive Funktion, die wir gerade gelernt haben, zu deaktivieren, indem wir den Code so ändern, dass unsere Änderung es erlaubt, das Überschreiben der Daten zu umgehen:
Erinnern wir uns an die Prozedur
sub_40E080
die den Funktionsaufruf CreateFileW
enthält. Da wir wissen, dass der Wert von 0xFFFFFFFFh
nach Abschluss der Funktion CreateFileW
nur dann erscheinen würde, wenn kein physisches Laufwerk vorhanden ist und in einem solchen Szenario die Funktion „Datenzerstörung“ einen Ausgang ausführt, ändern wir eine bedingte Verzweigungsanweisung an der 0x0040C7E4h
Adresse in eine unbedingte:
IDA Pro ist ein cooles Instrument, sodass wir einfach einen Befehl schreiben müssen.
Das Einzige, worauf wir achten müssen, ist die Befehlsgröße vorher und nachher, also lassen Sie uns überprüfen, wie viele Bytes von der bedingten Sequenz verwendet werden:
Wie wir sehen können, entspricht eine bedingte Verzweigungsanweisung
6
Bytes, während die bedingungslose Sequenz nur 5
nimmt, und deshalb sollten wir einen weiteren Befehl hinzufügen – NOP, um die Dinge gleich zu halten:Wir NOP den nächsten Befehl, unabhängig davon, was er vorher enthielt (auch wenn wir dort eine 0 sehen):
Wir überprüfen doppelt, dass die Befehlsadressen jetzt basierend auf der Adresse des folgenden Befehls gleichgestellt sind:
und drücken Abbrechen. Fertig.
Nach diesen Manipulationen sieht die Prozedur einfach aus: eintreten und austreten, eine leere Schleife erstellen (da wir nicht wissen, welche anderen Teile des Programms darin eingreifen könnten, belassen wir sie in diesem Zustand).Der letzte Beweis, der durch den Process Monitor festgestellt wurde, zeigt, dass keine physischen Laufwerke bei der Ausführung des zerlegten KillDisk geschädigt wurden:
Malware hat die Information über die Festplatte gelesen, sie untersucht und mit ihrem Geschäft fortgefahren, das wir in unseren nächsten Artikeln gründlich analysieren werden.
Quellen:
[1] – Hacker Disassembling Uncovered. Kris Kaspersky, 2009.
Übersetzt aus dem Original von Andrii Bezverkhyi | CEO SOC Prime