Remco SprootenRuben Groenewoud

PUMAKIT zum Entkrallen

PUMAKIT ist ein hochentwickeltes Loadable Kernel Module (LKM) Rootkit, das fortschrittliche Stealth-Mechanismen zur Verbergung seiner Präsenz und Aufrechterhaltung der Kommunikation mit Command-and-Control-Servern einsetzt.

30 Minuten LesezeitMalware-Analyse
Entfernung der Krallen von PUMAKIT

PUMAKIT auf einen Blick

PUMAKIT ist eine ausgeklügelte Malware, die ursprünglich bei der routinemäßigen Bedrohungssuche auf VirusTotal entdeckt und nach den von Entwicklern eingebetteten Zeichenfolgen benannt wurde, die in der Binärdatei gefunden wurden. Die mehrstufige Architektur besteht aus einem Dropper (cron), zwei speicherresidenten ausführbaren Dateien (/memfd:tgt und /memfd:wpn), einem LKM-Rootkit-Modul und einem SO-Userland-Rootkit (Shared Object).

Die Rootkit-Komponente, die von den Malware-Autoren als "PUMA" bezeichnet wird, verwendet einen internen Linux-Funktionstracer (ftrace), um verschiedene Systemaufrufe und mehrere Kernel-Funktionen 18 verknüpfen und so das Verhalten des Kernsystems zu manipulieren. Für die Interaktion mit PUMA werden einzigartige Methoden verwendet, einschließlich der Verwendung des Systemcalls rmdir() für die Rechteausweitung und spezieller Befehle zum Extrahieren von Konfigurations- und Laufzeitinformationen. Durch die gestaffelte Bereitstellung stellt das LKM-Rootkit sicher, dass es nur aktiviert wird, wenn bestimmte Bedingungen, wie z. B. Secure Boot-Prüfungen oder die Verfügbarkeit von Kernelsymbolen, erfüllt sind. Diese Bedingungen werden durch Scannen des Linux-Kernels überprüft, und alle erforderlichen Dateien werden als ELF-Binärdateien in den Dropper eingebettet.

Zu den wichtigsten Funktionen des Kernel-Moduls gehören die Ausweitung von Berechtigungen, das Verstecken von Dateien und Verzeichnissen, das Verbergen vor Systemwerkzeugen, Anti-Debugging-Maßnahmen und das Herstellen der Kommunikation mit Command-and-Control-Servern (C2).

Wichtigste Erkenntnisse

  • Mehrstufige Architektur: Die Malware kombiniert einen Dropper, zwei speicherresidente ausführbare Dateien, ein LKM-Rootkit und ein SO-Userland-Rootkit und wird nur unter bestimmten Bedingungen aktiviert.
  • Erweiterte Stealth-Mechanismen: Hooks 18 Systemaufrufen und verschiedenen Kernel-Funktionen, die ftrace() verwenden, um Dateien, Verzeichnisse und das Rootkit selbst zu verbergen und gleichzeitig Debugging-Versuchen auszuweichen.
  • Unique Privilege Escalation: Verwendet unkonventionelle Hooking-Methoden wie den rmdir() Syscall zur Eskalation von Berechtigungen und zur Interaktion mit dem Rootkit.
  • Kritische Funktionen: Umfasst Privilegienausweitung, C2-Kommunikation, Anti-Debugging und Systemmanipulation, um Persistenz und Kontrolle zu gewährleisten.

PUMAKIT Entdeckung

Bei der routinemäßigen Bedrohungssuche auf VirusTotal stießen wir auf eine faszinierende Binärdatei namens cron. Die Binärdatei wurde erstmals am 4. September 2024hochgeladen, wobei 0 Entdeckungen aufkommen, die den Verdacht auf eine mögliche Tarnung aufkommen ließen. Bei weiterer Untersuchung entdeckten wir ein weiteres verwandtes Artefakt, /memfd:wpn (deleted)71cc6a6547b5afda1844792ace7d5437d7e8d6db1ba995e1b2fb760699693f24, das am selben Tag hochgeladen wurde, ebenfalls mit 0 Erkennungen.

Was unsere Aufmerksamkeit erregte, waren die unterschiedlichen Zeichenketten, die in diese Binärdateien eingebettet waren, was auf eine mögliche Manipulation des vmlinuz Kernel-Pakets in /boot/hindeutete. Dies führte zu einer tieferen Analyse der Proben, die zu interessanten Erkenntnissen über ihr Verhalten und ihren Zweck führte.

PUMAKIT Code-Analyse

PUMAKIT, benannt nach seinem eingebetteten LKM-Rootkit-Modul (von den Malware-Autoren "PUMA" genannt), und Kitsune, das SO Userland-Rootkit, verwenden eine mehrstufige Architektur, die mit einem Dropper beginnt, der eine Ausführungskette einleitet. Der Prozess beginnt mit der Binärdatei cron , die zwei speicherresidente ausführbare Dateien erstellt: /memfd:tgt (deleted) und /memfd:wpn (deleted). Während /memfd:tgt als harmlose Cron-Binärdatei dient, fungiert /memfd:wpn als Rootkit-Loader. Der Loader ist für die Auswertung der Systembedingungen, die Ausführung eines temporären Skripts (/tmp/script.sh) und schließlich für die Bereitstellung des LKM-Rootkits verantwortlich. Das LKM-Rootkit enthält eine eingebettete SO-Datei - Kitsune - um mit dem Rootkit aus dem Userspace zu interagieren. Diese Ausführungskette wird im Folgenden dargestellt.

