Abuso delle pipeline multimediali Android e dei parser di immagini

Tip

Impara e pratica il hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Impara e pratica il hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Impara e pratica il hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Supporta HackTricks

Modalità di consegna: app di messaggistica ➜ MediaStore ➜ parser privilegiati

Le build OEM moderne eseguono regolarmente indicizzatori multimediali privilegiati che rieseguono la scansione di MediaStore per funzionalità “AI” o di condivisione. Sui firmware Samsung precedenti alla patch di aprile 2025, com.samsung.ipservice carica Quram (/system/lib64/libimagecodec.quram.so) e analizza automaticamente qualsiasi file che WhatsApp (o altre app) inserisce in MediaStore. In pratica un attaccante può inviare un DNG camuffato da IMG-*.jpg, attendere che la vittima tocchi “download” (1 clic), e il servizio privilegiato analizzerà il payload anche se l’utente non apre mai la galleria.

$ file IMG-2025-02-10.jpeg
TIFF image data ...
$ exiftool IMG-2025-02-10.jpeg | grep "Opcode List"
Opcode List 1 : [opcode 23], [opcode 23], ...

Punti chiave

  • La delivery si basa sulla re-parsing dei media di sistema (non sul client di chat) e quindi eredita i permessi di quel processo (accesso completo in lettura/scrittura alla gallery, capacità di inserire nuovi media, ecc.).
  • Qualsiasi image parser raggiungibile tramite MediaStore (vision widgets, wallpapers, AI résumé features, ecc.) diventa raggiungibile da remoto se l’attaccante riesce a convincere la vittima a salvare media.

0-click DD+/EAC-3 decoding path (Google Messages ➜ mediacodec sandbox)

Modern messaging stacks decodificano automaticamente anche l’audio per trascrizioni/ricerche. Su Pixel 9, Google Messages passa l’audio RCS/SMS in arrivo al Dolby Unified Decoder (UDC) dentro /vendor/lib64/libcodec2_soft_ddpdec.so prima che l’utente apra il messaggio, estendendo la superficie 0-click ai media codecs.

Vincoli chiave del parsing

  • Ogni DD+ syncframe ha fino a 6 block; ogni block può copiare fino a 0x1FF byte di skip data controllati dall’attaccante in un skip buffer (≈ 0x1FF * 6 byte per frame).
  • Lo skip buffer viene scansionato per EMDF: syncword (0xX8) + emdf_container_length (16b) + campi a lunghezza variabile. emdf_payload_size è parsato con un loop variable_bits(8) senza bound.
  • I byte del payload EMDF sono allocati dentro un bump allocator per-frame personalizzato chiamato “evo heap” e poi copiati byte-per-byte da un bit-reader limitato da emdf_container_length.

Integer-overflow → heap-overflow primitive (CVE-2025-54957)

  • ddp_udc_int_evo_malloc allinea alloc_size+extra a 8 byte tramite total_size += (8 - total_size) % total_size senza rilevazione di wrap. Valori vicini a 0xFFFFFFFFFFFFFFF9..FF si riducono a un total_size molto piccolo su AArch64.
  • Il loop di copia usa comunque la payload_length logica da emdf_payload_size, quindi i byte dell’attaccante sovrascrivono dati dell’evo-heap oltre il chunk sottodimensionato.
  • La lunghezza dell’overflow è esattamente limitata da emdf_container_length scelto dall’attaccante; i byte di overflow sono dati EMDF controllati dall’attaccante. Lo slab allocator viene resettato ad ogni syncframe, dando adiacenza prevedibile.

Primitive di lettura secondaria Se emdf_container_length > skipl, il parsing EMDF legge oltre i byte inizializzati dello skip (OOB read). Da solo rivela zeri/media noti, ma dopo aver corrotto metadata adiacenti dell’heap può leggere indietro la regione corrotta per validare l’exploit.

