Sie sind an Cloud Computing interessiert? Legen Sie jetzt los

Der ultimative Leitfaden zu Linux-Prozessinjektion

Ori David

Verfasser

Ori David

November 14, 2024

Ori David

Verfasser

Ori David

Ori David ist Sicherheitsforscher bei Akamai. Seine Forschung konzentriert sich auf offensive Sicherheit, Malware-Analyse und Bedrohungssuche.

Im Vergleich zu Windows kennen nur wenige die verschiedenen Injektionstechniken unter Linux.
Im Vergleich zu Windows kennen nur wenige die verschiedenen Injektionstechniken unter Linux.

Einführung

Techniken zur Prozessinjektion sind ein wichtiger Bestandteil des Toolsets eines Angreifers. Hiermit können Cyberkriminelle schädlichen Code innerhalb eines legitimen Prozesses ausführen, um eine Erkennung zu vermeiden, oder können in Remote-Prozessen Hooks einschleusen, um ihr Verhalten zu ändern. 

Prozessinjektionen auf Windows-Computern wurden ausführlich erforscht und sind relativ bekannt. Bei Linux-Computern ist das nicht der Fall. Obwohl einige großartige Ressourcen bereits Berichte zu dem Themaverfasst haben, scheinen die verschiedenen Injektionstechniken unter Linux nur wenige zu kennen – insbesondere im Vergleich zu Windows.

Wir haben uns von einer Übersicht zur Windows-Prozessinjektion inspirieren lassen, geschrieben von Amit Klein und Itzik Kotler von SafeBreach. Ziel ist es hierbei, eine umfassende Dokumentation der Linux-Prozessinjektion bereitzustellen. Wir werden uns auf „echte Prozessinjektion“ konzentrieren – also Techniken, die auf laufende Prozesse abzielen. Das bedeutet, dass wir Methoden ausschließen, die erfordern, dass die Binärdatei auf dem Datenträger geändert wird, der Prozess mit bestimmten Umgebungsvariablen ausgeführt wirdoder der Prozessladevorgang ausgenutzt wird.

Wir werden die Betriebssystemfunktionen beschreiben, die die Prozessinjektion unter Linux ermöglichen, sowie die verschiedenen Injektionsprimitiven, die sie zulassen. Wir behandeln Techniken, die zuvor beschrieben wurden, werden aber auch Injektionsvarianten hervorheben, die zuvor nicht dokumentiert wurden. Abschließend werden wir uns mit den Erkennungs- und Abwehrstrategien für die jeweiligen Techniken befassen.

Zusätzlich zu diesem Blogbeitrag veröffentlichen wir ein GitHub-Repository, das einen umfassenden PoC-Code (Proof-of-Concept) für die verschiedenen im Beitrag beschriebenen Injektionsprimitiven enthält. Diese harmlosen PoCs sollen Ihnen helfen, zu verstehen, wie eine schädliche Implementierung dieser Techniken aussehen könnte. Das kann Ihnen beim Aufbau und Testen von Erkennungsfunktionen helfen. Weitere Informationen finden Sie in der README-Datei.

Linux-Injektion im Vergleich zu Windows-Injektion

Die Zahl der bekannten Injektionstechniken auf Windows-Computern ist enorm und wächst immer weiter: von APC-Warteschlangen und NTFS-Transaktionen bis hin zu atom-Tabellen und Threadpools. Windows stellt viele Schnittstellen bereit, über die Angreifer mit Remote-Prozessen interagieren (und Code in diese injizieren) können.

Bei Linux sieht die Situation ganz anders aus: Die Interaktion mit Remote-Prozessen ist auf eine kleine Gruppe von Systemaufrufen beschränkt. Viele Funktionen, die die Injektion auf Windows-Computern ermöglichen, sind unter Linux nirgends zu finden. Es gibt keine APIs für die Speicherzuweisung in einem Remote-Prozess oder die Änderung des Remote-Speicherschutzes– und definitiv nicht für die Erstellung von Remote-Threads.

Dieser Unterschied wirkt sich auf die Struktur des Injektionsangriffs aus. Unter Windows besteht die Prozessinjektion in der Regel aus drei Schritten: Zuweisen → Schreiben → Ausführen. Zuerst weisen wir Arbeitsspeicher im Remote-Prozess zu, der zum Speichern unseres Codes verwendet wird. Dann schreiben wir unseren Code in diesen Speicher und führen ihn schließlich aus.

Bei Linux können wir den ersten Schritt nicht durchführen: die Zuweisung. Es gibt keine direkte Möglichkeit, Speicher in einem Remote-Prozess zuzuweisen. Aus diesem Grund sieht der Injektionsfluss etwas anders aus: Überschreiben → Ausführen → Wiederherstellen. Wir überschreiben den vorhandenen Arbeitsspeicher im Remote-Prozess mit unserer Payload, führen sie aus und stellen dann den vorherigen Status des Prozesses wieder her, damit er normal ausgeführt werden kann.

Interaktionsmethoden für Remote-Prozesse

Unter Linux ist die Interaktion mit dem Arbeitsspeicher von Remote-Prozessen auf drei Hauptmethoden beschränkt: ptrace, procfsund process_vm_writev. In den folgenden Abschnitten beschreiben wir jede davon kurz.

ptrace

ptrace ist ein Systemaufruf zum Debuggen von Remote-Prozessen. Der einleitende Prozess kann den debuggten Prozessspeicher und die Register prüfen und ändern. Debugger wie GDB werden mithilfe von ptrace implementiert, um den Debugging-Prozess zu steuern.