Dieses strukturierte Design ermöglicht es dem PUMAKIT, seine Nutzlast nur dann auszuführen, wenn bestimmte Kriterien erfüllt sind, was eine Tarnung gewährleistet und die Wahrscheinlichkeit einer Entdeckung verringert. Jede Phase des Prozesses ist sorgfältig darauf ausgelegt, ihr Vorhandensein zu verbergen, indem speicherresidente Dateien und präzise Überprüfungen der Zielumgebung genutzt werden.

In diesem Abschnitt werden wir tiefer in die Code-Analyse für die verschiedenen Phasen eintauchen und ihre Komponenten und ihre Rolle bei der Ermöglichung dieser ausgeklügelten mehrstufigen Malware untersuchen.

Phase 1: Cron-Übersicht

Die Binärdatei cron fungiert als Dropper. Die folgende Funktion dient als Hauptlogikhandler in einem PUMAKIT-Malware-Sample. Seine Hauptziele sind:

  1. Überprüfen Sie Befehlszeilenargumente für ein bestimmtes Schlüsselwort ("Huinder").
  2. Wenn sie nicht gefunden werden, betten Sie versteckte Nutzlasten vollständig aus dem Speicher ein und führen Sie sie aus, ohne sie im Dateisystem abzulegen.
  3. Falls gefunden, behandeln Sie bestimmte "extraction"-Argumente, um die eingebetteten Komponenten auf die Festplatte zu sichern und dann ordnungsgemäß zu beenden.

Kurz gesagt, die Malware versucht, unauffällig zu bleiben. Wenn es normal ausgeführt wird (ohne ein bestimmtes Argument), führt es versteckte ELF-Binärdateien aus, ohne Spuren auf der Festplatte zu hinterlassen, möglicherweise als legitimer Prozess getarnt (wie cron).

Wenn die Zeichenfolge Huinder nicht unter den Argumenten gefunden wird, wird der Code in if (!argv_) ausgeführt:

writeToMemfd(...): Dies ist ein Kennzeichen der dateilosen Ausführung. memfd_create ermöglicht es, dass die Binärdatei vollständig im Speicher vorhanden ist. Die Malware schreibt ihre eingebetteten Nutzlasten (tgtElfp und wpnElfp) in anonyme Dateideskriptoren, anstatt sie auf der Festplatte abzulegen.

fork() und execveat(): Die Malware spaltet sich in einen untergeordneten und einen übergeordneten Prozess auf. Das untergeordnete Element leitet seine Standardausgabe und seinen Fehler an /dev/null um, um das Hinterlassen von Protokollen zu vermeiden, und führt dann die Payload "weapon" (wpnElfp) mit execveat()aus. Das übergeordnete Element wartet auf das untergeordnete Element und führt dann die "Ziel"-Payload aus (tgtElfp). Beide Payloads werden aus dem Speicher und nicht aus einer Datei auf der Festplatte ausgeführt, was die Erkennung und forensische Analyse erschwert.

Die Wahl des execveat() ist interessant – es handelt sich um einen neueren Systemaufruf, der es ermöglicht, ein Programm auszuführen, auf das ein Dateideskriptor verweist. Dies unterstützt die dateilose Ausführung dieser Malware.

Wir haben festgestellt, dass es sich bei der tgt Datei um eine legitime cron Binärdatei handelt. Es wird in den Speicher geladen und nach dem Ausführen des Rootkit-Loaders (wpn) ausgeführt.

Nach der Ausführung bleibt die Binärdatei auf dem Host aktiv.

> ps aux
root 2138 ./30b26707d5fb407ef39ebee37ded7edeea2890fb5ec1ebfa09a3b3edfc80db1f

Nachfolgend finden Sie eine Liste der Dateideskriptoren für diesen Prozess. Diese Dateideskriptoren zeigen die speicherresidenten Dateien an, die vom Dropper erstellt wurden.

root@debian11-rg:/tmp# ls -lah /proc/2138/fd
total 0
dr-x------ 2 root root  0 Dec  6 09:57 .
dr-xr-xr-x 9 root root  0 Dec  6 09:57 ..
lr-x------ 1 root root 64 Dec  6 09:57 0 -> /dev/null
l-wx------ 1 root root 64 Dec  6 09:57 1 -> /dev/null
l-wx------ 1 root root 64 Dec  6 09:57 2 -> /dev/null
lrwx------ 1 root root 64 Dec  6 09:57 3 -> '/memfd:tgt (deleted)'
lrwx------ 1 root root 64 Dec  6 09:57 4 -> '/memfd:wpn (deleted)'
lrwx------ 1 root root 64 Dec  6 09:57 5 -> /run/crond.pid
lrwx------ 1 root root 64 Dec  6 09:57 6 -> 'socket:[20433]'

Anhand der Referenzen können wir die Binärdateien sehen, die im Beispiel geladen werden. Wir können die Bytes einfach in eine neue Datei kopieren, um sie mit Hilfe des Offsets und der Größen weiter zu analysieren.

Bei der Extraktion finden wir die folgenden zwei neuen Dateien:

  • Wpn: cb070cc9223445113c3217f05ef85a930f626d3feaaea54d8585aaed3c2b3cfe
  • Tgt: 934955f0411538eebb24694982f546907f3c6df8534d6019b7ff165c4d104136

Wir haben jetzt die Dumps der beiden Speicherdateien.

