Introdução
Vulnerabilidades recentes de escalonamento de privilégios no kernel do Linux, como Copy Fail (CVE-2026-31431), Copy Fail 2 e DirtyFrag, destacam como bugs sutis de corrupção do cache de páginas podem se tornar caminhos práticos e confiáveis para obter acesso root. Essas questões são especialmente relevantes para os defensores, pois a exploração envolve interfaces legítimas do kernel, execução local e código de prova de conceito curto. A vulnerabilidade Copy Fail foi relatada como explorada em ataques reais e foi adicionada ao catálogo de Vulnerabilidades Conhecidas e Exploradas da CISA.
Para ajudar a mitigar essas ameaças, o Elastic Security Labs desenvolveu uma lógica de detecção focada nos padrões de exploração dessas vulnerabilidades, em vez de apenas corresponder a uma implementação específica de prova de conceito.
Falha na cópia
Copy Fail é um bug lógico no modelo criptográfico authencesn do kernel do Linux. A vulnerabilidade encadeia AF_ALG e splice() para criar uma escrita controlada de 4 bytes no cache de páginas de qualquer arquivo legível. Na prática, isso corrompe a visão na memória de um binário setuid como /usr/bin/su e eleva os privilégios sem alterar o arquivo no disco. O exploit público é um script Python de 732 bytes que funciona em Ubuntu, Amazon Linux, RHEL e SUSE.
Fragmento sujo
DirtyFrag expande a mesma classe de bugs para a pilha de rede com duas variantes de gravação em cache de páginas. O caminho ESP usa associações de segurança XFRM via AF_NETLINK para executar operações criptográficas in-place em páginas emendadas, sobrescrevendo /usr/bin/su com um ELF mínimo de shell raiz. O caminho de fallback RxRPC usa AF_RXRPC com pcbc(fcrypt) para corromper /etc/passwd, limpando o campo de senha do root. Ambos os caminhos exigem unshare(CLONE_NEWUSER | CLONE_NEWNET) para obter capacidades de namespace antes de acionar a gravação do cache de páginas.
DirtyFrag não depende do módulo algif_aead , o que significa que sistemas que aplicaram apenas a mitigação Copy Fail ainda podem estar expostos.
Detecção
Para essas vulnerabilidades, focamos na detecção dos mecanismos e comportamentos subjacentes, e não apenas em uma implementação específica de exploração. Essa distinção é importante, pois o Copy Fail já possui diversas reimplementações públicas (Python, Go, Rust, C, Metasploit), e o DirtyFrag é distribuído como uma prova de conceito em C. Tentar detectar apenas uma Pessoa de Interesse específica deixa os defensores um passo atrás.
Primitivas de nível de chamada de sistema (Auditd)
Tanto Copy Fail quanto DirtyFrag dependem de socket(AF_ALG) para acessar o subsistema criptográfico do kernel e splice() para injetar páginas de arquivo somente leitura em buffers de rede onde operações criptográficas in-place corrompem o cache de páginas. DirtyFrag também usa socket(AF_RXRPC) como alternativa quando AF_ALG não está disponível. Esses primitivos são visíveis através da auditoria de chamadas de sistema auditd socket com valores hexadecimais a0 de 26 (AF_ALG) ou 21 (AF_RXRPC), e chamadas splice de processos não-root. Usamos esses sinais como indicadores de estágio inicial, correlacionados por meio de sequências EQL com a etapa final de escalonamento de privilégios, que consiste em obter o uid efetivo 0 de um chamador não 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 :
Criação de Namespace (Específico para DirtyFrag)
A cadeia de exploração do DirtyFrag também depende de unshare(CLONE_NEWUSER | CLONE_NEWNET) para obter capacidades de namespace. Associamos esse evento à execução de um processo raiz ou a uma chamada de sistema setuid(0) logo em seguida:
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*")
)]
Abuso genérico de binários SUID (eventos de execução de processo)
Também avaliamos opções de detecção usando apenas eventos de execução de processos, já que estes tendem a estar habilitados em mais ambientes do que a auditoria de chamadas de sistema do auditd. Uma etapa final comum para ambos os exploits é corromper ou influenciar a execução na memória de um binário SUID, como su, sudo, pkexec, passwd, chsh ou newgrp, fazendo com que ele execute código controlado pelo atacante como root.
A detecção busca execuções suspeitas em que o processo é executado com UID efetivo 0, o usuário real não é root, o processo pai também não é root, o binário SUID é iniciado com argumentos mínimos e o processo pai é um ambiente de execução de script, um comando de shell de uma linha ou um executável de um caminho gravável pelo usuário:
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
)
)
Sem depender da criação de um processo filho, também podemos buscar proativamente por atividades de exploração usando ES|QL. Tanto Copy Fail quanto DirtyFrag produzem uma sequência distinta de chamadas de sistema socket(AF_ALG) e splice() intercaladas do mesmo processo. Copy Fail itera 48 vezes para escrever 192 bytes, e DirtyFrag segue um padrão semelhante em seus caminhos ESP e RxRPC.
A consulta a seguir agrega essas chamadas de sistema por processo e exibe qualquer processo não-root que combine sockets AF_ALG ou AF_RXRPC com chamadas splice em grande 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
Regras do Auditd:
As seguintes regras podem ser adicionadas à sua configuração de integração com o Auditd para habilitar a visibilidade dessas primitivas de exploração:
-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
Regras de detecção:
- Exploração de possível falha de cópia (CVE-2026-31431) via socket AF_ALG
- Execução binária SUID suspeita
- Regra de Atividade Suspeita de Recurso do Kernel
- Manipulação de namespace usando Unshare
- Privilege Escalation via SUID/SGID
Mitigação
A detecção deve ser combinada com o reforço da segurança e a aplicação de patches. A principal solução para ambas as vulnerabilidades é atualizar o kernel do Linux assim que as correções de distribuição estiverem disponíveis.
Nos casos em que a aplicação de patches imediatos não seja possível, o bloqueio direcionado de módulos pode reduzir a superfície de ataque. Para a vulnerabilidade Copy Fail, desativar o módulo algif_aead impede o caminho AF_ALG AEAD usado pelo exploit:
echo "install algif_aead /bin/false" > /etc/modprobe.d/copyfail.conf
rmmod algif_aead 2>/dev/null
Para o DirtyFrag, desativar os módulos de rede afetados bloqueia as vias de exploração ESP e 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
Após aplicar qualquer uma das medidas de mitigação, descartar o cache de páginas garante que quaisquer páginas corrompidas na memória sejam descartadas:
echo 3 > /proc/sys/vm/drop_caches
Essas medidas de mitigação devem ser testadas em um ambiente de teste antes da implantação em produção, pois a desativação de módulos do kernel pode afetar VPNs IPsec, aplicativos criptográficos ou outros serviços, dependendo dos subsistemas afetados. A remoção do cache de páginas causa um breve pico de E/S e deve ser evitada durante períodos de pico de carga.
Restringir a criação de namespaces de usuários sem privilégios também protege contra o DirtyFrag e exploits semelhantes:
sysctl -w kernel.unprivileged_userns_clone=0
No RHEL/Fedora, use user.max_user_namespaces=0 em vez disso. Essa configuração pode afetar aplicativos que dependem de namespaces não privilegiados, como determinados ambientes de execução de contêineres e sandboxes de navegadores. Avalie a compatibilidade antes de se candidatar.
Referências:
- 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