Kopierfehler und DirtyFrag: Linux-Seitencache-Bugs in der Praxis

Diese Studie analysiert die Linux-Kernel-Schwachstellen Copy Fail und DirtyFrag zur Rechteausweitung, die subtile Fehler im Seitencache ausnutzen, um zuverlässige Wege zum Root-Zugriff zu schaffen. Darüber hinaus veröffentlicht Elastic Security Labs eine Erkennungslogik für diese Schwachstellen.

4 Minuten LesezeitDetektionstechnik

Einführung

Die jüngsten Schwachstellen zur Rechteausweitung im Linux-Kernel, Copy Fail (CVE-2026-31431), Copy Fail 2 und DirtyFrag, verdeutlichen, wie subtile Fehler in der Seitencache-Beschädigung zu praktischen und zuverlässigen Wegen zum Root-Zugriff werden können. Diese Problematik ist insbesondere für die Verteidiger relevant, da die Ausnutzung legitime Kernel-Schnittstellen, lokale Ausführung und kurzen Proof-of-Concept-Code beinhaltet. Die Sicherheitslücke Copy Fail wurde als in freier Wildbahn ausgenutzt gemeldet und in den Katalog bekannter ausgenutzter Sicherheitslücken der CISA aufgenommen.

Um diesen Bedrohungen entgegenzuwirken, hat Elastic Security Labs eine Erkennungslogik entwickelt, die sich auf die Ausnutzungsmuster dieser Schwachstellen konzentriert, anstatt nur eine bestimmte Proof-of-Concept-Implementierung abzugleichen.

Kopierfehler

Copy Fail ist ein Logikfehler in der authencesn -Kryptografievorlage des Linux-Kernels. Die Schwachstelle verknüpft AF_ALG und splice() um einen kontrollierten 4-Byte-Schreibvorgang in den Seitencache einer beliebigen lesbaren Datei zu erzeugen. In der Praxis führt dies zu einer Beschädigung der Speicheransicht einer Setuid-Binärdatei wie /usr/bin/su und zu einer Eskalation der Berechtigungen, ohne die Datei auf der Festplatte zu verändern. Der öffentlich zugängliche Exploit ist ein 732 Byte großes Python-Skript, das unter Ubuntu, Amazon Linux, RHEL und SUSE funktioniert.

DirtyFrag

DirtyFrag erweitert die gleiche Fehlerklasse auf den Netzwerk-Stack mit zwei Varianten für das Schreiben in den Seitencache. Der ESP-Pfad verwendet XFRM-Sicherheitsassoziationen über AF_NETLINK , um kryptografische Operationen direkt auf den gespleißten Seiten durchzuführen und /usr/bin/su mit einem minimalen Root-Shell-ELF zu überschreiben. Der RxRPC-Fallback-Pfad verwendet AF_RXRPC mit pcbc(fcrypt) , um /etc/passwd zu beschädigen und das Passwortfeld von root zu löschen. Beide Wege erfordern unshare(CLONE_NEWUSER | CLONE_NEWNET) , um Namespace-Funktionen zu erhalten, bevor der Seitencache-Schreibvorgang ausgelöst wird.

DirtyFrag ist nicht vom Modul algif_aead abhängig, was bedeutet, dass Systeme, die lediglich die Kopierfehler-Schutzmaßnahme angewendet haben, möglicherweise trotzdem gefährdet sind.

Erkennung

Bei diesen Schwachstellen konzentrierten wir uns auf die Erkennung der zugrundeliegenden Primitiven und Verhaltensweisen, nicht nur auf eine spezifische Exploit-Implementierung. Diese Unterscheidung ist wichtig, denn Copy Fail existiert bereits in mehreren öffentlichen Implementierungen (Python, Go, Rust, C, Metasploit), und DirtyFrag wird als öffentlicher C-Proof-of-Concept ausgeliefert. Der Versuch, nur einen bestimmten PoC aufzudecken, lässt die Verteidiger einen Schritt zurückfallen.