Phase 2: Übersicht über speicherresidente ausführbare Dateien

Wenn man sich die ELF-Datei /memfd:tgt ansieht, wird klar, dass dies die standardmäßige Ubuntu Linux Cron-Binärdatei ist. Es scheint keine Änderungen an der Binärdatei zu geben.

Die Datei /memfd:wpn ist interessanter, da sie die Binärdatei ist, die für das Laden des LKM-Rootkits verantwortlich ist. Dieser Rootkit-Loader versucht, sich zu verstecken, indem er ihn als /usr/sbin/sshd ausführbare Datei nachahmt. Es wird geprüft, ob bestimmte Voraussetzungen erfüllt sind, z. B. ob Secure Boot aktiviert ist und die erforderlichen Symbole verfügbar sind, und wenn alle Bedingungen erfüllt sind, wird das Rootkit des Kernelmoduls geladen.

Wenn wir uns die Ausführung in Kibana ansehen, können wir sehen, dass das Programm überprüft, ob Secure Boot aktiviert ist, indem es dmesgabfragt. Wenn die richtigen Bedingungen erfüllt sind, wird ein Shell-Skript mit dem Namen script.sh im /tmp Verzeichnis abgelegt und ausgeführt.

Dieses Skript enthält Logik zum Überprüfen und Verarbeiten von Dateien basierend auf ihren Komprimierungsformaten.

Hier ist, was es tut:

  • Die Funktion c() Dateien mit dem Befehl file überprüft, um zu überprüfen, ob es sich um ELF-Binärdateien handelt. Ist dies nicht der Fall, gibt die Funktion einen Fehler zurück.
  • Die Funktion d() versucht, eine bestimmte Datei mit verschiedenen Dienstprogrammen wie gunzip, unxz, bunzip2und anderen zu dekomprimieren, basierend auf den Signaturen der unterstützten Komprimierungsformate. Es verwendet grep und tail , um bestimmte komprimierte Segmente zu lokalisieren und zu extrahieren.
  • Das Skript versucht, eine Datei ($i) zu suchen und in /tmp/vmlinuxzu verarbeiten.

Nach der Ausführung von /tmp/script.shwird die Datei /boot/vmlinuz-5.10.0-33-cloud-amd64 als Eingabe verwendet. Der Befehl tr wird verwendet, um die magischen Zahlen von gzip (\037\213\010) zu lokalisieren. Anschließend wird ein Teil der Datei ab dem Byte-Offset +10957311 mit tailextrahiert, mit gunzipdekomprimiert und als /tmp/vmlinuxgespeichert. Die resultierende Datei wird dann überprüft, um festzustellen, ob es sich um eine gültige ELF-Binärdatei handelt.

Diese Sequenz wird mehrmals wiederholt, bis alle Einträge innerhalb des Skripts an die Funktion d()übergeben wurden.

d '\037\213\010' xy gunzip
d '\3757zXZ\000' abcde unxz
d 'BZh' xy bunzip2
d '\135\0\0\0' xxx unlzma
d '\211\114\132' xy 'lzop -d'
d '\002!L\030' xxx 'lz4 -d'
d '(\265/\375' xxx unzstd

Dieser Vorgang ist unten dargestellt.

Nachdem Sie alle Elemente im Skript durchlaufen haben, werden die /tmp/vmlinux - und /tmp/script.sh Dateien gelöscht.

Der Hauptzweck des Skripts besteht darin, zu überprüfen, ob bestimmte Bedingungen erfüllt sind, und wenn dies der Fall ist, die Umgebung für die Bereitstellung des Rootkits mithilfe einer Kernelobjektdatei einzurichten.

Wie in der obigen Abbildung gezeigt, sucht der Loader in der Linux-Kernel-Datei nach __ksymtab und __kcrctab Symbolen und speichert die Offsets.

Mehrere Zeichenketten zeigen, dass die Rootkit-Entwickler ihr Rootkit innerhalb des Droppers als "PUMA" bezeichnen. Basierend auf den Bedingungen gibt das Programm Meldungen aus wie:

PUMA %s
[+] PUMA is compatible
[+] PUMA already loaded

Darüber hinaus enthält die Kernel-Objektdatei einen Abschnitt mit dem Namen .puma-config, der die Assoziation mit dem Rootkit verstärkt.

Stufe 3: LKM Rootkit Übersicht

In diesem Abschnitt werfen wir einen genaueren Blick auf das Kernel-Modul, um die zugrunde liegende Funktionalität zu verstehen. Insbesondere werden wir die Funktionen für die Symbolsuche, den Hooking-Mechanismus und die Schlüsselsystemaufrufe untersuchen, die es ändert, um seine Ziele zu erreichen.

LKM Rootkit Übersicht: Symbolsuche und Hooking-Mechanismus

Die Fähigkeit des LKM-Rootkits, das Systemverhalten zu manipulieren, beginnt mit der Verwendung der Systemaufruftabelle und der Abhängigkeit von kallsyms_lookup_name() für die Symbolauflösung. Im Gegensatz zu modernen Rootkits, die auf die Kernel-Versionen 5.7 und höher abzielen, verwendet das Rootkit keine kprobes, was darauf hinweist, dass es für ältere Kernel konzipiert ist.

Diese Wahl ist von Bedeutung, da kallsyms_lookup_name() vor der Kernel-Version 5.7 exportiert wurde und leicht von Modulen genutzt werden konnte, auch von solchen ohne ordnungsgemäße Lizenzierung.

