Im ersten Teil dieser mehrteiligen Serie führten wir in die Grundprinzipien von Maschinen- und Assemblerbefehlen ein und zeigten, was hinter Anti-Disassembly steckt. Es wurde kein Code ausgeführt, weshalb wir von statischer Analyse sprachen. Heute beschäftigen wir uns mit der Störung der dynamischen Analyse, im speziellen mit Debugging, und daher heisst diese Folge Anti-Debugging.
Was ist eigentlich eine ausführbare Datei? Wie erkennt ein Betriebssystem, dass abzuarbeitende Maschinenbefehle vorliegen und es sich nicht um ein Bild handelt? Das hat etwas mit dieser Folge zu tun? Ganz bestimmt.
Es ist nicht einfach, eine geschlossene Definition für den Begriff Ausführbare Datei zu geben [1], da je nach Situation unterschiedliche Konzepte greifen. Eine Datei mit der Endung .bat (Batchdatei) wird von einem Windows-Betriebssystem als Kommandozeilen-Skript interpretiert – und ausgeführt. Eine Datei mit keiner Endung aber bestimmten Bytes am Anfang wird wiederum als PE-Datei erkannt – und ausgeführt. PE-Dateien sind den meisten PC-Benutzern bekannt: Diese Dateien haben typischerweise die Endung .exe.

Der Aufbau von PE-Dateien kann visuell wunderbar dargestellt werden [2]. Das Betriebssystem prüft beim Aufruf einer solchen Datei, ob eine gewisse Struktur vorliegt: Sind die ersten beiden Bytes 4D 5A, was hexadezimal für “MZ” (=Mark Zbikowski) steht? Befinden sich an einer bestimmten Position in der Datei die beiden Bytes 50 45, was hexadezimal für “PE” steht? Falls alle Checks erfolgreich sind, lädt das Betriebssystem die PE-Datei in den Speicher und positioniert die CPU (oder genauer gesagt den instruction pointer IP/EIP/RIP) auf den ersten Maschinenbefehl. Einfach ausgedrückt: es wurde ein Prozess gestartet. Damit das Betriebssystem die Übersicht über alle laufenden Prozesse nicht verliert, erhält jeder Prozess ein kleines Schildchen mit den wichtigsten Angaben: Umgebungsvariablen, Liste der geladenen Module, Adresse im Speicher – und Debugstatus. Richtig, Debugstatus.
Toll, doch was ist ein Debugger? Ein Debugger ist ein Programm, welches die Kontrolle über einen anderen Prozess übernehmen kann. Es kann zu jedem Zeitpunkt den anderen Prozess anhalten, Werte in Variablen bzw. an beliebigen Speicherstellen betrachten und ändern und sogar Maschinenbefehle betrachten und ändern. Diese Möglichkeiten helfen ungemein, eine Malware zu verstehen. Alles ist möglich. Und selbstverständlich, sobald ein Prozess von einem Debugger übernommen wird, wird das entsprechend im Schildchen des Prozesses angekreuzt.
Anti-Debugging for Dummies
Microsoft unterhält eine ausgezeichnete Dokumentation der meisten API-Funktionen. Die Bibliothek kernel32.dll enthält eine Funktion IsDebuggerPresent [3], welche von, beispielsweise, C/C++-Entwickler über die Datei WinBase.h (bzw. Windows.h) komfortabel verwendet werden kann, um eine einfache Anti-Debugging-Funktionalität zu implementieren: Wenn ein Debugger präsent ist, führe unauffällige Sachen aus, wenn ein Debugger nicht präsent ist, führe bösartige Dinge aus.
#include <Windows.h> int main( int argc, char* argv[] ) { if (IsDebuggerPresent()) { DoBenignThings(); } else { DoMaliciousThings(); } }
Dieses Beispiel ist trivial. Sobald man ein Programm entdeckt, das die Funktion IsDebuggerPresent() importiert, sollte man genauer hinschauen. Doch was macht diese Funktion eigentlich? Wie oben erwähnt, wird an jedem Prozess ein kleines Schildchen befestigt, mit den wichtigsten Meta-Informationen, welche das Betriebssystem benötigt, darunter auch den Debug-Status. Konkret heisst das Schildchen Process Environment Block (PEB) und ist ein struct. Mit Hilfe des Debuggers WinDBG (Teil des Windows SDK [4]) lassen sich solche Strukturen praktischerweise auflisten:
0:000> dt nt!_PEB ntdll!_PEB +0x000 InheritedAddressSpace : UChar +0x001 ReadImageFileExecOptions : UChar +0x002 BeingDebugged : UChar +0x003 BitField : UChar +0x003 ImageUsesLargePages : Pos 0, 1 Bit +0x003 IsProtectedProcess : Pos 1, 1 Bit +0x003 IsImageDynamicallyRelocated : Pos 2, 1 Bit +0x003 SkipPatchingUser32Forwarders : Pos 3, 1 Bit +0x003 IsPackagedProcess : Pos 4, 1 Bit +0x003 IsAppContainer : Pos 5, 1 Bit +0x003 IsProtectedProcessLight : Pos 6, 1 Bit +0x003 SpareBits : Pos 7, 1 Bit +0x004 Mutant : Ptr32 Void +0x008 ImageBaseAddress : Ptr32 Void +0x00c Ldr : Ptr32 _PEB_LDR_DATA +0x010 ProcessParameters : Ptr32 _RTL_USER_PROCESS_PARAMETERS ...
Im Kontext eines Prozesses befindet sich der PEB an einer ganz bestimmten Stelle im Speicher (Der Pointer zum PEB liegt hier: 32bit bei fs:[30], 64bit bei gs:[60]). Der Inhalt an dieser “Adresse plus 0x002” wird nach 1 (=BeingDebugged) oder 0 (=Not BeingDebugged) geprüft. Notabene, Anti-Anti-Debugging-Techniken setzen dieses Flag auf 0.
Mehr Anti-Debugging
Es gibt hunderte Anti-Debugging-Tricks[5]. Aus unerklärlichen Gründen hat der Autor einen Liebling unter diesen: TLS-callbacks. Diese sind bekannt, werden aber weiterhin verwendet. Ein vor einigen Monaten analysiertes Ice-IX-Sample (Bankentrojaner, 4bc267112b398630bb2780dce8b788fc) enthält – unter anderem – diesen Trick. Um TLS-callbacks zu verstehen, müssen wir wieder zurück zum Anfang. Eine PE-Datei enthält einen PE-Header, welcher mit “PE” beginnt. Die Position des ersten Maschinenbefehls wird darin definiert:

