
Portabilidade multiplataforma em C do Copy Fail Linux LPE (CVE-2026-31431). Divulgado em 2026-04-29 por Theori / Xint.
English (en) ∙ 日本語 (ja) ∙ 简体中文 (zh-cn) ∙ 한국어 (ko) ∙ Русский (ru)
Uma reimplementação em C multiplataforma do LPE (Escalada de Privilégio Local) do Linux Copy Fail (CVE-2026-31431), divulgado em 2026-04-29 por Theori / Xint. Veja o writeup canônico em copy.fail para a descrição completa da vulnerabilidade, cronograma e processo de descoberta da Theori.
O proof-of-concept lançado publicamente é um script Python de 732 bytes. Esta porta em C demonstra que o mesmo exploit pode ser expresso como C portátil compilável para qualquer arquitetura que o nolibc suporta, sem blobs hex por arquitetura ou assembly inline na própria fonte do projeto.
Autor desta porta: Tony Gies [email protected]. Descoberta e divulgação original: Theori / Xint.
copy-fail-c/
├── exploit.c o dropper (variante de mutação binária)
├── exploit-passwd.c o dropper (variante de inversão de UID do /etc/passwd)
├── vulnerable.c verificador de vulnerabilidade não destrutivo
├── payload.c o corpo que é dropado (setgid+setuid+execve sh)
├── utils.c, utils.h primitiva de mutação de cache de página compartilhada AF_ALG/splice
├── Makefile orquestração de compilação
├── nolibc/ fornecido de torvalds/linux tools/include/nolibc
└── README.md este arquivo
Após make:
├── payload tiny static ELF, embedded into the dropper as bytes
├── payload.o payload wrapped as a relocatable .o by `ld -r -b binary`
├── exploit dropper, variante de mutação binária
├── exploit-passwd dropper, variante de inversão de UID do /etc/passwd
└── vulnerable verificador de vulnerabilidade não destrutivo
exploit.c abre o binário alvo como somente leitura, então para cada janela de 4 bytes do payload embutido executa uma AEAD-decrypt falsa através do AF_ALG cuja entrada de ciphertext é fornecida via splice() a partir das páginas de cache de página do alvo. A otimização in-place do template authencesn trata as páginas de origem spliceadas tanto como entrada de ciphertext quanto como destino de plaintext, então o decrypt (com falha) já sobrescreveu a página de cache de página no momento em que a verificação de autenticação rejeita a requisição. Após 4 * N iterações a imagem em cache do alvo foi substituída byte a byte pelo payload. Executar execve() no alvo carrega as páginas mutadas; o inode no disco ainda é setuid root, então o kernel concede credenciais de root e executa o payload.
payload.c é C portátil simples: setgid(0); setuid(0); execve("/bin/sh", ...). nolibc fornece o _start, a maquinaria de syscall e o malabarismo de registradores por arquitetura.
Uma segunda variante, exploit-passwd.c, muta quatro bytes do cache de página do /etc/passwd em vez da imagem de um binário setuid. Ela não precisa de payload embutido e funciona em sistemas onde a rota de mutação binária está bloqueada, mas sua superfície de cashout é muito mais estreita.
vulnerable.c não é um exploit. Ele cria um testfile local contendo a string init, então executa a mesma primitiva patch_chunk() contra o cache de página do próprio arquivo para sobrescrever os bytes com vulnerable. Se o conteúdo lido de volta corresponder, o kernel em execução está na janela do CVE-2026-31431. O inode no disco nunca é modificado; testfile é removido na saída; a mutação do cache de página evapora com ele. Executa sem privilégios. Sai com 100 se vulnerável, 0 se a primitiva executou mas a mutação não ocorreu, 2 se a família de socket AF_ALG ou o template authencesn estiver indisponível, então o estado de patch não pode ser determinado, e 1 para outros erros em tempo de execução.
Padrão (nativo da arquitetura do host):
make
Compilação cruzada para aarch64 (ou qualquer outra arquitetura Linux para a qual um cross-toolchain está instalado):
make CC=aarch64-linux-gnu-gcc LD=aarch64-linux-gnu-ld
Arquiteturas suportadas pelo nolibc fornecido (de acordo com upstream): x86_64, i386, arm, aarch64, riscv32/64, mips, ppc, s390x, loongarch, m68k, sh, sparc. O nolibc despacha com base nas macros de arquitetura do compilador, então escolher o CC/LD correto é suficiente.
Necessário para compilar:
cc, gcc, ou qualquer variante cruzada)ld -r -b binary (ld do binutils e lld ambos suportam)linux/if_alg.h e <asm/unistd.h> (Debian/Ubuntu: linux-libc-dev; variantes cruzadas: normalmente trazidas pelo pacote cross-toolchain)Conjuntos de cabeçalhos anteriores ao Linux 5.6 são anteriores a __kernel_old_time_t e struct __kernel_old_timespec, que o nolibc fornecido usa. compat.h (forçadamente incluído na compilação do payload) os fornece quando ausentes, então um linux-libc-dev mais antigo ainda compila. É um no-op em cabeçalhos 5.6+.
Não há dependências de bibliotecas externas. O payload é compilado freestanding contra o nolibc; o dropper faz link contra a libc do host apenas para fprintf e perror.
A few small toolchain features carry most of the weight in keeping the source portable and the payload small.
nolibc/ é a substituição minúscula de libc apenas com cabeçalhos do kernel, fornecida de torvalds/linux tools/include/nolibc/. Ela fornece _start, uma macro syscall() portátil e wrappers inline de syscall, com as convenções de registradores por arquitetura codificadas em nolibc/arch-*.h. Compilar o payload com -nostdlib -static -ffreestanding -Inolibc produz um tiny static ELF que chama o kernel diretamente sem arrastar a inicialização do glibc, TLS init ou encanamento de stack-canary. Resultado após empacotamento e remoção de seções (ambos abaixo): ~720 bytes no x86_64, ~1.2 KB no aarch64, versus ~17 KB para o mesmo payload.c linkado contra musl-static ou ~700 KB contra glibc-static.
ld -r -b binary para embutirO Makefile transforma o payload ELF construído em payload.o via ld -r -b binary -o payload.o payload. O linker emite os bytes de entrada textualmente como a seção de dados de um arquivo objeto relocável e sintetiza três símbolos a partir do nome do arquivo de entrada:
_binary_payload_start endereço do primeiro byte do payload
_binary_payload_end endereço um byte após o último byte do payload
_binary_payload_size símbolo absoluto cujo valor é o tamanho em bytes
exploit.c declara os dois primeiros como extern const unsigned char[] e calcula o tamanho como _binary_payload_end - _binary_payload_start.
-Wl,-N mais max-page-size apertadoO payload é linkado estaticamente com -Wl,-N -Wl,-z,max-page-size=0x10, que colapsa .text/.rodata/.data em um único segmento LOAD com alinhamento de arquivo de 16 bytes em vez do padrão de 4 KB por segmento alinhado à página do kernel. Isso produz um aviso "RWX permissions" do ld, que é apenas informacional – a proteção de memória em tempo de execução do payload não importa para seu programa de propósito único. Sem essa flag, o mesmo código linka para ~13 KB no x86_64 (principalmente preenchimento zero entre segmentos); com ela, ~1.3 KB antes da remoção do cabeçalho de seção abaixo.