Im Februar 2020 debattierten Kernel-Entwickler über das Unexportieren von kallsyms_lookup_name() , um den Missbrauch durch nicht autorisierte oder bösartige Module zu verhindern. Eine gängige Taktik bestand darin, eine gefälschte MODULE_LICENSE("GPL") Deklaration hinzuzufügen, um Lizenzprüfungen zu umgehen und diesen Modulen den Zugriff auf nicht exportierte Kernel-Funktionen zu ermöglichen. Das LKM-Rootkit zeigt dieses Verhalten, wie aus seinen Zeichenfolgen hervorgeht:

name=audit
license=GPL

Diese betrügerische Verwendung der GPL-Lizenz stellt sicher, dass das Rootkit kallsyms_lookup_name() aufrufen kann, um Funktionsadressen aufzulösen und Kernel-Interna zu manipulieren.

Zusätzlich zu seiner Symbolauflösungsstrategie verwendet das Kernel-Modul den ftrace() -Hooking-Mechanismus, um seine Hooks einzurichten. Durch die Nutzung von ftrace()fängt das Rootkit Systemaufrufe effektiv ab und ersetzt deren Handler durch benutzerdefinierte Hooks.

Ein Beweis dafür ist z.B. die Verwendung von unregister_ftrace_function und ftrace_set_filter_ip , wie im obigen Code-Schnipsel gezeigt.

LKM Rootkit Übersicht: Übersicht über hooked syscalls

Wir haben den Syscall-Hooking-Mechanismus des Rootkits analysiert, um das Ausmaß der Beeinträchtigung der Systemfunktionalität durch PUMA zu verstehen. In der folgenden Tabelle sind die Systemaufrufe, die vom Rootkit eingebunden werden, die entsprechenden eingebundenen Funktionen und ihre potenziellen Zwecke zusammengefasst.

Wenn wir uns die Funktion cleanup_module() ansehen, können wir sehen, wie der ftrace() Hooking-Mechanismus mit der Funktion unregister_ftrace_function() rückgängig gemacht wird. Dadurch wird sichergestellt, dass der Callback nicht mehr aufgerufen wird. Danach werden alle Systemaufrufe zurückgegeben, um auf den ursprünglichen Systemaufruf und nicht auf den angehängten Systemaufruf zu verweisen. Dies gibt uns einen sauberen Überblick über alle Systemaufrufe, die angehakt wurden.

In den folgenden Abschnitten werden wir uns einige der gehookten Systemaufrufe genauer ansehen.

LKM Rootkit Übersicht: rmdir_hook()

Die rmdir_hook() im Kernel-Modul spielt eine entscheidende Rolle für die Funktionalität des Rootkits und ermöglicht es ihm, Verzeichnisentfernungsvorgänge zur Verschleierung und Kontrolle zu manipulieren. Dieser Hook beschränkt sich nicht nur auf das bloße Abfangen rmdir() Systemaufrufen, sondern erweitert seine Funktionalität, um die Rechteausweitung zu erzwingen und Konfigurationsdetails abzurufen, die in bestimmten Verzeichnissen gespeichert sind.

Dieser Haken verfügt über mehrere Überprüfungen. Der Hook erwartet, dass die ersten Zeichen des rmdir() syscall zaryawerden. Wenn diese Bedingung erfüllt ist, überprüft die angehängte Funktion das 6. Zeichen, d. h. den Befehl, der ausgeführt wird. Abschließend wird das 8. Zeichen geprüft, das Prozessargumente für den auszuführenden Befehl enthalten kann. Der Aufbau sieht folgendermaßen aus: zarya[char][command][char][argument]. Jedes Sonderzeichen (oder keines Zeichen) kann zwischen zarya und den Befehlen und Argumenten platziert werden.

Zum Zeitpunkt der Veröffentlichung haben wir die folgenden Befehle identifiziert:

BefehlZweck
zarya.c.0Retrieve the config
zarya.t.0Testen Sie die Funktionsweise
zarya.k.<pid>Ausblenden einer PID
zarya.v.0Holen Sie sich die laufende Version

Bei der Initialisierung des Rootkits wird mit dem rmdir() syscall-Hook überprüft, ob das Rootkit erfolgreich geladen wurde. Dazu wird der Befehl t aufgerufen.

ubuntu-rk:~$ rmdir test
rmdir: failed to remove 'test': No such file or directory
ubuntu-rk:~$ rmdir zarya.t
ubuntu-rk:~$

Wenn Sie den Befehl rmdir für ein nicht vorhandenes Verzeichnis verwenden, wird die Fehlermeldung "Keine solche Datei oder kein solches Verzeichnis" zurückgegeben. Bei Verwendung von rmdir auf zarya.twird keine Ausgabe zurückgegeben, die auf das erfolgreiche Laden des Kernelmoduls hinweist.

Ein zweiter Befehl ist v, der verwendet wird, um die Version des laufenden Rootkits abzurufen.

ubuntu-rk:~$ rmdir zarya.v
rmdir: failed to remove '240513': No such file or directory

Anstatt dass zarya.v dem Fehler "Fehler beim Entfernen von 'directory" hinzugefügt wird, wird die Rootkit-Version 240513 zurückgegeben.

Ein dritter Befehl ist c, der die Konfiguration des Rootkits ausgibt.