Syscall-Level-Primitive (Auditd)

Sowohl Copy Fail als auch DirtyFrag benötigen socket(AF_ALG) für den Zugriff auf das Kernel-Krypto-Subsystem und splice() , um schreibgeschützte Dateiseiten in Netzwerkpuffer einzufügen, wo kryptografische Operationen den Seitencache beschädigen. DirtyFrag verwendet zusätzlich socket(AF_RXRPC) als Fallback, wenn AF_ALG nicht verfügbar ist. Diese Primitiven sind durch auditd syscall auditing socket mit a0 Hex-Werten von 26 (AF_ALG) oder 21 (AF_RXRPC) und splice Aufrufen von Nicht-Root-Prozessen sichtbar. Wir verwenden diese als Frühindikatoren, die über EQL-Sequenzen mit dem letzten Schritt der Rechteausweitung – dem Erlangen der effektiven Benutzer-ID 0 von einem Nicht-Root-Aufrufer – korreliert sind:

sequence with maxspan=60s
  [any where host.os.type == "linux" and    
   (
    (event.category == "process" and auditd.data.syscall == "socket" and auditd.data.a0 in ("26", "21")) or 
    (event.category == "process" and auditd.data.syscall == "splice") or 
    (event.category == "network" and event.action == "bound-socket" and data_stream.dataset == "auditd_manager.auditd" and ?auditd.data.socket.family == "38") 
    )  
   and user.id != "0"]  by process.pid, host.id, user.id with runs=10
  [process where host.os.type == "linux"  and event.action == "executed" and 
   (
     (user.effective.id == "0" and user.id != "0") or 
     (process.name in ("bash", "sh", "zsh", "dash", "fish", "ksh", "busybox") and 
      process.args in ("-c", "--command", "-ic", "-ci", "-cl", "-lc", "-bash", "-sh", "-zsh", "-dash", "-fish", "-ksh"))
    )] by process.parent.pid, host.id, user.id

Example of matches :

Namensraumerstellung (DirtyFrag-spezifisch)

Die Exploit-Kette von DirtyFrag nutzt außerdem unshare(CLONE_NEWUSER | CLONE_NEWNET) , um Namespace-Berechtigungen zu erlangen. Wir bringen dieses Ereignis mit der Ausführung eines Root-Prozesses oder einem setuid(0) -Systemaufruf kurz darauf in Zusammenhang:

sequence by host.id, process.parent.pid with maxspan=30s
 [process where host.os.type == "linux" and 
  (
   (auditd.data.syscall == "unshare" and auditd.data.class == "namespace" and auditd.data.a0 in ("10000000", "50000000", "70000000", "10020000", "50020000", "70020000")) or 

   (process.name == "unshare" and  
    (process.args in ("--user", "--map-root-user", "--map-current-user") or process.args like ("-*U*", "-*r*")))
   ) and user.id != "0" and user.id != null]
 [process where host.os.type == "linux" and 
  user.id == "0" and user.id != null and 
  (
   process.name in ("su", "sudo", "pkexec", "passwd", "chsh", "newgrp", "doas", "run0", "sg", "dash", "sh", "bash", "zsh", "fish", 
                    "ksh", "csh", "tcsh", "ash", "mksh", "busybox", "rbash", "rzsh", "rksh", "tmux", "screen", "node") or 
   process.name like ("python*", "perl*", "ruby*", "php*", "lua*")
  )]

Generischer SUID-Binärmissbrauch (Prozessausführungsereignisse)

Wir haben auch die Erkennungsoptionen ausschließlich anhand von Prozessausführungsereignissen bewertet, da diese in mehr Umgebungen aktiviert sind als die Auditierung von Systemaufrufen mit auditd. Ein gemeinsamer letzter Schritt bei beiden Exploits ist die Beschädigung oder Beeinflussung der Ausführung einer SUID-Binärdatei im Arbeitsspeicher, wie z. B. su, sudo, pkexec, passwd, chsh, oder newgrp, wodurch diese vom Angreifer kontrollierten Code als Root ausführt.

