
Prova de conceito de exploit para CVE-2026-31431, uma escalada local de privilégios no kernel Linux via splice() e opções de socket manipuladas, permitindo escrita arbitrária na memória do kernel para sobrescrever binários setuid.
copyFail.py é um exploit de prova de conceito (PoC) que demonstra uma vulnerabilidade de escalonamento de privilégios local no kernel Linux. Ao abusar da chamada de sistema splice() em combinação com opções de socket manipuladas, o exploit obtém a capacidade de corromper memória arbitrária do kernel. Ele usa essa capacidade para sobrescrever qualquer binário setuid no disco com um executável ELF personalizado e, em seguida, executa o binário adulterado para obter privilégios elevados.
Tipo de ataque: Escalonamento de privilégios local (LPE) Impacto: Acesso total a shell root (ao mirar binários setuid-root) Complexidade: Baixa (script único, sem dependências externas)
Uso:
./copyFail_cleaned.py [binário_alvo] # padrão: /usr/bin/su
O PoC original tinha /usr/bin/su codificado, mas a vulnerabilidade subjacente do kernel se aplica a qualquer arquivo — o atacante pode sobrescrever qualquer binário setuid que escolher. A versão limpa aceita um argumento opcional de linha de comando para especificar o alvo, tornando esse comportamento explícito.
O kernel Linux fornece uma chamada de sistema splice() que copia dados entre dois descritores de arquivo inteiramente dentro do kernel — sem copiar dados para o espaço do usuário primeiro. Isso é chamado de "I/O de cópia zero" e é usado para desempenho em aplicações de alta vazão, como servidores web e proxies reversos.
A vulnerabilidade surge porque o kernel não valida adequadamente o estado interno de certas estruturas de socket quando splice() é usado em sockets PACKET que foram configurados com opções de socket de nível de protocolo não suportadas. Ao manipular cuidadosamente essas opções, um atacante pode corromper ponteiros do kernel e, em seguida, usar splice() para redirecionar escritas de memória do kernel para locais arbitrários.
Em termos simples: o kernel tem um atalho para mover dados internamente, e este exploit engana esse atalho para escrever dados no lugar errado — especificamente, na cópia em memória de um binário do sistema.
O exploit segue uma sequência clara de etapas:
O exploit cria um socket PACKET bruto (AF_PACKET / SOCK_RAW) e o vincula a endereços intencionalmente malformados. Em seguida, chama setsockopt() usando SOL_PNIO (nível de protocolo 279), que é uma constante de nível Solaris que o Linux não reconhece.
Por que isso importa: Quando o kernel encontra um nível de protocolo não suportado em setsockopt(), ele cai em um manipulador genérico que não valida os dados adequadamente. Isso deixa a memória do kernel em um estado inconsistente e explorável.
Após estabelecer uma conexão de socket via accept(), o exploit envia mensagens de controle (ancillary) manipuladas usando sendmsg(). Essas mensagens têm cabeçalhos de comprimento deliberadamente incompatíveis — alguns afirmam ser mais curtos do que são, outros afirmam ser mais longos.
Por que isso importa: O parser de mensagens de controle do kernel usa esses comprimentos para aritmética de ponteiros. Comprimentos incompatíveis fazem o parser ler ou escrever além dos limites pretendidos, corrompendo estruturas adjacentes do kernel — especificamente, ponteiros que splice() seguirá posteriormente.
O exploit cria um pipe e chama splice() duas vezes:
/usr/bin/su para o pipe.Por que isso importa: A chamada splice() segue os ponteiros corrompidos do kernel plantados na Etapa 2, fazendo com que escreva dados em um endereço de memória do kernel escolhido pelo atacante, em vez do buffer do socket. Isso dá ao exploit uma primitiva de escrita arbitrária na memória do kernel.
O payload do exploit é um executável ELF de 160 bytes (veja abaixo). Cada chamada à função do exploit escreve exatamente 4 bytes. Portanto, o loop principal executa 40 vezes (160 / 4 = 40), cada vez:
Após todas as 40 iterações, os primeiros 160 bytes de /usr/bin/su foram sobrescritos.
Finalmente, o exploit executa os.system("su"). O kernel carrega o /usr/bin/su modificado do cache de páginas (que agora contém o shellcode do atacante em vez do binário su real), e o shellcode é executado.
O payload embutido descomprime para um executável ELF x86-64 de 160 bytes contendo este shellcode:
; Tentativa de syscall 105 (execveat) — pode não estar disponível em kernels mais antigos
xor eax, eax
xor edi, edi
mov al, 0x69 ; syscall 105
syscall
; Fallback: syscall 59 (execve) — o caminho confiável
lea rdi, [rip+0xf] ; RDI = ponteiro para "/bin/sh"
xor esi, esi ; RSI = NULL (envp)
push 0x3b ; número da syscall 59
pop eax
cdq ; RDX = NULL (argv)
syscall
; Sair limpo
xor edi, edi
push 0x3c ; syscall 60 (exit)
pop eax
syscall
; Seção de dados: "/bin/sh\0\0\0"
O que faz: Gera /bin/sh sem argumentos, herdando os privilégios do processo que executou su. Se o exploit for executado como root (ou com capacidades que concedem acesso root ao su), o shell resultante é um shell root.
O que NÃO faz:
Este é um escalonamento de privilégios direto de uso único, consistente com um PoC de pesquisa.
copyFail.py
├── hex_to_bytes() — decodificador de string hex
├── exploit_splice() — exploit principal (configuração de socket + corrupção + splice)
│ ├── Fase 1: Criar socket PACKET, bind, setsockopt (SOL_PNIO)
│ ├── Fase 2: accept() da conexão
│ ├── Fase 3: sendmsg() com mensagens ancillary manipuladas
│ ├── Fase 4: pipe() + splice() para corromper memória do kernel
│ └── Fase 5: tentativa de recv() (consolida a corrupção)
└── Loop principal:
├── Abrir /usr/bin/su (somente leitura)
├── Descomprimir payload embutido (zlib → ELF de 160 bytes)
├── Loop: injetar 4 bytes por iteração (40 iterações no total)
└── Executar su adulterado → shell root
O exploit abre /usr/bin/su com O_RDONLY (somente leitura). Ele não precisa de acesso de escrita porque a corrupção acontece através do cache de páginas do kernel — a cópia em memória do arquivo que o kernel usa antes de gravar as alterações no disco. A escrita baseada em splice() contorna completamente as permissões normais de arquivo, indo direto para a memória do kernel.
/usr/bin/su modificado (compare o hash com o gerenciador de pacotes)setsockopt() com níveis de protocolo desconhecidossplice() em descritores de arquivo não regularessysctl para restringir a criação de sockets AF_PACKET por usuários sem privilégioscopyFail.py (como distribuído)copyFail_cleaned.py (mesmo diretório)net/packet/, fs/splice.c, net/core/sock.cEste relatório é apenas para fins de pesquisa de segurança e análise defensiva.
| Constante | Valor | Propósito no Exploit |
|---|
AF_PACKET | 17 | Família de socket de pacote bruto |
SOCK_RAW | 3 | Tipo de socket bruto |
SOL_PNIO | 279 | Nível de protocolo não suportado (Solaris) |
MSG_DONTWAIT | 0x400 | Flag de sendmsg não bloqueante |