ptrace unterstützt verschiedene Vorgänge, die durch einen ptrace -Anfragecode angegeben werden. Einige Beispiele sind PTRACE_ATTACH (der Elemente an einen Prozess anhängt), PTRACE_PEEKTEXT (der aus dem Prozessspeicher liest) und PTRACE_GETREGS (der die Prozessregister abruft). Snippet 1 zeigt ein Anwendungsbeispiel von ptrace.

  // Attach to the remote process
  ptrace(PTRACE_ATTACH, pid, NULL, NULL);
  wait(NULL);

  // Get registers state
  struct user_regs_struct regs;
  ptrace(PTRACE_GETREGS, pid, NULL, &regs);

Snippet 1: Anwendungsbeispiel von ptrace zum Abrufen der Register eines Remote-Prozesses

procfs

procfs ist ein spezielles Pseudo-Dateisystem, das als Schnittstelle zu laufenden Prozessen auf dem System fungiert. Der Zugriff erfolgt über das Verzeichnis /proc (Abbildung 1).

procfs is a special pseudo filesystem that acts as an interface to running processes on the system. It can be accessed through the /proc directory (Figure 1). Fig. 1: A directory listing of the /proc directory on a Linux machine

Jeder Prozess wird als Verzeichnis dargestellt, das nach seiner PID benannt ist. In diesem Verzeichnis gibt es Dateien mit Informationen über den Prozess. Beispielsweise enthält die Datei cmdline die Befehlszeile des Prozesses; die Datei environ enthält die Variablen der Prozessumgebung usw.

Mit procfs können wir auch mit dem Arbeitsspeicher des Remote-Prozesses interagieren. In jedem Prozessverzeichnis finden wir die Datei mem, eine spezielle Datei, die den gesamten Adressraum des Prozesses darstellt. Der Zugriff auf diese Speicherdatei eines Prozesses mit einem bestimmten Offset entspricht dem Zugriff auf den Prozessspeicher mit derselben Adresse.

Im Beispiel in Abbildung 2 haben wir das Dienstprogramm xxd verwendet, um 100 Byte aus der Prozess-Speicherdatei zu lesen, beginnend mit einem angegebenen Offset.

In the example in Figure 2, we used the xxd utility to read 100 bytes from the process mem file, starting at a specified offset. Fig. 2: Using xxd to read the procfs mem file

Wenn wir dieselbe Adresse im Speicher mithilfe von GDB prüfen, werden wir feststellen, dass der Inhalt identisch ist (Abbildung 3).

If we inspect the same address in memory using GDB, we will note that the contents are identical (Figure 3). Fig. 3: Using GDB to inspect the process memory at the same offset we read from the procfs mem file

Die Datei „maps“ ist eine weitere interessante Datei im Prozessverzeichnis (Abbildung 4). Diese Datei enthält Informationen über die verschiedenen Speicherbereiche im Prozessadressraum, einschließlich ihrer Adressbereiche und Speicherberechtigungen.

The maps file is another interesting file that can be found in the process directory (Figure 4). Fig. 4: Example contents of a process maps file

In den nächsten Abschnitten wird erläutert, warum die Möglichkeit, Speicherbereiche mit bestimmten Berechtigungen zu identifizieren, sehr nützlich sein kann.

process_vm_writev

Die dritte Methode zur Interaktion mit dem Arbeitsspeicher des Remote-Prozesses ist der Systemaufruf process_vm_writev . Mit diesem Systemaufruf können Daten in den Adressraum eines Remote-Prozesses geschrieben werden.

process_vm_writev empfängt einen Zeiger auf einen lokalen Puffer und kopiert seinen Inhalt an eine angegebene Adresse innerhalb des Remote-Prozesses. Ein Beispiel dafür, wie process_vm_writev verwendet wird, sehen Sie in Snippet 2.

  // Initialize local and remote iovec structs used to perform the syscall
  struct iovec local[1];
  struct iovec remote[1];

  // Place our data in the local iovec 
  local[0].iov_base = data;
  local[0].iov_len = data_len;

  // Point the remote iovec to the address in the remote process
  remote[0].iov_base = (void *)remote_address;
  remote[0].iov_len = data_len;

  // Write the local data to the remote address
  process_vm_writev(pid, local, 1, remote, 1, 0);

Snippet 2: Mit process_vm_writev können Daten in einen Remote-Prozess geschrieben werden

Schreiben von Code in einen Remote-Prozess

Jetzt, da wir die verschiedenen Methoden zur Interaktion mit anderen Prozessen kennen, wollen wir herausfinden, wie sie zur Codeinjektion verwendet werden können. Der erste Schritt des Injektionsangriffs besteht darin, unseren Shellcode in den Arbeitsspeicher des Remote-Prozesses zu schreiben. Wie bereits erwähnt, kann unter Linux kein neuer Speicher in einem Remote-Prozess direkt zugewiesen werden. Das bedeutet, dass wir keinen neuen Speicherabschnitt erstellen können. Wir müssen den vorhandenen Speicher des Zielprozesses nutzen.

Damit unser Code ausgeführt werden kann, müssen wir ihn in einen Speicherbereich mit Ausführungsberechtigungen schreiben. Einen solchen Bereich finden wir, indem wir die zuvor erwähnte procfs-maps-Datei analysieren und einen Speicherbereich mit Ausführungsberechtigungen (x) identifizieren (Abbildung 5).

We can find such a region by parsing the previously mentioned procfs maps file, and identifying a memory region with execute (x) permissions (Figure 5). Fig. 5: Identifying an executable memory region in the process maps file

Es gibt zwei Arten von ausführbaren Bereichen: beschreibbare und nicht beschreibbare. In den folgenden Abschnitten wird erläutert, wann und wie sie verwendet werden können.