Die Erkennung sucht nach verdächtigen Ausführungen, bei denen der Prozess mit der effektiven UID 0 läuft, der tatsächliche Benutzer nicht root ist, der übergeordnete Prozess ebenfalls nicht root ist, die SUID-Binärdatei mit minimalen Argumenten gestartet wird und der übergeordnete Prozess eine Skriptlaufzeitumgebung, ein Shell-Einzeiler oder eine ausführbare Datei aus einem für den Benutzer beschreibbaren Pfad ist:

process where event.type == "start" and event.action == "exec" and (
  (process.user.id == 0 and process.real_user.id != 0) or
  (process.group.id == 0 and process.real_group.id != 0)
) and (
  (process.name == "su" and process.args_count <= 2) or
  (process.name == "sudo" and process.args_count == 1) or
  (process.name == "pkexec" and process.args_count == 1) or
  (process.name == "passwd" and process.args_count <= 2)
) and
(
  process.parent.name like (".*", "python*", "perl*", "ruby*", "lua*", "php*", "node", "deno", "bun", "java") or
  process.parent.executable like ("./*", "/tmp/*", "/var/tmp/*", "/dev/shm/*", "/run/user/*", "/var/run/user/*", "/home/*/*") or
  (
    process.parent.name in ("bash", "dash", "sh", "tcsh", "csh", "zsh", "ksh", "fish", "mksh") and
    process.parent.args in ("-c", "-cl", "-lc", "--command", "-ic", "-ci", "-bash", "-sh", "-zsh", "-dash", "-fish", "-ksh") and
    process.parent.args_count <= 4
  )
)

Ohne auf das Erzeugen eines Kindprozesses angewiesen zu sein, können wir mit ES|QL auch proaktiv nach Exploitation-Aktivitäten suchen. Sowohl Copy Fail als auch DirtyFrag erzeugen einen charakteristischen Ausbruch von abwechselnden socket(AF_ALG) und splice() -Systemaufrufen vom selben Prozess. Copy Fail iteriert 48 Mal, um 192 Bytes zu schreiben, und DirtyFrag folgt einem ähnlichen Muster auf seinen ESP- und RxRPC-Pfaden.

Die folgende Abfrage aggregiert diese Systemaufrufe nach Prozess und zeigt alle Nicht-Root-Prozesse an, die AF_ALG oder AF_RXRPC -Sockets mit splice -Aufrufen auf Volume kombinieren:

FROM logs-auditd_manager.auditd-default*
| WHERE host.os.type == "linux" AND user.id != "0" AND
  (
    (event.category == "process" AND auditd.data.syscall == "socket" AND auditd.data.a0 IN ("26", "21")) OR
    (event.category == "process" AND auditd.data.syscall == "splice") OR
    (event.category == "network" AND event.action == "bound-socket" AND auditd.data.socket.family == "38")
  )
| EVAL
    is_af_alg   = CASE(auditd.data.syscall == "socket" AND auditd.data.a0 == "26", 1, 0),
    is_af_rxrpc = CASE(auditd.data.syscall == "socket" AND auditd.data.a0 == "21", 1, 0),
    is_splice   = CASE(auditd.data.syscall == "splice", 1, 0),
    is_bind_alg = CASE(event.action == "bound-socket" AND auditd.data.socket.family == "38", 1, 0)
| STATS
    socket_af_alg   = SUM(is_af_alg),
    socket_af_rxrpc = SUM(is_af_rxrpc),
    splice_count    = SUM(is_splice),
    bind_af_alg     = SUM(is_bind_alg),
    total_calls     = COUNT(*),
    first_seen      = MIN(@timestamp),
    last_seen        = MAX(@timestamp)
  BY host.name, user.name, process.executable, process.pid
