Der Inside-Out-Sicherheits Blog - Der Inside-Out-Sicherheits Blog

Stapelspeicher: Eine Übersicht (Teil 3) | Varonis

Geschrieben von Neil Fox | Sep 29, 2021 4:00:00 AM

Der Stapelspeicher ist ein Bereich im Speicher, der von Funktionen zum Speichern von Daten verwendet wird, beispielsweise lokale Variablen und Parameter. Diese können widerum von Malware ausgenutzt werden, um schädliche Aktivitäten auf einem kompromittierten Gerät auszuführen.

Stapelspeicher ist ein Thema, das mit Subnetting vergleichbar ist – es erfordert ein wenig Anstrengung und möglicherweise müssen Sie diesen Artikel mehrmals lesen, bevor der Groschen fällt. Ich hatte damit zunächst wirklich meine Probleme, aber sobald man es versteht, wird man durch dieses Wissen zu einem deutlich besseren Malware-Analysten. Fangen wir also an!

Dieser Beitrag ist der dritte in einer vierteiligen Serie über das Malware-Analysetool x64dbg:

  • Teil 1: Was ist x64dbg und wie benutzt man es?
  • Teil 2: Entpacken von Malware mit x64dbg
  • Teil 3: Stapelspeicher: Ein Überblick
  • Teil 4: x64dbg – Tutorial

Was ist Stapelspeicher?

Stapelspeicher wird häufig als „LIFO“ bezeichnet („Last in, first out“) Stellen Sie sich das wie Bausteine vor, die aufeinander gestapelt sind. Man kann keinen Stein aus der Mitte entfernen, da der Stapel sonst umfallen würde. Also kann man nur den obersten Stein zuerst nehmen. So funktioniert auch der Stapel.

In einem früheren Artikel habe ich erklärt, was die Register in x64dbg sind, sowie einige grundlegende Assembler-Anweisungen angegeben. Diese Informationen werden benötigt, um zu verstehen, wie der Stapel funktioniert. Wenn neue Daten zum Stapel hinzugefügt werden, verwendet die Malware den Befehl „PUSH“. Um ein Element vom Stapel zu entfernen, verwendet die Malware den Befehl „POP“. Daten können auch vom Stapel mit „POP“ in ein Register verschoben werden.

Das Register „ESP“ wird verwendet, um auf das nächste Element auf dem Stapel zu zeigen, und wird daher als „Stapelzeiger“ bezeichnet.

Der EBP, auch als „Rahmenzeiger“ bezeichnet, ist ein unveränderlicher Referenzpunkt für Daten auf dem Stapel. Damit kann das Programm berechnen, wie weit etwas im Stapel von diesem Punkt entfernt ist. Wenn also eine Variable zwei „Bausteine“ entfernt ist, dann ist sie [EBP+8], da jeder „Baustein“ im Stapel 4 Byte groß ist.

Jede Funktion in einem Programm erzeugt mit dieser Technik ihren eigenen Stapelrahmen, um ihre eigenen Variablen und Parameter zu referenzieren.

Stapelspeicher-Architektur

Das folgende Diagramm veranschaulicht den Aufbau des Stapels aus den einzelnen Bausteinen:

Niedrigere Speicheradressen stehen oben und höhere Speicheradressen unten.

Jede Funktion erzeugt ihren eigenen Stapelrahmen, sodass der Stapelrahmen im obigen Beispiel über einem anderen Rahmen auf dem Stapel liegen kann, der von einer anderen Funktion verwendet wird.

Der EBP wird, wie bereits erwähnt, als unveränderlicher Referenzpunkt auf dem Stapel gespeichert. Dies geschieht durch Verschieben des Wertes des ESP (des Stapelzeigers) in den EBP. Der ESP ist veränderlich, da er immer auf das obere Ende des Stapels verweist. Durch die Speicherung im EBP erhalten wir einen unveränderlichen Referenzpunkt im Stapel, und nun kann die Funktion seine Variablen und Parameter im Stapel von dieser Position aus referenzieren.

In diesem Beispiel werden die Parameter, die an die Funktion übergeben wurden, in „[EBP]+8“, „[EBP]+12“ und „[EBP]+16“ gespeichert. Wenn wir also „[EBP]+8“ sehen, ist das der Abstand von EBP auf dem Stapel.

Variablen werden gespeichert, nachdem die Ausführung der Funktion begonnen hat. Sie werden also weiter oben auf dem Stapel, aber in einem niedrigeren Adressraum gespeichert. In diesem Beispiel wird das also als „[EBP]-4“ angezeigt.

Lehrbuchbeispiel für Stapelspeicher