ubuntu-rk:~/testing$ ./dump_config "zarya.c"
rmdir: failed to remove '': No such file or directory
Buffer contents (hex dump):
7ffe9ae3a270  00 01 00 00 10 70 69 6e 67 5f 69 6e 74 65 72 76  .....ping_interv
7ffe9ae3a280  61 6c 5f 73 00 2c 01 00 00 10 73 65 73 73 69 6f  al_s.,....sessio
7ffe9ae3a290  6e 5f 74 69 6d 65 6f 75 74 5f 73 00 04 00 00 00  n_timeout_s.....
7ffe9ae3a2a0  10 63 32 5f 74 69 6d 65 6f 75 74 5f 73 00 c0 a8  .c2_timeout_s...
7ffe9ae3a2b0  00 00 02 74 61 67 00 08 00 00 00 67 65 6e 65 72  ...tag.....gener
7ffe9ae3a2c0  69 63 00 02 73 5f 61 30 00 15 00 00 00 72 68 65  ic..s_a0.....rhe
7ffe9ae3a2d0  6c 2e 6f 70 73 65 63 75 72 69 74 79 31 2e 61 72  l.opsecurity1.ar
7ffe9ae3a2e0  74 00 02 73 5f 70 30 00 05 00 00 00 38 34 34 33  t..s_p0.....8443
7ffe9ae3a2f0  00 02 73 5f 63 30 00 04 00 00 00 74 6c 73 00 02  ..s_c0.....tls..
7ffe9ae3a300  73 5f 61 31 00 14 00 00 00 73 65 63 2e 6f 70 73  s_a1.....sec.ops
7ffe9ae3a310  65 63 75 72 69 74 79 31 2e 61 72 74 00 02 73 5f  ecurity1.art..s_
7ffe9ae3a320  70 31 00 05 00 00 00 38 34 34 33 00 02 73 5f 63  p1.....8443..s_c
7ffe9ae3a330  31 00 04 00 00 00 74 6c 73 00 02 73 5f 61 32 00  1.....tls..s_a2.
7ffe9ae3a340  0e 00 00 00 38 39 2e 32 33 2e 31 31 33 2e 32 30  ....89.23.113.20
7ffe9ae3a350  34 00 02 73 5f 70 32 00 05 00 00 00 38 34 34 33  4..s_p2.....8443
7ffe9ae3a360  00 02 73 5f 63 32 00 04 00 00 00 74 6c 73 00 00  ..s_c2.....tls..

Da die Nutzlast mit Null-Bytes beginnt, wird keine Ausgabe zurückgegeben, wenn zarya.c über einen rmdir Shell-Befehl ausgeführt wird. Indem wir ein kleines C-Programm schreiben, das den Systemaufruf umschließt und die hexadezimale/ASCII-Darstellung ausgibt, können wir sehen, wie die Konfiguration des Rootkits zurückgegeben wird.

Anstatt den kill() syscall zu verwenden, um Root-Rechte zu erhalten (wie es die meisten Rootkits tun), nutzt das Rootkit auch zu diesem Zweck den rmdir() syscall. Das Rootkit verwendet die Funktion prepare_creds , um die IDs für Anmeldeinformationen in 0 (root) zu ändern, und ruft commit_creds für diese geänderte Struktur auf, um Root-Rechte innerhalb des aktuellen Prozesses zu erhalten.

Um diese Funktion auszulösen, müssen wir das 6. Zeichen auf 0setzen. Der Nachteil dieses Hooks besteht darin, dass er dem aufrufenden Prozess Root-Rechte gibt, diese aber nicht beibehält. Beim Ausführen von zarya.0passiert nichts. Wenn wir jedoch diesen Hook mit einem C-Programm aufrufen und die Privilegien des aktuellen Prozesses ausgeben, erhalten wir ein Ergebnis. Ein Ausschnitt des verwendeten Wrapper-Codes wird unten angezeigt:

[...]
// Print the current PID, SID, and GID
pid_t pid = getpid();
pid_t sid = getsid(0);  // Passing 0 gets the SID of the calling process
gid_t gid = getgid();

printf("Current PID: %d, SID: %d, GID: %d\n", pid, sid, gid);

// Print all credential-related IDs
uid_t ruid = getuid();    // Real user ID
uid_t euid = geteuid();   // Effective user ID
gid_t rgid = getgid();    // Real group ID
gid_t egid = getegid();   // Effective group ID
uid_t fsuid = setfsuid(-1);  // Filesystem user ID
gid_t fsgid = setfsgid(-1);  // Filesystem group ID

printf("Credentials: UID=%d, EUID=%d, GID=%d, EGID=%d, FSUID=%d, FSGID=%d\n",
    ruid, euid, rgid, egid, fsuid, fsgid);

[...]

Beim Ausführen der Funktion können wir folgende Ausgabe erhalten:

ubuntu-rk:~/testing$ whoami;id
ruben
uid=1000(ruben) gid=1000(ruben) groups=1000(ruben),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),117(lxd)

ubuntu-rk:~/testing$ ./rmdir zarya.0
Received data:
zarya.0
Current PID: 41838, SID: 35117, GID: 0
Credentials: UID=0, EUID=0, GID=0, EGID=0, FSUID=0, FSGID=0

Um diesen Hook zu nutzen, haben wir ein kleines C-Wrapper-Skript geschrieben, das den Befehl rmdir zarya.0 ausführt und prüft, ob es nun auf die /etc/shadow Datei zugreifen kann.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <errno.h>