Schreiben von Code in den RX-Speicher

Gilt für:ptrace, procfs mem

Idealerweise möchten wir einen Speicherbereich mit Schreib- und Ausführungsberechtigungen identifizieren, mit dem wir unseren Code schreiben und ausführen können. Doch in Wirklichkeit haben die meisten Prozesse keinen Bereich mit solchen Berechtigungen, da es als schlechte Praxis gilt, WX-Speicher zuzuweisen. Stattdessen sind wir normalerweise auf Lese- und Ausführungsberechtigungen beschränkt.

Interessanterweise stellt sich heraus, dass diese Beschränkung mit zwei der Methoden, die wir gerade beschrieben haben, umgangen werden kann: ptrace und procfs mem. Beide Mechanismen sind so implementiert, dass sie Speicherberechtigungen umgehen und an beliebige Adressen schreiben können –auch ohne Schreibberechtigung. Weitere Details zu diesem Verhalten für procfs finden Sie in diesem Blogbeitrag.

Das bedeutet, dass wir unabhängig von Schreibberechtigungen immer ptrace oder procfs mem verwenden können, um unseren Code in einen ausführbaren Bereich des Remote-Speichers zu schreiben.

ptrace

Um unsere Payload in einen Remote-Prozess zu schreiben, können wir die ptrace-Anfragen POKETEXT oder POKEDATA verwenden. Diese identischen Anfragen ermöglichen das Schreiben eines Datenworts in den Arbeitsspeicher des Remote-Prozesses. Durch wiederholtes Aufrufen können wir unsere gesamte Payload in den Ziel-Prozessspeicher kopieren. Ein Beispiel hierfür ist in Snippet 3 dargestellt.

  ptrace(PTRACE_ATTACH, pid, NULL, NULL);
  wait(NULL);

  // write payload to remote address
  for (size_t i = 0; i < payload_size; i += 8, payload++) 
  {
    ptrace(PTRACE_POKETEXT, pid, address + i, *payload);
  }

Snippet 3: Verwenden von ptrace POKETEXT, um unsere Payload in den Arbeitsspeicher des Remote-Prozesses zu schreiben

procfs mem

Um unsere Payload mithilfe von procfs in einen Remote-Prozess zu schreiben, müssen wir sie einfach mit dem richtigen Offset in die mem-Datei schreiben. Alle Änderungen an der mem-Datei werden auf den Prozessspeicher angewendet. Zur Durchführung dieser Vorgänge können wir die normalen Datei-APIs verwenden (Snippet 4).

  // Open the process mem file
  FILE *file = fopen("/proc/<pid>/mem", "w");

  // Set the file index to our required offset, representing the memory address
  fseek(file, address, SEEK_SET);

  // Write our payload to the mem file
  fwrite(payload, sizeof(char), payload_size, file);

Snippet 4: Verwenden der procfs-mem-Datei zum Schreiben von Daten in den Arbeitsspeicher eines Remote-Prozesses

Schreiben von Code in WX-Speicher

Gilt für:ptrace, procfs mem, process_vm_writev

Wie bereits besprochen, umgehen sowohl ptrace als auch procfs mem Speicherberechtigungen und ermöglichen es uns, unseren Code in nicht beschreibbare Speicherbereiche zu schreiben. Bei process_vm_writev ist dies jedoch nicht der Fall: process_vm_writev hält sich an Speicherberechtigungen und erlaubt daher nur das Schreiben von Daten in Bereiche mit Schreibzugriff.

Aus diesem Grund besteht unsere einzige Möglichkeit darin, nach beschreibbaren Bereichen zu suchen. Nicht alle Prozesse enthalten solche Bereiche, aber wir können sicherlich einige finden.

Mit dem Befehl in Snippet 5 wird die maps-Datei aller Prozesse auf dem System gescannt, und Bereiche mit Schreib- und Ausführungsberechtigungen werden ermittelt (Abbildung 6).

  find /proc -type f -wholename "*/maps" -exec grep -l "wx" {} +

Snippet 5: Mit dem Befehl „find“ können Prozesse mit Speicherbereichen gefunden werden, die Schreib- und Ausführungsberechtigungen umfassen

The command in Snippet 5 will scan the maps file of all processes on the system and identify regions with write and execute permissions (Figure 6). Fig. 6: Example output of finding processes with WX memory regions

Nachdem wir einen solchen Bereich gefunden haben, können wir mit process_vm_writev unseren Code in den Bereich schreiben (Snippet 6).

  // Initialize local and remote iovec structs used to perform the syscall
  struct iovec local[1];
  struct iovec remote[1];

  // Place our payload in the local iovec 
  local[0].iov_base = payload;
  local[0].iov_len = payload_len;

  // Point the remote iovec to the address of our wx memory region
  remote[0].iov_base = (void *)wx_address;
  remote[0].iov_len = payload_len;

// Write the local data to the remote address
process_vm_writev(pid, local, 1, remote, 1, 0);

Snippet 6: Schreiben einer Payload in einen Remote-WX-Bereich mit process_vm_writev

Übernahme des Remote-Ausführungsablaufs

Nachdem wir unseren Code in den Arbeitsspeicher des Remote-Prozesses geschrieben haben, müssen wir ihn ausführen. In den nächsten Abschnitten werden hierzu verschiedene Techniken beschrieben.

Unsere Forschung konzentrierte sich auf amd64-Rechner. Bei anderen Architekturen können geringfügige Unterschiede bestehen, aber die allgemeinen Konzepte sollten gleich bleiben.

Ändern des Befehlszeigers im Prozess

Gilt für:ptrace

