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:
- Überprüfen Sie Befehlszeilenargumente für ein bestimmtes Schlüsselwort (
"Huinder"
). - 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.
- 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 dmesg
abfragt. 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 Befehlfile
ü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 wiegunzip
,unxz
,bunzip2
und anderen zu dekomprimieren, basierend auf den Signaturen der unterstützten Komprimierungsformate. Es verwendetgrep
undtail
, um bestimmte komprimierte Segmente zu lokalisieren und zu extrahieren. - Das Skript versucht, eine Datei (
$i
) zu suchen und in/tmp/vmlinux
zu verarbeiten.
Nach der Ausführung von /tmp/script.sh
wird 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 tail
extrahiert, mit gunzip
dekomprimiert und als /tmp/vmlinux
gespeichert. 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 zarya
werden. 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:
Befehl | Zweck |
---|---|
zarya.c.0 | Retrieve the config |
zarya.t.0 | Testen Sie die Funktionsweise |
zarya.k.<pid> | Ausblenden einer PID |
zarya.v.0 | Holen 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.t
wird 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 0
setzen. Der Nachteil dieses Hooks besteht darin, dass er dem aufrufenden Prozess Root-Rechte gibt, diese aber nicht beibehält. Beim Ausführen von zarya.0
passiert 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_PRELOAD
manipuliert.
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/syslog
gespeichert. 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.
Überwachbar | Typ | Name | Referenz |
---|---|---|---|
30b26707d5fb407ef39ebee37ded7edeea2890fb5ec1ebfa09a3b3edfc80db1f | SHA256 | cron | PUMAKIT Dropper |
cb070cc9223445113c3217f05ef85a930f626d3feaaea54d8585aaed3c2b3cfe | SHA256 | /memfd:wpn (deleted ) | PUMAKIT Lader |
934955f0411538eebb24694982f546907f3c6df8534d6019b7ff165c4d104136 | SHA256 | /memfd:tgt (deleted) | Cron-Binärdatei |
8ef63f9333104ab293eef5f34701669322f1c07c0e44973d688be39c94986e27 | SHA256 | libs.so | Kitsune – Referenz zu freigegebenen Objekten |
8ad422f5f3d0409747ab1ac6a0919b1fa8d83c3da43564a685ae4044d0a0ea03 | SHA256 | some2.elf | PUMAKIT-Variante |
bbf0fd636195d51fb5f21596d406b92f9e3d05cd85f7cd663221d7d3da8af804 | SHA256 | some1.so | Kitsune Shared-Objekt-Variante |
bc9193c2a8ee47801f5f44beae51ab37a652fda02cd32d01f8e88bb793172491 | SHA256 | puma.ko | LKM rootkit |
1aab475fb8ad4a7f94a7aa2b17c769d6ae04b977d984c4e842a61fb12ea99f58 | SHA256 | kitsune.so | Kitsune |
sec.opsecurity1[.]art | Domain-Name | PUMAKIT C2 Server | |
rhel.opsecurity1[.]art | Domain-Name | PUMAKIT C2 Server | |
89.23.113[.]204 | IPv4-ADDR | PUMAKIT 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.