Introduction
Les récentes vulnérabilités d'escalade des privilèges du noyau Linux, Copy Fail (CVE-2026-31431), Copy Fail 2 et DirtyFrag, montrent comment de subtils bogues de corruption du cache des pages peuvent devenir des chemins pratiques et fiables vers la racine. Ces questions sont particulièrement pertinentes pour les défenseurs car l'exploitation implique des interfaces de noyau légitimes, une exécution locale et un code de preuve de concept court. Copy Fail a été signalé comme exploité dans la nature et a été ajouté au catalogue des vulnérabilités exploitées connues de la CISA.
Pour aider à atténuer ces menaces, Elastic Security Labs a développé une logique de détection axée sur les schémas d'exploitation autour de ces vulnérabilités plutôt que de correspondre uniquement à une mise en œuvre spécifique de la preuve de concept.
Échec de la copie
Copy Fail est un bogue logique dans le modèle cryptographique authencesn du noyau Linux. La vulnérabilité associe AF_ALG et splice() pour créer une écriture contrôlée de 4 octets dans le cache de page de n'importe quel fichier lisible. En pratique, cela corrompt la vue en mémoire d'un binaire setuid tel que /usr/bin/su et permet d'augmenter les privilèges sans modifier le fichier sur le disque. L'exploit public est un script Python de 732 octets qui fonctionne sur Ubuntu, Amazon Linux, RHEL et SUSE.
DirtyFrag
DirtyFrag étend la même classe de bogues à la pile réseau avec deux variantes d'écriture dans le cache de page. Le chemin ESP utilise les associations de sécurité XFRM via AF_NETLINK pour effectuer des opérations cryptographiques sur place sur les pages épissées, en remplaçant /usr/bin/su par un ELF minimal de type "root-shell". Le chemin de repli RxRPC utilise AF_RXRPC avec pcbc(fcrypt) pour corrompre /etc/passwd, en effaçant le champ du mot de passe de root. Dans les deux cas, unshare(CLONE_NEWUSER | CLONE_NEWNET) doit acquérir des capacités d'espace de noms avant de déclencher l'écriture dans le cache de la page.
DirtyFrag ne dépend pas du module algif_aead, ce qui signifie que les systèmes qui n'ont appliqué que l'atténuation Copy Fail peuvent encore être exposés.
Détection
Pour ces vulnérabilités, nous nous sommes concentrés sur la détection des primitives et du comportement sous-jacents, et pas seulement sur la mise en œuvre d'un exploit spécifique. Cette distinction est importante, car Copy Fail a déjà fait l'objet de plusieurs réimplémentations publiques (Python, Go, Rust, C, Metasploit), et DirtyFrag est livré sous la forme d'une preuve de concept publique en C. En essayant de ne détecter qu'un PoC spécifique, les défenseurs prennent du retard.
Primitives de niveau Syscall (Auditd)
Copy Fail et DirtyFrag s'appuient tous deux sur socket(AF_ALG) pour accéder au sous-système cryptographique du noyau et sur splice() pour injecter des pages de fichiers en lecture seule dans les tampons du réseau où les opérations cryptographiques en place corrompent le cache des pages. DirtyFrag utilise également socket(AF_RXRPC) comme solution de repli lorsque AF_ALG n'est pas disponible. Ces primitives sont visibles grâce à l'audit de syscall auditd socket avec a0 valeurs hexagonales de 26 (AF_ALG) ou 21 (AF_RXRPC), et splice appels à partir de processus non root. Nous les utilisons comme des signaux précoces, corrélés par des séquences EQL avec l'étape finale d'escalade des privilèges consistant à obtenir l'uid 0 de la part d'un appelant non root :
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 :
Création d'un espace de noms (spécifique aux DirtyFrag)
La chaîne d'exploitation de DirtyFrag s'appuie également sur unshare(CLONE_NEWUSER | CLONE_NEWNET) pour obtenir des capacités d'espace de noms. Nous établissons une corrélation entre cet événement et l'exécution d'un processus racine ou d'un appel de système setuid(0) peu de temps après :
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*")
)]
Abus de binaire SUID générique (événements d'exécution de processus)
Nous avons également évalué les options de détection en utilisant uniquement les événements d'exécution de processus, car ceux-ci sont généralement activés dans un plus grand nombre d'environnements que l'audit syscall d'auditd. L'étape finale commune à ces deux exploits consiste à corrompre ou à influencer l'exécution en mémoire d'un binaire SUID tel que su, sudo, pkexec, passwd, chsh, ou newgrp, afin qu'il exécute un code contrôlé par l'attaquant en tant que super-utilisateur.
La détection recherche des exécutions suspectes lorsque le processus s'exécute en tant qu'UID effectif 0, que l'utilisateur réel n'est pas root, que le processus parent n'est pas root non plus, que le binaire SUID est lancé avec un minimum d'arguments et que le processus parent est un runtime de script, un shell one-liner ou un exécutable à partir d'un chemin d'accès enregistrable par l'utilisateur :
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
)
)
Sans dépendre de l'apparition d'un processus enfant, nous pouvons également chasser de manière proactive les activités d'exploitation à l'aide d'ES|QL. Copy Fail et DirtyFrag produisent tous deux une rafale distinctive d'appels syscall socket(AF_ALG) et splice() entrelacés à partir du même processus. Copy Fail itère 48 fois pour écrire 192 octets, et DirtyFrag suit un schéma similaire sur ses chemins ESP et RxRPC.
La requête suivante agrège ces appels de système par processus et met en évidence tout processus non racine combinant des sockets AF_ALG ou AF_RXRPC avec des appels splice au volume :
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
Règles d'audit :
Les règles suivantes peuvent être ajoutées à votre configuration d'intégration Auditd pour activer la visibilité sur ces primitives d'exploitation :
-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
Règles de détection :
- Exploitation potentielle de l'échec de la copie (CVE-2026-31431) via la socket AF_ALG
- Exécution binaire SUID suspecte
- Caractéristique suspecte du noyau Règle d'activité
- Manipulation de l'espace de noms à l'aide de Unshare
- Privilege Escalation via SUID/SGID
Atténuation
La détection doit être associée au renforcement et à l'application de correctifs. La principale solution pour remédier à ces deux vulnérabilités consiste à mettre à jour le noyau Linux une fois que les correctifs de distribution sont disponibles.
Lorsqu'il n'est pas possible d'appliquer immédiatement des correctifs, le blocage ciblé des modules peut réduire la surface d'attaque. Pour Copy Fail, la désactivation du module algif_aead empêche le chemin AF_ALG AEAD utilisé par l'exploit :
echo "install algif_aead /bin/false" > /etc/modprobe.d/copyfail.conf
rmmod algif_aead 2>/dev/null
Dans le cas de DirtyFrag, la désactivation des modules réseau concernés bloque les voies d'exploitation ESP et RxRPC :
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
Après l'application de l'une ou l'autre mesure d'atténuation, l'abandon du cache de pages garantit que toutes les pages en mémoire précédemment corrompues sont supprimées :
echo 3 > /proc/sys/vm/drop_caches
Ces mesures d'atténuation doivent être testées dans un environnement d'essai avant d'être déployées en production, car la désactivation des modules du noyau peut avoir un impact sur les VPN IPsec, les applications cryptographiques ou d'autres services en fonction des sous-systèmes concernés. L'abandon du cache de page provoque un bref pic d'E/S et doit être évité en cas de pic de charge.
Restreindre la création d'espaces de noms pour les utilisateurs non privilégiés permet également de se prémunir contre DirtyFrag et d'autres exploits similaires :
sysctl -w kernel.unprivileged_userns_clone=0
Sur RHEL/Fedora, utilisez plutôt user.max_user_namespaces=0. Ce paramètre peut affecter les applications qui s'appuient sur des espaces de noms non privilégiés, tels que certains conteneurs d'exécution et les bacs à sable des navigateurs. Évaluez la compatibilité avant de poser votre candidature.
Références :
- https://copy.fail/
- https://xint.io/blog/copy-fail-linux-distributions
- https://github.com/V4bel/dirtyfrag/tree/master
- https://github.com/0xdeadbeefnetwork/Copy_Fail2-Electric_Boogaloo/
- https://access.redhat.com/security/vulnerabilities/RHSB-2026-003
- https://ubuntu.com/blog/copy-fail-vulnerability-fixes-available
- https://aws.amazon.com/security/security-bulletins/rss/2026-027-aws/
- https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=a664bf3d603d