Wenn wir einen Prozess mit ptrace anhängen, wird seine Ausführung angehalten, und wir können die Prozessregister, einschließlich Befehlszeiger, prüfen und ändern. Das kann mithilfe der ptrace-Anfragen SETREGS und GETREGS erfolgen. Um den Ausführungsablauf des Prozesses zu ändern, können wir mit ptrace den Befehlszeiger zur Adresse unseres Shellcodes ändern.

Im Beispiel in Snippet 7 haben wir die folgenden drei Schritte ausgeführt:

  1. Aktuelle Registerwerte mit der ptrace-Anfrage GETREGS abrufen

  2. Befehlszeiger so ändern, dass er auf unsere Payload-Adresse verweist (erhöht um 2, was wir später besprechen werden)

  3. Änderung mithilfe der SETREGS-Anfrage auf den Prozess anwenden

  // Get old register state.
  struct user_regs_struct regs;
  ptrace(PTRACE_GETREGS, pid, NULL, &regs);
 
  // Modify the instruction pointer to point to our payload
  regs.rip = payload_address + 2;

  // Modify the registers
  ptrace(PTRACE_SETREGS, pid, NULL, &regs);

Snippet 7: Verwendung von ptrace SETREGS, um den Befehlszeiger auf unsere Payload zu verweisen

SETREGS ist die „traditionelle“ und am meisten dokumentierte Methode zur Änderung der Prozessregister, aber auch eine andere ptrace-Anfrage kann dazu verwendet werden: POKEUSER.

Die POKEUSER-Anfrage ermöglicht das Schreiben von Daten in den Prozessbereich USER eine Struktur (definiert in sys/user.h), die Informationen über den Prozess enthält, einschließlich der Register. Durch Aufruf von POKEUSER mit dem richtigen Offset können wir den Befehlszeiger mit der Adresse unseres Codes überschreiben und das gleiche Ergebnis wie zuvor erzielen (Snippet 8).

  // calculate the offset of the RIP register, based on the USER struct definition
  rip_offset = 16 * sizeof(unsigned long);
  ptrace(PTRACE_POKEUSER, pid, rip_offset, payload_address + 2);

Snippet 8: Verwendung von ptrace POKEUSER, um den Befehlszeiger auf unsere Payload zu verweisen

Unsere Implementierung der POKEUSER-Verwendung zur Änderung von RIP finden Sie in unserem Repository.

RIP += 2: Wann und warum?

Wie in Snippet 7 und Snippet 8 zu sehen ist, wird bei der Änderung von RIP zur Adresse unserer Payload RIP auch um 2 erhöht. Das tun wir, um ein interessantes Verhalten von ptrace abzufangen: Manchmal wird nach dem Trennen von einem Prozess mit ptrace der Wert von RIP um 2 reduziert. Finden wir heraus, warum das passiert.

Wenn wir einen Prozess mit ptrace anhängen, unterbrechen wir möglicherweise einen Systemaufruf, der derzeit im Kernel ausgeführt wird. Um sicherzustellen, dass der Systemaufruf ordnungsgemäß ausgeführt wird, führt der Kernel ihn erneut aus, wenn wir die Verbindung zum Prozess aufheben.

Während der Systemaufruf ausgeführt wird, verweist RIP bereits auf den nächsten auszuführenden Befehl. Um den Systemaufruf erneut auszuführen, verringert der Kernel den RIP-Wert um 2 – die Größe des syscall-Befehls in amd64. Nach dieser Änderung verweist RIP erneut auf den syscall-Befehl, wodurch er zu einem anderen Zeitpunkt ausgeführt wird (Abbildung 7).

After this change, RIP will point to the syscall instruction again, causing it to run another time (Figure 7). Fig. 7: The effect of using ptrace on a process during syscall execution

Wenn wir bei einer Codeinjektion einen Prozess während eines Systemaufrufs unterbrechen, können Probleme auftreten. Nachdem wir RIP so geändert haben, dass es auf unseren Code verweist, verringert der Kernel den neuen Wert trotzdem um 2, was eine 2-Byte-Lücke vor unserem Shellcode verursacht, die wahrscheinlich dazu führt, dass er fehlschlägt (Abbildung 8).

After we modify RIP to point to our code, the kernel will still decrement the new value by 2, leading to a 2-byte gap before our shellcode, which will likely cause it to fail (Figure 8). Fig. 8: Using ptrace to point RIP to our shellcode results in a junk instruction executing

Um dieses Verhalten zu berücksichtigen, werden wir zwei Maßnahmen ergreifen: Wir stellen unserem Shellcode zwei NOP-Befehle (No Operation) voran und verweisen RIP auf die Adresse unseres Shellcodes + 2. Damit wird sichergestellt, dass unser Code ordnungsgemäß ausgeführt wird.

Wenn wir den Prozess während eines Systemaufrufs unterbrochen haben, verringert der Kernel den neuen RIP-Wert. Das führt dazu, dass er auf die Startadresse des Shellcodes verweist. Dieser enthält zwei NOPs, die wir in unseren eigentlichen Code einschieben.

Wenn wir den Prozess nicht während eines Systemaufrufs unterbrochen haben, wird der neue RIP nicht verringert. Das führt dazu, dass die beiden NOPs übersprungen werden und unser Code ausgeführt wird. Diese beiden Szenarien sind in Abbildung 9 dargestellt.

These 2 scenarios are depicted in Figure 9 Fig. 9: Overcoming the ptrace RIP interaction

Ändern des aktuellen Befehls

Gilt für:ptrace, procfs mem