Ricetta di exploit

  1. Costruire un EMDF con emdf_payload_size enorme (tramite variable_bits(8)) in modo che il padding dell’allocatore vada in wrap su un chunk piccolo.
  2. Impostare emdf_container_length alla lunghezza d’overflow desiderata (≤ budget totale di skip); inserire i byte di overflow nel payload EMDF.
  3. Modellare l’evo heap per-frame così che la piccola allocazione sia prima delle strutture target dentro il buffer statico del decoder (≈693 KB) o il buffer dinamico (≈86 KB) allocato una volta per istanza del decoder.
  4. Opzionalmente scegliere emdf_container_length > skipl per leggere indietro i dati sovrascritti dallo skip buffer dopo la corruzione.

Quram’s DNG Opcode Interpreter Bugs

I file DNG incorporano tre liste di opcode applicate in stadi diversi di decodifica. Quram copia l’API di Adobe, ma il suo handler di Stage-3 per DeltaPerColumn (opcode ID 11) si fida dei bounds del plane forniti dall’attaccante.

Bounds dei plane errati in DeltaPerColumn

  • Gli attaccanti impostano plane=5125 e planes=5123 anche se le immagini Stage-3 espongono solo i plane 0–2 (RGB).
  • Quram calcola opcode_last_plane = image_planes + opcode_planes invece di plane + count, e non verifica mai se il range risultante dei plane rientra nell’immagine.
  • Il loop quindi scrive un delta in raw_pixel_buffer[plane_index] con un offset completamente controllato (es., plane 5125 ⇒ offset 5125 * 2 bytes/pixel = 0x2800). Ogni opcode aggiunge un valore float a 16-bit (0x6666) alla posizione target, fornendo una primitiva di heap OOB add precisa.

Trasformare incrementi in scritture arbitrarie

  • L’exploit prima corrompe QuramDngImage.bottom/right di Stage-3 usando 480 DeltaPerColumn malformati così che opcode futuri trattino coordinate enormi come in-bounds.
  • Gli opcode MapTable (opcode 7) vengono poi diretti a quei bounds finti. Usando una tabella di sostituzione tutta zeri o un DeltaPerColumn con delta -Inf, l’attaccante azzera qualunque regione, poi applica delta addizionali per scrivere valori esatti.
  • Poiché i parametri degli opcode risiedono nei metadata DNG, il payload può codificare centinaia di migliaia di scritture senza toccare direttamente la memoria di processo.

Heap Shaping Under Scudo

Scudo raggruppa le allocazioni per size. Quram per caso alloca i seguenti oggetti con chunk di dimensione identica 0x30, quindi finiscono nella stessa regione (spaziatura di 0x40 byte sull’heap):

  • descriptor QuramDngImage per Stage 1/2/3
  • QuramDngOpcodeTrimBounds e opcode vendor Unknown (ID ≥14, incluso ID 23)

L’exploit ordina le allocazioni per posizionare i chunk in modo deterministico:

  1. Opcode Stage-1 Unknown(23) (20.000 entries) spruzzano chunk da 0x30 che poi vengono freed.
  2. Stage-2 freea quegli opcode e piazza un nuovo QuramDngImage nella regione liberata.
  3. 240 entry Stage-2 Unknown(23) vengono freeate, e Stage-3 alloca immediatamente il suo QuramDngImage più un nuovo raw pixel buffer della stessa dimensione, riutilizzando quegli slot.
  4. Un TrimBounds costruito corre per primo nella lista 3 e alloca un altro raw pixel buffer prima di freeare lo stato di Stage-2, garantendo l’adiacenza “raw pixel buffer ➜ QuramDngImage”.
  5. 640 TrimBounds aggiuntivi sono marcati minVersion=1.4.0.1 così il dispatcher li salta, ma i loro oggetti sottostanti restano allocati e più tardi diventano bersagli primitivi.

Questa coreografia mette il raw buffer di Stage-3 immediatamente prima del QuramDngImage di Stage-3, quindi l’overflow basato sui plane ribalta campi dentro il descriptor anziché causare crash di stato casuale.