Jedes Byte des PE-Headers hat eine Bedeutung und kann mit den entsprechenden Tools übersichtlich dargestellt werden. Ein Auszug:

Analysieren wir diese Datei statisch, das heisst mit einem Disassembler, so wird diese Startadresse (2D 13, immer relativ zur ImageBase) bestätigt: 0x00400000 + 0x2D13 = 0x00402D13.

Soweit alles klar. Wir laden die Malware in einen Debugger und starten. Typischerweise stoppen die Debugger automatisch bei der Adresse des Startpunkts (<ModuleEntryPoint>):

Ohne es zu bemerken führte die Malware zu diesem Zeitpunkt bereits Code aus. Aber wie? Es gibt doch im PE Header nur ein Feld, einen Startpunkt zu definieren!? Betriebssysteme bieten eine Vielzahl von Mechanismen an, um Entwicklern Möglichkeiten zu bieten, Programmanforderungen umzusetzen. Beispielsweise können Objekte als thread_local/ThreadStatic deklariert werden, so dass diese innerhalb eines jeden Threads als eigenständiges Objekt existieren – ohne den ganzen Overhead, welcher threadübergreifende Programmierung mit sich bringt. Die Initialisierung solcher Objekte beginnt bereits vor dem eigentlichen Programmstart. Warum also nicht die Initialisierung eines thread_local Objektes nutzen, um böse Sachen zu machen? Auch das ist Anti-Debugging.
Übrigens: TLS-callbacks sind sehr einfach zu erkennen: Der PE-Header listet ein TLS Data Dictionary auf und auch ein guter Disassembler zeigt die möglichen Startpunkte – mehr als einer ist eher auf der verdächtigen Seite.

In der nächsten Folge befassen wir uns mit einer Technik, welche sich Anti-Virtualization bzw. Anti-VM nennt und, wenn in einer virtuellen Umgebung gestartet, dem Analysten eine gutartige Funktionalität vorgaukeln kann.