Eine weitere interessante Datei in procfs ist syscall . Diese Datei enthält Informationen über den Systemaufruf, der derzeit vom Prozess ausgeführt wird: die syscall-Nummer, die Argumente, die an ihn übergeben wurden, den Stapelzeiger und – für unsere Zwecke am interessantesten – den Befehlszeiger im Prozess (Abbildung 10). Selbst wenn der Prozess derzeit keinen Systemaufruf ausführt, sind die Stapel- und Befehlszeiger des Prozesses weiterhin in der syscall-Datei vorhanden.

Another interesting file in procfs is the syscall file. This file holds information about the syscall that is currently executed by the process — the syscall number, the arguments that were passed to it, the stack pointer, and (most interesting for our cause) the process instruction pointer (Figure 10). Fig. 10: The structure of the procfs syscall file

Anhand dieser Informationen können wir die Kontrolle über den Ausführungsablauf des Prozesses übernehmen. Wenn wir die Adresse des nächsten auszuführenden Befehls kennen, können wir sie mit unseren eigenen Befehlen überschreiben.

Um das zu implementieren, kann ein Angreifer die folgenden vier Schritte ausführen:

  1. Prozessausführung durch Senden eines SIGSTOP-Signals stoppen

  2. Durch Lesen der syscall-Datei des Prozesses die Adresse des nächsten auszuführenden Befehls angeben

  3. Shellcode an die identifizierte Adresse schreiben

  4. Prozessausführung durch Senden eines SIGSTOP-Signals fortsetzen

Snippet 9 stellt einen Pseudocode für diesen Prozess bereit.

  // Suspend the process by sending a SIGSTOP signal
  kill(pid, SIGSTOP);

  // Open the syscall file
  FILE *syscall_file = fopen("/proc/<pid>/syscall", "r");

  // Extract the instruction pointer from the syscall file
  long instruction_pointer = ... 

  // Write our payload to the address of the current instruction pointer using 
  procfs mem
  FILE *mem_file = fopen("/proc/<pid>/mem", "w");
  fseek(mem_file, instruction_pointer, SEEK_SET);
  fwrite(payload, sizeof(char), payload_size, mem_file);

  // Resume execution by sending a SIGCONT signal
  kill(pid, SIGCONT);

Snippet 9: Verwendung von procfs mem zum Ändern des Prozessspeichers an der aktuellen Adresse des Befehlszeigers, um den Ausführungsablauf des Prozesses zu übernehmen

Im Beispiel in Snippet 9 wird diese Technik mithilfe der Datei „procfs mem“ implementiert. Es ist jedoch wichtig zu beachten, dass ptrace POKETEXT ebenfalls zum Schreiben der Payload in den Speicher verwendet werden kann.

Wie bereits erwähnt, ist process_vm_writev durch Speicherberechtigungen eingeschränkt, d. h., es kann nur beschreibbare Speicherbereiche ändern. Die Wahrscheinlichkeit, Code zu finden, der aus einem WX-Speicherbereich ausgeführt wird, ist gering,. Dadurch ist process_vm_writev für dieses Primitiv weniger zuverlässig.

Erfahren Sie mehr über unsere Implementierung dieser Technik unter Verwendung der Datei „procfs mem“.

Stapel-Übernahmen

Gilt für:ptrace, Datei „procfs mem“, process_vm_writev

Ein weiterer interessanter Speicherbereich ist der Prozessstapel, der auch mit der maps-Datei identifiziert werden kann. Obwohl der Stapelspeicher nicht ausführbar ist (Abbildung 11), können wir ihn dennoch verwenden, um den Ausführungsablauf des Prozesses zu übernehmen.

Although the stack memory is not executable (Figure 11), we can still use it to hijack the execution flow of the process. Fig. 11: Identifying the process stack address using the maps file

Wenn eine Funktion aufgerufen wird, wird die Rückgabeadresse der aufrufenden Funktion an den Stapel übermittelt. Wenn die Funktion nicht mehr ausgeführt wird, ruft der Prozessor diese Rückgabeadresse aus dem Stapel ab und springt dorthin (Abbildung 12).

When the function finishes execution, the processor takes this return address from the stack and jumps to it (Figure 12). Fig. 12: Return address on the stack pointing to an address in main

Um diesen Mechanismus auszunutzen, können wir eine Rückgabeadresse im Stapel identifizieren und mit einer neuen Adresse überschreiben, die auf unseren Shellcode verweist. Nach Ausführung der aktuellen Funktion wird unser Code ausgeführt (Abbildung 13).

To abuse this mechanism, we can identify a return address on the stack and overwrite it with a new address that points to our shellcode. As soon as the current function finishes execution, our code will run (Figure 13). Fig. 13: Overwriting a return address on the stack to point to the attackers code

Um den oberen Teil des Stapels zu identifizieren, können wir die zuvor erwähnte Datei „procfs syscall“ analysieren, die auch den Wert des Registers des Stapelzeigers enthält. 

Diese Technik kann mit den folgenden sechs Schritten durchgeführt werden:

  1. Prozessausführung durch Senden eines SIGSTOP-Signals stoppen

  2. Stapelzeiger des Prozesses durch Analyse der Datei „procfs syscall“ identifizieren

  3. Prozessstapel scannen und eine Rückgabeadresse identifizieren

  4. Mit einer der oben genannten Schreibprimitiven unsere Payload in den Prozessspeicher injizieren

  5. Rückgabeadresse mit der Adresse unserer Payload überschreiben

  6. Prozessausführung durch Senden eines SIGSTOP-Signals fortsetzen

Nach Ausführung der aktuellen Funktion wird unsere Payload ausgeführt.

