Einführung
Dies ist der erste Teil einer zweiteiligen Serie über Linux-Rootkits. In diesem ersten Teil konzentrieren wir uns auf die Theorie hinter der Funktionsweise von Rootkits: ihre Taxonomie, Evolution und die Hooking-Techniken, die sie verwenden, um den Kernel zu unterwandern. Im zweiten Teil wenden wir uns der defensiven Seite zu und befassen uns mit der Erkennungstechnik. Dabei behandeln wir praktische Ansätze zur Identifizierung und Reaktion auf diese Bedrohungen in Produktionsumgebungen.
Was sind Rootkits?
Rootkits sind getarnte Schadsoftware, die entwickelt wurde, um schädliche Aktivitäten wie Dateien, Prozesse, Netzwerkverbindungen, Kernelmodule oder Benutzerkonten zu verbergen. Ihr Hauptzweck ist die Persistenz und die Umgehung von Sicherheitsvorfällen, wodurch Angreifer langfristigen Zugriff auf wertvolle Ziele wie Server, Infrastruktur und Unternehmenssysteme erhalten können. Im Gegensatz zu anderen Malware-Formen konzentrieren sich Rootkits eher darauf, unentdeckt zu bleiben, als sofort Ziele zu verfolgen.
Wie funktionieren Rootkits?
Rootkits manipulieren das Betriebssystem, um die Art und Weise zu verändern, wie es Informationen für Benutzer und Sicherheitstools präsentiert. Sie arbeiten im Benutzermodus oder innerhalb des Kernels. Benutzerspace-Rootkits modifizieren Benutzerprozesse mithilfe von Techniken wie LD_PRELOAD oder Library Hijacking. Rootkits im Kernelbereich laufen mit höchsten Berechtigungen und verändern Kernelstrukturen, fangen Systemaufrufe ab oder laden bösartige Module. Diese tiefe Integration verleiht ihnen zwar starke Umgehungsmöglichkeiten, erhöht aber gleichzeitig das operative Risiko.
Warum sind Rootkits so schwer zu erkennen?
Rootkits im Kernelbereich können Kernfunktionen des Betriebssystems manipulieren, Sicherheitswerkzeuge untergraben und Spuren vor der Sichtbarkeit im Benutzermodus verbergen. Sie hinterlassen oft nur minimale Spuren ihrer Anwesenheit im System und vermeiden offensichtliche Indikatoren wie neue Prozesse oder Dateien, was die herkömmliche Erkennung erschwert. Die Identifizierung von Rootkits erfordert oft Speicherforensik, Kernel-Integritätsprüfungen oder Telemetrie unterhalb der Betriebssystemebene.
Warum Rootkits für Angreifer ein zweischneidiges Schwert sind
Rootkits bieten zwar Tarnung und Kontrolle, bergen aber auch operative Risiken. Kernel-Rootkits müssen präzise auf die jeweiligen Kernelversionen und -umgebungen abgestimmt sein. Fehler wie die fehlerhafte Speicherverwaltung oder das falsche Abfangen von Systemaufrufen können zu Systemabstürzen (Kernel-Panics) führen und den Angreifer sofort entlarven. Zumindest lenken diese Ausfälle unerwünschte Aufmerksamkeit auf das System – ein Szenario, das der Angreifer aktiv zu vermeiden versucht, um seinen Einflussbereich zu erhalten.
Auch Kernel-Updates bergen Herausforderungen: Änderungen an APIs, Speicherstrukturen oder Systemaufrufen können die Funktionalität von Rootkits beeinträchtigen und so die Persistenz angreifbar machen. Die Entdeckung verdächtiger Module oder Hooks löst typischerweise eine eingehende forensische Untersuchung aus, da Rootkits stark auf gezielte, hochqualifizierte Angriffe hindeuten. Für Angreifer sind Rootkits risikoreiche, aber potenziell sehr lohnende Werkzeuge; für Verteidiger bietet diese Anfälligkeit Möglichkeiten zur Erkennung durch Überwachung auf niedriger Ebene.
Windows vs Linux Rootkits
Das Windows-Rootkit-Ökosystem
Windows steht bei der Rootkit-Entwicklung im Vordergrund. Angreifer nutzen Kernel-Hooks, Treiber und undokumentierte Systemaufrufe aus, um Schadsoftware zu verstecken, Anmeldeinformationen zu stehlen und sich dauerhaft im System einzunisten. Eine ausgereifte Forschungsgemeinschaft und die weitverbreitete Nutzung in Unternehmensumgebungen treiben die kontinuierliche Innovation voran, einschließlich Techniken wie DKOM, PatchGuard-Umgehungen und Bootkits.
Robuste Sicherheitstools und Microsofts Härtungsmaßnahmen veranlassen Angreifer dazu, immer ausgefeiltere Methoden anzuwenden. Windows bleibt aufgrund seiner dominanten Stellung auf Unternehmensendpunkten und Endgeräten für Endverbraucher attraktiv.
Das Linux-Rootkit-Ökosystem
Linux-Rootkits haben in der Vergangenheit weniger Aufmerksamkeit erhalten. Die Fragmentierung über verschiedene Distributionen und Kernelversionen hinweg erschwert die Erkennung und Entwicklung. Zwar gibt es akademische Forschung, doch viele der verwendeten Werkzeuge sind veraltet, und in produktiven Linux-Umgebungen mangelt es oft an spezialisierter Überwachung.
Die Rolle von Linux in den Bereichen Cloud, Container, IoT und High Performance Computing hat es jedoch zu einem zunehmenden Ziel gemacht. In der Praxis wurden bereits Linux-Rootkits bei Angriffen auf Cloud-Anbieter, Telekommunikationsunternehmen und Regierungen eingesetzt. Zu den wichtigsten Herausforderungen für Angreifer gehören:
- Unterschiedliche Kernel behindern die Kompatibilität zwischen verschiedenen Distributionen.
- Lange Betriebszeiten verlängern Kernel-Inkompatibilitäten.
- Sicherheitsfunktionen wie SELinux, AppArmor und Modulsignierung erhöhen den Schwierigkeitsgrad.
Zu den spezifischen Bedrohungen für Linux gehören:
- Container & Kubernetes: Neue Persistenzvektoren durch Container-Escape.
- IoT-Geräte: veraltete Kernel mit minimaler Überwachung.
- Produktionsserver: Headless-Systeme ohne Benutzerinteraktion, wodurch die Transparenz eingeschränkt wird.
Da Linux die moderne Infrastruktur dominiert, stellen Rootkits eine unterschätzte, aber zunehmende Bedrohung dar. Die Verbesserung der Erkennung, der Werkzeuge und der Forschung im Bereich Linux-spezifischer Techniken wird immer dringlicher.
Evolution der Linux-Rootkit-Implementierungsmodelle
In den letzten zwei Jahrzehnten haben sich Linux-Rootkits von einfachen Benutzerlandtechniken zu fortgeschrittenen, im Kernel ansässigen Implantaten weiterentwickelt, die moderne Kernel-Schnittstellen wie eBPF und io_uring nutzen. Jede Phase dieser Entwicklung spiegelt sowohl die Innovation der Angreifer als auch die Reaktion der Verteidiger wider und treibt die Rootkit-Designs in Richtung größerer Heimlichkeit, Flexibilität und Widerstandsfähigkeit.
Dieser Abschnitt beschreibt diese Entwicklung und geht dabei auf wichtige Merkmale, den historischen Kontext und Beispiele aus der Praxis ein.
Anfang der 2000er Jahre: Benutzerland-Rootkits für gemeinsam genutzte Objekte (SO)
Die ersten Linux-Rootkits arbeiteten ausschließlich im Benutzermodus, ohne dass eine Kernelmodifikation erforderlich war. Sie nutzten Techniken wie LD_PRELOAD oder die Manipulation von Shell-Profilen, um bösartige Shared Objects in legitime Binärdateien einzuschleusen. Durch das Abfangen von Standard-libc-Funktionen wie opendir, readdir und fopen könnten diese Rootkits die Ausgabe von Diagnosetools wie ps, ls und netstat manipulieren. Dieser Ansatz erleichterte zwar ihren Einsatz, doch ihre Abhängigkeit von Benutzerschnittstellen bedeutete, dass sie im Vergleich zu Kernel-basierten Implantaten in Bezug auf Tarnung und Reichweite eingeschränkt waren; sie konnten leicht durch einfache Neustarts oder Konfigurationsresets gestört werden. Prominente Beispiele sind das Jynx-Rootkit (2009), das libc -Funktionen nutzte, um Dateien und Verbindungen zu verbergen, und Azazel (2013), das Shared Object Injection mit optionalen Kernel-Mode-Funktionen kombinierte. Die grundlegenden Techniken für diesen Missbrauch dynamischer Linker wurden bekanntlich in Phrack Magazine #61 im Jahr 2003 detailliert beschrieben.
Mitte der 2000er- bis 2010er-Jahre: Loadable Kernel Module (LKM) Rootkits
Als die Verteidiger immer besser darin wurden, Manipulationen im Benutzermodus zu erkennen, verlagerten die Angreifer ihren Zugriff auf den Kernelbereich über ladbare Kernelmodule (LKMs). Obwohl LKMs legitime Erweiterungen sind, nutzen böswillige Akteure sie, um mit vollen Berechtigungen zu arbeiten, indem sie sys_call_table abfangen, ftrace manipulieren oder interne verkettete Listen verändern, um Prozesse, Dateien, Sockets und sogar das Rootkit selbst zu verbergen. Obwohl LKMs eine hohe Kontrollierbarkeit und starke Tarnfähigkeiten bieten, werden sie in stark frequentierten Umgebungen einer eingehenden Prüfung unterzogen. Sie sind über manipulierte Kernelzustände, Einträge in /proc/modules oder spezialisierte LKM-Scanner erkennbar und werden zunehmend durch moderne Schutzmechanismen wie Secure Boot, Modulsignierung und Linux Security Modules (LSMs) behindert. Klassische Beispiele aus dieser Ära sind Adore-ng (2004+), ein LKM, das sich durch Syscall-Hooking selbst verbergen kann; Diamorphine (2016), ein beliebter Hooker, der auf vielen Distributionen weiterhin funktionsfähig ist; und Reptile (2020), eine moderne Variante mit Backdoor-Funktionen.
Ende der 2010er Jahre: eBPF-basierte Rootkits
Um der zunehmenden Erkennung von LKM-basierten Bedrohungen zu entgehen, begannen Angreifer, eBPF zu missbrauchen, ein Subsystem, das ursprünglich für sicheres Paketfiltern und Kernel-Tracing entwickelt wurde. Seit Linux 4.8+ hat sich eBPF zu einer programmierbaren virtuellen Maschine im Kernel weiterentwickelt, die in der Lage ist, Code an syscall-Hooks, kprobes, tracepoints oder Ereignisse des Linux-Sicherheitsmoduls anzuhängen. Diese Implantate laufen im Kernel-Bereich, vermeiden aber das Laden herkömmlicher Module, wodurch sie Standard-LKM-Scanner wie rkhunter und chkrootkit sowie Secure-Boot-Beschränkungen umgehen können. Da sie nicht in /proc/modules erscheinen und für typische Modul-Auditmechanismen im Wesentlichen unsichtbar sind, benötigen sie CAP_BPF oder CAP_SYS_ADMIN (oder selten unprivilegierten BPF-Zugriff), um eingesetzt zu werden. Diese Ära ist geprägt von Tools wie Triple Cross (2022), einem Proof-of-Concept, das eBPF-Programme einfügt, um Systemaufrufe wie execve abzufangen, und Boopkit (2022), das einen verdeckten C2-Kanal vollständig über eBPF implementiert, sowie zahlreichen Defcon-Präsentationen, die sich mit diesem Thema auseinandersetzen.
Ab 2025: io_uring-basierte Rootkits (aufkommend)
Die neueste Weiterentwicklung nutzt io_uring, eine leistungsstarke asynchrone E/A-Schnittstelle, die in Linux 5.1 (2019) eingeführt wurde und es Prozessen ermöglicht, Systemoperationen über gemeinsam genutzte Speicherringe zu bündeln. Obwohl io_uring zur Reduzierung des Systemaufruf-Overheads und damit zur Leistungssteigerung entwickelt wurde, haben Red-Team-Anwender gezeigt, dass es missbraucht werden kann, um unauffällige Benutzerland-Agenten oder Kernel-Kontext-Rootkits zu erstellen, die auf Systemaufrufen basierende EDRs umgehen. Durch die Verwendung von io_uring_enter zur Stapelverarbeitung von Datei-, Netzwerk- und Prozessvorgängen erzeugen diese Rootkits weit weniger beobachtbare Systemaufrufereignisse, was herkömmliche Erkennungsmechanismen erschwert und die Einschränkungen umgeht, die für LKMs und eBPF gelten. Obwohl es sich noch um experimentelle Ansätze handelt, unterstreichen Beispiele wie RingReaper (2025), das io_uring verwendet, um gängige Systemaufrufe wie read, write, connect und unlink heimlich zu ersetzen, sowie die Forschung von ARMO , dass dies ein vielversprechender Ansatz für die zukünftige Entwicklung von Rootkits ist, der ohne spezielle Instrumentierung schwer nachzuverfolgen ist.
Das Design von Linux-Rootkits wurde im Laufe der Zeit immer wieder an verbesserte Abwehrmechanismen angepasst. Da das Laden von LKM immer schwieriger wird und die Überwachung von Systemaufrufen immer ausgefeilter wird, haben sich Angreifer alternativen Schnittstellen wie eBPF und io_uring zugewandt. Mit dieser Entwicklung geht es im Kampf nicht mehr nur um die Erkennung, sondern auch darum, die Mechanismen zu verstehen, mit denen Rootkits sich in den Kern des Systems einfügen, angefangen bei ihren Hooking-Strategien und ihrer internen Architektur.
##Rootkit-Interna und Hooking-Techniken
Das Verständnis der Architektur von Linux-Rootkits ist für deren Erkennung und Abwehr unerlässlich. Die meisten Rootkits folgen einem modularen Design mit zwei Hauptkomponenten:
- Loader: Installiert oder injiziert das Rootkit und kann sich dauerhaft einnisten. Obwohl nicht unbedingt notwendig, ist eine separate Loader-Komponente häufig in Malware-Infektionsketten zu sehen, die Rootkits einsetzen.
- Nutzlast: Führt schädliche Aktionen aus, wie z. B. das Verstecken von Dateien, das Abfangen von Systemaufrufen oder die verdeckte Kommunikation.
Die Nutzdaten setzen stark auf Hooking-Techniken, um den Ausführungsablauf zu verändern und Tarnung zu erreichen.
Rootkit-Loader-Komponente
Der Loader ist die Komponente, die für das Übertragen des Rootkits in den Speicher, die Initialisierung seiner Ausführung und in vielen Fällen für die Etablierung von Persistenz oder die Eskalation von Berechtigungen verantwortlich ist. Seine Rolle besteht darin, die Lücke zwischen dem ersten Zugriff (z. B. durch Exploits, Phishing oder Fehlkonfigurationen) und der vollständigen Bereitstellung eines Rootkits zu schließen.
Je nach Rootkit-Modell kann der Loader vollständig im Benutzermodus arbeiten, über Standard-Systemschnittstellen mit dem Kernel interagieren oder die Schutzmechanismen des Betriebssystems gänzlich umgehen. Im Allgemeinen lassen sich Loader in drei Klassen einteilen: Malware-basierte Dropper, Benutzerland-Rootkit-Initialisierer und benutzerdefinierte Kernel-Space-Loader. Darüber hinaus können Rootkits auch manuell von einem Angreifer über Benutzerraumwerkzeuge wie insmod geladen werden.
Malware-basierte Dropper
Malware-Dropper sind leichtgewichtige Programme, die oft nach dem ersten Zugriff eingesetzt werden und deren einziger Zweck darin besteht, eine Rootkit-Payload herunterzuladen oder zu entpacken und auszuführen. Diese Dropper operieren typischerweise im Benutzermodus, erweitern aber ihre Berechtigungen und interagieren mit Funktionen auf Kernel-Ebene.
Gängige Techniken sind:
- Modul-Injection: Schreiben einer bösartigen
.ko-Datei auf die Festplatte und Aufruf voninsmododermodprobe, um sie als Kernelmodul zu laden. - Syscall-Wrapper: Verwendung eines Wrappers um
init_module()oderfinit_module()um ein LKM direkt über Syscalls zu laden. - In-Memory-Injektion: Nutzung von Schnittstellen wie
ptraceodermemfd_create, wodurch häufig Festplattenartefakte vermieden werden. - BPF-basiertes Laden: Verwendung von Hilfsprogrammen wie
bpftool,tcoder direktenbpf()Systemaufrufen zum Laden und Anhängen von eBPF-Programmen an Kernel-Tracepoints oder LSM-Hooks.
Benutzerlandlader
Im Falle von Shared-Object-Rootkits kann der Loader auf die Änderung der Benutzerkonfiguration oder der Umgebungseinstellungen beschränkt sein:
- Missbrauch des dynamischen Linkers: Durch Setzen von
LD_PRELOAD=/path/to/rootkit.sokann das bösartige Shared Object libc-Funktionen überschreiben, wenn die Zielbinärdatei ausgeführt wird. - Persistenz durch Profilmodifikation: Das Einfügen von Vorladekonfigurationen in
.bashrc,.profile, oder globale Dateien wie/etc/profilegewährleistet die kontinuierliche Ausführung über Sitzungen hinweg.
Obwohl diese Lader trivial in der Implementierung sind, bleiben sie in schwach verteidigten Umgebungen oder als Teil mehrstufiger Infektionsketten wirksam.
Benutzerdefinierte Kernel-Loader
Fortgeschrittene Rootkits können benutzerdefinierte Kernel-Loader enthalten, die so konstruiert sind, dass sie die Standard-Modulladepfade vollständig umgehen. Diese Loader interagieren direkt mit Low-Level-Kernel-Schnittstellen oder Speichergeräten, um das Rootkit in den Speicher zu schreiben, wobei sie häufig Kernel-Audit-Logs oder die Überprüfung der Modulsignatur umgehen.
Reptile enthält beispielsweise eine Benutzerspace-Binärdatei als Loader, wodurch es das Rootkit laden kann, ohne insmod oder modprobe aufzurufen; allerdings ist es weiterhin auf den Systemaufruf init_mod angewiesen, um das Modul in den Speicher zu laden.
Zusätzliche Laderfunktionen
Der Malware-Loader übernimmt oft eine erweiterte Rolle, die über die einfache Initialisierung hinausgeht, und wird zu einer multifunktionalen Komponente der Angriffskette. Ein wichtiger Schritt für diese fortgeschrittenen Loader ist die Erhöhung der Berechtigungen, bei der sie versuchen, Root-Zugriff zu erlangen, bevor sie die eigentliche Nutzlast laden. Dies geschieht häufig durch Ausnutzung lokaler Kernel-Schwachstellen, eine gängige Taktik, die beispielsweise durch die „Dirty Pipe“-Schwachstelle (CVE-2022-0847) veranschaulicht wird. Sobald die Zugriffsrechte gesichert sind, ist es die Aufgabe des Laders, die Spuren zu verwischen. Dies beinhaltet das Vernichten von Ausführungsspuren durch Löschen von Einträgen aus kritischen Dateien wie bash_history, Kernel-Logs, Audit-Logs oder dem Hauptverzeichnis des Systems syslog. Um schließlich die erneute Ausführung nach einem Systemneustart zu gewährleisten, sorgt der Loader für Persistenz, indem er Mechanismen wie systemd Einheiten, cron Jobs, udev Regeln oder Änderungen an Initialisierungsskripten installiert. Diese multifunktionalen Verhaltensweisen verwischen oft die Unterscheidung zwischen einem bloßen „Loader“ und vollwertiger Malware, insbesondere bei komplexen, mehrstufigen Infektionen.
Nutzlastkomponente
Die Nutzlast liefert Kernfunktionen: Tarnung, Kontrolle und Persistenz. Es gibt mehrere primäre Methoden, die ein Angreifer anwenden könnte. Benutzerspace-Payloads, oft auch als SO-Rootkits bezeichnet, funktionieren, indem sie Standard-C-Bibliotheksfunktionen wie readdir oder fopen über den dynamischen Linker kapern. Dies ermöglicht es ihnen, die Ausgabe gängiger Systemwerkzeuge wie ls, netstat und ps zu manipulieren. Obwohl sie im Allgemeinen einfacher einzusetzen sind, ist ihr Einsatzbereich begrenzt.
Im Gegensatz dazu arbeiten Kernel-Space-Payloads mit vollen Systemrechten. Sie können Dateien und Prozesse direkt vor /proc verbergen, den Netzwerk-Stack manipulieren und Kernelstrukturen modifizieren. Ein modernerer Ansatz beinhaltet eBPF-basierte Rootkits, die im Kernel gespeicherten Bytecode nutzen, der an Systemaufruf-Tracepoints oder Hooks des Linux Security Module (LSM) angehängt ist. Diese Kits bieten Stealth-Funktionalität ohne die Notwendigkeit externer Module und sind daher besonders effektiv in Umgebungen mit Secure Boot oder Modulsignierungsrichtlinien. Tools wie bpftool vereinfachen deren Laden, was die Erkennung erschwert. Schließlich nutzen io_uring-basierte Payloads die asynchrone I/O-Batching-Funktion über io_uring_enter (verfügbar ab Linux 5.1), um die herkömmliche Systemaufrufüberwachung zu umgehen. Dies ermöglicht unauffällige Datei-, Netzwerk- und Prozessoperationen bei gleichzeitiger Minimierung der Offenlegung von Telemetriedaten.
Linux-Rootkits – Hooking-Techniken
Aufbauend auf dieser grundlegenden Basis wenden wir uns nun dem Kern der meisten Rootkit-Funktionen zu: dem Hooking. Im Kern geht es beim Hooking darum, die Ausführung von Funktionen oder Systemaufrufen abzufangen und zu verändern, um bösartige Aktivitäten zu verbergen oder neue Verhaltensweisen einzuschleusen. Durch die Umleitung des normalen Programmablaufs können Rootkits Dateien und Prozesse verbergen, Sicherheitsereignisse herausfiltern oder das System heimlich überwachen, oft ohne offensichtliche Spuren zu hinterlassen. Hooking kann sowohl im Benutzermodus als auch im Kernelmodus implementiert werden, und im Laufe der Jahre haben Angreifer zahlreiche Hooking-Techniken entwickelt, von veralteten Methoden bis hin zu modernen Ausweichmanövern. In diesem Teil werden wir uns eingehend mit den gängigen Hooking-Techniken von Linux-Rootkits befassen und jede Methode anhand von Beispielen und realen Rootkit-Samples (wie Reptile, Diamorphine, PUMAKIT und in jüngerer Zeit FlipSwitch) veranschaulichen, um zu verstehen, wie sie funktionieren und wie die Kernel-Evolution sie herausgefordert hat.
Das Konzept des Einhakens
Im Prinzip bezeichnet Hooking die Praxis, einen Funktions- oder Systemaufruf abzufangen und ihn an bösartigen Code umzuleiten. Auf diese Weise kann ein Rootkit die zurückgegebenen Daten oder das Verhalten verändern, um seine Anwesenheit zu verbergen oder Systemvorgänge zu manipulieren. Ein Rootkit könnte beispielsweise den Systemaufruf abfangen, der die Dateien in einem Verzeichnis auflistet (getdents), sodass alle Dateinamen übersprungen werden, die mit den eigenen Dateien des Rootkits übereinstimmen, wodurch diese Dateien für Benutzerbefehle wie ls „unsichtbar“ werden.
Hooking beschränkt sich nicht auf Kernel-Interna; es kann auch im Benutzermodus auftreten. Frühe Linux-Rootkits operierten ausschließlich im Benutzermodus, indem sie bösartige gemeinsam genutzte Objekte in Prozesse einschleusten. Techniken wie die Verwendung der Umgebungsvariablen LD_PRELOAD des dynamischen Linkers ermöglichen es einem Rootkit, Standard-C-Bibliotheksfunktionen (z. B. getdents, readdir, und fopen) in Benutzerprogrammen zu überschreiben. Das bedeutet, dass beim Ausführen eines Tools wie ps oder netstat der in das Rootkit eingeschleuste Code Aufrufe zum Auflisten von Prozessen oder Netzwerkverbindungen abfängt und die schädlichen herausfiltert. Diese Benutzerland-Hooks benötigen keine Kernel-Berechtigungen und sind relativ einfach zu implementieren.
Zu den bemerkenswerten Beispielen gehören JynxKit (2012) und Azazel (2014), Benutzermodus-Rootkits, die Dutzende von libc Funktionen abfangen, um Prozesse, Dateien und Netzwerkports zu verbergen und sogar Hintertüren zu aktivieren. Allerdings hat das Hooking auf Benutzerebene erhebliche Einschränkungen: Es ist leichter zu erkennen und zu entfernen und bietet nicht die tiefgreifende Kontrolle, die Hooks auf Kernel-Ebene bieten. Aus diesem Grund haben die meisten modernen und in freier Wildbahn verbreiteten Linux-Rootkits auf Kernel-Space-Hooking umgestellt, trotz der höheren Komplexität und des größeren Risikos, da Kernel-Hooks das Betriebssystem und die Sicherheitstools auf niedriger Ebene umfassend austricksen können.
Im Kernel bedeutet Hooking typischerweise die Veränderung von Kernel-Datenstrukturen oder -Code, sodass der Kernel beim Ausführen einer bestimmten Operation (z. B. beim Öffnen einer Datei oder beim Ausführen eines Systemaufrufs) den Code des Rootkits anstelle (oder zusätzlich) des legitimen Codes aufruft. Im Laufe der Jahre haben die Entwickler des Linux-Kernels stärkere Schutzmechanismen gegen unautorisierte Modifikationen eingeführt, aber die Angreifer haben mit immer ausgefeilteren Hooking-Methoden reagiert. Im Folgenden werden wir die wichtigsten Hooking-Techniken im Kernel-Bereich untersuchen, angefangen bei älteren Methoden (die heute weitgehend veraltet sind) bis hin zu modernen Techniken, die versuchen, die aktuellen Kernel-Abwehrmechanismen zu umgehen. In jedem Unterabschnitt wird die Technik erläutert, ein vereinfachtes Codebeispiel gezeigt und ihre Anwendung in bekannten Rootkits sowie ihre Grenzen angesichts der heutigen Linux-Sicherheitsvorkehrungen diskutiert.
Hooking-Techniken im Kernel
Interrupt Descriptor Table (IDT) Hooking
Einer der ersten Kernel-Hooking-Tricks unter Linux bestand darin, die Interrupt Descriptor Table (IDT) anzugreifen. Auf 32-Bit-x86-Linux wurden Systemaufrufe früher über einen Software-Interrupt (int 0x80) ausgelöst. Die IDT ist eine Tabelle, die Interruptnummern Handleradressen zuordnet. Durch die Modifizierung des IDT-Eintrags für 0x80 könnte ein Rootkit den Systemaufruf-Einstiegspunkt kapern, bevor der eigene Systemaufruf-Dispatcher des Kernels die Kontrolle erlangt. Mit anderen Worten: Wenn ein Programm einen Systemaufruf über int 0x80 auslöste, sprang die CPU zuerst zum benutzerdefinierten Handler des Rootkits, wodurch das Rootkit Aufrufe auf der untersten Ebene filtern oder umleiten konnte. Nachfolgend ein vereinfachtes Codebeispiel für IDT-Hooking (zur Veranschaulichung):
// Install the IDT hook
static int install_idt_hook(void) {
// Get pointer to IDT table
idt_table = get_idt_table();
// Save original syscall handler (int 0x80 = entry 128)
original_syscall_entry = idt_table[0x80];
// Calculate original handler address
original_syscall_handler = (void*)(
(original_syscall_entry.offset_high << 16) |
original_syscall_entry.offset_low
);
// Install our hook
idt_table[0x80].offset_low = (unsigned long)custom_int80_handler & 0xFFFF;
idt_table[0x80].offset_high =
((unsigned long)custom_int80_handler >> 16)
& 0xFFFF;
// Keep same selector and attributes as original
// idt_table[0x80].selector and type_attr remain unchanged
printk(KERN_INFO "IDT hook installed at 0x80\n");
return 0;
}
Der obige Code legt einen neuen Handler für Interrupt 0x80 fest und leitet den Ausführungsfluss zum Handler des Rootkits um, bevor eine Systemaufrufbehandlung stattfindet. Dies ermöglicht es dem Rootkit, das Verhalten von Systemaufrufen vollständig unterhalb der Ebene der Systemaufruftabelle abzufangen oder zu verändern. IDT-Hooking wird von älteren und speziell für Bildungszwecke entwickelten Rootkits wie SuckIT verwendet.
Das Einhaken von IDTs ist heute größtenteils eine historische Technik. Es funktionierte nur auf älteren Linux-Systemen, die den int 0x80 -Mechanismus verwenden (32-Bit-x86-Kernel vor Linux 2.6). Moderne 64-Bit-Linux-Systeme verwenden die Befehle sysenter/syscall anstelle der Software-Interrupts, sodass der IDT-Eintrag für 0x80 nicht mehr für Systemaufrufe verwendet wird. Darüber hinaus ist IDT-Hooking stark architekturabhängig (nur x86) und funktioniert nicht auf modernen Kerneln mit x86_64 oder anderen Architekturen.
Syscall-Tabellen-Hooking
Syscall-Tabellen-Hooking ist eine klassische Rootkit-Technik, bei der die Systemaufruf-Dispatch-Tabelle des Kernels, bekannt als sys_call_table, modifiziert wird. Diese Tabelle ist ein Array von Funktionszeigern, wobei jeder Eintrag einer bestimmten Systemaufrufnummer entspricht. Durch Überschreiben eines Zeigers in dieser Tabelle kann ein Angreifer einen legitimen Systemaufruf, wie z. B. getdents64, kill oder read, an einen bösartigen Handler umleiten. Ein Beispiel wird unten angezeigt.
asmlinkage int (*original_getdents64)(
unsigned int,
struct linux_dirent64 __user *,
unsigned int);
asmlinkage int hacked_getdents64(
unsigned int fd,
struct linux_dirent64 __user *dirp,
unsigned int count)
{
int ret = original_getdents64(fd, dirp, count);
// Filter hidden entries from dirp
return ret;
}
write_cr0(read_cr0() & ~0x10000); // Disable write protection
sys_call_table[__NR_getdents64] = hacked_getdents64;
write_cr0(read_cr0() | 0x10000); // Re-enable write protection
Um in diesem Beispiel die Tabelle zu ändern, müsste ein Kernelmodul zuerst den Schreibschutz für die Speicherseite deaktivieren, auf der sich die Tabelle befindet. Der folgende Assembler-Code (wie in Diamorphine zu sehen) demonstriert, wie das 20. Bit (Schreibschutz) des CR0 -Steuerregisters gelöscht werden kann, obwohl die write_cr0 -Funktion nicht mehr an Module exportiert wird:
static inline void
write_cr0_forced(unsigned long val)
{
unsigned long __force_order;
asm volatile(
"mov %0, %%cr0"
: "+r"(val), "+m"(__force_order));
}
Sobald der Schreibschutz deaktiviert ist, kann die Adresse eines Systemaufrufs in der Tabelle durch die Adresse einer bösartigen Funktion ersetzt werden. Nach der Änderung wird der Schreibschutz wieder aktiviert. Bekannte Beispiele für Rootkits, die diese Technik nutzten, sind Diamorphine, Knark und Reveng_rtkit. Syscall-Tabellen-Hooking hat mehrere Einschränkungen:
- Kernelhärtung (seit 2.6.25) verbirgt
sys_call_table. - Die Kernel-Speicherseiten wurden schreibgeschützt gemacht (
CONFIG_STRICT_KERNEL_RWX). - Sicherheitsfunktionen wie Secure Boot und der Kernel-Sperrmechanismus können Änderungen an CR0 verhindern.
Die endgültigste Lösung kam mit dem Linux-Kernel 6.9, der die Art und Weise, wie Systemaufrufe auf der x86-64-Architektur ausgeführt werden, grundlegend veränderte. Vor Version 6.9 führte der Kernel Systemaufrufe aus, indem er den Handler direkt im sys_call_table -Array nachschlug:
// Pre-v6.9 Syscall Dispatch
asmlinkage const sys_call_ptr_t sys_call_table[] = {
#include <asm/syscalls_64.h>
};
Ab Kernel 6.9 wird die Systemaufrufnummer in einer Switch-Anweisung verwendet, um den entsprechenden Handler zu finden und auszuführen. Die sys_call_table existiert zwar noch, wird aber nur noch aus Kompatibilitätsgründen mit Tracing-Tools verwendet und kommt im Ausführungspfad des Systemaufrufs nicht mehr zum Einsatz.
// Kernel v6.9+ Syscall Dispatch
long x64_sys_call(const struct pt_regs *regs, unsigned int nr)
{
switch (nr) {
#include <asm/syscalls_64.h>
default: return __x64_sys_ni_syscall(regs);
}
};
Aufgrund dieser architektonischen Änderung hat das Überschreiben von Funktionszeigern in sys_call_table bei Kerneln ab Version 6.9 keinen Einfluss auf die Ausführung von Systemaufrufen, wodurch die Technik völlig wirkungslos wird. Dies führte uns zu der Annahme, dass das Patchen der Syscall-Tabelle nicht mehr praktikabel sei. Vor kurzem veröffentlichten wir jedoch die FlipSwitch- Technik, die zeigt, dass dieser Ansatz noch lange nicht tot ist. Diese Methode nutzt spezielle Registermanipulationsgeräte, um die Schreibschutzmechanismen des Kernels kurzzeitig zu deaktivieren. Dadurch kann ein Angreifer die „Unveränderlichkeit“ des modernen Systemaufrufpfads umgehen und selbst in diesen gehärteten Umgebungen wieder Hooks einführen.
Anstatt auf die datenbasierte sys_call_table abzuzielen, konzentriert sich FlipSwitch auf den kompilierten Maschinencode der neuen Syscall-Dispatcher-Funktion des Kernels, x64_sys_call. Da der Kernel jetzt eine massive Switch-Case-Anweisung zur Ausführung von Systemaufrufen verwendet, enthält jeder Systemaufruf eine fest codierte call -Anweisung in der Binärdatei des Dispatchers. FlipSwitch durchsucht den Speicher der x64_sys_call -Funktion, um die spezifische "Signatur" eines Ziel-Systemaufrufs zu finden, typischerweise einen 0xe8 -Opcode (die CALL -Anweisung), gefolgt von einem 4-Byte-relativen Offset, der auf den ursprünglichen, legitimen Handler verweist.
Sobald diese Aufrufstelle im Dispatcher identifiziert ist, verwendet das Rootkit Gadgets, um das Write Protect (WP)-Bit im CR0-Steuerregister zu löschen und so vorübergehend Schreibzugriff auf die ausführbaren Codesegmente des Kernels zu erhalten. Der ursprüngliche relative Offset wird dann mit einem neuen Offset überschrieben, der auf eine bösartige, vom Angreifer kontrollierte Funktion verweist. Dies bewirkt im Wesentlichen, dass beim Aufruf des Systems ein Schalter umgelegt wird, sodass sichergestellt wird, dass der Kernel, wenn er versucht, den Zielsystemaufruf über seinen modernen Switch-Anweisungspfad auszuführen, stattdessen zum Rootkit umgeleitet wird. Dies ermöglicht ein zuverlässiges und präzises Abfangen von Systemaufrufen, das trotz der architektonischen Härtung des 6.9-Kernels erhalten bleibt.
Inline-Hooking / Funktionsprolog-Patching
Inline-Hooking ist eine Alternative zum Hooking über Zeigertabellen. Anstatt einen Zeiger in einer Tabelle zu verändern, patcht Inline-Hooking den Code der Zielfunktion selbst. Das Rootkit schreibt einen Sprungbefehl an den Anfang (Prolog) einer Kernelfunktion, der die Ausführung auf den eigenen Code des Rootkits umleitet. Diese Technik ähnelt dem Funktions-Hot-Patching oder der Funktionsweise von Benutzermodus-Hooks unter Windows (z. B. das Ändern der ersten Bytes einer Funktion, um zu einem Umweg zu springen).
Ein Rootkit könnte beispielsweise eine Kernelfunktion wie do_sys_open anvisieren (die Teil der Behandlung des Systemaufrufs zum Öffnen von Dateien ist). Indem die ersten paar Bytes von do_sys_open mit einer x86 JMP -Anweisung für bösartigen Code überschrieben werden, stellt das Rootkit sicher, dass bei jedem Aufruf do_sys_open stattdessen die Routine des Rootkits ausgeführt wird. Die bösartige Routine kann dann beliebige Aktionen ausführen (z. B. prüfen, ob der zu öffnende Dateiname auf einer Liste versteckter Dateien steht und den Zugriff verweigern) und optional die ursprüngliche do_sys_open aufrufen, um mit dem normalen Verhalten für nicht versteckte Dateien fortzufahren.
unsigned char *target = (unsigned char *)kallsyms_lookup_name("do_sys_open");
unsigned long hook = (unsigned long)&malicious_function;
int offset = (int)(hook - ((unsigned long)target + 5));
unsigned char jmp[5] = {0xE9};
memcpy(&jmp[1], &offset, 4);
// Memory protection omitted for brevity
memcpy(target, jmp, 5);
asmlinkage long malicious_function(
const char __user *filename,
int flags, umode_t mode) {
printk(KERN_INFO "do_sys_open hooked!\n");
return -EPERM;
}
Dieser Code überschreibt den Anfang von do_sys_open() mit einer JMP -Anweisung, die die Ausführung auf bösartigen Code umleitet. Das Open-Source-Rootkit Reptile nutzt in großem Umfang Inline-Funktionspatching über ein eigens entwickeltes Framework namens KHOOK (auf das wir später noch eingehen werden).
Die Inline-Hooks von Reptile zielen auf Funktionen wie sys_kill und andere ab und ermöglichen so Hintertürbefehle (z. B. löst das Senden eines bestimmten Signals an einen Prozess aus, dass das Rootkit seine Berechtigungen erhöht oder den Prozess versteckt). Ein weiteres Beispiel ist Suterusu, das für einige seiner Hooks ebenfalls Inline-Patching angewendet hat.
Inline-Hooking ist anfällig und risikoreich: Das Überschreiben des Prologs einer Funktion reagiert empfindlich auf Unterschiede in der Kernelversion und im Compiler (daher benötigen Hooks oft Patches pro Build oder Disassemblierung zur Laufzeit), es kann leicht zum Systemabsturz führen, wenn Anweisungen oder die gleichzeitige Ausführung nicht korrekt behandelt werden, und es erfordert das Umgehen moderner Speicherschutzmechanismen (W^X, CR0 WP, Modulsignierung/Sperre) oder das Ausnutzen von Sicherheitslücken, um den Kerneltext beschreibbar zu machen.
Virtual Filesystem Hooking
Die Virtual Filesystem (VFS)-Schicht in Linux bietet eine Abstraktion für Dateivorgänge. Wenn Sie beispielsweise ein Verzeichnis lesen (wie ls /proc), ruft der Kernel schließlich eine Funktion auf, um die Verzeichniseinträge zu durchlaufen. Dateisysteme definieren ihre eigenen Dateioperationen mit Funktionszeigern für Aktionen wie iterate_shared (zum Auflisten des Verzeichnisinhalts) oder Lesen/Schreiben für Datei-E/A. Beim VFS-Hooking werden diese Funktionszeiger durch vom Rootkit bereitgestellte Funktionen ersetzt, um die Art und Weise zu manipulieren, wie das Dateisystem Daten darstellt.
Im Wesentlichen kann ein Rootkit sich in das VFS einklinken, um Dateien oder Verzeichnisse zu verbergen, indem es sie aus den Verzeichnisauflistungen herausfiltert. Ein gängiger Trick: Man hängt die Funktion ab, die die Verzeichniseinträge durchläuft, und lässt sie alle Dateinamen überspringen, die einem bestimmten Muster entsprechen. Die file_operations -Struktur für Verzeichnisse (insbesondere in /proc oder /sys) ist ein häufiges Ziel, da das Verstecken bösartiger Prozesse oft das Verstecken von Einträgen unter /proc/<pid> beinhaltet.
Betrachten Sie dieses Beispiel eines Hooks für eine Verzeichnisauflistungsfunktion:
static iterate_dir_t original_iterate;
static int malicious_filldir(
struct dir_context *ctx,
const char *name, int namelen,
loff_t offset, u64 ino,
unsigned int d_type)
{
if (!strcmp(name, "hidden_file"))
return 0; // Skip hidden_file
return ctx->actor(ctx, name, namelen, offset, ino, d_type);
}
static int malicious_iterate(struct file *file, struct dir_context *ctx)
{
struct dir_context new_ctx = *ctx;
new_ctx.actor = malicious_filldir;
return original_iterate(file, &new_ctx);
}
// Hook installation
file->f_op->iterate = malicious_iterate;
Diese Ersatzfunktion filtert versteckte Dateien bei Verzeichnisauflistungen heraus. Durch das Einhängen auf VFS-Ebene muss das Rootkit weder Systemaufruftabellen noch Low-Level-Assembler manipulieren; es nutzt einfach die Dateisystemschnittstelle aus. Adore-NG, ein einst populäres Linux-Rootkit, nutzte VFS-Hooking, um Dateien und Prozesse zu verbergen. Es hat die Funktionszeiger für die Verzeichnisiteration so angepasst, dass Einträge für bestimmte Prozess-IDs und Dateinamen verborgen werden. Viele andere Kernel-Rootkits verfügen über ähnlichen Code, um sich selbst oder ihre Artefakte über VFS-Hooks zu verbergen.
VFS-Hooking wird zwar immer noch häufig verwendet, hat aber aufgrund von Änderungen der Kernelstruktur-Offsets zwischen den Versionen Einschränkungen, die dazu führen können, dass die Hooks nicht mehr funktionieren.
Ftrace-basiertes Hooking
Moderne Linux-Kernel beinhalten ein leistungsstarkes Tracing-Framework namens ftrace (Function Tracer). Ftrace ist für Debugging und Leistungsanalyse gedacht und ermöglicht es, Hooks (Callbacks) an nahezu jeden Ein- oder Ausgang einer Kernelfunktion anzuhängen, ohne den Kernelcode direkt zu verändern. Es funktioniert, indem es den Kernel-Code zur Laufzeit dynamisch und kontrolliert modifiziert (oft durch das Einbinden eines leichtgewichtigen Trampolins, das den Tracing-Handler aufruft). Wichtig ist, dass ftrace eine API für Kernelmodule bereitstellt, um Trace-Handler zu registrieren, sofern bestimmte Bedingungen erfüllt sind (z. B. dass der Kernel mit ftrace-Unterstützung kompiliert wurde und die debugfs-Schnittstelle verfügbar ist).
Rootkits haben begonnen, ftrace zu missbrauchen, um Hooks auf eine weniger offensichtliche Weise zu implementieren. Anstatt manuell eine JMP in eine Funktion einzufügen, kann ein Rootkit die ftrace-Maschinerie des Kernels bitten, dies in seinem Namen zu tun; im Wesentlichen wird der Hook dadurch „legitimiert“. Das bedeutet, dass das Rootkit weder die Adresse der Funktion finden noch Seitenschutzmechanismen ändern muss; es registriert einfach einen Callback für den Funktionsnamen, den es abfangen möchte, und der Kernel installiert den Hook.
Hier ein vereinfachtes Beispiel für die Verwendung von ftrace zum Abfangen des Systemaufrufhandlers mkdir :
static int __init hook_init(void) {
target_addr = kallsyms_lookup_name(SYSCALL_NAME("sys_mkdir"));
if (!target_addr) return -ENOENT;
real_mkdir = (void *)target_addr;
ops.func = ftrace_thunk;
ops.flags = FTRACE_OPS_FL_SAVE_REGS
| FTRACE_OPS_FL_RECURSION_SAFE
| FTRACE_OPS_FL_IPMODIFY;
if (ftrace_set_filter_ip(&ops, target_addr, 0, 0)) return -EINVAL;
return register_ftrace_function(&ops);
}
Dieser Hook fängt die Funktion sys_mkdir ab und leitet sie über einen bösartigen Handler um. Neuere Rootkits wie KoviD, Singularity und Umbra nutzen ftrace-basierte Hooks. Diese Rootkits registrieren ftrace-Callbacks für verschiedene Kernelfunktionen (einschließlich Systemaufrufe), um diese entweder zu überwachen oder zu manipulieren.
Der Hauptvorteil von ftrace-Hooking besteht darin, dass es keine offensichtlichen Spuren in globalen Tabellen oder gepatchtem Code hinterlässt. Das Hooking erfolgt über legitime Kernel-Schnittstellen. Für ein ungeübtes Auge sieht alles normal aus; sys_call_table ist intakt, Funktionsprologe werden nicht manuell vom Rootkit überschrieben (sie werden vom ftrace-Mechanismus überschrieben, aber das ist ein übliches und zulässiges Vorkommnis in einem Kernel mit aktiviertem Tracing). Darüber hinaus können ftrace-Hooks oft spontan aktiviert/deaktiviert werden und sind von Natur aus weniger aufdringlich als manuelle Patches.
Obwohl ftrace-Hooking leistungsstark ist, ist es durch Umgebungs- und Berechtigungsgrenzen eingeschränkt (wenn es von außerhalb des Kernels verwendet wird). Es erfordert Zugriff auf die Tracing-Schnittstelle (debugfs) und CAP_SYS_ADMIN -Berechtigungen, die auf gehärteten oder containerisierten Systemen möglicherweise nicht verfügbar sind, da selbst die UID 0 durch Namespaces, LSMs oder Secure-Boot-Sperrrichtlinien eingeschränkt ist. Debugfs kann aus Sicherheitsgründen im Produktionsbetrieb auch ausgehängt oder schreibgeschützt sein. Während ein Root-Benutzer mit vollen Berechtigungen typischerweise ftrace verwenden kann, deaktivieren oder beschränken moderne Schutzmechanismen diese Funktionen häufig, was die Praktikabilität von ftrace-basierten Hooks in hochgradig gehärteten Umgebungen verringert.
Kprobes Hooking
Kprobes ist eine weitere Kernel-Funktion, die für Debugging und Instrumentierung gedacht ist und von Angreifern für Rootkit-Hooking umfunktioniert wurde. Kprobes ermöglichen es, zur Laufzeit dynamisch in nahezu jede Kernel-Routine einzubrechen, indem ein Probe-Handler registriert wird. Wenn die angegebene Anweisung ausgeführt werden soll, speichert die kprobe-Infrastruktur den Zustand und übergibt die Kontrolle an den benutzerdefinierten Handler. Nach der Ausführung des Handlers (Sie können sogar Register oder den Befehlszeiger ändern) setzt der Kernel die normale Ausführung des ursprünglichen Codes fort. Vereinfacht ausgedrückt ermöglichen kprobes das Anhängen einer benutzerdefinierten Callback-Funktion an eine beliebige Stelle im Kernelcode (Funktionseintritt, spezifische Anweisung usw.), ähnlich wie ein Haltepunkt mit einem Handler.
Die Verwendung von kprobes für bösartige Hooking-Zwecke beinhaltet in der Regel das Abfangen einer Funktion, um entweder zu verhindern, dass sie etwas tut, oder um Informationen zu erlangen. Ein häufiger Anwendungsfall in modernen Rootkits: Da viele wichtige Symbole (wie sys_call_table oder kallsyms_lookup_name) nicht mehr exportiert werden, kann ein Rootkit einen kprobe auf einer Funktion einsetzen, die Zugriff auf dieses Symbol hat, und es stehlen. Die kprobe-Struktur und -Registrierung sind unten dargestellt.
// Declare a kprobe targeting the symbol "kallsyms_lookup_name"
static struct kprobe kp = {
.symbol_name = "kallsyms_lookup_name"
};
// Function pointer type matching kallsyms_lookup_name
typedef unsigned long
(*kallsyms_lookup_name_t)(const char *name);
// Global pointer to the resolved kallsyms_lookup_name
kallsyms_lookup_name_t kallsyms_lookup_name;
// Register the kprobe; kernel resolves kp.addr
// to the address of the symbol
register_kprobe(&kp);
// Assign resolved address to our function pointer
kallsyms_lookup_name =
(kallsyms_lookup_name_t) kp.addr;
// Unregister the kprobe (only needed it once)
unregister_kprobe(&kp);
Diese Sonde dient dazu, den Symbolnamen für kallsyms_lookup_name zu ermitteln, typischerweise ein Vorläufer für das Hooking der Systemaufruftabelle. Obwohl diese Technik in den ursprünglichen Commits nicht vorhanden war, wurde sie in einem kürzlich erschienenen Update von Diamorphine verwendet. Es platziert eine kprobe, um den Zeiger von kallsyms_lookup_name selbst zu erhalten (oder verwendet eine kprobe auf einer bekannten Funktion, um indirekt das zu erhalten, was es benötigt). In ähnlicher Weise verwenden andere Rootkits eine temporäre kprobe, um Symbole zu finden, und melden diese nach Abschluss der Suche wieder ab, um dann auf anderem Wege Hooks durchzuführen. Kprobes können auch verwendet werden, um das Verhalten direkt zu beeinflussen (nicht nur um Adressen zu finden). Oder ein jprobe (ein spezialisierter kprobe) kann eine Funktion vollständig umleiten. Allerdings ist es schwierig und unüblich, mit kprobes die Funktionalität vollständig zu ersetzen, da es einfacher ist, entweder zu patchen oder ftrace zu verwenden, wenn man eine Funktion konsequent übernehmen möchte. Kprobes werden häufig für intermittierende oder zusätzliche Ableitungen verwendet.
Kprobes sind zwar nützlich, aber begrenzt: Sie verursachen zusätzlichen Laufzeitaufwand und können Systeme destabilisieren, wenn sie auf stark frequentierten oder eingeschränkten Low-Level-Funktionen platziert werden (rekursive Probes werden unterdrückt), daher müssen Angreifer die Probe-Punkte sorgfältig auswählen; sie sind außerdem überprüfbar und können Kernel-Warnungen auslösen oder von der Systemüberwachung protokolliert werden, und aktive Probes sind unter /sys/kernel/debug/kprobes/list sichtbar (unerwartete Einträge sind daher verdächtig); einige Kernel werden möglicherweise ohne kprobe/debug-Unterstützung kompiliert.
Kernel Hook Framework
Wie bereits erwähnt, erstellen Angreifer mit Hilfe des Reptile-Rootkits manchmal übergeordnete Frameworks, um ihre Hooks zu verwalten. Kernel Hook (KHOOK) ist ein solches Framework (entwickelt vom Autor von Reptile), das die schmutzige Arbeit des Inline-Patchings abstrahiert und eine übersichtlichere Schnittstelle für Rootkit-Entwickler bietet. Im Wesentlichen handelt es sich bei KHOOK um eine Bibliothek, mit der Sie eine zu hookende Funktion und Ihre Ersatzfunktion angeben können. Sie kümmert sich um die Modifizierung des Kernel-Codes und bietet gleichzeitig eine Zwischenlösung, um die ursprüngliche Funktion sicher aufzurufen. Zur Veranschaulichung hier ein Beispiel, wie man ein KHOOK-ähnliches Makro (basierend auf der Verwendung in Reptile) verwenden könnte, um den Kill-Systemaufruf abzufangen:
// Creates a replacement for sys_kill:
// long sys_kill(long pid, long sig)
KHOOK_EXT(long, sys_kill, long, long);
static long khook_sys_kill(long pid, long sig) {
// Signal 0 is used to check if a process
// exists (without sending a signal)
if (sig == 0) {
// If the target is invisible (hidden by
// a rootkit), pretend it doesn't exist
if (is_proc_invisible(pid)) {
return -ESRCH; // No such process
}
}
// Otherwise, forward the call to the original sys_kill syscall
return KHOOK_ORIGIN(sys_kill, pid, sig);
}
KHOOK arbeitet mit Inline-Funktionspatching, indem Funktionsprologe durch einen Sprung zu vom Angreifer kontrollierten Handlern überschrieben werden. Das obige Beispiel veranschaulicht, wie sys_kill() an einen bösartigen Handler umgeleitet wird, wenn das Kill-Signal 0 ist.
Obwohl KHOOK das Inline-Patching vereinfacht, erbt es dennoch alle seine Nachteile: Es verändert den Kernel-Text, um Sprung-Stubs einzufügen, sodass Schutzmechanismen wie Kernel-Lockdown, Secure Boot oder W^X es blockieren können. Sie sind außerdem architektur- und versionsabhängig (häufig auf x86 beschränkt und funktionieren nicht mit Kernel 5.x+), was sie anfällig für verschiedene Builds macht.
Hooking-Techniken im Benutzermodus
Userspace-Hooking ist eine Technik, die auf die libc-Schicht oder andere gemeinsam genutzte Bibliotheken abzielt, auf die über den dynamischen Linker zugegriffen wird, um häufig verwendete API-Aufrufe von Benutzerwerkzeugen abzufangen. Beispiele für solche Aufrufe sind readdir, getdents, open, fopen, fgets, und connect. Durch das Einfügen von Ersatzfunktionen kann ein Angreifer gewöhnliche Benutzerwerkzeuge wie ps, ls, lsof und netstat manipulieren, um veränderte oder "bereinigte" Ansichten zurückzugeben. Dies dient dazu, Prozesse, Dateien oder Sockets zu verbergen oder Spuren von Schadcode zu verdecken.
Die gängigen Methoden zur Umsetzung dieser Methode spiegeln wider, wie der dynamische Linker Symbole auflöst, oder beinhalten die Modifizierung des Prozessspeichers. Zu diesen Methoden gehören die Verwendung der Umgebungsvariablen LD_PRELOAD oder LD_AUDIT , um ein frühes Laden einer bösartigen Shared-Object-Datei (.so) zu erzwingen, die Änderung von ELF DT_*-Einträgen oder Bibliothekssuchpfaden, um einer feindlichen Bibliothek Priorität einzuräumen, oder die Durchführung von GOT/PLT-Überschreibungen zur Laufzeit innerhalb eines Prozesses. Das Überschreiben der GOT/PLT beinhaltet typischerweise das Ändern der Speicherschutzeinstellungen (mprotect), das Schreiben des neuen Codes (write) und anschließend das Wiederherstellen der ursprünglichen Einstellungen (restore) nach der Injektion.
Eine eingebundene Funktion ruft üblicherweise das eigentliche libc-Symbol mit dlsym(RTLD_NEXT, ...) für ihren normalen Betrieb auf. Anschließend werden die Ergebnisse nur für die Ziele gefiltert oder verändert, die ausgeblendet werden sollen. Ein einfaches Beispiel für einen LD_PRELOAD -Filter für die readdir() -Funktion ist unten dargestellt.
#define _GNU_SOURCE // GNU extensions (RTLD_NEXT)
#include <dlfcn.h> // dlsym(), RTLD_NEXT
#include <dirent.h> // DIR, struct dirent, readdir()
#include <string.h> // strstr()
// Pointer to the original readdir()
static struct dirent *(*real_readdir)(DIR *d);
struct dirent *readdir(DIR *d) {
if (!real_readdir) // resolve original once
real_readdir =
dlsym(RTLD_NEXT, "readdir");
struct dirent *ent;
// Fetch next dir entry from real readdir
while ((ent = real_readdir(d)) != NULL) {
// If name contains the secret marker,
// skip this entry (hide it)
if (strstr(ent->d_name, ".secret"))
continue;
return ent; // return visible entry
}
return NULL; // no more entries
}
Dieses Beispiel ersetzt readdir() im Prozess, indem es eine Bibliothek bereitstellt, die vor dem eigentlichen libc aufgelöst wird, wodurch effektiv Dateinamen ausgeblendet werden, die einem Filter entsprechen. Historische Tools zum Verstecken des Benutzermodus und leichtgewichtige „Rootkits“ haben LD_PRELOAD oder GOT/PLT-Patching verwendet, um Prozesse, Dateien und Sockets zu verbergen. Angreifer injizieren außerdem gemeinsam genutzte Objekte in bestimmte Dienste, um gezielte Tarnung zu erreichen, ohne Kernelmodule zu benötigen.
Die Benutzermodus-Interposition betrifft nur Prozesse, die die schädliche Bibliothek laden (oder in die sie eingeschleust wird). Für die systemweite Persistenz ist es anfällig (Service-/Unit-Dateien, bereinigte Umgebungen, setuid/statische Binärdateien erschweren dies). Die Erkennung ist im Vergleich zu Kernel-Hooks unkompliziert: Es wird auf verdächtige LD_PRELOAD/LD_AUDIT -Einträge, unerwartet zugeordnete Shared Objects in /proc/<pid>/maps, Diskrepanzen zwischen On-Disk-Bibliotheken und In-Memory-Importen oder veränderte GOT-Einträge geprüft. Integritätswerkzeuge, Service-Supervisoren (systemd) und einfache Prozessspeicherinspektionen decken diese Technik in der Regel auf.
Hooking-Techniken mit eBPF
Ein neueres Rootkit-Implementierungsmodell beinhaltet den Missbrauch von eBPF (extended Berkeley Packet Filter). eBPF ist ein Subsystem in Linux, das es privilegierten Benutzern ermöglicht, Bytecode-Programme in den Kernel zu laden. Obwohl sie oft als „abgeschottete VM“ bezeichnet wird, beruht ihre Sicherheit tatsächlich auf einem statischen Verifizierer, der sicherstellt, dass der Bytecode sicher ist (keine Endlosschleifen, kein illegaler Speicherzugriff), bevor er per JIT-Kompilierung in nativen Maschinencode für eine nahezu latenzfreie Ausführung übersetzt wird.
Anstatt einen LKM einzufügen, um das Kernelverhalten zu modifizieren, kann ein Angreifer ein oder mehrere eBPF-Programme laden, die sich an sensible Kernelereignisse anhängen. Man kann beispielsweise ein eBPF-Programm schreiben, das sich an den Systemaufruf-Eintrag für execve anhängt (über einen kprobe oder tracepoint), wodurch es die Prozessausführung überwachen oder manipulieren kann. In ähnlicher Weise kann eBPF auf der LSM-Ebene eingreifen (z. B. bei Benachrichtigungen zur Programmausführung), um bestimmte Aktionen zu verhindern oder zu verbergen. Ein Beispiel wird unten angezeigt.
// Attach this eBPF program to the tracepoint for sys_enter_execve
SEC("tp/syscalls/sys_enter_execve")
int tp_sys_enter_execve(struct sys_execve_enter_ctx *ctx) {
// Get the current process's PID and TID as a 64-bit value
// Upper 32 bits = PID, Lower 32 bits = TID
__u64 pid_tgid = bpf_get_current_pid_tgid();
// Delegate handling logic to a helper function
return handle_tp_sys_enter_execve(ctx, pid_tgid);
}
Zwei prominente Beispiele aus der Öffentlichkeit sind TripleCross und Boopkit. TripleCross demonstrierte ein Rootkit, das eBPF nutzte, um Systemaufrufe wie execve zum Zweck der Persistenz und Tarnung abzufangen. Boopkit nutzte eBPF als verdeckten Kommunikationskanal und Hintertür, indem es eBPF-Programme anhängte, die Socket-Puffer manipulieren konnten (wodurch eine entfernte Partei über speziell präparierte Pakete mit dem Rootkit kommunizieren konnte). Hierbei handelt es sich um Machbarkeitsstudien, die jedoch die Durchführbarkeit von eBPF bei der Rootkit-Entwicklung bewiesen haben.
Die Hauptvorteile bestehen darin, dass eBPF-Hooking das Laden eines LKM nicht erfordert und mit modernen Kernel-Schutzmechanismen kompatibel ist. Für eBPF-fähige Kernel ist dies eine leistungsstarke Technik. Doch obwohl sie mächtig sind, unterliegen sie auch Beschränkungen. Sie benötigen erhöhte Berechtigungen zum Laden, sind durch die Sicherheitsprüfungen des Verifizierers eingeschränkt, sind über Neustarts hinweg flüchtig (was eine separate Persistenz erfordert) und sind zunehmend durch Audit-/Forensik-Tools auffindbar. Der Einsatz von eBPF wird insbesondere auf Systemen sichtbar sein, die normalerweise keine eBPF-Tools verwenden.
Ausweichtechniken mit io_uring
Obwohl io_uring nicht zum Hooking verwendet wird, verdient es eine lobende Erwähnung als eine der jüngsten Ergänzungen der von Rootkits verwendeten EDR-Umgehungstechniken. io_uring ist eine asynchrone, ringpufferbasierte I/O-API, die es Prozessen ermöglicht, Stapel von I/O-Anforderungen (SQEs) zu übermitteln und Abschlüsse (CQEs) mit minimalem Systemaufruf-Overhead zu erhalten. Es handelt sich nicht um ein Hooking-Framework, aber sein Design verändert die Syscall-/Sichtbarkeitsoberfläche und legt leistungsstarke Kernel-Primitive (registrierte Puffer, feste Dateien, zugeordnete Ringe) offen, die Angreifer für heimliche E/A, Syscall-vermeidende Arbeitsabläufe oder, in Kombination mit einer Schwachstelle, als Exploit-Primitiv missbrauchen können, das zur Installation von Hooks auf einer niedrigeren Ebene führt.
Angriffsmuster lassen sich in zwei Klassen einteilen: (1) Ausweichung/Leistungsmissbrauch: Ein bösartiger Prozess nutzt io_uring , um viele Lese-/Schreib-/Metadatenoperationen in großen Batches durchzuführen, sodass herkömmliche Detektoren pro Systemaufruf weniger Ereignisse oder atypische Muster erkennen; und (2) Ausnutzung von Sicherheitslücken: Fehler in io_uring -Oberflächen (Ringzuordnungen, registrierte Ressourcen) waren in der Vergangenheit der Vektor für die Rechteausweitung, woraufhin ein Angreifer Kernel-Hooks auf herkömmliche Weise installieren kann. io_uring umgeht außerdem einige libc-Wrapper, wenn der Code Operationen direkt übermittelt, sodass Benutzer-Hooking, das libc-Aufrufe abfängt, umgangen werden kann. Ein einfacher Submit/Reap-Ablauf ist unten dargestellt:
// Minimal io_uring usage (error handling omitted)
// io_uring context (SQ/CQ rings shared with kernel)
struct io_uring ring;
// Initialize ring with space for 16 SQEs
io_uring_queue_init(16, &ring, 0);
// Grab a free submission entry (or NULL if full)
struct io_uring_sqe *sqe =
io_uring_get_sqe(&ring);
// Prepare SQE as a read(fd, buf, len, offset=0)
io_uring_prep_read(sqe, fd, buf, len, 0);
// Submit pending SQEs to the kernel (non-blocking)
io_uring_submit(&ring);
struct io_uring_cqe *cqe;
// Block until a completion is available
io_uring_wait_cqe(&ring, &cqe);
// Mark the completion as handled (free slot)
io_uring_cqe_seen(&ring, cqe);
Das obige Beispiel zeigt eine Übermittlungswarteschlange, die viele Dateioperationen mit einem oder wenigen io_uring_enter -Systemaufrufen an den Kernel weiterleitet und so die Systemaufruftelemetrie pro Operation reduziert.
Angreifer, die an einer unauffälligen Datenerfassung oder einem Datenabfluss mit hohem Durchsatz interessiert sind, können auf io_uring umschalten, um das Systemaufrufrauschen zu reduzieren. io_uring installiert nicht von Natur aus globale Hooks oder verändert das Verhalten anderer Prozesse; es ist prozesslokal, es sei denn, es wird mit einer Rechteausweitung kombiniert. Die Erkennung ist möglich, indem die io_uring Systemaufrufe (io_uring_enter, io_uring_register) instrumentiert und auf anomale Muster geachtet wird: ungewöhnlich große Batches, viele registrierte Dateien/Puffer oder Prozesse, die umfangreiche Batch-Metadatenoperationen durchführen. Auch Unterschiede in der Kernelversion spielen eine Rolle: io_uring Funktionen entwickeln sich schnell weiter, daher können Angreifertechniken versionsabhängig sein. Da io_uring einen laufenden bösartigen Prozess voraussetzt, können die Verteidiger diesen oft unterbrechen und seine Ringe, registrierten Dateien und Speicherabbildungen untersuchen, um Missbrauch aufzudecken.
Fazit
Hooking-Techniken in Linux haben sich weit von der einfachen Überschreibung eines Zeigers in einer Tabelle entfernt. Wir beobachten nun, dass Angreifer legitime Kernel-Instrumentierungsframeworks (ftrace, kprobes, eBPF) ausnutzen, um schwerer zu erkennende Hooks einzuschleusen. Jede Methode, von IDT- und Syscall-Tabellen-Patches bis hin zu Inline-Hooks und dynamischen Sonden, hat ihre eigenen Vor- und Nachteile in Bezug auf Tarnung und Stabilität. Die Verteidiger müssen sich all dieser möglichen Angriffsvektoren bewusst sein. In der Praxis kombinieren moderne Rootkits häufig mehrere Hooking-Techniken, um ihre Ziele zu erreichen. Beispielsweise verwendet PUMAKIT einen direkten syscall-Tabellen-Hook und ftrace-Hooks, und Diamorphine verwendet syscall-Hooks plus einen kprobe, um das Symbol-Hidden zu umgehen. Dieser mehrschichtige Ansatz bedeutet, dass die Erkennungswerkzeuge viele Aspekte des Systems überprüfen müssen: IDT-Einträge, Syscall-Tabellen, modellspezifische Register (für Sysenter-Hooks), Integrität der Funktionsprologe, Inhalt kritischer Funktionszeiger in Strukturen (VFS usw.), aktive ftrace-Operationen, registrierte kprobes und geladene eBPF-Programme.
Im zweiten Teil dieser Reihe gehen wir von der Theorie zur Praxis über. Mit dem hier vermittelten Verständnis der Rootkit-Taxonomie und der Hooking-Techniken werden wir uns auf die Entwicklung von Erkennungssystemen konzentrieren und praktische Erkennungsstrategien entwickeln und anwenden, um diese Bedrohungen in realen Linux-Umgebungen zu identifizieren.
