
Análise, POC e cadeia de exploração arm64 para um heap overflow de DACL no cifs.ko do Linux (CVE-2026-97555), escalando para uma sobrescrita de credenciais uid-0 na build de referência.
English | 简体中文
Estouro de heap no kernel durante a reescrita de DACL do cliente SMB. id_mode_to_cifs_acl()
dimensiona o descritor de segurança de saída a partir da DACL retornada pelo servidor,
mas replace_sids_and_copy_aces() reescreve cada ACE com o novo
SID de owner/group (até 68 bytes). Um servidor que retorna uma DACL com SIDs curtos
faz, portanto, com que a reescrita ultrapasse a alocação e escreva além
do objeto kmalloc.
Este repositório disponibiliza a análise, um POC que reproduz a escrita entre objetos e um EXP que transforma o estouro em uma cadeia ponta a ponta controlada (vazamento de heap, recuperação de page-table, escrita arbitrária em PTE, sobrescrita da própria credencial para uid 0) na build de referência.
| Item | Valor |
|---|---|
| CVE | CVE-2026-97555 |
| Classe | Estouro de buffer de heap (escrita, CWE-122) em id_mode_to_cifs_acl() |
| Componente | cifs.ko (cliente SMB) |
| Afetado | Linux cifs.ko sem a correção (validado contra um kernel Android baseado em GKI 6.12) |
| Patch | upstream 0ee150794c75 (stable fb736ab4fb8c) |
| Build de referência | kernel Android 16 baseado em GKI 6.12, arm64, páginas de 4 KiB, struct page de 64 bytes |
| POC | Escrita entre objetos reproduzida (SET_INFO security descriptor de 696 bytes) |
| EXP | Cadeia completa até um shell uid-0 na build de referência |
/proc/kpageflags como root. Obter a capacidade de montagem
é um artifício de teste, não parte do bug, e está fora do escopo
deste repositório.uid/suid/euid/fsuid de sua própria credencial;
nenhum campo de capability ou securebits é tocado. A limpeza é um reboot.src/exp.c (endereços de símbolos, geometria do vmemmap, offsets de BTF)
pertencem à build de referência. Recalcule-as para qualquer outra
build antes de executar; veja scripts/derive_offsets.sh.id_mode_to_cifs_acl() calcula a alocação a partir de dacl_ptr->size ou
num_aces * sizeof(struct smb_ace), e então replace_sids_and_copy_aces()
reconstrói cada ACE com o novo SID de owner/group. Quando os SIDs do servidor são
curtos (0-subauth) e o novo SID tem 68 bytes, a DACL reescrita é maior
que a alocação e build_sec_desc() escreve além do objeto slab.
A instrumentação dinâmica FORTIFY do kernel verifica os armazenamentos inlined do cabeçalho de ACE,
mas as cópias de SID passam pela função out-of-line cifs_copy_sid() e
não são instrumentadas. Uma DACL alinhada de modo que todo cabeçalho de ACE permaneça dentro de
nsecdesclen - 28 alcança a cópia vulnerável.
O layout do servidor cross7 (7 ACEs, tamanhos de origem 76/76/76/44/44/16/16) faz
com que a alocação seja exatamente 512 bytes; a cópia de SID de 68 bytes do 7º ACE começa no
offset 492 e cruza a fronteira do objeto, e o descritor reescrito
cresce para 696 bytes — 184 bytes além do objeto. O descritor expandido é
ainda entregue no SET_INFO, então a escrita é observável do lado do servidor.
fb736ab4fb8c.kpageflags, mount,
chown) e uma janela com SELinux permissivo (ou uma permissão de política
equivalente) para o upcall de idmap do cifs.src/poc_smb2_acl.py, montado com
cifsacl/modefromsid através do cliente de kernel padrão
(scripts/mount_cifs.sh). A montagem em si é um artifício privilegiado; como
o testador obtém esse privilégio está fora deste repositório.cifs.idmap pré-carregadas para os ids alvo: o ambiente de referência
não possui helper de upcall /sbin/cifs.idmap (src/poc.c e src/exp.c
as pré-carregam; src/acl_key_preload.c faz isso de forma autônoma).# host
python3 src/poc_smb2_acl.py # ACL_LAYOUT=cross7 for the overflow
adb reverse tcp:1468 tcp:1468
# device (root)
sh /data/local/tmp/e2e/mount_cifs.sh 127.0.0.1 share /mnt/cifs
make && make push
/data/local/tmp/poc /mnt/cifs
Evidência esperada: o log do servidor imprime
SET_INFO security: len=696 ... (cross7), ou seja, um descritor maior que a
alocação de 512 bytes, e scripts/trace_acl02.sh mostra a
entrada/retorno de id_mode_to_cifs_acl em torno do chown.
O POC apenas aciona o caminho vulnerável; ele não faz grooming, vazamento ou escalada. O acionamento realiza uma escrita fora dos limites no kernel: builds cujo layout dispara as verificações dinâmicas do FORTIFY podem entrar em pânico, então execute-o apenas em um dispositivo de teste que você possa reiniciar.
O EXP é a cadeia completa, de ponta a ponta no build de referência:
cross7 reescreve o
datalen do payload de uma chave vizinha; keyctl_read despeja 64 KiB incluindo um array
pipe_buffer ativo. O ponteiro anon_pipe_buf_ops vazado fornece o deslocamento do KASLR e
a página de buffer P fornece o alvo real struct page.buffer[0] de um hit pipe é
reescrito com page=P, len=0x1234, ops=default_pipe_buf_ops.P; uma falha em
base a recupera como uma página de tabela de páginas, condicionada a um kretprobe que comprova
que __alloc_pages_noprof retornou P a partir de __pte_alloc.P+16 (PTE[2]); seus primeiros oito bytes são um
descritor de self-map para o próprio P. O userland então lê e escreve a
página de tabela de páginas através de base+8 KiB.KPF_SLAB são mapeados através de
PTEs novos e varridos em busca do comm desta tarefa; pid/tgid, a janela do mapa linear
e cred->uid/euid fazem a verificação cruzada do acerto, e VAs novos, nunca usados, re-verificam a página física.setresuid(2000) real (o
kernel aloca um cred novo), lê task->cred através do self-map,
zera apenas uid/suid/euid/fsuid, confirma getuid()==0, desarma o
self-map e executa execve de /system/bin/sh. O shell é executado como uid 0.make exp
make push
# on the device, as root:
sh /data/local/tmp/e2e/launch_exp.sh /mnt/cifs 10 1 r1 4000 400 2400 7 0 0 cred
# then read /data/local/tmp/e2e/e2e_status.txt and
# /data/local/tmp/e2e/root_shell.txt
O estágio cred é opcional através do último argumento (cred); o scout para
após a validação somente leitura. O EXP mantém suas PTEs dentro do processo
e a limpeza é um reboot.
Requisitos: Android NDK r29 (substitua com NDK=), GNU make, adb.