Da wir mit allen Methoden der Prozessinteraktion den Stapel ändern können, können sie alle zur Implementierung dieser Technik verwendet werden. Unsere Implementierung dieser Technik mit dem Systemaufruf „process_vm_writev“ finden Sie in unserem Repository.

ROP-Stapel-Übernahme

Gilt für:ptrace, Datei „procfs mem“, process_vm_writev

Das Interessante an der Technik zur Stapel-Übernahme ist, dass wir damit den Ausführungsablauf des Prozesses übernehmen können, ohne dass ein ausführbarer Speicher oder ein Register geändert werden muss. Damit die Technik genutzt werden kann, müssen wir jedoch zum Shellcode springen, der sich in einem ausführbaren Speicherbereich befindet. Wir können versuchen, einen WX-Bereich zu finden (wie beschrieben), oder ptrace/procfs mem verwenden, um in nicht beschreibbaren Speicher zu schreiben.

Aber was, wenn wir diese Maßnahmen vermeiden wollen? Nun, wir haben noch einen Trick im Ärmel: rückgabeorientierte Programmierung (ROP). Wenn wir in den Prozessstapel schreiben können, können wir ihn mit einer ROP-Kette überschreiben (Abbildung 14). Da wir ausführbare Gadgets verwenden, die sich bereits im Prozessspeicher befinden, können wir eine Payload erstellen, ohne neuen ausführbaren Code zu schreiben.

But what if we want to avoid these actions? Well, we have another trick up our sleeve — return-oriented programming (ROP). By using our ability to write to the process stack, we can overwrite it with a ROP chain (Figure 14). Fig. 14: Injecting a ROP chain to the process stack

 Diese Technik besteht aus den folgenden sieben Schritten:

  1. Prozessausführung durch Senden eines SIGSTOP-Signals stoppen

  2. Stapelzeiger des Prozesses durch Analyse der Datei „procfs syscall“ identifizieren

  3. Prozessstapel scannen und eine Rückgabeadresse identifizieren

  4. Mit einer der oben genannten Schreibprimitiven unsere Payload ohne Ausführungsberechtigungen in einen Bereich mit Schreibzugriff injizieren

  5. Eine ROP-Kette erstellen, um mprotect aufzurufen, und den Speicherbereich unserer ausführbaren Shellcode-Datei markieren

  6. Stapel mit der ROP-Kette überschreiben, beginnend mit der Adresse der angegebenen Rückgabeadresse

  7. Prozessausführung durch Senden eines SIGSTOP-Signals fortsetzen

Nach Ausführung der aktuellen Funktion wird unsere ROP-Kette ausgeführt, wodurch der Shellcode ausführbar wird und der Vorgang zu diesem Code springt.

Dieses Konzept wurde von Rory McNamara von AON Cyber Labs in seinem Blogbeitrag beschrieben, in dem es um profs-mem-Injektion geht.

Diese Technik erfordert keine Änderung von nicht beschreibbaren Speicherbereichen und kann daher mit allen Prozessinteraktionstechniken durchgeführt werden, einschließlich process_vm_writev.

Erfahren Sie mehr über unsere Implementierung dieser Technik mit process_vm_writev. Soweit wir wissen, ist dies die erste öffentliche Demonstration einer Injektionstechnik, die nur auf dem Systemaufruf „process_vm_writev“ basiert.

GOT-Übernahme

Gilt für:ptrace, Datei „procfs mem“, process_vm_writev

Ein weiterer interessanter Speicherabschnitt, der normalerweise beschreibbar ist, ist die GOT. Die globale Offset-Tabelle (GOT) ist ein Speicherabschnitt, der als Teil des Verschiebungsprozesses dynamisch verknüpfter ELF-Dateien verwendet wird. Wir gehen hier nicht auf die vollständigen Details ein, sondern konzentrieren uns auf den Teil, der für unseren Zweck relevant ist: der Abschnitt, in dem die Adressen von Funktionen gespeichert sind, die vom Programm importiert wurden. Wenn das Programm eine Funktion aus einer Remote-Bibliothek aufruft, löst es die Speicheradresse auf, indem es auf die GOT zugreift (Abbildung 15).

Whenever the program calls a function from a remote library, it resolves its memory address by accessing the GOT (Figure 15). Fig. 15: Resolving a library function address using the GOT

Dieser Mechanismus kann von einem Angreifer ausgenutzt werden, um den Ausführungsablauf eines Prozesses zu übernehmen. Der GOT-Speicher ist normalerweise beschreibbar. Das bedeutet, dass ein Angreifer jede der darin enthaltenen Adressen mit der Adresse seiner Payload überschreiben kann. Beim nächsten Aufruf der Funktion durch den Prozess wird dann stattdessen der Angreifercode ausgeführt (Abbildung 16).

 The GOT memory is normally writable, meaning that an attacker can overwrite any of the addresses inside it with the address of their payload. The next time the function is called by the process, the attacker code will execute instead (Figure 16). Fig. 16: Modifying a function in the GOT to point to the attacker payload

Diese Technik besteht aus den folgenden vier Schritten:

  1. Prozessausführung durch Senden eines SIGSTOP-Signals stoppen

  2. GOT-Speicherbereich durch Analyse der maps-Datei identifizieren

  3. Adressen im entsprechenden Abschnitt mit der Adresse unserer Payload überschreiben

  4. Prozessausführung durch Senden eines SIGSTOP-Signals fortsetzen

 

Wenn eine unserer überschriebenen Funktionen aufgerufen wird, wird unsere Payload ausgeführt.

Ein Speicherschutz, der sich auf diesen Angriff auswirken könnte, ist Full RELRO.Das Kompilieren einer Binärdatei mit dieser Einstellung bewirkt, dass der GOT-Speicher schreibgeschützt ist, und kann so möglicherweise ein Überschreiben verhindern.