Zur Veranschaulichung sehen Sie hier ein Beispiel für ein einfaches C-Programm, das eine Funktion namens „addFunc“ aufruft, die zwei Zahlen addiert (1+4) und das Ergebnis auf dem Bildschirm ausgibt.

  1. #include “stdio.h”
  2. int addFunc (int a, int b);
  3. int main (void) {
  4. int x = addFunc(1,4);
  5. printf(“%d\n”, x);
  6. return 0;
  7. }
  8. int addFunc(int a, int b) {
  9. int c = a + b;
  10. return c;

Wenn wir uns den Funktionscode von „addFunc“ genauer anschauen, sehen wir, dass zwei Parameter (a und b) als Argumente übergeben werden und eine lokale Variable „c“, in der das Ergebnis gespeichert ist, zurückgegeben wird. Sobald das Programm kompiliert ist, können wir es in x64dbg laden. Nachfolgend sehen Sie den Assembler-Code für dieses Programm:

  1. push ebp
  2. mov ebp,esp
  3. sub esp,10
  4. mov edx,dword ptr ss:[ebp+8]
  5. mov eax,dword ptr ss:[ebp+C]
  6. add eax,edx
  7. mov dword ptr ss:[ebp-4],eax
  8. mov eax,dword ptr ss:[ebp-4]
  9. leave
  10. ret

Die ersten drei Linien werden als Funktionsprolog bezeichnet. Hier wird im Stapel Platz für die Funktion gemacht.

push ebp behält den ESP, den vorherigen Stapelrahmenzeiger, bei, damit am Ende der Funktion zu ihm zurückgekehrt werden kann. Ein Stapelrahmen wird zum Speichern lokaler Variablen verwendet, und jede Funktion verfügt über einen eigenen Stapelrahmen im Speicher.

mov ebp, esp verschiebt die aktuelle Stapelposition in den EBP, der die Basis des Stapels bildet. Wir haben jetzt einen Referenzpunkt, der es uns ermöglicht, unsere auf dem Stapel gespeicherten lokalen Variablen zu referenzieren. Der Wert von EBP ändert sich jetzt nicht mehr.

sub esp, 10 vergrößert den Stapel um 16 Bytes (10 in Hex), um Platz auf dem Stapel für alle Variablen zu reservieren, die wir referenzieren müssen.

Nachfolgend sehen Sie, wie der Stapel für dieses Programm aussehen würde. Alle verwendeten Daten werden in einem Speicherbereich übereinander gestapelt, wie im Diagramm oben dargestellt.

EBP-10

EBP-C

EBP-8

EBP-4 (int c)

EBP = Wird zu Beginn der Funktion auf den Stapel gepusht. Das ist der Anfang unseres Stapelrahmens.

EBP+4 = Rückgabeadresse der vorherigen Funktion

EBP+8 = Parameter 1 (int a)

EBP+C = Parameter 2 (int b)

In diesem Beispiel können wir durch einen Blick auf den Stapel sehen, dass wir Speicher für vier lokale Variablen zugewiesen bekommen haben. Allerdings haben wir nur eine Variable, nämlich „int c“.

mov edx,dword ptr ss:[ebp+8] – Hier verschieben wir „int a“ mit dem Wert 1 in das EDX-Register.

Der wichtige Teil ist hier [ebp+8]. Dieser steht in eckigen Klammern – hier wird also der Speicher direkt adressiert. Dies referenziert die Position im Speicher, die sich 8 Bytes höher im Stapel befindet als das, was im EBP ist.

Ich habe bereits erwähnt, dass die Parameter, die an eine Funktion übergeben werden, immer in einem höheren Adressraum liegen, der sich weiter unten im Stapel befindet. Unsere Parameter „int a“ und „int b“ wurden vor dem Anlegen des Stapelrahmens an die Funktion übergeben und befinden sich deshalb in „ebp+8“ und „ebp+c“.

mov eax,dword ptr ss:[ebp+C] – Das Gleiche wie oben, allerdings referenzieren wir jetzt „ebp+C“, also „int b“, den Wert „4“, und verschieben es in das EAX-Register.

add eax, edx – Hiermit wird die Addition durchgeführt und das Egebnis in „EAX“ gespeichert.

mov dword ptr ss:[ebp-4],eax – Hiermit verschieben wir das in „EAX“ gespeicherte Ergebnis in die lokale Variable „int c“.

Die lokale Variable „c“ ist innerhalb der Funktion definiert und befindet sich daher an einer niedrigeren Speicheradresse als das obere Ende des Stapels. Da sie sich also innerhalb des Stapelrahmens befindet und eine Größe von 4 Bytes hat, können wir einfach etwas von dem Platz verwenden, den wir zuvor für die Variablen zugewiesen haben, indem wir 10 von esp subtrahieren. In diesem Fall verwenden wir „EBP-4“.

mov eax,dword ptr ss:[ebp-4] – Die meisten Funktionen geben den Wert zurück, der in „EAX“ gespeichert ist. Oben ist also der Rückgabewert in „EAX“ und wir verschieben ihn in die Variable „c“. Durch diese Operation wird er also einfach zurück in „EAX“ platziert und ist bereit für die Rückgabe.

Leave – Dies ist eine Maske für eine Operation, die „EBP“ wieder in „ESP“ verschiebt und ihn per Pop vom Stapel entfernt. Es wird also der Stapelrahmen der Funktion vorbereitet, die diese Funktion aufgerufen hat.

ret – Hiermit wird zur Rückgabeadresse gesprungen, um zur aufrufenden Funktion zurückzukehren, die einen gut erhaltenen Stapelrahmen hat, weil wir uns diesen am Anfang dieser Funktion gemerkt haben.

Praktisches Beispiel: Stapelspeicher und x64dbg

Im vorherigen Artikel habe ich gezeigt, wie man Malware mit x64dbg entpackt. Nun können wir uns einige der Funktionen ansehen, die von der Malware verwendet werden, und wie dabei der Stapel benutzt wird.

Öffnen Sie zunächst die entpackte Malware in x64dbg. In diesem Beispiel heißt meine Malware einfach „267_unpacked.bin“.

Navigieren Sie zum Einstiegspunkt der Malware, indem Sie „Debug“ und dann „Run“ auswählen.

Wir sind jetzt am Einstiegspunkt der Malware und ich habe zwei Fenster hervorgehoben, die die Stapelspeicherinformationen enthalten:

Das erste Fenster enthält Parameter, die zum Stapel hinzugefügt (gepusht) wurden. Wir wissen, dass es sich dabei um Parameter und nicht um Variablen handelt, da sie „esp+“ und nicht „esp-“ sind, wie zuvor erläutert.

Das zweite Fenster ist der eigentliche Stapelspeicher.

Die erste Spalte ist die Liste der Adressen im Stapelspeicher. Wie bereits erwähnt befinden sich die höheren Adressen unten und die niedrigeren Adressen oben.

Die zweite Spalte enthält die Daten, die auf den Stapel verschoben (gepusht) wurden. Die blauen Klammern stellen dabei jeweils einzelne Stapelrahmen dar. Denken Sie daran, dass jede Funktion über einen eigenen Stapelrahmen verfügt, um ihre eigenen Parameter zu speichern.

Die dritte Spalte enthält Informationen, die von x64dbg automatisch ausgefüllt werden. In diesem Beispiel sehen wir Adressen, zu denen x64dbg zurückkehrt, sobald eine Funktion ausgeführt wurde.

Im Bild unten ist „push ebp“ der erste Befehl, auf den „EIP“ zeigt. Der aktuelle Wert in „EBP“, den ich im Bild unten hervorgehoben habe, ist „0038FDE8“.

Mit Blick auf das Stapelfenster habe ich diese Adresse markiert, die der aktuelle Basiszeiger des Stapelrahmens ist.

Durch Drücken von „Step over“ (überspringen) wird „EBP“ dann auf den Stapel gepusht, so dass die Malware nach Abschluss dieser Funktion zu dieser Adresse zurückkehren kann.

Wir müssen jetzt unseren aktuellen Stapelzeiger in „ESP“ verschieben. Dies ist die unten hervorgehobene Adresse „0038FDDC“.

Wenn Sie diesen Befehl ausführen, wird „ESP“ in das „EBP“- Register verschoben, das unten hervorgehoben ist.

Als Nächstes muss die Malware Platz auf dem Stapel schaffen, indem sie „420“ vom ESP subtrahiert. Es wird Subtraktion verwendet, da Platz im unteren Adressraum geschaffen wird, der höher im Stapel ist. Die folgende Abbildung zeigt den unteren Adressraum über dem aktuellen Stapelrahmen.

Der Befehl „sub esp, 420“ aktualisiert nun den Stapel.

Beachten Sie, dass wir jetzt einen niedrigeren Adressraum haben, der sich weiter oben im Stapel befindet. „ESP“ wurde jetzt aktualisiert und zeigt auf die neue Position oben im Stapel.

Das ist ein häufiges Muster, das Sie zu Beginn von Funktionen in Malware sehen werden und das Ihnen nach einiger Zeit vertraut sein wird.

Als Nächstes kommen drei Push-Anweisungen, die die Werte von drei Registern auf den Stapel pushen. Wenn wir diese Anweisungen überspringen, wird der Stapel wie erwartet aktualisiert, und ebenso das Parameter-Fenster.

Als Nächstes gibt es einige Funktionen, die vom Malware-Autor geschrieben wurden. Werfen wir einen Blick auf eine dieser Funktionen, um zu sehen, was sie tut und welche Rolle der Stapel spielt.

Im folgenden Bild befindet sich mein Mauszeiger über der Funktion „267_unpacked.101AEC9“. In x64dbg taucht dadurch ein Popup auf, das eine Vorschau dieser Funktion zeigt. Dadurch kann der Benutzer einen Teil des Assembler-Codes der Funktion sehen, die aufgerufen wird. In diesem Popup können wir sehen, dass eine große Anzahl von Strings in Variablen verschoben wird. Auch hier wissen wir aufgrund der Syntax „ebp-“, dass es sich um Variablen handelt. Diese Strings sind verschleierte Windows-API-Aufrufe, mit denen die Malware verschiedene Aktionen durchführt, z. B. Prozesse und Dateien auf der Festplatte erstellt.

Indem wir in diese Funktion hineinspringen (step into), können wir mit x64dbg einen genaueren Blick darauf werfen, was darin vor sich geht und welche Rolle dabei der Stapel spielt.

Es wird ein neuer Stapelrahmen erstellt, den ich unten rechts hervorgehoben habe. Und wie erwartet ist hier erneut der Funktionsprolog.

Wenn wir in diese Funktion hineinspringen, wird nun „ESP“ aktualisiert. Dabei handelt es sich um die Adresse „0038F9AC“ im Stapelspeicher, die die Rückgabeadresse zur Main-Funktion enthält. Außerdem wird Platz auf dem Stapel geschaffen, indem 630 von „ESP“ subtrahiert wird. Die Anweisungen, die mit „mov“ beginnen, verschieben dann die gehashten Funktionsnamen in ihre eigenen Variablen.

Wenn wir im Assembler-Code nach unten scrollen, kommen wir zum Ende der Funktion und sehen einige Funktionsaufrufe. Diese werden verwendet, um die Hashes zu entschleiern, die gerade in Variablen verschoben wurden.

Die von mir markierten Befehle werden als „Funktionsepilog“ bezeichnet, der den Stapel nach Abschluss der Funktion bereinigt. Ich werde zu diesen Anweisungen navigieren, indem ich die Anweisung, an der ich interessiert bin – „add esp, C“ – auswähle und in der Symbolleiste „Debug“ und „Run until selection“ (bis zur Auswahl ausführen) wähle.

Dadurch wird „EIP“ auf die von uns markierte Anweisung aktualisiert und es wird der Stapel vor der Bereinigung angezeigt.

Im Funktionsprolog musste die Malware, um Platz auf dem Stapel zu schaffen, von „ESP“ subtrahieren, damit sie den Platz auf dem Stapel zuweisen konnte, der sich im unteren Adressraum befand. Nun müssen wir den zugewiesenen Platz entfernen. Das tun wir mit dem Befehl „add esp, C“ und addieren so den Hex-Wert „C“ zum Stapel, so dass wir uns nach unten in den höheren Adressraum bewegen.

Das folgende Bild zeigt den aktualisierten Stapel, nachdem „add esp, C“ ausgeführt wurde.

Als nächstes folgt der Befehl „mov esp, ebp“, der den Wert in „EBP“ in „ESP“ verschiebt. Unser aktueller „EBP“ ist „0042F3EC“. Wenn wir durch die Daten im Stapelfenster nach unten scrollen, können wir sehen, dass diese Adresse unseren alten „ESP“ enthält, der der Stapelzeiger ist.

Dieser Befehl bereinigt nun den Stapel.

Der Befehl „pop ebp“ öffnet dann die Adresse „00E0CDA8“, die oben im Stapel gespeichert wurde, und verschiebt sie in „EBP“.

Wenn nun die nächste Anweisung „ret“ ausgeführt wird, kehren wir zur Adresse „00E0CDA8“ zurück.

Das obige Bild zeigt, dass wir nun zur Main-Funktion der Malware zurückgekehrt sind und uns an der Adresse „00E0CDA8“ befinden. Wir sind also direkt nach der gerade in x64dbg analysierten Funktion.

Jetzt wissen Sie, wie das Reverse Engineering von Malware mit x64dbg funktioniert! Im nächsten Artikel zeige ich Ihnen, wie Sie das Wissen aus den letzten Blogbeiträgen für praktisch angewandtes Reverse Engineering nutzen können.

Um sicherzustellen, dass Ihr Unternehmen für die Erkennung von und die Reaktion auf Bedrohungen gerüstet ist, melden Sie sich für eine Demo von Datalert an und informieren Sie sich über die Best Practices, die Sie zum Schutz vor Malware implementieren können.