Skip to content
KitploitKITPLOIT
StrumentiBlog
Invia
StrumentiBlog
Invia

Strumenti di Hacking, PenTest e Cybersecurity per il tuo Arsenale di Sicurezza!

Kitploit è una directory di strumenti di hacking, cybersecurity e pentesting. Scopri gli ultimi aggiornamenti dei progetti per trovare vulnerabilità, analizzare sistemi, automatizzare i test e rafforzare la tua sicurezza.

··Feed·Contatto·Privacy·© 2026 Kitploit

Directory degli strumenti

Categorie

Vedi tutte le categorie
Loading categories
Pixel_GPU_Exploit — Exploit del kernel Android 14 per Pixel7/8 Pro | Kitploit
Strumenti/GitHubGitHub/0x36/pixel_gpu_exploit
Sicurezza AndroidEscalation di PrivilegiMemory ForensicsAnalisi delle VulnerabilitàExploitApprendimento e FormazioneBinary Exploitation
GitHub0x36/pixel_gpu_exploit

Pixel_GPU_Exploit

Exploit del kernel Android 14 per Pixel7/8 Pro

Vedi Repository
557882 anni faRevisionato da Kitploit

Più Popolari

Vedi tutti →

Scopri gli strumenti più utilizzati dalla nostra community.

Esplora tutti gli strumenti

Sfoglia la nostra collezione di strumenti

Vedi tutti gli strumenti →
Condividi

Mali GPU Kernel LPE

Questo articolo fornisce un'analisi approfondita di due vulnerabilità del kernel nella GPU Mali, raggiungibili dalla sandbox applicativa predefinita, che ho identificato e segnalato autonomamente a Google. Include un exploit del kernel che raggiunge capacità arbitrarie di lettura/scrittura del kernel. Di conseguenza, disabilita SELinux ed eleva i privilegi a root sui modelli Google Pixel 7 e 8 Pro con le seguenti versioni di Android 14:

  • Pixel 8 Pro: google/husky/husky:14/UD1A.231105.004/11010374:user/release-keys
  • Pixel 7 Pro: google/cheetah/cheetah:14/UP1A.231105.003/11010452:user/release-keys
  • Pixel 7 Pro: google/cheetah/cheetah:14/UP1A.231005.007/10754064:user/release-keys
  • Pixel 7: google/panther/panther:14/UP1A.231105.003/11010452:user/release-keys (di m4b4 (Marcel))

Vulnerabilità

Questo exploit sfrutta due vulnerabilità: un overflow di interi derivante da una patch incompleta nel comando ioctl gpu_pixel_handle_buffer_liveness_update_ioctl, e una fuga di informazioni nei buffer dei messaggi del timeline stream.

Underflow del buffer in gpu_pixel_handle_buffer_liveness_update_ioctl() dovuto a una correzione errata dell'overflow di interi

Google ha risolto un overflow di interi nel comando ioctl gpu_pixel_handle_buffer_liveness_update_ioctl in questo commit. All'inizio, quando ho segnalato il problema, pensavo che il bug fosse causato da un difetto nella patch descritta in precedenza. Dopo aver riesaminato il report, mi sono reso conto che la mia analisi della vulnerabilità era imprecisa. Nonostante la mia iniziale ipotesi che la patch fosse incompleta, essa risolve e previene effettivamente un underflow nel calcolo. Questo mi ha portato a sospettare che la modifica non fosse stata applicata nelle build di produzione. Tuttavia, sebbene io possa causare un underflow nel calcolo, non è possibile causare un overflow. Ciò suggerisce che il comando ioctl sia stato parzialmente corretto, anche se non con la patch mostrata sopra. L'analisi con IDA ha rivelato che un'altra patch incompleta è stata distribuita nelle release di produzione, e questa patch non è presente in alcun ramo git del modulo kernel della GPU Mali.

Questa vulnerabilità è stata scoperta per la prima volta nell'ultima versione di Android e segnalata il 19 novembre 2023. In seguito, Google mi ha informato che l'aveva già identificata internamente e le aveva assegnato CVE-2023-48409 nel bollettino sulla sicurezza Android di dicembre, etichettandola come problema duplicato. Sebbene sia riuscito a verificare che il bug era stato identificato internamente mesi prima della mia segnalazione (in base alla data del commit intorno al 30 agosto), permangono dei dubbi. In particolare, è strano che i livelli di patch di sicurezza (SPL) di ottobre e novembre dei dispositivi più recenti fossero ancora affetti da questa vulnerabilità — non ho indagato sulle versioni precedenti. Pertanto, non sono in grado di determinare in modo conclusivo se si trattasse davvero di un problema duplicato e se la patch appropriata fosse effettivamente prevista per dicembre prima della mia segnalazione, o se ci sia stata una svista nella gestione di questa vulnerabilità.

Ad ogni modo, ciò che rende potente questo bug è il seguente:

  • Il buffer info.live_ranges è completamente controllato dall'utente.
  • I valori che causano l'overflow sono input controllati dall'utente; pertanto, possiamo provocare un overflow nel calcolo, così che il puntatore info.live_ranges possa trovarsi a un offset arbitrario prima dell'inizio dell'indirizzo di kernel di buff.
  • Anche la dimensione dell'allocazione è un input controllato dall'utente, il che offre la possibilità di richiedere un'allocazione di memoria da qualsiasi allocatore slab generico.

Questa vulnerabilità condivide somiglianze con la vulnerabilità di buffer underflow DeCxt::RasterizeScaleBiasData() che ho trovato e sfruttato nel kernel di iOS 15 nel 2022.

Fuga di puntatori del kernel nei buffer dei messaggi del Timeline Stream

La GPU Mali implementa un timeline stream personalizzato progettato per raccogliere informazioni, serializzarle e successivamente scriverle in un ring buffer seguendo un formato specifico. Gli utenti possono invocare il comando ioctl kbase_api_tlstream_acquire per ottenere un file descriptor che consente loro di leggere da questo ring buffer. Il formato dei messaggi è il seguente:

  • Un header del pacchetto

  • Un ID del messaggio

  • Un buffer di messaggi serializzato, il cui contenuto specifico dipende dall'ID del messaggio. Ad esempio, la funzione __kbase_tlstream_tl_kbase_kcpuqueue_enqueue_fence_wait serializza i puntatori del kernel kbase_kcpu_command_queue e dma_fence nel buffer dei messaggi, causando la fuga di puntatori del kernel verso un processo dello spazio utente.```c void __kbase_tlstream_tl_kbase_kcpuqueue_enqueue_fence_wait( struct kbase_tlstream *stream, const void *kcpu_queue, const void *fence ) { const u32 msg_id = KBASE_TL_KBASE_KCPUQUEUE_ENQUEUE_FENCE_WAIT; const size_t msg_size = sizeof(msg_id) + sizeof(u64) + sizeof(kcpu_queue) + sizeof(fence) ; char *buffer; unsigned long acq_flags; size_t pos = 0;

    buffer = kbase_tlstream_msgbuf_acquire(stream, msg_size, &acq_flags);

    pos = kbasep_serialize_bytes(buffer, pos, &msg_id, sizeof(msg_id)); pos = kbasep_serialize_timestamp(buffer, pos); pos = kbasep_serialize_bytes(buffer, pos, &kcpu_queue, sizeof(kcpu_queue)); pos = kbasep_serialize_bytes(buffer, pos, &fence, sizeof(fence));

    kbase_tlstream_msgbuf_release(stream, acq_flags); }

root@kitploit:~
L'exploit proof of concept fa trapelare l'indirizzo dell'oggetto `kbase_kcpu_command_queue` monitorando l'ID del messaggio `KBASE_TL_KBASE_NEW_KCPUQUEUE`, che viene inviato dalla funzione `kbasep_kcpu_queue_new` ogni volta che viene allocato un nuovo oggetto kcpu queue.

Google mi ha informato che la vulnerabilità è stata segnalata a marzo 2023 ed è stata assegnata la [CVE-2023-26083](https://source.android.com/docs/security/bulletin/2023-07-01) nel loro bollettino di sicurezza. Ciononostante, sono riuscito a riprodurre il problema sugli ultimi dispositivi Pixel forniti con i livelli di patch di sicurezza (SPL) di ottobre e novembre, il che indica che la correzione non era stata applicata correttamente o non era stata applicata affatto. Successivamente, Google ha risolto rapidamente il problema nel bollettino degli aggiornamenti di sicurezza di dicembre senza riconoscere il merito, e in seguito mi ha informato che il problema era stato considerato un duplicato. La logica alla base della classificazione di questo problema come duplicato, tuttavia, rimane discutibile.

## Sfruttamento
---
Quindi ho due vulnerabilità interessanti. La prima offre una potente capacità: modificare il contenuto di qualsiasi indirizzo di kernel allineato a 16 byte che si trovi prima dell'indirizzo dell'oggetto ~buff~ allocato. La seconda vulnerabilità fornisce indizi sulle potenziali posizioni degli oggetti all'interno della memoria del kernel.

### Note sui valori buffer_count e live_ranges_count
Con il controllo totale sui campi `buffer_count` e `live_ranges_count`, ho la flessibilità di selezionare lo slab di destinazione e l'offset preciso in cui intendo scrivere. Tuttavia, selezionare i valori per `buffer_count` e `live_ranges_count` richiede un'attenta valutazione a causa di diversi vincoli e fattori:
- Entrambi i valori sono correlati e l'overflow si verificherà solo se tutti i controlli di nuova introduzione vengono bypassati.
- Il requisito che l'offset negativo sia allineato a 16 byte limita la possibilità di scrivere in una posizione qualsiasi. Tuttavia, in genere questo non rappresenta un ostacolo significativo.
- Optare per un offset più ampio comporta la scrittura di una grande quantità di dati in aree di memoria che potrebbero non essere i bersagli previsti. Ad esempio, se la dimensione dell'allocazione va in overflow a `0x3004`, il puntatore `live_ranges` verrebbe impostato a `-0x4000` byte dallo spazio allocato per l'oggetto `buff`. La funzione `copy_from_user` scriverebbe quindi `0x7004` byte, in base al calcolo di `update->live_ranges_count` moltiplicato per 4. Di conseguenza, questa operazione comporterebbe la sovrascrittura dell'area di memoria compresa tra il puntatore `live_ranges` e l'allocazione di `buff` da parte di dati controllati dall'utente. È quindi essenziale assicurarsi con attenzione che nessun oggetto critico di sistema in quell'intervallo venga sovrascritto accidentalmente. Dato che l'operazione coinvolge una chiamata a `copy_from_user`, si potrebbe pensare di innescare un `EFAULT` rimuovendo deliberatamente la mappatura della regione di memoria indesiderata successiva al buffer di origine dell'utente, per impedire la scrittura di dati in posizioni sensibili. Tuttavia, questo approccio è inefficace, poiché se la funzione `raw_copy_from_user` fallisce, azzererà i byte rimanenti nel buffer di destinazione del kernel. Questo comportamento è implementato per garantire che, in caso di copia parziale dovuta a un errore, il resto del buffer del kernel non contenga dati non inizializzati.```c
static inline __must_check unsigned long
_copy_from_user(void *to, const void __user *from, unsigned long n)
{
	unsigned long res = n;
	might_fault();
	if (!should_fail_usercopy() && likely(access_ok(from, n))) {
		instrument_copy_from_user(to, from, n);
		res = raw_copy_from_user(to, from, n);
	}
	if (unlikely(res))
		memset(to + (n - res), 0, res);
	return res;
}

Considerando ciò, dobbiamo selezionare attentamente l'oggetto da sovrascrivere e i dati da scrivere.

Scegliere l'Oggetto Giusto da Sovrascrivere

Poiché sono bloccato da questo sfortunato controllo, la mia strategia è identificare un oggetto che, se azzerato, non produca alcun esito indesiderato. Ma, prima di arrivare a questo, c'è un altro problema da affrontare. Ricordate quando nella scorsa parte ho detto che posso scegliere qualsiasi dimensione di allocazione e quindi qualsiasi allocatore di slab cache generico per servire il mio buffer di allocazione? Non è corretto, perché è di nuovo a causa di copy_from_user! È dovuto alla mitigazione CONFIG_HARDENED_USERCOPY. Essa vieta di specificare una dimensione che non corrisponde alla dimensione della slab cache associata al buffer di destinazione del kernel (in questo caso, un oggetto heap). Determina se la pagina del buffer è una pagina slab e, in tal caso, recupera il corrispondente kmem_cache->size e determina se la dimensione fornita dall'utente non lo supera; altrimenti, il kernel va semplicemente in crash a causa della mancata corrispondenza delle dimensioni. Quindi, in altre parole, non posso prendere di mira oggetti che appartengono all'allocatore generico, MA posso comunque prendere di mira oggetti che hanno dimensioni grandi (cioè quelli serviti direttamente dall'allocatore di pagine).

Il primo pensiero che mi è venuto in mente è stato usare la tecnica pipe_buffer, che è una tecnica molto elegante per ottenere primitive arbitrarie di lettura/scrittura. Non entrerò nei dettagli della tecnica, ma i lettori sono invitati a leggere questo fantastico blog di Interrupt Labs. Quando si costruisce un oggetto pipe, l'oggetto pipe_buffer viene inizialmente creato in un array di 16 elementi; tuttavia, la dimensione dell'array può essere regolata usando fcntl(F_SETPIPE_SZ). Pertanto, l'allocazione dell'array pipe_buffer può essere regolata in modo che possa essere servita dall'allocatore di pagine, rendendolo un oggetto bersaglio perfetto da attaccare.

Dopo aver selezionato l'oggetto pipe_buffer come candidato bersaglio, il passo successivo verso il raggiungimento di r/w nel kernel è sovrascrivere il suo contenuto con la vulnerabilità di underflow, che mi permetterà di leggere/scrivere da/verso qualsiasi posizione di memoria la cui pagina sovrascrive il campo pipe_buffer->page. Poiché la vulnerabilità mi permette di scrivere dati arbitrari, posso controllare l'intero contenuto di 'pipe_buffer,' incluso il suo campo page, e per farlo, devo allocare l'array pipe_buffer prima del vulnerabile oggetto kbuff e devono essere uno accanto all'altro.

Posizionare gli Oggetti pipe_buffer e buff Adiacenti

Ho spruzzato la memoria del kernel con molti oggetti kbase_kcpu_command_queue seguiti da un mucchio di array pipe_buffer. Non posso semplicemente usare gli array pipe_buffer da soli come fonte primaria per lo spraying a causa della limitazione imposta da pipe_max_size. Pertanto, ho deciso di iniziare a spruzzare con l'oggetto kbase_kcpu_command_queue. Ho scelto l'oggetto kbase_kcpu_command_queue per due motivi: la sua dimensione di allocazione è 0x38C8, quindi gestita dall'allocatore di pagine, e posso ottenere deterministicamente il suo indirizzo di kernel usando il bug di leak di informazioni del kernel, rendendolo un buon oggetto con cui spruzzare e anche un buon oggetto bersaglio (come vedremo nella prossima sezione).

Come menzionato prima, ho usato fcntl(F_SETPIPE_SZ) per aumentare la dimensione dell'allocazione dell'array pipe_buffer in modo che possa essere servita dall'allocatore di pagine. Per essere più specifico, ho scelto la dimensione di allocazione come ==0x4000 bytes (4 * PAGE_SIZE)== per essere coerente con le allocazioni di kbase_kcpu_command_queue.

Ottenere un Indirizzo di struct page

Per usare correttamente pipe_buffer, è richiesto un indirizzo di pagina. Essere in grado di identificare l'indirizzo di kernel di un oggetto kbase_kcpu_command_queue che posso creare e distruggere deliberatamente lo rende un buon candidato da usare, e trovare la sua struct page corrispondente può essere ottenuto usando virt_to_page .

Contenuti da Scrivere nel pipe_buffer

Quindi l'oggetto pipe_buffer è come segue:```c struct pipe_buffer { struct page *page; unsigned int offset, len; const struct pipe_buf_operations *ops; unsigned int flags; unsigned long private; };

root@kitploit:~
Come già menzionato, il campo `page` deve includere un indirizzo di pagina valido. I campi `offset` e `len` non devono superare `PAGE_SIZE`, altrimenti la pipe incrementerà i contatori head/tail, con conseguente utilizzo di un nuovo oggetto `pipe_buffer` e perdita del controllo sul pipe buffer fittizio.

Inoltre, il campo `flags` deve essere `PIPE_BUF_FLAG_CAN_MERGE`, così che le successive chiamate a `pipe_write`, invece di incrementare ciecamente il contatore head e usare il pipe buffer successivo, controllino prima se c'è spazio nel `pipe_buffer` corrente che possa contenere la richiesta di scrittura o meno; se c'è, si limiteranno ad aggiungere dati allo stesso pipe buffer a partire dal valore memorizzato nel campo `len`.

Per evitare che il device vada in crash su `pipe_buf_confirm`, che viene chiamata da `pipe_write` e `pipe_read`, il puntatore `ops` deve essere anch'esso un indirizzo di kernel valido con il campo `ops->confirm` impostato a _NULL_. Posso semplicemente usare un offset all'interno dell'oggetto `kbase_kcpu_command_queue` trapelato che sia NULL e che non cambi in nessuna circostanza.

### Scelta del valore di offset ottimale per l'underflow

Mentre le dimensioni di allocazione di `buff` ,`kbase_kcpu_command_queue` e `pipe_buffer` sono di ~0x4000~ byte, ho scelto di causare l'underflow del buffer con **0x8000** byte. Perché ?

Diamo una breve occhiata a come i `pipe_buffers` vengono aggiornati durante le operazioni di lettura e scrittura. Supponiamo di poter modellare il `pipe_buffer` in modo che appaia così:```c
struct pipe_buffer {
	.page = virt_to_page(addr),
	.offset =  0,
	.len = 0x40,
	.ops = kcpu_addr + 0x50,
	.flags = PIPE_BUF_FLAG_CAN_MERGE,
	unsigned long private = 0
};

Sebbene il bug offra la possibilità di controllare arbitrariamente il contenuto di questo oggetto, lo fa solo una volta, perché l'oggetto in underflow viene liberato immediatamente dopo che la chiamata ioctl termina. Questo in realtà rappresenta un problema, perché devo aggiornare manualmente l'oggetto pipe_buffer per renderlo di nuovo utilizzabile, poiché ogni operazione di lettura/scrittura su pipe:

  • Il campo .page non viene aggiornato; rimane lo stesso e, quando il buffer è vuoto, viene rilasciato, cosa che non voglio che accada perché il campo .ops non è impostato correttamente.
  • Poiché il pipe_buffer aggiorna il campo .offset durante un'operazione di lettura, non posso leggere nuovamente la stessa regione di memoria.
  • I dati scritti nel pipe_buffer vengono aggiunti al buffer a partire dal valore di .len (presumendo che il flag PIPE_BUF_FLAG_CAN_MERGE sia impostato) e .len viene aggiornato di conseguenza. In altre parole, non possiamo scrivere dati nello stesso identico indirizzo due volte.

Di conseguenza, a meno che non aggiorni correttamente il pipe_buffer dopo ogni operazione di lettura o scrittura, non posso leggere e scrivere dalla stessa pipe allo stesso tempo. Ecco perché eseguire l'underflow con 0x8000 byte è molto più pratico: invece di sovrascrivere un singolo pipe_buffer, sovrascriverò due distinte istanze di pipe_buffer appartenenti a due oggetti pipe distinti: una sarà considerata per le operazioni di lettura e l'altra per le operazioni di scrittura.```c #define PIPE_BUF_FLAG_CAN_MERGE 0x10 /* can merge buffers */

pipe_read = (struct pipe_buffer *)( ptr); pipe_read->page = virt_to_page(ta->kcpu_kaddr); pipe_read->offset = 0; pipe_read->len = 0xfff; pipe_read->ops = (const void *)(ta->kcpu_kaddr + 0x50); pipe_read->flags = PIPE_BUF_FLAG_CAN_MERGE; pipe_read->private = 0;

pipe_write = (struct pipe_buffer )( ptr + 0x4000); pipe_write->page = virt_to_page(ta->kcpu_kaddr); pipe_write->offset = 0; pipe_write->len = 0; / This is the starting position of the pipe_write */ pipe_write->ops = (const void *)(ta->kcpu_kaddr + 0x50); pipe_write->flags = PIPE_BUF_FLAG_CAN_MERGE; pipe_write->private = 0;

root@kitploit:~
Il `pipe_read` è un buffer di pipe fittizio (fake pipe buffer) che verrà utilizzato per leggere dati dalla pagina di destinazione a partire da `.offset = 0` fino a `0xfff` byte, mentre `pipe_write` è un `pipe_buffer` fittizio che verrà utilizzato per scrivere dati a partire da `.len = 0` fino a `0xfff` byte.  
È anche molto importante ricordare che scrivere più di `PAGE_SIZE` byte spingerà la pipe a incrementare il contatore head, usando quindi un nuovo `pipe_buffer` appena allocato e perdendo il controllo sul nostro `pipe_write` fittizio. D'altro canto, svuotare (leggere `0xfff` dati da) il buffer `fake_read` dice al kernel di rilasciare la pagina effettiva chiamando `ops→release` causando il crash del kernel perché non ho ancora un indirizzo di testo del kernel.  
Anche se sono riuscito a separare le operazioni di lettura e scrittura della pipe in modo che un'operazione di scrittura su un'estremità della pipe non interferisca con l'altro buffer della pipe e viceversa, non ho ancora risolto il problema principale: come aggiornare in modo affidabile il pipe buffer? La risposta ovvia che mi è venuta in mente è stata semplicemente ripetere il processo di spray ancora e ancora dopo ogni chiamata di lettura o scrittura della pipe. E questo non ha senso perché avrebbe avuto un impatto significativo sull'affidabilità dell'exploit. Nella sezione successiva, dividerò l'obiettivo in due sotto-obiettivi: per cominciare, mi concentrerò solo sul campo `.page`, seguito poi dai campi `.len/.offset`.

### Modifica del campo pipe_buffer→page

Con mia sorpresa, non devo né ho bisogno di aggiornare affatto il campo `.page`, perché posso sovrascrivere `pipe_buffer→page` per farlo puntare all'indirizzo di pagina della `kbase_kcpu_command_queue` trapelata. Pertanto, **tutto ciò che devo fare è rilasciare l'oggetto `kbase_kcpu_command_queue` e sovrapporlo a un nuovo oggetto `pipe_buffer`. Sì! Ora ho un `pipe_buffer→page` che punta a un legittimo oggetto `pipe_buffer`!**  
Sostituire `kbase_kcpu_command_queue` con `pipe_buffer` ci dà la capacità di manipolare un legittimo pipe buffer senza dover aggiornare regolarmente il campo `.page`. Tuttavia, devo ancora gestire i campi `.len` e `.offset`.

### Modifica dei campi pipe_buffer→len/offset

Come ho già menzionato, eseguire letture/scritture sulla pipe aggiorna i campi `.len` e `.offset`, rendendo inutilizzabili le successive operazioni di lettura/scrittura sulla stessa pagina, anche se eseguite tramite le due pipe distinte. Ecco un altro trucco: **esiste una tecnica per leggere/scrivere dati senza nemmeno toccare i campi `.len/.offset`!**. Ed è possibile ottenerlo facendo fallire (faulting) le chiamate `copy_page_from_iter` e `copy_page_to_iter` su `pipe_read/write`! Sì, proprio come `copy_to/from_user`, `copy_page_to/from_iter` copia dati da/verso lo spazio utente che viene passato attraverso la struttura `iov_iter`, e può essere mandata in fault.

Per continuare con l'esempio precedente, se vogliamo scrivere 8 byte di dati a un indirizzo, la dimensione del buffer nello spazio utente deve essere 8, seguita da un'area di memoria non mappata o non leggibile, e poi passare `9` come argomento di dimensione alla system call `write`, indicando la quantità di dati che vogliamo scrivere. Questa operazione scriverà 8 byte e fallirà sul _nono_ perché incontra una posizione di memoria non mappata/non leggibile. Di conseguenza, i dati sono stati effettivamente scritti nel buffer del kernel di destinazione e il campo `.len` non è stato modificato. La funzione kernel `pipe_write` semplicemente ritornerà senza aggiornare il campo `buf->len`.```c
		if ((buf->flags & PIPE_BUF_FLAG_CAN_MERGE) &&
		    offset + chars <= PAGE_SIZE) {
			ret = pipe_buf_confirm(pipe, buf);
			if (ret)
				goto out;

			ret = copy_page_from_iter(buf->page, offset, chars, from);
			if (unlikely(ret < chars)) {
				ret = -EFAULT;
				goto out;
			}

			buf->len += ret;
			if (!iov_iter_count(from))
				goto out;
		}

Lo stesso vale per le operazioni di lettura; se desideriamo leggere 8 byte, rendi il nono byte del buffer illeggibile e poi dichiara semplicemente di voler leggere 9 byte: i dati verranno copiati nel buffer utente senza modificare il campo .offset. Di conseguenza, siamo in grado di eseguire operazioni di lettura/scrittura illimitate su qualsiasi indirizzo di memoria del kernel senza dover ripetere il processo di spray.

Ottenere i privilegi di root

Ora che dispongo di una solida primitiva di lettura/scrittura arbitraria, ho esaminato tutti i struct page nell'array VMEMMAP_START per determinare l'indirizzo iniziale del testo del kernel usando la tecnica descritta nel post del blog Interrupt Labs. Poi ho realizzato che init_task viene azzerato negli aggiornamenti di sicurezza Android di novembre, quindi ho usato kthreadd_task al suo posto. Disporre dell'indirizzo del kernel di kthreadd_task mi ha permesso di scorrere la lista task->tasks e ottenere l'indirizzo del kernel del mio task current, quindi azzerare la struttura cred per ottenere i privilegi di root.

In seguito, ho capito che scansionare tutti gli indirizzi di pagina non era necessario perché avevo già l'indirizzo del testo del kernel di anon_pipe_buf_ops da un oggetto pipe_buffer. Con queste informazioni, potevo dedurre l'indirizzo base del testo del kernel, bypassando efficacemente KASLR.

Disabilitare SELinux

L'exploit disabilita anche SELinux: con l'indirizzo base del testo del kernel, mi basta trovare la posizione della struttura globale selinux_state e poi azzerare il valore .enforcing.

Prova di concetto

La prova di concetto che accompagna il report è stata testata su dispositivi Pixel 7 e 8 Pro con Android 14 e gli ASB di ottobre e novembre, raggiungendo un tasso di successo di quasi il 100%. È anche importante menzionare che l'exploit non funzionerà così com'è su altri dispositivi a causa dell'uso di alcuni offset hardcoded. Per aggiungere il supporto a un nuovo dispositivo, è necessario fornire quanto segue:

  • offset di kthreadd_task dall'indirizzo base del kernel.
  • offset di selinux_state dall'indirizzo base del kernel.
  • offset delle strutture task_struct->cred, task_struct->pid e task_struct->tasks.
  • offset di anon_pipe_buf_ops dall'indirizzo base del kernel.

Compilazione

Per compilare l'exploit come binario autonomo usa il seguente comando, quindi usa adb shell per eseguirlo:```sh $ aarch64-linux-androidXX-clang++ -static-libstdc++ -w -Wno-c++11-narrowing -DUSE_STANDALONE -o poc poc.cpp -llog $ adb push poc /data/local/tmp/ $ adb shell /data/local/tmp/poc

root@kitploit:~
Puoi anche eseguire l'exploit tramite un'app di Android Studio incorporando questa directory al suo interno e assicurati di disabilitare gli inutili avvisi C++ aggiungendo `-w -Wno-c++11-narrowing` al file cmake.

### Demo```shell
$ adb logcat  |grep -i EXPLOIT
11-28 16:04:12.500  7989  7989 E EXPLOIT : [+] Target device: 'google/husky/husky:14/UD1A.231105.004/11010374:user/release-keys' 0xa9027bfdd10203ff 0xa90467faa9036ffc
11-28 16:04:15.563  7989  7989 E EXPLOIT : [+] Got the kcpu_id (0) kernel address = 0xffffff8901390000  from context (0x0)
11-28 16:04:18.441  7989  7989 E EXPLOIT : [+] Got the kcpu_id (255) kernel address = 0xffffff89b0bf8000  from context (0xff)
11-28 16:04:18.442  7989  7989 E EXPLOIT : [+] Found corrupted pipe with size 0xfff
11-28 16:04:18.442  7989  7989 E EXPLOIT : [+] SUCCESS! we have a fake pipe_buffer (0)!
11-28 16:04:18.444  7989  7989 E EXPLOIT : 10 00 39 01 89 FF FF FF  10 00 39 01 89 FF FF FF  | ..9.......9.....
11-28 16:04:18.444  7989  7989 E EXPLOIT : 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  | ................
11-28 16:04:18.444  7989  7989 E EXPLOIT : 00 B0 CD 12 C0 FF FF FF  00 00 00 00 00 00 00 00  | ................
11-28 16:04:18.444  7989  7989 E EXPLOIT : 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  | ................
11-28 16:04:18.445  7989  7989 E EXPLOIT : [+] Freeing kcpu_id = 0 (0xffffff8901390000)
11-28 16:04:18.446  7989  7989 E EXPLOIT : [+] Allocating 61 pipes with 256 slots
11-28 16:04:18.462  7989  7989 E EXPLOIT : [+] Successfully overlapped the kcpuqueue object with a pipe buffer
11-28 16:04:18.463  7989  7989 E EXPLOIT : 40 AB BA 26 FE FF FF FF  00 00 00 00 30 00 00 00  | @..&........0...
11-28 16:04:18.463  7989  7989 E EXPLOIT : 70 37 8D F1 DA FF FF FF  10 00 00 00 00 00 00 00  | p7..............
11-28 16:04:18.463  7989  7989 E EXPLOIT : 00 00 00 00 00 00 00 00                           | ........
11-28 16:04:18.463  7989  7989 E EXPLOIT : [+] pipe_buffer {.page = 0xfffffffe26baab40, .offset = 0x0, .len = 0x30, ops = 0xffffffdaf18d3770}
11-28 16:04:18.463  7989  7989 E EXPLOIT : [+] kernel base = 0xffffffdaf0010000, kthreadd_task = 0xffffff8002da3780 selinux_state = 0xffffffdaf28a3168
11-28 16:04:20.097  7989  7989 E EXPLOIT : [+] Found our own task struct 0xffffff88416c5c80
11-28 16:04:20.097  7989  7989 E EXPLOIT : [+] Successfully got root: getuid() = 0 getgid() = 0
11-28 16:04:20.097  7989  7989 E EXPLOIT : [+] Successfully disabled SELinux
11-28 16:04:20.102  7989  7989 E EXPLOIT : [+] Cleanup  ... OK
Scarica lo strumento