Dennoch wird RELRO diesen Angriff in den meisten Fällen nicht verhindern können.

  • ptrace und procfs mem umgehen Speicherberechtigungen, wodurch RELRO irrelevant wird. 

  • RELRO wirkt sich auf die Binärdatei des Prozesses selbst aus, jedoch nicht auf die geladenen Bibliotheken. Wenn der Prozess eine Bibliothek lädt, die ohne RELRO kompiliert wurde, ist deren GOT beschreibbar, sodass wir sie überschreiben können.

Unsere Implementierung dieser Technik mit dem Systemaufruf „process_vm_writev“ finden Sie in unserem Repository.

Zusammenfassung der Ausführungsprimitiven

In der Tabelle werden alle möglichen Ausführungsprimitiven zusammengefasst, die wir beschrieben haben, einschließlich Informationen dazu, mit welchen Methoden sie implementiert werden könnten.

The table summarizes all the possible execution primitives that we described, and with which methods they could be implemented. All the possible execution primitives and the methods that could be used to implement them

Einschränkungen bei der Interaktion mit Remote-Prozessen

Es gibt mehrere Einstellungen, die bestimmen, wie wir mit den eben beschriebenen Methoden mit Remote-Prozessen interagieren können. In diesem Abschnitt werden wir kurz auf die beiden wichtigsten eingehen.

ptrace_scope

ptrace_scope ist eine Einstellung, die festlegt wer ptrace in Remote-Prozessen verwenden darf. Sie kann folgende Werte aufweisen:

0 – Prozesse können mit jedem anderen Prozess im System verknüpft werden, sofern dieser dieselbe UID aufweist.

1 – Normale Prozesse können nur mit ihren untergeordneten Prozessen verknüpft werden. Berechtigte Prozesse (mit CAP_SYS_PTRACE) können weiterhin mit nicht verwandten Prozessen verknüpft werden. Dies ist die Standardeinstellung in vielen Verteilungen.

2 – Nur Prozesse mit CAP_SYS_PTRACE können mit Prozessen verknüpft werden. Diese Funktion wird in der Regel nur dem Root gewährt.

3 – Die Verknüpfung mit Remote-Prozessen ist deaktiviert.

Trotz ihres Namens wirkt sich diese Einstellung auch auf die Fähigkeit aus, auf die procfs-Speicherdatei von Remote-Prozessen zuzugreifen und process_vm_writev darauf anzuwenden.

Das Attribut „dumpable“

Jeder Prozess in Linux ist mit dem Attribut „dumpable“ konfiguriert, das standardmäßig auf „true“ gesetzt ist. Ein Prozess wird automatisch „undumpable“, wenn bestimmte Umständeauftreten oder der Prozess durch manuelles Aufrufen von prctlkonfiguriert wird.

Wenn ein Prozess nicht gedumpt werden kann, können wir mit keiner der oben genannten Methoden aus der Ferne darauf zugreifen. Diese Einstellung setzt andere außer Kraft – ein Prozess, der nicht gedumpt werden kann, kann nicht aus der Ferne geändert werden.

Ein Hinweis zur Prozesswiederherstellung

Bei allen hier beschriebenen Injektionsmethoden muss der Prozessstatus auf irgendeine Weise geändert werden: Die Prozessregister müssen geändert oder der ausführbare Speicher, eine Rückgabeadresse im Stapel oder die GOT überschrieben werden. All diese Maßnahmen ändern den normalen Ausführungsablauf des Prozesses und führen zu unerwartetem Verhalten, nachdem unsere Payload abgeschlossen wurde.

Das kann problematisch sein, wenn der Zielprozess parallel zu unserer injizierten Payload ausgeführt werden soll. Um sicherzustellen, dass der Prozess weiterhin normal ausgeführt wird, müssen wir den ursprünglichen Status wiederherstellen. Der allgemeine Wiederherstellungsablauf umfasst die folgenden acht Schritte:

  1. Zu überschreibenden Speicherinhalt mit einem Remote-Lese-Primitiv sichern

  2. Aktuellen Inhalt der Prozessregister sichern (mit ptrace oder unserem Shellcode)

  3. Unsere Payload ausführen (z. B. Datei mit gemeinsamen Objekt (SO) laden, die Code in einem separaten Thread ausführt)

  4. Dem Injektionsprozess nach Abschluss unserer Payload anzeigen, dass die Ausführung abgeschlossen ist (durch Auslösen einer Unterbrechung)

  5. Remote-Prozess unterbrechen

  6. Status des Prozessregisters wiederherstellen

  7. Überschriebenen Speicher wiederherstellen

  8. Prozessausführung wiederaufnehmen

Die Implementierungsdetails können je nach verwendeter Injektionsmethode geringfügig variieren. Der allgemeine Plan sollte jedoch befolgt werden. Adam Chesters Blogbeitrag zu Linux-ptrace-Injektion enthält ein detailliertes Beispiel für die Prozesswiederherstellung nach einer ptrace-basierten Injektion.

Unser Ziel mit diesem Beitrag war es, einen Überblick über Injektionstechniken zu geben, mit denen sich Verteidiger vertraut machen können, um dann eine angemessene Erkennung aufzubauen. Da unser Schwerpunkt auf der Verteidigung liegt, haben wir uns entschieden, die Wiederherstellungsschritte für die verschiedenen Techniken – die Angreifer benötigen, um sie vollständig zu nutzen – nicht näher zu beschreiben.

Erkennung und Abwehr

