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
AndroidKernelVulnerability — Attivazione e analisi della vulnerabilità del kernel Android CVE-2019-2215 | Kitploit
Strumenti/GitHubGitHub/sharif-dev/androidkernelvulnerability
Sicurezza AndroidEscalation di PrivilegiAnalisi StaticaAnalisi Dinamica (Sandboxing)ExploitApprendimento e FormazioneBinary ExploitationLab e Pratica

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
GitHub
sharif-dev/androidkernelvulnerability

AndroidKernelVulnerability

Attivazione e analisi della vulnerabilità del kernel Android CVE-2019-2215

Vedi Repository
72193 anni faRevisionato da Kitploit

Vulnerabilità del Kernel Android

Panoramica

Nel novembre 2017 è stato rilevato un bug use-after-free nel kernel linux dal sistema syzkaller. A febbraio 2018 è stato corretto in alcuni kernel linux e versioni android.

Questa correzione non è mai stata inclusa nei bollettini di sicurezza mensili di Android, quindi non è stata applicata a molti dispositivi appena rilasciati, come Pixel e Pixel2.

A settembre 2019, Android è stato informato delle implicazioni di sicurezza di questo bug da Project Zero. Successivamente, Android ha assegnato CVE-2019-2215 a questa vulnerabilità per renderla più formale e conosciuta.

CVE-2019-2215 è un use-after-free in binder.c che consente l'elevazione dei privilegi (ottenere l'accesso root) da un'applicazione android. Non è necessaria alcuna interazione da parte dell'utente per sfruttare questa vulnerabilità. Richiede solo l'installazione di un'applicazione locale dannosa.

Qui introdurremo questa vulnerabilità del kernel android in maggiori dettagli e useremo questa vulnerabilità per ottenere l'accesso root (escalation dei privilegi) dell'intero dispositivo android.

Useremo questo Proof of Concept (PoC):

https://github.com/cloudfuzz/android-kernel-exploitation

Innescare la vulnerabilità

Per prima cosa ti mostreremo un modo per innescare questa vulnerabilità su un emulatore android e causare un crash del kernel. Poi, per vedere quanto potrebbe essere pericolosa, continueremo usando il PoC per ottenere l'accesso root nel dispositivo android simulato. Quindi analizzeremo il codice del kernel per capire qual è la causa (analisi statica e analisi dinamica).

Dopo l'analisi, vedremo come abbiamo ottenuto l'accesso root. Infine vedremo come questa vulnerabilità viene mitigata tramite patch.

