
Analisi, POC e catena di exploit arm64 per un heap overflow DACL in cifs.ko su Linux (CVE-2026-97555), con escalation fino alla sovrascrittura delle credenziali uid-0 sulla build di riferimento.
English | 简体中文
Overflow dell'heap del kernel nella riscrittura del DACL del client SMB. id_mode_to_cifs_acl()
dimensiona il security descriptor di output in base al DACL restituito dal server,
ma replace_sids_and_copy_aces() riscrive ogni ACE con il nuovo
SID di owner/group (fino a 68 byte). Un server che restituisce un DACL con SID corti
fa quindi sì che la riscrittura superi l'allocazione e scriva oltre
l'oggetto kmalloc.
Questo repository contiene l'analisi, un POC che riproduce la scrittura cross-object e un EXP che trasforma l'overflow in una catena end-to-end controllata (leak dell'heap, reclaim delle page-table, scrittura arbitraria di PTE, sovrascrittura delle proprie credenziali a uid 0) sulla build di riferimento.
| Voce | Valore |
|---|---|
| CVE | CVE-2026-97555 |
| Classe | Heap buffer overflow (scrittura, CWE-122) in id_mode_to_cifs_acl() |
| Componente | cifs.ko (client SMB) |
| Affetto | Linux cifs.ko senza la correzione (validato su un kernel Android basato su GKI 6.12) |
| Patch | upstream 0ee150794c75 (stable fb736ab4fb8c) |
| Build di riferimento | kernel Android 16 basato su GKI 6.12, arm64, pagine da 4 KiB, struct page da 64 byte |
| POC | Scrittura cross-object riprodotta (SET_INFO security descriptor da 696 byte) |
| EXP | Catena completa fino a una shell uid-0 sulla build di riferimento |
/proc/kpageflags come root. Ottenere la capability di mount
è un fixture di test, non parte del bug, ed è fuori dall'ambito di
questo repository.uid/suid/euid/fsuid delle proprie credenziali;
nessun campo capability o securebits viene toccato. La pulizia è un riavvio.src/exp.c (indirizzi dei simboli, geometria della vmemmap, offset
BTF) appartengono alla build di riferimento. Ricalcolale per qualsiasi altra
build prima dell'esecuzione; vedi scripts/derive_offsets.sh.id_mode_to_cifs_acl() calcola l'allocazione da dacl_ptr->size o
num_aces * sizeof(struct smb_ace), poi replace_sids_and_copy_aces()
ricostruisce ogni ACE con il nuovo SID di owner/group. Quando i SID del server sono
corti (0-subauth) e il nuovo SID è di 68 byte, il DACL riscritto è più lungo
dell'allocazione e build_sec_desc() scrive oltre l'oggetto slab.
La strumentazione dinamica FORTIFY del kernel controlla gli store inline dell'header
ACE, ma le copie dei SID passano attraverso cifs_copy_sid() out-of-line e
non sono strumentate. Un DACL allineato in modo che ogni header ACE rimanga dentro
nsecdesclen - 28 raggiunge la copia vulnerabile.
Il layout del server cross7 (7 ACE, dimensioni sorgente 76/76/76/44/44/16/16) rende
l'allocazione esattamente 512 byte; la copia del SID da 68 byte del 7° ACE inizia
all'offset 492 e attraversa il confine dell'oggetto, e il descriptor riscritto
cresce fino a 696 byte — 184 byte oltre l'oggetto. Il descriptor espanso
viene comunque consegnato in SET_INFO, quindi la scrittura è osservabile dal lato
server.
fb736ab4fb8c.kpageflags, mount,
chown) e una finestra SELinux permissiva (o un'allowance di policy
equivalente) per l'upcall idmap di cifs.src/poc_smb2_acl.py, montata con
cifsacl/modefromsid tramite il client kernel standard
(scripts/mount_cifs.sh). Il mount stesso è un fixture privilegiato; come
il tester ottenga quel privilegio è fuori da questo repository.cifs.idmap precaricate per gli id target: l'ambiente di riferimento
non ha un helper upcall /sbin/cifs.idmap (src/poc.c e src/exp.c
le precaricano; src/acl_key_preload.c lo fa in modo autonomo).# 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
Evidenza attesa: il log del server stampa
SET_INFO security: len=696 ... (cross7), ovvero un descrittore più lungo
dell'allocazione da 512 byte, e scripts/trace_acl02.sh mostra
l'ingresso/ritorno di id_mode_to_cifs_acl attorno al chown.
Il POC attiva solo il percorso vulnerabile; non esegue grooming, leak o escalation. Il trigger esegue una scrittura out-of-bounds nel kernel: le build il cui layout fa scattare i controlli dinamici FORTIFY possono andare in panic, quindi eseguilo solo su un dispositivo di test che puoi riavviare.
L'EXP è la catena completa, end to end sulla build di riferimento:
cross7 riscrive il datalen del payload di una
chiave adiacente; keyctl_read riversa 64 KiB inclusa un array
pipe_buffer attivo. Il puntatore anon_pipe_buf_ops trapelato fornisce
lo slide KASLR e la pagina del buffer P fornisce il vero target
struct page.buffer[0] di una
pipe hit viene riscritto con page=P, len=0x1234, ops=default_pipe_buf_ops.P; un fault
su base la recupera come pagina di page-table, vincolato da una kretprobe
che prova che __alloc_pages_noprof ha restituito P da __pte_alloc.P+16 (PTE[2]); i suoi primi otto byte
sono un descrittore di self-map per P stesso. Lo userland legge e scrive
poi la pagina di page-table tramite base+8 KiB.KPF_SLAB vengono mappati
tramite PTE fresche e scansionati alla ricerca del comm di questo task;
pid/tgid, la finestra della linear-map e cred->uid/euid verificano
incrociando il riscontro, e VA fresche, mai usate, riverificano la pagina
fisica.setresuid(2000) (il kernel alloca una cred nuova), legge task->cred
tramite la self-map, azzera solo uid/suid/euid/fsuid, conferma
getuid()==0, disarma la self-map ed esegue execve di /system/bin/sh.
La shell viene eseguita come 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
Lo stage cred è opt-in tramite l'ultimo argomento (cred); scout si ferma
dopo la validazione in sola lettura. L'EXP mantiene le sue PTE all'interno del processo
e la pulizia consiste in un riavvio.
Requisiti: Android NDK r29 (sovrascrivibile con NDK=), GNU make, adb.