| EVAL
    duration_seconds = DATE_DIFF("seconds", first_seen, last_seen),
    distinct_syscalls = CASE(
      socket_af_alg > 0 AND splice_count > 0 AND bind_af_alg > 0, "af_alg+splice+bind",
      socket_af_alg > 0 AND splice_count > 0, "af_alg+splice",
      socket_af_rxrpc > 0 AND splice_count > 0, "af_rxrpc+splice",
      socket_af_alg > 0, "af_alg_only",
      socket_af_rxrpc > 0, "af_rxrpc_only",
      splice_count > 0, "splice_only",
      "other"
    )
| WHERE total_calls >= 10 AND
  (socket_af_alg > 0 OR socket_af_rxrpc > 0) AND
  splice_count > 0
| SORT total_calls DESC
| LIMIT 50

Auditd-Regeln:

Die folgenden Regeln können Ihrer Auditd- Integrationskonfiguration hinzugefügt werden, um die Sichtbarkeit dieser Exploit-Primitive zu ermöglichen:

-a always,exit -F arch=b64 -S socket -k socket_syscall
-a always,exit -F arch=b32 -S socketcall -k socket_syscall
-a always,exit -F arch=b64 -S splice -k splice-syscall
-a always,exit -F arch=b32 -S splice -k splice-syscall
-a always,exit -F arch=b64 -S bind -k socket_bound
-a always,exit -F arch=b32 -S bind -k socket_bound

Erkennungsregeln:

Milderung

Die Erkennung sollte mit Härtung und Patching einhergehen. Die wichtigste Maßnahme zur Behebung beider Sicherheitslücken ist die Aktualisierung des Linux-Kernels, sobald entsprechende Patches der Distributionen verfügbar sind.

Wo eine sofortige Behebung von Problemen nicht möglich ist, kann durch gezieltes Blockieren von Modulen die Angriffsfläche verringert werden. Bei Copy Fail verhindert die Deaktivierung des Moduls algif_aead den vom Exploit verwendeten AF_ALG AEAD-Pfad:

echo "install algif_aead /bin/false" > /etc/modprobe.d/copyfail.conf
rmmod algif_aead 2>/dev/null

Bei DirtyFrag blockiert die Deaktivierung der betroffenen Netzwerkmodule sowohl den ESP- als auch den RxRPC-Exploit-Pfad:

printf 'install esp4 /bin/false\ninstall esp6 /bin/false\ninstall rxrpc /bin/false\n' > /etc/modprobe.d/dirtyfrag.conf
rmmod esp4 esp6 rxrpc 2>/dev/null

Nach Anwendung einer der beiden Abhilfemaßnahmen stellt das Löschen des Seitencaches sicher, dass alle zuvor beschädigten Seiten im Arbeitsspeicher verworfen werden:

echo 3 > /proc/sys/vm/drop_caches

Diese Maßnahmen sollten vor dem Produktiveinsatz in einer Testumgebung geprüft werden, da die Deaktivierung von Kernelmodulen Auswirkungen auf IPsec-VPNs, Kryptoanwendungen oder andere Dienste haben kann, je nachdem, welche Subsysteme betroffen sind. Das Löschen des Seitencaches verursacht einen kurzen I/O-Spitzenwert und sollte während Spitzenlastzeiten vermieden werden.

Die Einschränkung der Erstellung von Namespaces durch nicht privilegierte Benutzer erhöht auch die Sicherheit gegen DirtyFrag und ähnliche Exploits:

sysctl -w kernel.unprivileged_userns_clone=0

Auf RHEL/Fedora verwenden Sie stattdessen user.max_user_namespaces=0 . Diese Einstellung kann sich auf Anwendungen auswirken, die auf nicht privilegierte Namensräume angewiesen sind, wie z. B. bestimmte Container-Laufzeitumgebungen und Browser-Sandboxes. Vor der Anwendung die Kompatibilität prüfen.

Referenzen: