Analyse vom ShadowHammer ASUS-Angriff
Kaspersky hat kürzlich einen Artikel über einen Supply-Chain-Angriff namens „Operation ShadowHammer“ veröffentlicht. Darin wird beschrieben, wie die Angreifer gezielt Malware über das „ASUS Live Update Utility“ verbreiteten. Unsere Kollegen Alex Davies und Matt Hillman von Countercept analysierten in ihrem Beitrag (LINK), wie die First-Stage-Payload bei diesem Angriff funktionierte. Die deutsche Übersetzung könmnen Sie hier nachlesen.
Einführung
Am 25. März 2019 veröffentlichte Kaspersky einen größeren Bericht über einen Angriff auf ASUS:
Im Januar 2019 entdeckten wir einen komplexen Supply-Chain-Angriff, von dem das ‚ASUS Live Update‘-Hilfsprogramm betroffen war. Der Angriff fand zwischen Juni und November 2018 statt und betraf nach unseren Messungen eine sehr große Anzahl von Nutzern …
Das Ziel des Angriffs war eine unbekannte Menge von Anwendern, die chirurgisch exakt über die MAC-Adressen ihrer Netzwerk-Adapter identifiziert wurden. Um dies zu erreichen, hatten die Angreifer eine Liste von MAC-Adressen fest in die Trojaner-Software einprogrammiert. Diese Liste wurde verwendet, um die vorgesehenen Ziele dieser umfangreichen Operation zu identifizieren.
Der Originalbericht enthält sehr viel mehr wertvolle Informationen, obwohl die technischen Details auf dieser Ebene begrenzt blieben. Um mehr über diesen Angriff zu erfahren, beschlossen wir, uns die Payloads genauer anzusehen.
Historie der Aktivitäten
Der Bericht von Kaspersky bezieht sich auf ein Zip-File, das eine Kopie des ASUS Live Update-Hilfsprogramms darstellt. In diesem Zip befanden sich drei Dateien: zwei MSIs und eine Datei namens Setup.exe. Beim Nachvollziehen der Historie dieser Dateien und bei der Untersuchung der Dateien selbst bestätigte sich, dass Setup.exe von den Angreifern mit einer Backdoor versehen worden war.
Um besser verstehen zu können, wie die Angreifer vorgingen, haben wir historische Samples von VirusTotal untersucht. Kaspersky berichtet, dass der Angriff von Juni bis November 2018 stattfand – was wir auf Basis der bei VirusTotal eingereichten Samples bestätigen konnten. Das erste entsprechende Sample datiert vom 29. Juni und das jüngste vom 17. November.
Eine ausführliche Analyse dieser Samples brachte zutage, dass mindestens zwei verschiedene Backdoor-Varianten verbaut wurden. Von Juni bis September nutzten die Angreifer einen verschlüsselten Payload zusammen mit einer pepatchten WinMain, um die Durchführung umzuleiten.
Seit September wurde eine besser verborgene Backdoor verwendet, die eine verschleierte Shellcode-Payload mit Decoder beinhaltete und über die Funktion _crtExitProcess ausgeführt wurde. Es wurde auch festgestellt, dass alle Samples den gleichen C2-Kanal mit asushotfix[.]com verwenden, der erstmals am 5. Mai 2018 mit der IP-Adresse 141.105.71[.]116 in Russland registriert wurde.
Bei der Pivotisierung auf diese IP-Adresse wurden auch die folgenden zusätzlichen Domains gefunden:
Domain | Registriert |
host2[.]infoyoushouldknow[.]biz | 2013-04-27 |
nano2[.]baeflix[.]xyz | 2016-03-24 |
asushotfix[.]com | 2018-05-22 |
www[.]asushotfix[.]com | 2018-07-13 |
homeabcd[.]com | 2018-09-05 |
simplexoj[.]com | 2018-09-11 |
Es ist unklar, welche Rolle diese Domänen gespielt haben, aber es besteht die große Wahrscheinlichkeit, dass sie von derselben Bedrohungsgruppe bei anderen Angriffen verwendet wurden.
Im nächsten Abschnitt wollen wir das Sample genauer ansehen, das von Kaspersky als MD5:55a7aa5f0e52ba4d78c145811c830107 referenziert wird.
Laden des Shellcodes
Die Binärdatei des Setup.exe erscheint als legitime Datei: Sie ist signiert, die Meta-Informationen entsprechen legitimen Files und der Großteil des Codes stimmt mit anderen legitimen Dateien überein. Dennoch brachte eine genauere Untersuchung zutage, dass anormaler ausführbarer Code eingebettet worden war.
Als wir tiefer in die Datei einstiegen, entdeckten wir, dass der Code modifiziert worden war – er enthält nun eine neue Funktion, um diese Payload zu extrahieren, zu dekodieren und auszuführen. Der Aufruf, mit dem dieser Prozess gestartet wird, wurde in der Funktion crtExitProcess platziert. Dies sorgt vermutlich dafür, dass der Updater wie erwartet abläuft und korrekt die Updates installiert, bevor die schädliche Payload ausgeführt wird.
Bei der Untersuchung der Shellcode-Dropping-Funktionen fanden wir heraus, dass im Setup.exe-Prozess zunächst mit einem VirtualAlloc-Call Memory bereitgestellt wird, in die dann der Shellcode geschrieben wird:
Interessanterweise werden in diesem Schritt nur die ersten 16 Bytes der Payload in die Memory kopiert, bevor sie dekodiert werden. Diese Bytes enthalten tatsächlich die Größe der Payload, die dann an einen zweiten VirtualAlloc-Call weitergereicht wird. Anschließend wird die eigentliche Shellcode geschrieben, dekodiert und ausgeführt.
Um die Decoding-Routine kümmern wir uns hier nicht weiter, aber ein ähnlicher Code wurde kürzlich von Winnti genutzt.
Analyse des Shellcodes
Soweit wir ihn bisher analysiert haben, führt der Shellcode die folgenden Aktionen aus:
- Auflösung der Library-Funktionen, die er später aufrufen muss
- Die Base-Adresse des ersten Kernel32 wird gefunden durch die Übertragung von Strukturen des PEB und Abgleich des Modulnamens auf die Zeichen k, l und Punkt (.)
- Der Table des Moduls PE wird geparst, um den Export-Table zu finden
- Funktionen werden mit einer Custom-Funktion gehasht und durch Iteration bei jedem Export gematcht
- Funktionen in anderen Modulen werden auf die gleiche Weise gefunden, allerdings unterstützt mit LoadLibraryExW, um an die Base-Adresse zu kommen; diese Funktion ist als eine der ersten in Kernel32 platziert
- Durch den Aufruf IPHLPAPI.GetAdaptersAddresses werden die MAC-Adressen gefunden
- Die MAC-Adressen werden mit MD5 gehasht
- Die MD5-Hashes werden mit einer Hardcode-Liste abgeglichen
- Bei fehlendem Match wird ein geheimnisvolles IDX-File auf der Festplatte abgelegt
- Wenn eine MAC-Adresse übereinstimmt, wird über einen proxyfähigen API-Call von einer URL ein Payload der zweiten Stufe heruntergeladen. Diese geht direkt in die RWX-Memory und wird dort aufgerufen
Mehr Details zu jedem dieser Schritte folgen unten.
Auflösung der Funktion
Der Shellcode startet mit der Lokalisierung einiger Library-Funktionen, die er verwenden möchte. Dies ist, grob gesagt, ein Prozess in zwei Schritten: Zunächst werden LoadLibraryExW und GetProcAddress in kernel32.dll gesucht. Danach werden weitere Funktionen von verschiedenen DLLs aufgelöst, ausgestattet mit der Adresse von LoadLibraryExW zur Benutzung auf Stufe zwei.
Für den ersten Schritt wird zunächst die Base-Adresse von kernel32.dll benötigt. Um diese zu finden, wird der Thread Information Block (TIB) genutzt. Mit ihm wird schließlich InInitializationOrderModuleList lokalisiert, die eine Liste der im Prozess geladenen Module enthält.
Die Strukturen, die abgefragt werden, um hierher zu gelangen, sind:
TIB -> PEB -> Ldr -> InInitializationOrderModuleList
Tatsächlich ist InInitializationOrderModuleList vom Typ _LIST_ENTRY, also eine doppelt verlinkte Liste, und ihr „Flink“ (oder Forward Link) führt zur Übertragung dieser Modul-Liste. Jeder Eintrag beinhaltet ein BaseDllName-Feld, und dieses Feld wird bei jedem Eintrag gecheckt, um zu sehen, ob es mit kernel32.dll matcht.
Zur Verschleierung wird der Name „kernel32.dll“ nicht direkt abgefragt. Stattdessen wird geprüft, ob die Zeichen k, l, und Punkt (.) an der passenden Stelle im String auftreten – und zwar zweimal, einmal als Klein- und einmal als Großbuchstabe. Tatsächlich wird sogar nur das erste Byte jedes 2-Byte-Unicode-Zeichens geprüft, was in der Praxis funktioniert, aber sicher nicht der offizielle Weg ist, ein Unicode-Zeichen zu vergleichen.
Dieser ganze Prozess ist hier im kommentierten Code abgebildet:
Wenn der kernel32.dll-Eintrag einmal gefunden ist, kann sein DllBase-Feld und somit die Base-Adresse des Moduls ausgelesen werden. Diese wird mit einer Funktion im Shellcode genutzt, welche eine Modul-Base-Adresse und einen gebräuchlichen Hash-Value als Funktionsnamen akzeptiert. Diese Funktion parst den PE-Header des Moduls in die Memory, um den Export-Table zu lokalisieren. Dann iteriert sie durch jeden Export und lässt eine einfache, hash-ähnliche Funktion auf den Namen laufen. Mit Auffindung des passenden Hash-Values ist zugleich die Zielfunktion im Export-Table lokalisiert, ohne dass der Funktionsname direkt in den Code integriert werden müsste. Die Adresse der Funktion aus dem Export-Table wird für den späteren Gebrauch gespeichert.
Diese Export-Table-Suche mit Kommentaren ist hier zu sehen – der Hash-Code steht im grauen Block:
Der zweite Schritt bei der Auflösung der Funktion nutzt die gleiche Funktion, die im Export-Table nach den gehashten Funktionsnamen sucht. Da sie aber Funktionen von einigen anderen DLLs benötigt, nutzt sie LoadLibraryExW, die sie im ersten Schritt beim Bezug der Modul-Base-Adresse erhalten hat.
Die folgende Abbildung zeigt, wo all die anderen Hash-Values für die Funktionsnamen im Code gefunden wurden, kommentiert mit den Modulen und Funktionsnamen, auf die sie sich beziehen:
Diese Funktionsadressen werden in einer Struktur gespeichert, auf die der Rest des Codes häufig über einen Register-Base-Pointer zugreift. Die folgenden Offsets zeigen, welche Funktion jeweils aufgerufen wird.
Offset | Funktion |
0x4 | kernel32.VirtualAlloc |
0x8 | kernel32.GetModuleFileNameW |
0xC | kernel32.WritePrivateProfileStringW |
0x10 | kernel32.GetSystemTimeAsFileTime |
0x14 | kernel32.FileTimeToSystemTime |
0x18 | kernel32.VirtualFree |
0x1C | ntdll.memcpy |
0x20 | ntdll.memcmp |
0x24 | ntdll.memset |
0x28 | ntdll.swprintf |
0x2C | ntdll.sprintf |
0x30 | ntdll.strncat |
0x34 | ntdll.MD5Init |
0x38 | ntdll.MD5Update |
0x3C | ntdll.MD5Final |
0x40 | IPHLPAPI.GetAdaptersAddresses |
0x44 | wininet.InternetOpenA |
0x48 | wininet.InternetOpenUrlA |
0x4C | wininet.InternetQueryDataAvailable |
0x50 | wininet.InternetReadFile |
0x4 | kernel32.VirtualFree |
Wenn man diese Offsets kennt und definieren kann, werden die Codes sehr viel besser lesbar.
Wir kommen von hier:
… hierhin:
Für den Fall, dass es jemand nützlich findet, bringen wir hier etwas Python-Code, der hilft, diese Hashes zu produzieren und Übereinstimmungen mit echten Funktionsnamen zu finden – leicht abgekürzt:
import numpy # We expect, and require, that int_scalars overflow occurs, so ignore numpy.warnings.filterwarnings('ignore') find_hashes = [ 0x431A42C9, 0x0C2CBC15A, ... function hashes ... ] names = [ ... list of exported functions in target DLLs ... ] hashes_2s_compliment = {} for hash in find_hashes: twoscomp = hash if twoscomp >= 1<<31: twoscomp -= 1<<32 hashes_2s_compliment[twoscomp] = hash mul_by = numpy.int32(0x21) for name in names: name_hash = numpy.int32(0) for char in name: name_hash = name_hash * mul_by name_hash += numpy.int32(ord(char)) if name_hash in hashes_2s_compliment: print('{}: {}'.format(hex(hashes_2s_compliment[name_hash]), name))
MAC-Addressen
Bewaffnet mit diesen Funktionen setzt der Shellcode seine Arbeit fort und geht in die Phase der MAC-Validierung über. Hier können wir sehen, wie er die MD5-Hashes der MAC-Adressen auf der Maschine bekommt, indem er eine Funktion im Shellcode aufruft, der wir den Namen get_macs_and_md5 gegeben haben. Diese wird zweimal aufgerufen. Das erste Mal, um die Anzahl der MAC-Adressen zu erfahren, damit die richtige Menge an Speicher bereitgestellt werden kann, um all die MD5-Hashes zu speichern. Beim zweiten Mal werden dann tatsächlich die MD5-Hashes generiert und gespeichert.
Die MAC-Adressen werden erhalten durch den Aufruf von GetAdaptersAddresses. Mit AF_UNSPEC werden alle Interfaces bezogen.
Und der tatsächliche MD5-Aufruf:
Diese MD5-Hashes werden dann abgeglichen mit einem Set von in den Shellcode hartkodierten Hashes, wie in diesem Beispiel gezeigt:
Hier ist die Verzweigung zu sehen, die ganz am Ende der Entry-Funktion im Shellcode vorgenommen wird, je nachdem, ob es einen Treffer bei den MAC-Adressen gab oder nicht:
Download und Ausführung der zweiten Payload
Falls es zu einem Match bei den MAC-Adressen kommt, fährt der Shellcode mit dem zweiten Download aus dem Internet fort. Die URL für diese Stage liegt hardkodiert als fester Wert vor. Diese Werte sind ‚little-endian‘ (der am wenigsten signifikante Wert wird zuerst gespeichert), so dass die String-Fragmente rückwärts erscheinen, wenn sie als ASCII angezeigt werden:
Das ergibt die URL:
https://asushotfix[.]com/logo2[.]jpg
Die URL wird mit einer proxyfähigen Funktion ausgegeben:
Die Daten werden von der URL direkt in eine read/write/execute-Speicherregion heruntergeladen. Schließlich wird der zweite Code aufgerufen:
Zum Zeitpunkt unserer Analyse war die zweite Payload unter der URL nicht mehr verfügbar. Wahrscheinlich werden dazu in den nächsten Wochen weitere Informationen folgen.
Die Entdeckung von ShadowHammer
Es gibt eine Reihe von Indikatoren, nach denen Experten zur Abwehr von Bedrohungen suchen können, unter anderem die Hashes von Dateien, von abgelegten Dateien und von netzwerkbasierten IOCs (Indicator of Compromise).
SHA-256 (zusammen mit dem Monat, in dem sie gesehen wurden)
- bca9583263f92c55ba191140668d8299ef6b760a1e940bddb0a7580ce68fef82 June
- 6aedfef62e7a8ab7b8ab3ff57708a55afa1a2a6765f86d581bc99c738a68fc74 July
- ac0711afee5a157d084251f3443a40965fc63c57955e3a241df866cfc7315223 July
- e78e8d384312b887c01229a69b24cf201e94997d975312abf6486b3363405e9d Sep
- 736bda643291c6d2785ebd0c7be1c31568e7fa2cfcabff3bd76e67039b71d0a8 Sep
- 9bac5ef9afbfd4cd71634852a46555f0d0720b8c6f0b94e19b1778940edf58f6 Sep
- 9a72f971944fcb7a143017bc5c6c2db913bbb59f923110198ebd5a78809ea5fc Oct
- 357632ee16707502ddb74497748af0ec1dec841a5460162cb036cfbf3901ac6f Oct
- 9842b08e0391f3fe11b3e73ca8fa97f0a20f90b09c83086ad0846d81c8819713 Nov
Abgelegte Dateien
- C:\Users\<user>\idx.ini
- C:\Users\<user>\Appdata\idx.ini
Netzwerk
- asushotfix[.]com
- 105.71[.]116
- hxxps://asushotfix[.]com/logo[.]jpg
- hxxps://asushotfix[.]com/logo2[.]jpg
PDB Indikator
- June sample – D:\C++\AsusShellCode\Release\AsusShellCode.pdb
Zusammenfassung
Der ShadowHammer-Angriff ist ein großartiges Beispiel einer Supply-Chain-Attacke, bei der der Angreifer ein vertrauenswürdiges Update-Hilfsprogramm nutzt, um Schadsoftware auf sehr gezielte Weise global zu verbreiten. Wie in der Kaspersky-Analyse erwähnt, hat der Angriff einige Ähnlichkeiten zu dem der BARIUM-Gruppe. Dies lässt eine Fortsetzung und sogar Steigerung in der Größe und Komplexität ihrer Aktivitäten vermuten.
Aus der Perspektive der Defensive lässt sich feststellen, dass die Aktionen auf der ersten Stufe des Angriffs heimlich erfolgen und schwer zu entdecken sind. Das zeigt allein schon der hohe zeitliche Aufwand, der nötig war, um den Angriff aufzudecken. Es ist aber durchaus denkbar, dass bessere Indikatoren entdeckt werden, wenn mehr Informationen über die zweite Payload verfügbar sind.
Zur Unterstützung bei der Echtzeit-Detektion, aber auch in der Retrospektive, empfehlen wir dringend, dass Organisationen ein Endpoint-Monitoring und -Response-System mit einem EDR-Agenten einführen. Diese geben die Sichtbarkeit und Kontrolle, die für die Bekämpfung solcher Bedrohungen benötigt wird.
Kategorien