
Exploit del kernel Android GKI 6.12 per CVE-2026-43499, che concatena un bug di rollback di rt_mutex con la sovrascrittura dello stack di pselect per ottenere i privilegi di root su dispositivi Samsung e Pixel.
Un exploit del kernel Linux per la linea GKI 6.12 di Android (dispositivi
Samsung e Pixel) che prende di mira CVE-2026-43499: un rollback difettoso
di remove_waiter() in rt_mutex_start_proxy_lock() che usa current invece
di waiter::task, lasciando il pi_blocked_on di un waiter puntato al suo
(later-popped) kernel stack rt_mutex_waiter. Combinato con una
sovrascrittura dello stack fd_set di pselect() e una camminata fake rb_tree
guidata da sched_setattr del consumer, produce una primitiva di scrittura
kernel deterministica e root completo.
L'exploit viene eseguito come libreria condivisa LD_PRELOAD (preload.so) e
installa un demone su root piu un wallpaper come artefatti post-root.
Stato: sviluppo attivo. Il target m1q (ZF1) e il focus del bring-up. Il bootstrap pipei tmp_page-uname e la rotta attiva corrente: la camminata e provata pulita on-device (build #33: regioni rt_mutex seedate per-child) e la scansione completa dei 168 candidati e in corso. La rotta configfs CFI e un vicolo cieco su m1q (Rust ashmem — nessuno slot fops iniettabile) e resta la rotta per i target C-ashmem. Gli offset per-target variano da dispositivo a dispositivo — verificarli rispetto al binario del kernel effettivo prima di fidarsene.
Quando FUTEX_CMP_REQUEUE_PI riaccoda un waiter e la camminata della catena
di rilevamento deadlock del requeue restituisce -EDEADLK,
__rt_mutex_start_proxy_lock() esegue il rollback tramite remove_waiter()
(rtmutex.c:1535). Il rollback rimuove correttamente il waiter dall'albero di
attesa ma azzera il pi_blocked_on del chiamante del requeue invece di
waiter->task->pi_blocked_on. Il pi_blocked_on del WAITER resta pendente sul
suo rt_mutex_waiter nello stack, che viene rimosso una volta che il futex va
in timeout.
Dopo il timeout, la regione dello stack kernel del waiter viene riutilizzata:
core_sys_select() copia i tre fd_set in quel buffer dello stack (il percorso
stack nfds < 344 su ZF1). Un array di parole fd_set artigianale
ri-materializza un finto rt_mutex_waiter / finto task / finto rt_mutex (con
una radice rb_tree i cui puntatori sono controllati). Un thread consumer
chiama poi sched_setattr_tid(waiter) → rt_mutex_adjust_pi() →
rb_erase_cached, che produce una scrittura su indirizzo arbitrario di un
valore controllato.
L'intera primitiva richiede il ciclo della catena PI: l'owner detiene
f_pi_target e si blocca anche su f_pi_chain (detenuto dal waiter), quindi
la camminata della catena incontra owner → chain → waiter → target → owner e
fallisce con -EDEADLK. Rimuovere il ciclo fa si che il requeue ritorni
successo con effetto kernel nullo.
sched_blocked_reason (primaria su m1q, provata on-device):
il return PC salvato di un kworker bloccato (stack_trace_save_tsk) viene
letto dal ring buffer e confrontato con l'offset worker_thread compilato
nel binario. Viene eseguita prima che venga usata qualsiasi parola della
rotta boot_id, cosi i target di scrittura data-alias (data_addr() = alias
p0 + slide_p0_offset) restano corretti a uno slide non nullo.SLIDE_LOGGERS_0_1 nei dati sysctl di boot_id
tramite un alias linear-map; il valore leakato ricostruisce stext.
Parcheggiata su m1q: il suo target W1
(SLIDE_RANDOM_BOOT_ID_DATA_OFF, RAM fisica bassa) ha scrivibilita
dipendente dalla pagina — lo slide sposta il target attraverso i confini
di pagina a ogni boot, quindi ~6/7 esecuzioni vanno in fault. Viene
esercitata esplicitamente solo tramite SLIDE_FORCE_BOOTID (test del
meccanismo) con tree_pc/tree_left reindirizzati alla pagina spruzzata.Le parole fd_set di pselect ri-materializzano un finto rt_mutex_waiter sullo
stack kernel del waiter; il sched_setattr_tid(waiter) di un thread consumer
lo percorre tramite rt_mutex_adjust_pi(). Mappa parole ZF1 (verificata via
disasm): PSELECT_WAITER_WORD_SHIFT = 0, parola 12 = task (@+0x50), parola
13 = lock (@+0x58), parola 14 = wake_state (@+0x60 = 3). Con un finto
rt_mutex seedato la camminata si completa pulitamente: le W1
*(tree_left)=tree_pc / W2 *(tree_pc&~3+8)=tree_left di rb_erase di [7]
sono le uniche scritture kernel; [11] (setprio / dequeue_pi) e il wake di [9]
vengono entrambi saltati (il finto waiter locale a prio 100 tiene il nodo
dello stack fuori dal top-waiter). Ogni completamento entrato nella camminata
dalla build #19 sopravvive on-device; i crash deterministici precedenti erano
un errore di posizionamento del payload di 8 byte (SKB_DATA_DELTA), non un
fault del corpo della camminata.
Fase opzionale STAGE3=1 che fork-a un figlio bridge, scambia il suo
mm->pgd con una tabella di pagine fake preparata (3 livelli: PGD→L1→L2,
foglia RX loop + foglia RW stack) e lascia che il figlio esegua un blob
assembly di soli registri che patcha le proprie cred attraverso una finestra
di scansione fisica da 2MB — phys R/W su tutta la RAM senza le primitive
configfs/pipe. Attualmente cablata solo nel target m1q e non e stata
eseguita on-device.
41 target in src/targets/<codename>-<build>/, ognuno richiede come minimo un
target.h (offset dei simboli del kernel, KIMAGE_TEXT_BASE, base della
direct-map, offset delle struct). I target Pixel (comet, tokay, tegu,
caiman, komodo, frankel, mustang, rango, stallion, blazer)
sovrascrivono i sorgenti condivisi; i target Samsung (m1q-*) aggiungono
logica specifica del dispositivo.
make list-projects # full list
I layout delle struct del kernel differiscono per dispositivo — gli offset devono essere verificati rispetto al binario del kernel effettivo / kallsyms per ogni target.
Output: build/<PROJECT>/bin/preload.so (libreria condivisa caricata via
LD_PRELOAD).
# Default project
CC=clang make
# Specific device target (default: blazer-CP2A.260605.012)
CC=clang make PROJECT=m1q-BP4A.251205.006
# Without CC=clang: uses NDK if found ($NDK_ROOT / $ANDROID_NDK_HOME /
# $ANDROID_NDK_ROOT), otherwise host clang + Android sysroot
make PROJECT=m1q-BP4A.251205.006
# Show build configuration
make info
# Clean
make clean
La build incorpora un binario PIE su_daemon (src/su_daemon.c, compilato in
build/embed/su_daemon_aarch64_pie) e assets/wallpaper.webp in preload.so
tramite src/su_blob.S / src/wallpaper_blob.S.
Pusha gli output della build in /data/local/tmp, poi esegui l'exploit sotto
LD_PRELOAD. Esegui SENZA tee su un target di bring-up attivo — tee bufferizza
il proprio stdio e perde la coda del log su un kernel panic (lo stdout
dell'exploit e non bufferizzato, quindi reindirizzare direttamente a un file
preserva ogni riga):
adb push build/m1q-BP4A.251205.006/bin/preload.so /data/local/tmp/preload.so
adb push build/embed/su_daemon_aarch64_pie /data/local/tmp/su_daemon_aarch64_pie
adb shell "chmod 755 /data/local/tmp/preload.so /data/local/tmp/su_daemon_aarch64_pie"
adb shell "LD_PRELOAD=/data/local/tmp/preload.so \
/data/local/tmp/su_daemon_aarch64_pie \
> /data/local/tmp/output.log 2>&1"
In caso di successo il demone ascolta su /data/local/tmp/temp_su.sock e su
viene installato in /apex/com.android.virt/bin.
tools/run_pipei.sh pusha il binario, ne verifica lo sha256 on-device (un
preload.so obsoleto produce verdetti silenziosamente illeggibili — la scala
ha eseguito una volta una build < #22 il cui formato di log differisce), poi
esegue la scansione completa dei 168 candidati suddivisa in chunk 3x56 cosi un
panic perde al massimo un chunk (~10 min). Lo stdout dell'exploit viene
trasmesso direttamente attraverso adb (nessun redirect on-device — un tee
lato dispositivo perde la coda su panic); una copia lato host di ogni chunk
viene mantenuta in /tmp/m1q_pipei_chunk{1..3}_stream.log:
sh tools/run_pipei.sh
PASS = qualsiasi chunk mostra tmp_page uname changed + after uname='CatOS.
PIPEI_CHILD_REGIONS deve essere impostato almeno alla dimensione del chunk,
altrimenti le regioni rt_mutex per-child restano non seedate e la scansione va
in panic al candidato 0. La copertura completa richiede anche
TMP_UNAME_PIPEI_SPLIT_ORDER_PAGES=1 — senza di esso viene scansionata solo
una pagina del blocco mm liberato di 8 pagine. run_pipei_slim.sh e la
variante diagnostica a singolo candidato; run_m1q_ladder.sh e la batteria
walk/isolation (oracolo B16-2, meccanismo slide, passi della scansione).
exploit/
Makefile
assets/ # wallpaper.webp (embedded post-root artifact)
src/
main.c # run_exploit() entry point (shared)
preload.c # LD_PRELOAD constructor, su/wallpaper install
util.c # addr translation, page prep, configfs helpers
pipe.c # pipe-based physical read/write
root.c # cred patching / task walk
slide.c # KASLR slide (tracefs + boot_id routes)
fops.c # pselect corruption + fops manipulation
faketables.c # stage-3 fake page-table builders (3-level)
stage3.c # pgd-swap bridge child (no-op unless configured)
stage3_loop.S, stage3_poll.S # bridge blob + enter trampoline
su_daemon.c # root daemon (compiled as separate PIE)
su_blob.S, wallpaper_blob.S # embedded binaries
offset.h # pulls per-target target.h via -DTARGET_CONFIG_H
kernelsnitch/ # mm_struct futex-hash leak library
targets/<codename>-<build>/ # per-device target.h + optional overrides
poc/poc.c # standalone POC (separate from the exploit)
I target sovrascrivono i sorgenti condivisi tramite il meccanismo pick_src
del Makefile: se src/targets/<PROJECT>/<file>.c esiste viene usato al posto
del condiviso src/<file>.c; altrimenti viene usata la versione condivisa.
-EDEADLK (errno 35). Un ciclo
mancante si manifesta come success=1 con effetto kernel nullo.PSELECT_WAITER_WORD_SHIFT deve essere 0 (profondita stack ZF1
verificate dall'ELF). Gli altri target hanno ancora default 1 — verificare
per dispositivo prima di fidarsene.p0_data_alias() usa DIRECT_MAP_BASE (la linear map), che differisce
da P0_PAGE_OFFSET su ZF1. Gli alias dati per oggetti kernel dereferenziati
devono usare la linear map.pr_error() e fatale (exit(-1)), a differenza delle macro
warning/info.SLIDE_LOGGERS_0_1, non
SLIDE_NFULNL_LOGGER; la parola e un alias linear-map non randomizzato,
quindi la rotta boot_id e indipendente dallo slide (usarla solo a slide=0 —
eseguire prima il leak tracefs).Questo e codice di exploit per dispositivi Android ed e destinato alla ricerca di sicurezza, a test di vulnerabilita autorizzati e ad attivita difensive su hardware di proprieta o per cui si ha esplicita autorizzazione al test. L'uso improprio su dispositivi non propri e illegale. L'autore non e responsabile per dispositivi brickati, kernel brickati o garanzie decadute.
mm_structmm_structwrite_iter iniettabile e l'oggetto heap
ASHMEM_SET_NAME e un KVec il cui layout non si allinea con il
private_data di configfs_bin_write_iter. m1q passa a una scrittura
kmalloc: recupera il blocco mm order-3 leakato come oggetti
pipe_inode_info e usa la scrittura pselect W1 (*(tree_left) = tree_pc)
per sovrascrivere lo slot tmp_page di un candidato (+0x90) con la pagina
del namespace UTS (init_uts_ns). Una scrittura fanout pianta un nome
marker all'offset sysname e uname() riporta "CatOS" — una scrittura
kernel verificabile senza dipendenza da oggetti statici. I target C-ashmem
mantengono il percorso configfs.preload.c installa il demone su incorporato (montato su tmpfs in
/apex/com.android.virt/bin, piu varianti adbd-namespace e locali) e
scambia il wallpaper.| Variabile | Default | Scopo |
|---|
PSELECT_ROUTE_SHIFT | compile-time | Shift A/B della parola fd_set per la rotta principale (m1q) |
SLIDE_SHIFT | compile-time | Shift A/B della parola fd_set per la rotta slide |
PSELECT_ROUTE_DELAY_USEC | 50000 | Ritardo del consumer prima di sched_setattr (deve essere > 0) |
SLIDE_CONSUME_USEC, SLIDE_ENTER_DELAY_USEC, SLIDE_CONSUMER_CORE | — | Timing/pinning della rotta slide |
SKIP_SLIDE | 0 | Usa il fallback direct-map (slide = 0) |
SLIDE_ONLY | 0 | Esegui solo lo slide KASLR ed esci |
SLIDE_P0_OFFSET, SLIDE_BOOTID_OFF | — | Override degli offset slide p0-alias / boot_id |
SLIDE_FORCE_BOOTID | 0 | Forza la rotta boot_id pselect dopo lo slide tracefs (test del meccanismo) |
KSNITCH_COLLISIONS | 4 | Conteggio collisioni KernelSnitch |
STAGE3 | 0 | Abilita la fase figlio bridge pgd-swap (m1q) |
STAGE3_DRYRUN | 0 | Prepara/verifica le tabelle poi abortisce prima dello swap |
PAGE_RECLAIM_SENDS, PSELECT_SIMPLE_LAYOUT | — | Ritocchi allo spray/layout fd-set |
TMP_PAGE_UNAME | 0 | Esegui il bootstrap pipei tmp_page-uname di m1q |
TMP_UNAME_PIPEI_SWEEP, TMP_UNAME_PIPEI_ALIGN_ORDER, TMP_UNAME_PIPEI_SPLIT_ORDER_PAGES | — | Modalita scansione; split_order scansiona tutte le 8 pagine del blocco mm liberato |
TMP_UNAME_PIPEI_SLOT, TMP_UNAME_PIPEI_SLOT_CANDIDATES | — | Intervallo di scansione (inizio chunk / conteggio) |
PIPEI_CHILD_REGIONS | 0 | Regioni rt_mutex seedate per-child; DEVE essere >= chunk della scansione o le regioni non seedate vanno in panic al candidato 0 |
PIPEI_RECLAIM_COUNT, PIPEI_DRAIN_COUNT | — | Coppie pipe di reclaim/drain; aumentare in caso di reclaim miss |
PIPEI_SPONGE_MB | 3072 | Spugna flood Order-0 mantenuta durante il reclaim (build #29) |
PSELECT_W0_PRIO_OVERRIDE | 130 | Prio del top-waiter; inerte con regioni seedate (guardia legacy stale-tree, solo A/B) |
PSELECT_SKB_DELTA_OVERRIDE | 0 | Offset intra-pagina del payload A/B (solo se un dispositivo differisce da -0xe78) |
TMP_UNAME_NAME, TMP_UNAME_HOLD_SEC | "CatOS", 5 | Nome marker / tempo di hold dopo il successo |
SKB_DATA_DELTA e -0xe78 (misurato on-device, build #19) — la vera testa
del frag skb e 8 byte in meno del -0xe80 derivato dal disasm; quell'errore di
8 byte ha causato ogni crash deterministico di pselect dalle build #6-18
(walk-entered → deref spazzatura). Valore condiviso GKI 6.12; le copie
per-target di util.c portano una manopola A/B env SKB_DATA_DELTA. NON
impilare PSELECT_SKB_DELTA_OVERRIDE sopra la costante cablata.ASHMEM_SET_NAME non si
allinea con configfs_bin_write_iter. Usa il bootstrap pipei tmp_page. Le
guardie PSELECT_WRITE_RIGHT_SPRAYED / PSELECT_PI_TOP_TASK_SELF in
target.h di m1q reindirizzano le scritture [11] della camminata fuori dai
fops statici RO.payload+0x6000 + idx*0x40) dentro la pagina recuperata. Le scansioni con
fake_lock condiviso andavano in panic al candidato ~9-12 (nodi stale dello
stack kernel riciclati), e le regioni vuote della build #32 andavano in panic
al candidato 0 (wake [9] owner==NULL sul fake_task riempito di 0x41).
PIPEI_CHILD_REGIONS deve essere impostato o le regioni restano non seedate.