int main() {
    const char *directory = "zarya.0";

    // Attempt to remove the directory
    if (syscall(SYS_rmdir, directory) == -1) {
        fprintf(stderr, "rmdir: failed to remove '%s': %s\n", directory, strerror(errno));
    } else {
        printf("rmdir: successfully removed '%s'\n", directory);
    }

    // Execute the `id` command
    printf("\n--- Running 'id' command ---\n");
    if (system("id") == -1) {
        perror("Failed to execute 'id'");
        return 1;
    }

    // Display the contents of /etc/shadow
    printf("\n--- Displaying '/etc/shadow' ---\n");
    if (system("cat /etc/shadow") == -1) {
        perror("Failed to execute 'cat /etc/shadow'");
        return 1;
    }

    return 0;
}

Erfolgreich.

ubuntu-rk:~/testing$ ./get_root
rmdir: successfully removed 'zarya.0'

--- Running 'id' command ---
uid=0(root) gid=0(root) groups=0(root),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),117(lxd),1000(ruben)

--- Displaying '/etc/shadow' ---
root:*:19430:0:99999:7:::
[...]

Obwohl in der Funktion rmdir() mehr Befehle verfügbar sind, werden wir vorerst mit der nächsten fortfahren und sie möglicherweise einer zukünftigen Veröffentlichung hinzufügen.

LKM Rootkit Übersicht: getdents() und getdents64() Hooks

Die getdents_hook() und getdents64_hook() im Rootkit sind für die Manipulation von Verzeichnisauflistungs-Systemaufrufen verantwortlich, um Dateien und Verzeichnisse vor Benutzern zu verbergen.

Die Systemaufrufe getdents() und getdents64() werden verwendet, um Verzeichniseinträge zu lesen. Das Rootkit hakt diese Funktionen ein, um alle Einträge herauszufiltern, die bestimmten Kriterien entsprechen. Insbesondere Dateien und Verzeichnisse mit dem Präfix zov_ werden für jeden Benutzer ausgeblendet, der versucht, den Inhalt eines Verzeichnisses aufzulisten.

Zum Beispiel:

ubuntu-rk:~/getdents_hook$ mkdir zov_hidden_dir

ubuntu-rk:~/getdents_hook$ ls -lah
total 8.0K
drwxrwxr-x  3 ruben ruben 4.0K Dec  9 11:11 .
drwxr-xr-x 11 ruben ruben 4.0K Dec  9 11:11 ..

ubuntu-rk:~/getdents_hook$ echo "this file is now hidden" > zov_hidden_dir/zov_hidden_file

ubuntu-rk:~/getdents_hook$ ls -lah zov_hidden_dir/
total 8.0K
drwxrwxr-x 2 ruben ruben 4.0K Dec  9 11:11 .
drwxrwxr-x 3 ruben ruben 4.0K Dec  9 11:11 ..

ubuntu-rk:~/getdents_hook$ cat zov_hidden_dir/zov_hidden_file
this file is now hidden

Hier kann die Datei zov_hidden direkt über ihren gesamten Pfad aufgerufen werden. Beim Ausführen des Befehls ls wird er jedoch nicht in der Verzeichnisliste angezeigt.

Etappe 4: Kitsune SO Übersicht

Beim tieferen Eintauchen in das Rootkit wurde eine weitere ELF-Datei in der Kernel-Objektdatei identifiziert. Nachdem wir diese Binärdatei extrahiert hatten, stellten wir fest, dass dies die /lib64/libs.so Datei ist. Bei der Untersuchung stießen wir auf mehrere Verweise auf Zeichenfolgen wie Kitsune PID %ld. Dies deutet darauf hin, dass das SO von den Entwicklern als Kitsune bezeichnet wird. Kitsune kann für bestimmte Verhaltensweisen verantwortlich sein, die im Rootkit beobachtet werden. Diese Referenzen stimmen mit dem breiteren Kontext überein, wie das Rootkit Interaktionen im Benutzerbereich über LD_PRELOADmanipuliert.

Diese SO-Datei spielt eine Rolle bei der Erreichung der Persistenz- und Stealth-Mechanismen, die für dieses Rootkit von zentraler Bedeutung sind, und ihre Integration in die Angriffskette zeigt die Raffinesse ihres Designs. Wir werden nun zeigen, wie Sie jeden Teil der Angriffskette erkennen und/oder verhindern können.

PUMAKIT Ausführung Kettenerkennung und -prävention

In diesem Abschnitt werden verschiedene EQL/KQL-Regeln und YARA-Signaturen angezeigt, die verschiedene Teile der PUMAKIT-Ausführungskette verhindern und erkennen können.

Stufe 1: Cron

Bei der Ausführung des Droppers wird ein ungewöhnliches Ereignis im Syslog gespeichert. Das Ereignis gibt an, dass ein Prozess mit einem ausführbaren Stack gestartet wurde. Das ist ungewöhnlich und interessant zu beobachten:

[  687.108154] process '/home/ruben_groenewoud/30b26707d5fb407ef39ebee37ded7edeea2890fb5ec1ebfa09a3b3edfc80db1f' started with executable stack

Wir können dies über die folgende Abfrage suchen:

host.os.type:linux and event.dataset:"system.syslog" and process.name:kernel and message: "started with executable stack"

Diese Nachricht wird in /var/log/messages oder /var/log/sysloggespeichert. Wir können dies erkennen, indem wir das Syslog über Filebeat oder die Elastic-Agent-Systemintegration lesen.

Phase 2: Speicherresidente ausführbare Dateien

