Der ultimative Leitfaden zu Linux-Prozessinjektion
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, ®s);
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).
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.
Wenn wir dieselbe Adresse im Speicher mithilfe von GDB prüfen, werden wir feststellen, dass der Inhalt identisch ist (Abbildung 3).
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.
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).
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
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:
Aktuelle Registerwerte mit der ptrace-Anfrage GETREGS abrufen
Befehlszeiger so ändern, dass er auf unsere Payload-Adresse verweist (erhöht um 2, was wir später besprechen werden)
Änderung mithilfe der SETREGS-Anfrage auf den Prozess anwenden
// Get old register state.
struct user_regs_struct regs;
ptrace(PTRACE_GETREGS, pid, NULL, ®s);
// Modify the instruction pointer to point to our payload
regs.rip = payload_address + 2;
// Modify the registers
ptrace(PTRACE_SETREGS, pid, NULL, ®s);
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).
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).
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.
Ä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.
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:
Prozessausführung durch Senden eines SIGSTOP-Signals stoppen
Durch Lesen der syscall-Datei des Prozesses die Adresse des nächsten auszuführenden Befehls angeben
Shellcode an die identifizierte Adresse schreiben
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.
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).
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).
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:
Prozessausführung durch Senden eines SIGSTOP-Signals stoppen
Stapelzeiger des Prozesses durch Analyse der Datei „procfs syscall“ identifizieren
Prozessstapel scannen und eine Rückgabeadresse identifizieren
Mit einer der oben genannten Schreibprimitiven unsere Payload in den Prozessspeicher injizieren
Rückgabeadresse mit der Adresse unserer Payload überschreiben
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.
Diese Technik besteht aus den folgenden sieben Schritten:
Prozessausführung durch Senden eines SIGSTOP-Signals stoppen
Stapelzeiger des Prozesses durch Analyse der Datei „procfs syscall“ identifizieren
Prozessstapel scannen und eine Rückgabeadresse identifizieren
Mit einer der oben genannten Schreibprimitiven unsere Payload ohne Ausführungsberechtigungen in einen Bereich mit Schreibzugriff injizieren
Eine ROP-Kette erstellen, um mprotect aufzurufen, und den Speicherbereich unserer ausführbaren Shellcode-Datei markieren
Stapel mit der ROP-Kette überschreiben, beginnend mit der Adresse der angegebenen Rückgabeadresse
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).
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).
Diese Technik besteht aus den folgenden vier Schritten:
Prozessausführung durch Senden eines SIGSTOP-Signals stoppen
GOT-Speicherbereich durch Analyse der maps-Datei identifizieren
Adressen im entsprechenden Abschnitt mit der Adresse unserer Payload überschreiben
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.
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:
Zu überschreibenden Speicherinhalt mit einem Remote-Lese-Primitiv sichern
Aktuellen Inhalt der Prozessregister sichern (mit ptrace oder unserem Shellcode)
Unsere Payload ausführen (z. B. Datei mit gemeinsamen Objekt (SO) laden, die Code in einem separaten Thread ausführt)
Dem Injektionsprozess nach Abschluss unserer Payload anzeigen, dass die Ausführung abgeschlossen ist (durch Auslösen einer Unterbrechung)
Remote-Prozess unterbrechen
Status des Prozessregisters wiederherstellen
Überschriebenen Speicher wiederherstellen
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.
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.