Reusing Vendor “Unknown” Opcodes as Data Blobs

Samsung lascia il bit alto settato negli ID opcode vendor-specific (es., ID 23), il che istruisce l’interprete ad allocare la struttura ma saltarne l’esecuzione. L’exploit abusa di quegli oggetti dormienti come heap controllati dall’attaccante:

  • Le entries Unknown(23) delle liste opcode 1 e 2 servono come scratchpad contiguo per memorizzare byte del payload (JOP chain a offset 0xf000 e un comando shell a 0x10000 relativo al raw buffer).
  • Poiché l’interprete tratta ancora ogni oggetto come un opcode quando la lista 3 viene processata, conquistare la vtable di un oggetto è sufficiente per iniziare l’esecuzione dei dati dell’attaccante.

Crafting Bogus MapTable Objects & Bypassing ASLR

Gli oggetti MapTable sono più grandi dei TrimBounds, ma una volta che la corruzione del layout si verifica, il parser legge volentieri parametri extra out-of-bounds:

  1. Usare la primitiva di scrittura lineare per sovrascrivere parzialmente un puntatore vtable di TrimBounds con una tabella di sostituzione MapTable costruita che mappa i due byte bassi da una vtable TrimBounds vicina alla vtable MapTable. Solo i byte bassi differiscono tra le build Quram supportate, quindi una singola lookup table da 64K può gestire sette versioni firmware e ogni slide ASLR da 4 KB.
  2. Patchare il resto dei campi di TrimBounds (top/left/width/planes) così che l’oggetto si comporti come un MapTable valido quando eseguito più tardi.
  3. Eseguire l’opcode fake su memoria azzerata. Poiché il puntatore alla substitution table in realtà punta alla vtable di un altro opcode, i byte di output diventano leaked indirizzi low-order da libimagecodec.quram.so o dal suo GOT.
  4. Applicare ulteriori passaggi MapTable per convertire quei leak di due byte in offset verso gadget come __ink_jpeg_enc_process_image+64, QURAMWINK_Read_IO2+124, qpng_check_IHDR+624, e l’entry di libc __system_property_get. Gli attaccanti ricostruiscono effettivamente indirizzi completi dentro la loro regione di spray di opcode senza API native di disclosure di memoria.

Triggering the JOP ➜ system() Transition

Una volta che i puntatori ai gadget e il comando shell sono piazzati nello spray di opcode:

  1. Un’ultima ondata di scritture DeltaPerColumn aggiunge 0x0100 all’offset 0x22 del QuramDngImage di Stage-3, spostando il suo raw buffer pointer di 0x10000 così che ora punti alla stringa del comando dell’attaccante.
  2. L’interprete comincia ad eseguire la coda di 1040 opcode Unknown(23). La prima entry corrotta ha la sua vtable sostituita con la tabella falsificata a offset 0xf000, così QuramDngOpcode::aboutToApply risolve qpng_read_data (la 4a voce) dalla fake table.
  3. I gadget concatenati eseguono: caricano il puntatore QuramDngImage, aggiungono 0x20 per puntare al raw buffer pointer, lo dereferenziano, copiano il risultato in x19/x0, poi saltano attraverso slot GOT riscritti a system. Poiché il raw buffer pointer ora è la stringa dell’attaccante, il gadget finale esegue system(<shell command>) dentro com.samsung.ipservice.

Note sulle varianti dell’allocator

Esistono due famiglie di payload: una tarata per jemalloc, un’altra per scudo. Differiscono nell’ordine dei blocchi opcode per ottenere l’adiacenza ma condividono le stesse primitive logiche (bug DeltaPerColumn ➜ MapTable zero/write ➜ bogus vtable ➜ JOP). La quarantine disabilitata di Scudo rende il reuse della freelist da 0x30 byte deterministico, mentre jemalloc si affida al controllo della size-class tramite sizing di tile/subIFD.

References

Tip

Impara e pratica il hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Impara e pratica il hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Impara e pratica il hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Supporta HackTricks