Per causare un crash del kernel innescando questa vulnerabilità, usiamo questi passaggi:

  1. Per prima cosa, ti serve un sistema operativo linux con 'gdb' e 'python' installati.
  2. Clona il repository del PoC. (https://github.com/cloudfuzz/android-kernel-exploitation)
  3. Installa l'emulatore android e l'android NDK (installando Android Studio)
  4. Clona il codice sorgente del kernel android. (Verrà utilizzato il branch 'q-goldfish-android-goldfish-4.14-dev')
  5. Questo kernel è già stato patchato; lo modifichiamo per reintrodurre la vulnerabilità nel codice del kernel.
  6. Ora dobbiamo compilare il kernel dal codice sorgente. Compiliamo il nostro kernel con KASan.
  7. Facciamo il boot del kernel compilato e avviamo il nostro emulatore con esso.
  8. Poi, usando 'trigger.cpp' nel repository del PoC, inneschiamo un crash (usando il comando 'adb').
  9. Useremo 'root-me.py' nel PoC e il comando 'gdb' per ottenere l'accesso root nel dispositivo emulato.

Guarda il seguente video:

[video]

Analisi statica

In questa (e nella prossima) sezione capiremo perché il crash avviene usando l'analisi statica e dinamica.

Qui analizzeremo il codice del kernel (analisi statica) per capire il problema. In crash_report.txt c'è il report di KASan che dice che questo è un bug use-after-free. Significa che un oggetto viene allocato nell'heap (e ne abbiamo un riferimento), poi liberiamo l'oggetto dall'heap e infine lo richiamiamo erroneamente tramite un riferimento. In questo report viene stampata la stack trace di queste tre fasi.

Se ricordi da prima, abbiamo usato 'trigger.cpp' nel PoC.

Ecco il codice principale di trigger.cpp:``` int main() { //1 int fd, epfd; //2 struct epoll_event event = {.events = EPOLLIN}; //3 fd = open("/dev/binder", O_RDONLY); //4 epfd = epoll_create(1000); //5 epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event); //6 ioctl(fd, BINDER_THREAD_EXIT, NULL); }

root@kitploit:~
Vediamo cosa fa 'trigger.cpp'.

In Android (come in altri sistemi operativi basati su Unix) abbiamo alcuni processi. Ogni programma che eseguiamo crea uno (o più) processi e questi processi sono gestiti dal sistema operativo. Il SO può passare dall'uno all'altro (multi-tasking) o terminare un processo, ecc. Per ragioni di sicurezza, i processi sono isolati gli uni dagli altri per impostazione predefinita.

In alcuni casi, un processo potrebbe aver bisogno di scambiare dati con un altro processo. Viene chiamata comunicazione tra processi (IPC). Ci sono diversi modi per i processi di comunicare in linux. Android ha introdotto uno specifico meccanismo IPC chiamato **'Binder'**. Binder è un driver del kernel che facilita la comunicazione tra processi.

In android, l'IPC può essere effettuato chiamando direttamente alcuni metodi del kernel (la maggior parte si trova in drivers/binder.c) o usando implementazioni ad alto livello (ad esempio in java).






![alt_text](https://raw.githubusercontent.com/sharif-dev/AndroidKernelVulnerability/master/images/binder.jpg)


Per usare **binder**, dobbiamo aprire il modulo binder del kernel. Questo viene fatto usando la riga 3 di trigger.cpp. Poi abbiamo un puntatore a file descriptor; usando questo fd, l'iniziatore e i destinatari dell'IPC possono essere identificati dal kernel.

Tutte le interazioni con il driver avverranno tramite un piccolo insieme di comandi **'ioctl**' (BINDER_THREAD_EXIT, BINDER_WRITE_READ, ...).

Maggiori informazioni su binder: [link1](https://www.nds.ruhr-uni-bochum.de/media/attachments/files/2012/03/binder.pdf), [link2](http://rts.lab.asu.edu/web_438/project_final/CSE_598_Android_Architecture_Binder.pdf)

In linux, abbiamo un concetto chiamato '**event polling**'. L'API '**epoll**' viene usata quando vogliamo monitorare più file descriptor (i file descriptor sono ciò che otteniamo quando apriamo un driver o lavoriamo con IO, ecc).

**epoll** è una struct del kernel che ha due campi importanti.



*   interest list = elenco dei file descriptor che vogliamo monitorare.
*   ready list = elenco dei file descriptor pronti per I/O.

Per usare l'event polling, prima creiamo una epoll (riga 4), poi aggiungiamo o eliminiamo (EPOLL_CTL_ADD) un evento (&event) associato a un file descriptor (**fd**) alla nostra epoll creata (**epfd**) chiamando il metodo **epoll_ctl** del kernel.

=> `epoll_event event` è un evento, attivato quando il file associato (fd) è disponibile per un'operazione di lettura.

Ora capiamo cosa fa **trigger.cpp** (non c'è bisogno di andare in profondità!).  Apre il modulo **binder**, crea una **epoll** per ascoltarlo quando è pronto. Poi alla riga 6, usciamo dal binder che abbiamo avviato alla riga 3.


#### Allocazione:

Chiamando open(), chiamiamo in realtà **open_binder()** (implementazione di open() in binder.c); in open_binder(), verrà creata una nuova struct **'binder_proc'** e:

   ` fd->pricate_data = binder_proc`

Chiamando **epoll_create(),** verrà creata una nuova struct epoll e aggiunta a una struttura a coda.






![alt_text](https://raw.githubusercontent.com/sharif-dev/AndroidKernelVulnerability/master/images/event_poll.jpg)


Chiamando **epoll_ctrl(epdf, ADD, fd, event)**, viene creato un nuovo **ep_item**, associa **fd **(file descriptor da ascoltare) a questo **ep_item**, e viene inserito nell'**albero rosso-nero** di event_poll ( una struttura dati in ep per salvare gli ep_item ). Chiama anche **ep_item_poll(),** questo metodo gestisce l'associazione della funzione di callback a ep_item.

Viene creata una nuova struct **binder_thread** (l'allocazione avviene qui), la collega alla **binder_proc **(creata sopra), poi viene creata una struct **epoll_entry**, ha due liste, ** epoll_entry->wait** e **epoll_entry->whead**, entrambe queste liste hanno un puntatore alla **binder_thread** creata prima.

Poi **epoll_entry** viene collegato a ep_item (**ep_item->pwqlist** è una lista che contiene questo epoll_entry).





![alt_text](https://raw.githubusercontent.com/sharif-dev/AndroidKernelVulnerability/master/images/even_poll.jpg)



#### Deallocazione:

Chiamando **ioctl(fd, ...)**, si accede a **binder_proc** tramite fd->private_data, quindi la struct **binder_thread** verrà **deallocata** dalla memoria.


#### Uso:

Quando il nostro processo corrente termina, verrà chiamato **epoll_ctl(epfd, DEL, fd, event)**.

Chiama **ep_remove(event_poll, ep_item)**. Questo metodo ottiene la **epoll_entry** da **ep_item->pwqlist**, poi ottiene la wait list di **ep_item (ep_tem->wait)**, è una lista concatenata, e vuole rimuovere uno degli elementi di questa wait list.

Usa il seguente codice (viene usato pseudocodice):```
entry = wait->entry;
entry.next.prev = entry.prev;
entry.prev.next = entry.next;

Qui wait->entry è un puntatore a binder_thread che è stato rimosso dalla memoria! Quindi è un use-after-free e causa un bug!!

  • In precedenza, abbiamo usato codici simili ai codici reali del kernel; potrebbero differire in alcuni dettagli. (in alcuni casi si usa binder_thread invece di binder_thread->wait)

Riepilogo:

Abbiamo creato un event_poll che ha un red_black_tree; ogni nodo è un ep_item che ha un campo che è una lista di epoll_entry; ogni epoll_entry ha due puntatori alla struct binder_thread (wait, whead).

Chiamando ioctl() abbiamo liberato il binder_thread dalla memoria; poi, durante l'uscita, questa struct viene acceduta tramite un puntatore che era ancora disponibile!

Analisi dinamica

L'analisi dinamica è il test e la valutazione di un programma eseguendo dati in tempo reale; per trovare errori in un programma mentre è in esecuzione.

passaggi:

  1. Compilare il kernel Android senza KASan

    Lo compiliamo senza KASAN per monitorare le operazioni di scrittura e unlink e ciò che accade realmente dopo l'operazione di unlink.

  2. Avviare l'emulatore con il kernel appena compilato

  3. Lanciare l'emulatore

  4. Usare GDB per collegarsi all'istanza di QEMU

  5. Compilare il trigger della vulnerabilità e inviarlo al dispositivo virtuale

  6. Impostare i breakpoint in GDB

    Caricare lo script Python personalizzato (dynamic-analysis.py nel repo): Per tracciare le chiamate di funzione e dumpare la struttura binder_thread prima e dopo la sua liberazione. Inoltre, dumpare la stessa struttura binder_thread prima e dopo che l'operazione di unlink è stata eseguita.

    In questo file prima eliminiamo tutti i breakpoint e poi impostiamo 2 breakpoint (BP); Il primo simbolo è “binder_free_thread” (traccerà la funzione binder_free_thread) prima che binder_thread venga liberato, verrà chiamata la funzione di stop; quindi parametri e simbolo verranno mostrati con (gb.write(....) ) e poi verrà chiamato il metodo di callback (lo impostiamo su set_dump_binder_thread ); In questa funzione binder_thread_address verrà impostato nella nostra variabile globale e gdb.execute invia qualsiasi output prodotto dal comando all'output standard di GDB.

    Il secondo simbolo è “remove_wait_queue” (traccerà la funzione remove_wait_queue); i parametri che vogliamo osservare sono "wq_head", "wq_entry" e per l'uscita verrà impostato il breakpoint wait.c:52. La loro funzione di callback è dump_binder_thread . Questi breakpoint mostreranno cosa accade prima e dopo l'operazione di unlink.

risultato:

  • prima parte del risultato:
root@kitploit:~
binder_free_thread(thread=0xffff88800c18f200)(enter)
0xffff88800c18f200:	0xffff88806793c000	 0x0000000000000001
0xffff88800c18f210:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f220:	0xffff88800c18f220	 0xffff88800c18f220
0xffff88800c18f230:	0x0000002000001b35	 0x0000000000000001
0xffff88800c18f240:	0x0000000000000000	 0xffff88800c18f248
0xffff88800c18f250:	0xffff88800c18f248	 0x0000000000000000
0xffff88800c18f260:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f270:	0x0000000000000003	 0x0000000000007201
0xffff88800c18f280:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f290:	0x0000000000000003	 0x0000000000007201
0xffff88800c18f2a0:	0x0000000000000000	 0xffff88805c05cae0
0xffff88800c18f2b0:	0xffff88805c05cae0	 0x0000000000000000
0xffff88800c18f2c0:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f2d0:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f2e0:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f2f0:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f300:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f310:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f320:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f330:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f340:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f350:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f360:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f370:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f380:	0x0000000000000000	 0x0000000000000001
0xffff88800c18f390:	0xffff88806d4bb200

nel nostro codice python avevamo i seguenti codici:``` gdb.write ( "{function}({param})(enter)\n".format( function=self.function_name, param=params ) )

root@kitploit:~
e si tratta della funzione binder_free_thread, quindi il parametro per questa funzione è un puntatore a binder_thread:```
static void binder_free_thread(struct binder_thread *thread)
{
        [...]
        kfree(thread);
}

nel risultato avevamo(binder_free_thread(thread=0xffff88800c18f200)(enter)).

le righe successive mostrano il risultato dell'esecuzione, con il comando sottostante otteniamo l'offset di binder_thread.wait:``` p offsetof(struct binder_thread, wait)

root@kitploit:~
il risultato è 0xa0 e se mettiamo wait.head invece di wait nel comando, il risultato sarà 0xa8 e contiene `0xffff88805c05cae0````
0xffff88805c05cae0 is pointer to eppoll_entry->wait.entry which is of type struct list_head
  • seconda parte del risultato
root@kitploit:~
remove_wait_queue(wq_head=0xffff88800c18f2a0, wq_entry=0xffff88805c05cac8)(enter)
0xffff88800c18f200:	0xffff88800c18f600	 0x0000000000000001
0xffff88800c18f210:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f220:	0xffff88800c18f220	 0xffff88800c18f220
0xffff88800c18f230:	0x0000002000001b35	 0x0000000000000001
0xffff88800c18f240:	0x0000000000000000	 0xffff88800c18f248
0xffff88800c18f250:	0xffff88800c18f248	 0x0000000000000000
0xffff88800c18f260:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f270:	0x0000000000000003	 0x0000000000007201
0xffff88800c18f280:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f290:	0x0000000000000003	 0x0000000000007201
0xffff88800c18f2a0:	0x0000000000000000	 0xffff88805c05cae0
0xffff88800c18f2b0:	0xffff88805c05cae0	 0x0000000000000000
0xffff88800c18f2c0:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f2d0:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f2e0:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f2f0:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f300:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f310:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f320:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f330:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f340:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f350:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f360:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f370:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f380:	0x0000000000000000	 0x0000000000000001
0xffff88800c18f390:	0xffff88806d4bb200

In remove_wait_queue(wq_head=0xffff88800c18f2a0, wq_entry=0xffff88805c05cac8)(enter), wq_head è l'indirizzo di binder_thread.wait e wq_entry sono i dati di wait.head.

dopo questa operazione avverrà l'unlink.

  • Terza parte del risultato:
root@kitploit:~
Breakpoint 3 at 0xffffffff802aa5be: file /home/ashfaq/workshop/android-4.14-dev/goldfish/kernel/sched/wait.c, line 53.
remove_wait_queue_wait.c:52(exit)
0xffff88800c18f200:	0xffff88800c18f600	 0x0000000000000001
0xffff88800c18f210:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f220:	0xffff88800c18f220	 0xffff88800c18f220
0xffff88800c18f230:	0x0000002000001b35	 0x0000000000000001
0xffff88800c18f240:	0x0000000000000000	 0xffff88800c18f248
0xffff88800c18f250:	0xffff88800c18f248	 0x0000000000000000
0xffff88800c18f260:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f270:	0x0000000000000003	 0x0000000000007201
0xffff88800c18f280:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f290:	0x0000000000000003	 0x0000000000007201
0xffff88800c18f2a0:	0x0000000000000000	 0xffff88800c18f2a8
0xffff88800c18f2b0:	0xffff88800c18f2a8	 0x0000000000000000
0xffff88800c18f2c0:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f2d0:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f2e0:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f2f0:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f300:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f310:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f320:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f330:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f340:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f350:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f360:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f370:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f380:	0x0000000000000000	 0x0000000000000001
0xffff88800c18f390:	0xffff88806d4bb200

La parte evidenziata è dovuta a questo. Il risultato è dopo l'operazione di unlink e possiamo vedere che per l'unlink scrive l'indirizzo (0xffff88800c18f2a0 + 0x8) in next e previous.

ciò significa:

(puntatore a binder_thread->wait.head) = binder_thread->wait.head.next = `binder_thread->wait.head.prev

Sfruttare la vulnerabilità

In questa parte mostreremo come possiamo usare questo bug per ottenere accesso root.

Ricorda da quanto sopra: avevamo una struct binder_thread, che è stata liberata e riutilizzata tramite un puntatore. Questo è il codice della struct binder_thread:``` struct binder_thread { struct binder_proc proc; struct rb_node rb_node; struct list_head waiting_thread_node; int pid; int looper; / only modified by this thread*/ bool looper_need_return; /* can be written by other thread*/ struct binder_transaction *transaction_stack; struct list_head todo; bool process_todo; struct binder_error return_error; struct binder_error reply_error; wait_queue_head_t wait; struct binder_stats stats; atomic_t tmp_ref; bool is_dead; struct task_struct *task; };

root@kitploit:~
Uno dei campi è il puntatore **task_struct**; questa struct ha un campo chiamato **addr_limit**.

Quando vogliamo accedere a un indirizzo in un processo, viene verificato se l'indirizzo si trova in **user-space** o meno; se si trova in **kernel-space** l'accesso dovrebbe essere bloccato. Questo controllo viene eseguito confrontando il nostro indirizzo con **addr_limit**; se il nostro indirizzo è minore di **addr_limit**, abbiamo un accesso valido.

**addr_limit** separa in pratica lo user-space dal kernel-space, quindi modificando questo campo in **task_struct** abbiamo pieno accesso al kernel-space e possiamo fare qualsiasi cosa!

Qui abbiamo due passaggi per sfruttare la vulnerabilità:

1. trovare l'indirizzo di task_struct nel kernel-space
2. modificare addr_limit in task_struct


#### Trovare l'indirizzo di task_struct

Nel kernel, possiamo eseguire I/O vettorizzato, il che significa che possiamo scrivere o leggere più di un blocco di dati da o verso un descrittore di file (file, socket, ecc.).

L'I/O vettorizzato viene eseguito usando i metodi **writev**, **readv**, **recvmsg** e  **iovec** struct.```
struct iovec 
{ 
     void __user *iov_base; /* BSD uses caddr_t (1003.1g requires void *) */ \
     __kernel_size_t iov_len; /* Must be size_t (1003.1g) */ \
};

Utilizzando l'I/O vettorizzato, eseguiamo operazioni di I/O su un array di buffer (iovec). Ogni iovec ha un puntatore a un buffer (iov_base) e la dimensione del buffer (iov_len).

I/O vettorizzato

Ad esempio, se vogliamo scrivere un array di buffer in un file (fd), chiamiamo

writev(fd, iovecStack, count)

Questo metodo (come readv e recvmsg) copia prima di tutto l'array di iovec (iovecStack) nello spazio kernel, poi legge da questi buffer e scrive su fd.

  • la prima parte (la copia dell'array di iovec nello spazio kernel) è simile in tutti e tre i metodi.

Possiamo scrivere e leggere buffer usando pipe; pipe è una struct che ci fornisce due descrittori di file, uno per la lettura e uno per la scrittura. Pipe ha una lunghezza in byte; quando un processo scrive in una pipe più della sua lunghezza, la pipe blocca quel processo e attende che un altro processo legga da quella pipe (usando il descrittore di file di lettura di quella pipe).

pipe in Linux

Il kernel cerca di allocare memoria (per una struct) in base alla sua dimensione. Ad esempio, quando abbiamo liberato la struct binder_thread dalla memoria (vedi analisi statica), e dopo questo se abbiamo una struct con una dimensione simile a binder_thread, ha buone probabilità di essere allocata nello stesso posto della binder_thread liberata.

Prima creiamo uno iovecStack (un array di struct iovec) con una dimensione simile alla struct binder_thread.

Poi liberiamo binder_thread dalla memoria (vedi la parte ‘free’ dell'analisi statica)

Poi chiamiamo writev() su questo iovecStack. La prima parte di questo metodo copia iovecStack nello spazio kernel,

È molto probabile che il nostro iovecStack venga allocato nello stesso posto della binder_thread liberata.

Se abbiamo abbastanza iovec in iovecStack, la memoria nella posizione di binder_thread appare così:

alt_text

Si vede che iovecStack[10].iov_base, iovecStack[10].iov_len e iovecStack[11].iov_base saranno nello stesso posto dei campi wait.lock, wait.head.next e wait.head.prev di binder_thread.

Nella parte ‘use’ dell'analisi statica abbiamo visto che il crash è avvenuto a causa di un accesso alla parte wait di binder_thread durante il processo di unlinking (rimozione di un elemento da una lista concatenata).

Durante il processo di unlinking, wait.head viene scollegato e i campi next e prev di quest'ultimo punterebbero al campo wait di binder_thread (unlinking).

alt_text

Prima che avvenga la seconda parte di writev (la scrittura dagli iovec al file), se eseguiamo il processo di unlink, iovecStack[10].iov_len e iovecStack[11].iov_base verranno sovrascritti da un indirizzo del kernel. Poi, dopo aver eseguito il resto di writev, quando vuole elaborare iovecStack[11], legge da iovecStack[11].iov_base (=indirizzo di wait in binder_thread) una lunghezza di dati pari a iovecStack[11].iov_len.

Se iovecStack[11].iov_len è sufficiente, leggiamo dal campo wait fino al campo task_struct della binder_thread, così otteniamo il puntatore a task_struct.

  • durante la parte di free nell'analisi statica, non tutta la binder_thread veniva liberata e parti come task_struct rimangono (non l'abbiamo menzionato per semplicità).

Quindi per ottenere il puntatore a task_struct facciamo quanto segue:

  1. creiamo una pipe e uno stack di iovec
  2. creiamo binder_thread ed event_poll e li colleghiamo (come abbiamo fatto per l'analisi statica)
  3. facciamo fork di un processo figlio (ora abbiamo processo padre e figlio)
  4. nel processo padre chiamiamo writev: importerà iovecStack nello spazio kernel (prima di continuare, blocca writev scrivendo dati spazzatura nella pipe, ad esempio.)
  5. nel processo figlio eseguiamo l'unlink. poi leggiamo i dati spazzatura così il processo padre viene notificato.
  6. nel processo padre continuiamo il resto di writev() e leggiamo dagli iovec nello spazio kernel e scriviamo nel file.
  7. Ora il file contiene la lettura da wait in giù, della binder_thread nello spazio kernel (task_struct è 0xe8 byte dopo il campo wait).

vedi exploit.cpp nel repo.

Modifica di addr_limit in task_struct

Ora abbiamo l'indirizzo di task_struct nello spazio kernel (task_ptr).

Qui usiamo socket_pair invece di pipe. E usiamo recvmsg() per leggere dal socket e scrivere negli iovec.

Passaggi:

  1. prima facciamo le inizializzazioni come sopra
  2. facciamo fork di un processo figlio
  3. scriviamo alcuni dati spazzatura in socket_pair
  4. nel processo padre chiamiamo recvmsg():
    1. importa gli iovec nello spazio kernel
    2. poi legge i dati spazzatura e attende (blocca) per ricevere altri dati dal socket.
  5. nel processo figlio eseguiamo l'operazione di unlink
  6. nel processo figlio scriviamo questi dati nel socket:``` static uint64_t finalSocketData[] = { 0x1, // iovecStack[10].iov_len 0x41414141, // iovecStack[11].iov_base 0x8 + 0x8 + 0x8 + 0x8, // iovecStack[11].iov_len (uint64_t) ((uint8_t *) task_ptr + OFFSET_OF_ADDR_LIMIT_IN_TASK_STRUCT), // iovecStack[12].iov_base 0xFFFFFFFFFFFFFFFE // addr_limit value };
root@kitploit:~
Dopo la scrittura, **recvmsg**() inizia a leggere dal socket e a scrivere in **iovecStack**. A causa della spazzatura, ha scritto fino a **iovecStack**[10].

Quindi inizia a scrivere **finalSocketData** in **iovecStack**[12], ottiene l'indirizzo da **iovecStack[12].iov_base**, che è l'indirizzo di** wait in binder_thread **a causa dell'operazione di unlink, **iovecStack[12].iov_len**  è impostato a 4 byte, quindi scrive:

*   0x1 in iovecStack[10].iov_len
*   0x41414141 in iovecStack[11].iov_base
*   0x8 + 0x8 + 0x8 + 0x8 in iovecStack[11].iov_len
*   **pointer_to_addr_limit** in iovecStack[12].iov_base

ora 4 byte sono stati scritti in **iovecStack[11]**, quindi **recvmsg**() passa a **iovecStack[12]** per scrivere il resto di **finalSocketData**:

scrive **0xFFFFFFFFFFFFFFFE **nell'indirizzo in** iovecStack[12].iov_base** che è impostato a **pointer_to_addr_limit**, il che significa che recvmsg() modifica addr_limit a 0xFFFFFFFFFFFFFFFE (non 0xFFFFFFFFFFFFFFFF a causa di alcuni problemi con arm64).

Ora lo spazio utente si espande a circa tutto lo spazio kernel! (e può fare qualsiasi cosa!!)

# Patch

binder_poll() passa la waitqueue thread->wait su cui si può dormire in attesa di lavoro. Quando un thread che usa epoll esce esplicitamente usando BINDER_THREAD_EXIT, la waitqueue viene liberata, ma <span style="text-decoration:underline;">non viene mai rimossa dalla corrispondente struttura dati epoll</span>. Quando il processo successivamente esce, il codice di pulizia di epoll tenta di accedere alla waitlist, il che provoca una use-after-free.

Previeni questo usando POLLFREE quando il thread esce.

avevamo questo codice:```
static int binder_thread_release(struct binder_proc *proc, struct binder_thread *thread)
{
        .
        .
        .
        int active_transactions = 0;
        .
        .
        .
        binder_thread_dec_tmpref(thread);
        return active_transactions;
}

Queste righe sono state aggiunte al codice sorgente:

root@kitploit:~
static int binder_thread_release(struct binder_proc *proc,
binder_thread *thread)
{
	.
	.
	.
	/*
	 * If this thread used poll, make sure we remove the waitqueue
	 * from any epoll data structures holding it with POLLFREE.
	 * waitqueue_active() is safe to use here because we're holding
	 * the inner lock.
	*/
	  if ((thread->looper & BINDER_LOOPER_STATE_POLL) && waitqueue_active(&thread->wait)) {
		wake_up_poll(&thread->wait, EPOLLHUP | POLLFREE);
	  }
	  binder_inner_proc_unlock(thread->proc);
	  /*
	   * This is needed to avoid races between wake_up_poll() above and
	   * and ep_remove_waitqueue() called for other reasons (eg the epoll file
	   * descriptor being closed); ep_remove_waitqueue() holds an RCU read
	   * lock, so we can be sure it's done after calling synchronize_rcu().
	  */
	  if (thread->looper & BINDER_LOOPER_STATE_POLL)
		synchronize_rcu();
	  .
	  .
	  .
}

vedi il codice completo qui

Definizioni

Kernel

Il sistema operativo è uno strato di software responsabile di far funzionare tutto l'hardware in modo più efficiente e di costruire un'infrastruttura su cui possono funzionare le applicazioni che usi; il suo nucleo è il kernel.

Sfruttamento delle vulnerabilità

L'idea alla base dello sfruttamento delle vulnerabilità è semplice: il software ha bug, e i bug fanno sì che il software si comporti male o esegua in modo errato un compito che era stato progettato per svolgere correttamente; sfruttare un bug significa trasformare questo cattivo comportamento in un vantaggio per gli attaccanti.

I bug che sono sfruttabili vengono chiamati vulnerabilità.

Privilegiato e non privilegiato

Un utente o processo privilegiato è colui che ha accesso completo al dispositivo.

La maggior parte delle architetture dei set di istruzioni fornisce almeno due modalità di esecuzione:

privilegiata : tutte le istruzioni a livello macchina sono accessibili.

non privilegiata : solo un sottoinsieme delle istruzioni è accessibile.

Vulnerabilità UAF

Le vulnerabilità Use-After-Free sono un tipo di difetto di corruzione della memoria che può essere sfruttato dagli hacker per eseguire codice arbitrario.

Use-After-Free si riferisce specificamente al tentativo di accedere alla memoria dopo che è stata liberata, che può causare il crash del programma o, nel caso di un difetto Use-After-Free, può potenzialmente portare all'esecuzione di codice arbitrario o addirittura consentire la piena esecuzione di codice in remoto.

CVE-2019-2215

È un use-after-free in Binder nel kernel Android. Il bug è una vulnerabilità di escalation locale dei privilegi che consente la compromissione totale di un dispositivo vulnerabile. Se combinato con un exploit del renderer del browser, questo bug potrebbe compromettere completamente un dispositivo tramite un sito web dannoso. È raggiungibile dall'interno della sandbox di Chrome.

Nota: funziona su Pixel 1 e 2, ma non su Pixel 3 e 3a. Vedi questo attacco.

Bollettini di sicurezza Android

I bollettini di sicurezza Android sono un elenco pubblicato da Google (mensilmente). Questo elenco contiene le vulnerabilità di sicurezza risolte che interessano il framework Android, il kernel Linux, ecc.

Syzkaller

Syzkaller è un fuzzer per il kernel. Il fuzzing è una tecnica di test in cui un programma automatizzato produce input semi-casuali per un programma target per verificare se viene innescato un bug. Il fuzzing è particolarmente utile per trovare bug di corruzione della memoria nei programmi C o C++.

Project Zero

Project Zero è un team di analisti di sicurezza impiegati da Google con il compito di trovare vulnerabilità zero-day. Una vulnerabilità zero-day è una vulnerabilità sconosciuta a coloro che dovrebbero correggerla.

GDB

GDB è l'acronimo di GNU Project Debugger, il debugger più popolare per i sistemi UNIX per eseguire il debug di programmi C e C++. GDB consente di eseguire il programma fino a un certo punto, poi fermarsi e stampare i valori di determinate variabili in quel punto, oppure scorrere il programma una riga alla volta e stampare i valori di ciascuna variabile dopo l'esecuzione di ogni riga.

Puoi collegare il tuo emulatore Android con un gdbserver per il debug.

Kernel Address Sanitizer

Il Kernel Address SANitizer (KASAN) è un rilevatore dinamico di errori di memoria progettato per trovare bug fuori dai limiti (out-of-bounds) e use-after-free. KASAN usa la strumentazione in fase di compilazione per inserire controlli di validità prima di ogni accesso alla memoria e, pertanto, richiede una versione del compilatore che lo supporti. Gli accessi alla memoria del kernel possono essere verificati tramite la shadow map per vedere se sono validi.

QEMU

QEMU (Quick Emulator) è un emulatore libero e open-source che esegue la virtualizzazione hardware. L'emulatore Android deriva dall'emulatore QEMU; aggiunge il supporto per l'avvio di dispositivi Android, emula l'hardware tipico di Android (OpenGL, GPS, GSM, sensori) e un'interfaccia GUI. L'emulatore Android estende QEMU in vari modi.

Analisi statica

Nell'analisi statica, usiamo il codice sorgente di un programma per trovare un bug o qualsiasi problema. Non eseguiamo il programma.

Analisi dinamica

Nell'analisi dinamica, analizziamo il comportamento di un programma mentre è in esecuzione. Ad esempio fornendo input speciali.

Android NDK

Android NDK è un set di strumenti che ti consente di eseguire codice nativo come C e C++ su un dispositivo Android.

Android Goldfish

Il kernel Android Goldfish viene usato per eseguire il codice del kernel in un emulatore Android. Può essere clonato e modificato e poi compilato per essere usato in un emulatore.

Android Debug Bridge

ADB è uno strumento da riga di comando. Aiuta a comunicare con un dispositivo Android in esecuzione e a ottenere una shell da esso. Può essere usato per scopi di debug.

Riferimenti

https://googleprojectzero.blogspot.com/2019/11/bad-binder-android-in-wild-exploit.html

https://nvd.nist.gov/vuln/detail/CVE-2019-2215

https://www.tutorialspoint.com/gnu_debugger

https://www.kernel.org/doc/html/latest/dev-tools/kasan.html

https://android.googlesource.com/kernel/goldfish/

https://man7.org/linux/man-pages/man7/epoll.7.html

https://www.scaler.com/topics/c/debugging-c-program/

...

Scarica lo strumento
  • avviare adb shell ed eseguire il PoC del trigger