Wir können sofort eine ungewöhnliche Ausführung eines Dateideskriptors sehen. Dies kann durch die folgende EQL-Abfrage erkannt werden:

process where host.os.type == "linux" and event.type == "start" and event.action == "exec" and process.parent.executable like "/dev/fd/*" and not process.parent.command_line == "runc init"

Dieser Dateideskriptor bleibt bis zum Ende des Prozesses das übergeordnete Element des Droppers, was dazu führt, dass auch mehrere Dateien über diesen übergeordneten Prozess ausgeführt werden:

file where host.os.type == "linux" and event.type == "creation" and process.executable like "/dev/fd/*" and file.path like (
  "/boot/*", "/dev/shm/*", "/etc/cron.*/*", "/etc/init.d/*", "/var/run/*"
  "/etc/update-motd.d/*", "/tmp/*", "/var/log/*", "/var/tmp/*"
)

Nachdem /tmp/script.sh gelöscht wurde (erkannt durch die obigen Abfragen), können wir seine Ausführung erkennen, indem wir die Erkennung von Dateiattributen und die Dearchivierungsaktivität abfragen:

process where host.os.type == "linux" and event.type == "start" and event.action == "exec" and 
(process.parent.args like "/boot/*" or process.args like "/boot/*") and (
  (process.name in ("file", "unlzma", "gunzip", "unxz", "bunzip2", "unzstd", "unzip", "tar")) or
  (process.name == "grep" and process.args == "ELF") or
  (process.name in ("lzop", "lz4") and process.args in ("-d", "--decode"))
) and
not process.parent.name == "mkinitramfs"

Das Skript sucht weiterhin über den Befehl tail nach dem Speicher des Linux-Kernel-Images. Dies kann zusammen mit anderen Tools zur Speichersuche durch die folgende Abfrage erkannt werden:

process where host.os.type == "linux" and event.type == "start" and event.action == "exec" and
(process.parent.args like "/boot/*" or process.args like "/boot/*") and (
  (process.name == "tail" and (process.args like "-c*" or process.args == "--bytes")) or
  (process.name == "cmp" and process.args == "-i") or
  (process.name in ("hexdump", "xxd") and process.args == "-s") or
  (process.name == "dd" and process.args : ("skip*", "seek*"))
)

Sobald /tmp/script.sh Ausführung abgeschlossen ist, werden /memfd:tgt (deleted) und /memfd:wpn (deleted) erstellt. Die tgt ausführbare Datei, bei der es sich um die gutartige ausführbare Cron-Datei handelt, erstellt eine /run/crond.pid Datei. Dies ist nichts Bösartiges, sondern ein Artefakt, das durch eine einfache Abfrage erkannt werden kann.

file where host.os.type == "linux" and event.type == "creation" and file.extension in ("lock", "pid") and
file.path like ("/tmp/*", "/var/tmp/*", "/run/*", "/var/run/*", "/var/lock/*", "/dev/shm/*") and process.executable != null

Die wpn ausführbare Datei wird, wenn alle Bedingungen erfüllt sind, das LKMrootkit laden.

Stufe 3: Rootkit-Kernel-Modul

Das Laden des Kernel-Moduls kann über Auditd Manager erkannt werden, indem die folgende Konfiguration angewendet wird:

-a always,exit -F arch=b64 -S finit_module -S init_module -S delete_module -F auid!=-1 -k modules
-a always,exit -F arch=b32 -S finit_module -S init_module -S delete_module -F auid!=-1 -k modules

Und mit der folgenden Abfrage:

driver where host.os.type == "linux" and event.action == "loaded-kernel-module" and auditd.data.syscall in ("init_module", "finit_module")

Weitere Informationen zur Nutzung von Auditd mit Elastic Security zur Verbesserung Ihrer Linux-Detection-Engineering-Erfahrung finden Sie in unserer Studie Linux Detection Engineering with Auditd, die auf der Website der Elastic Security Labs veröffentlicht wurde.

Bei der Initialisierung beschädigt der LKM den Kernel, da er nicht signiert ist.

audit: module verification failed: signature and/or required key missing - tainting kernel

Wir können dieses Verhalten durch die folgende KQL-Abfrage erkennen:

host.os.type:linux and event.dataset:"system.syslog" and process.name:kernel and message:"module verification failed: signature and/or required key missing - tainting kernel"

Außerdem verfügt das LKM über fehlerhaften Code, was dazu führt, dass es mehrmals zu einem Segfault kommt. Zum Beispiel:

Dec  9 13:26:10 ubuntu-rk kernel: [14350.711419] cat[112653]: segfault at 8c ip 00007f70d596b63c sp 00007fff9be81360 error 4
Dec  9 13:26:10 ubuntu-rk kernel: [14350.711422] Code: 83 c4 20 48 89 d0 5b 5d 41 5c c3 48 8d 42 01 48 89 43 08 0f b6 02 41 88 44 2c ff eb c1 8b 7f 78 e9 25 5c 00 00 c3 41 54 55 53 <8b> 87 8c 00 00 00 48 89 fb 85 c0 79 1b e8 d7 00 00 00 48 89 df 89

Dies kann durch eine einfache KQL-Abfrage erkannt werden, die nach Segfaults in der kern.log Datei fragt.

host.os.type:linux and event.dataset:"system.syslog" and process.name:kernel and message:segfault