Wie wir gerade besprochen haben, gibt es viele Techniken, mit denen Angreifer Prozessinjektionen auf Linux-Computern durchführen können. Glücklicherweise müssen für all diese Methoden ungewöhnliche Maßnahmen durchgeführt werden, die erkannt werden können. In den nächsten Abschnitten werden die verschiedenen Strategien beschrieben, die zur Erkennung und Abwehr von Prozessinjektionen unter Linux implementiert werden könnten.

Injektions-Systemaufrufe

In diesem Beitrag haben wir drei Methoden zur Interaktion mit Remote-Prozessen verwendet: ptrace, procfs und process_vm_writev. Da diese Methoden böswillig eingesetzt werden können, sollten sie überwacht werden.

Installieren Sie zunächst eine Protokolllösung auf Linux-Computern. Um die Ausführung von Systemaufrufen zu überwachen, können ein eBPF-basiertes Protokollierungsprogramm wie Sysmon für Linux oder Tracee von Aqua Security verwenden (das bereits Regeln implementiert, die viele der in diesem Beitrag beschriebenen Techniken abdecken).

Nach der Einrichtung der Protokollierung empfehlen wir Unternehmen, die normale Verwendung der „Injektions-Systemaufrufe“ in ihrer Umgebung zu analysieren und eine Baseline bekannter gültiger Anwendungsfälle zu erstellen. Nachdem eine solche Baseline erstellt wurde, sollte jede Abweichung untersucht werden, um einen potenziellen Angriff auszuschließen. Weitere Überlegungen zu einzelnen Aspekten werden in den nächsten Abschnitten beschrieben.

Verwenden Sie im Idealfall wenn möglich ptrace_scope, um die Verwendung dieser Systemaufrufe einzuschränken oder sie vollständig zu verhindern.

ptrace

In den meisten Produktionsumgebungen wird der ptrace-Systemaufruf wahrscheinlich eher selten verwendet. Nachdem Sie eine Baseline für die gültige ptrace-Verwendung festgelegt haben, empfehlen wir die Analyse einer ungewöhnlichen ptrace-Verwendung.

Die folgenden ptrace-Anfragen ermöglichen die Änderung von Remote-Prozessen und sollten als äußerst verdächtig betrachtet werden:

  • POKEDATA/POKETEXT

  • POKEUSER

  • SETREGS

procfs

Für das Schreiben in die procfs-mem-Datei gibt es einige legitime Anwendungsfälle, aber dieses Verhalten wird wahrscheinlich nicht sehr häufig auftreten. Nach dem Erstellen einer Baseline gültiger Anwendungsfälle empfehlen wir, alle ungewöhnlichen Schreibvorgänge zu analysieren.

Es ist wichtig, auch das procfs-Verzeichnis /proc/<pid>/task zu berücksichtigen. Dieses Verzeichnis stellt Informationen über die verschiedenen Threads des Prozesses bereit. Jeder Thread hat sein eigenes procfs-Verzeichnis, das alle wichtigsten procfs-Dateien enthält, die wir behandelt haben, einschließlich der mem-, maps- und syscall-Dateien.

In Abbildung 17 sehen Sie, dass das Lesen der syscall-Datei aus dem Verzeichnis /proc/<pid> dem Lesen aus dem Verzeichnis /proc/<pid>/task/<pid> entspricht, das den Hauptthread des Prozesses darstellt.

In Figure 17, we can see that reading the syscall file from the /proc/<pid> directory is equivalent of reading from the /proc/<pid>/task/<pid> directory, which represents the main thread of the process. Fig. 17: Example of using the /proc/<pid>/task directory

process_vm_writev

Auch hier können wir durch Erstellung einer Baseline für legitime Verwendungen dieses Systemaufrufs ungewöhnliche Abweichungen identifizieren. Jeder unbekannte Prozess, der in den Speicher anderer Prozesse schreibt, sollte als verdächtig betrachtet und analysiert werden.

Erkennen von Prozessanomalien

Neben der direkten Erkennung der Prozessinjektion können wir auch versuchen, deren Nebenwirkungen zu erkennen. Wenn Code in einen Remote-Prozess injiziert wird, ändert sich sein Verhalten. Zusätzlich zu den normalen Maßnahmen des Prozesses werden jetzt auch die Maßnahmen der Payload vom selben Prozess ausgeführt.

Diese Verhaltensänderung kann eine Erkennungsmöglichkeit bieten. Durch die Erstellung einer Baseline für das normale Prozessverhalten können wir verdächtige Abweichungen erkennen, die möglicherweise darauf hindeuten, dass eine Codeinjektion stattgefunden hat. Einige Beispiele für solche Verhaltensweisen umfassen das Auslösen anormaler untergeordneter Prozesse, das Laden unbekannter SO-Dateien oder die Kommunikation über anormale Ports.

Die Forscher von Akamai haben diesen Ansatz dokumentiert und gezeigt, wie Codeinjektion durch die Analyse von Netzwerkanomalien identifiziert werden kann.

Zusammenfassung

Angreifer haben viele verschiedene Möglichkeiten, Injektionsangriffe auf Linux-Computern durchzuführen. Obwohl diese Techniken für Angreifer sehr nützlich sein können, bieten sie auch wertvolle Erkennungsmöglichkeiten für Verteidiger. Durch die Implementierung solider Protokollierungs- und Erkennungsfunktionen auf Linux-Computern können Unternehmen ihre Sicherheitslage erheblich verbessern.



Ori David

Verfasser

Ori David

November 14, 2024

Ori David

Verfasser

Ori David

Ori David ist Sicherheitsforscher bei Akamai. Seine Forschung konzentriert sich auf offensive Sicherheit, Malware-Analyse und Bedrohungssuche.