Sobald das Kernel-Modul geladen ist, können wir Spuren der Befehlsausführung durch den kthreadd Prozess sehen. Das Rootkit erstellt neue Kernel-Threads, um bestimmte Befehle auszuführen. Das Rootkit führt beispielsweise in kurzen Abständen die folgenden Befehle aus:

cat /dev/null
truncate -s 0 /usr/share/zov_f/zov_latest

Wir können diese und weitere potenziell verdächtige Befehle durch eine Abfrage wie die folgende erkennen:

process where host.os.type == "linux" and event.type == "start" and event.action == "exec" and process.parent.name == "kthreadd" and (
  process.executable like ("/tmp/*", "/var/tmp/*", "/dev/shm/*", "/var/www/*", "/bin/*", "/usr/bin/*", "/usr/local/bin/*") or
  process.name in ("bash", "dash", "sh", "tcsh", "csh", "zsh", "ksh", "fish", "whoami", "curl", "wget", "id", "nohup", "setsid") or
  process.command_line like (
    "*/etc/cron*", "*/etc/rc.local*", "*/dev/tcp/*", "*/etc/init.d*", "*/etc/update-motd.d*",
    "*/etc/ld.so*", "*/etc/sudoers*", "*base64 *", "*base32 *", "*base16 *", "*/etc/profile*",
    "*/dev/shm/*", "*/etc/ssh*", "*/home/*/.ssh/*", "*/root/.ssh*" , "*~/.ssh/*", "*autostart*",
    "*xxd *", "*/etc/shadow*"
  )
) and not process.name == "dpkg"

Wir können auch die Methode der Rootkits zur Erhöhung von Berechtigungen erkennen, indem wir den rmdir Befehl auf ungewöhnliche UID/GID-Änderungen analysieren.

process where host.os.type == "linux" and event.type == "change" and event.action in ("uid_change", "guid_change") and process.name == "rmdir"

Abhängig von der Ausführungskette können auch mehrere andere Verhaltensregeln ausgelöst werden.

Eine YARA-Signatur, um sie alle zu beherrschen

Elastic Security hat eine YARA-Signatur erstellt, um PUMAKIT (den Dropper (cron), den Rootkit-Loader (/memfd:wpn), das LKM-Rootkit und die Kitsune-Shared-Object-Dateien zu identifizieren. Die Signatur wird unten angezeigt:

rule Linux_Trojan_Pumakit {
    meta:
        author = "Elastic Security"
        creation_date = "2024-12-09"
        last_modified = "2024-12-09"
        os = "Linux"
        arch = "x86, arm64"
        threat_name = "Linux.Trojan.Pumakit"

    strings:
        $str1 = "PUMA %s"
        $str2 = "Kitsune PID %ld"
        $str3 = "/usr/share/zov_f"
        $str4 = "zarya"
        $str5 = ".puma-config"
        $str6 = "ping_interval_s"
        $str7 = "session_timeout_s"
        $str8 = "c2_timeout_s"
        $str9 = "LD_PRELOAD=/lib64/libs.so"
        $str10 = "kit_so_len"
        $str11 = "opsecurity1.art"
        $str12 = "89.23.113.204"
    
    condition:
        4 of them
}

Beobachtungen

Die folgenden Observablen wurden in dieser Studie diskutiert.

ÜberwachbarTypNameReferenz
30b26707d5fb407ef39ebee37ded7edeea2890fb5ec1ebfa09a3b3edfc80db1fSHA256cronPUMAKIT Dropper
cb070cc9223445113c3217f05ef85a930f626d3feaaea54d8585aaed3c2b3cfeSHA256/memfd:wpn (deleted)PUMAKIT Lader
934955f0411538eebb24694982f546907f3c6df8534d6019b7ff165c4d104136SHA256/memfd:tgt (deleted)Cron-Binärdatei
8ef63f9333104ab293eef5f34701669322f1c07c0e44973d688be39c94986e27SHA256libs.soKitsune – Referenz zu freigegebenen Objekten
8ad422f5f3d0409747ab1ac6a0919b1fa8d83c3da43564a685ae4044d0a0ea03SHA256some2.elfPUMAKIT-Variante
bbf0fd636195d51fb5f21596d406b92f9e3d05cd85f7cd663221d7d3da8af804SHA256some1.soKitsune Shared-Objekt-Variante
bc9193c2a8ee47801f5f44beae51ab37a652fda02cd32d01f8e88bb793172491SHA256puma.koLKM rootkit
1aab475fb8ad4a7f94a7aa2b17c769d6ae04b977d984c4e842a61fb12ea99f58SHA256kitsune.soKitsune
sec.opsecurity1[.]artDomain-NamePUMAKIT C2 Server
rhel.opsecurity1[.]artDomain-NamePUMAKIT C2 Server
89.23.113[.]204IPv4-ADDRPUMAKIT C2 Server

Abschließende Erklärung

PUMAKIT ist eine komplexe und heimliche Bedrohung, die fortschrittliche Techniken wie Syscall-Hooking, speicherresidente Ausführung und einzigartige Methoden zur Rechteausweitung verwendet. Das Design mit mehreren Architekturen unterstreicht die zunehmende Raffinesse von Malware, die auf Linux-Systeme abzielt.

Elastic Security Labs wird PUMAKIT weiterhin analysieren, sein Verhalten überwachen und Aktualisierungen oder neue Varianten verfolgen. Durch die Verfeinerung von Erkennungsmethoden und den Austausch umsetzbarer Erkenntnisse wollen wir dazu beitragen, dass Verteidiger immer einen Schritt voraus sind.