Abuser les pipelines multimédia Android et les parseurs d’images

Tip

Apprenez et pratiquez le hacking AWS :HackTricks Training AWS Red Team Expert (ARTE)
Apprenez et pratiquez le hacking GCP : HackTricks Training GCP Red Team Expert (GRTE) Apprenez et pratiquez le hacking Azure : HackTricks Training Azure Red Team Expert (AzRTE)

Soutenir HackTricks

Livraison : Applications de messagerie ➜ MediaStore ➜ Parseurs privilégiés

Les builds OEM modernes lancent régulièrement des indexeurs multimédia privilégiés qui parcourent à nouveau MediaStore pour des fonctionnalités “AI” ou de partage. Sur les firmwares Samsung antérieurs au patch d’avril 2025, com.samsung.ipservice charge Quram (/system/lib64/libimagecodec.quram.so) et analyse automatiquement tout fichier que WhatsApp (ou d’autres apps) dépose dans MediaStore.

En pratique, un attaquant peut envoyer un DNG déguisé en IMG-*.jpg, attendre que la victime appuie sur “download” (1-clic), et le service privilégié analysera la charge utile même si l’utilisateur n’ouvre jamais la galerie.

$ 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], ...

Points clés

  • La livraison repose sur le re-parsing média du système (pas le client de chat) et hérite donc des permissions de ce processus (accès lecture/écriture complet à la galerie, capacité à déposer de nouveaux médias, etc.).
  • Tout image parser accessible via MediaStore (vision widgets, wallpapers, fonctionnalités AI de résumé, etc.) devient accessible à distance si l’attaquant parvient à convaincre une cible d’enregistrer un média.

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

Les stacks de messagerie modernes décodent aussi automatiquement l’audio pour la transcription/la recherche. Sur Pixel 9, Google Messages remet l’audio incoming RCS/SMS au Dolby Unified Decoder (UDC) situé dans /vendor/lib64/libcodec2_soft_ddpdec.so avant que l’utilisateur n’ouvre le message, étendant la surface 0-click aux media codecs.

Contraintes clés de parsing

  • Chaque DD+ syncframe contient jusqu’à 6 blocks ; chaque block peut copier jusqu’à 0x1FF octets de skip data contrôlée par l’attaquant dans un skip buffer (≈ 0x1FF * 6 octets par frame).
  • Le skip buffer est scanné pour EMDF : syncword (0xX8) + emdf_container_length (16b) + champs de longueur variable. emdf_payload_size est parsé avec une boucle non bornée variable_bits(8).
  • Les octets de payload EMDF sont alloués dans un bump allocator per-frame personnalisé appelé “evo heap” puis copiés octet par octet depuis un bit-reader borné par emdf_container_length.

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

  • ddp_udc_int_evo_malloc aligne alloc_size+extra à 8 octets via total_size += (8 - total_size) % total_size sans détection de wrap. Des valeurs proches de 0xFFFFFFFFFFFFFFF9..FF se réduisent à un total_size minuscule sur AArch64.
  • La boucle de copie utilise toujours la payload_length logique issue de emdf_payload_size, donc les octets de l’attaquant écrasent des données de l’evo-heap au-delà du chunk sous-dimensionné.
  • La longueur d’overflow est précisément plafonnée par emdf_container_length choisi par l’attaquant ; les octets d’overflow sont des données EMDF contrôlées par l’attaquant. Le slab allocator est réinitialisé à chaque syncframe, donnant une adjacency prédictible.

Primitive de lecture secondaire Si emdf_container_length > skipl, le parsing EMDF lit au-delà des skip bytes initialisés (OOB read). Pris seul, it leaks zeros/known media, mais après avoir corrompu des métadonnées heap adjacentes il peut relire la région corrompue pour valider l’exploit.

Recette d’exploitation

  1. Construire un EMDF avec un emdf_payload_size énorme (via variable_bits(8)) de sorte que le padding de l’allocateur wrappe en un petit chunk.
  2. Définir emdf_container_length à la longueur d’overflow désirée (≤ budget total de skip data) ; placer les octets d’overflow dans le payload EMDF.
  3. Modeler l’evo heap per-frame pour que la petite allocation soit placée avant les structures cibles à l’intérieur du buffer statique du decoder (≈693 KB) ou du buffer dynamique (≈86 KB) alloué une fois par instance de decoder.
  4. Optionnellement choisir emdf_container_length > skipl pour relire les données écrasées depuis le skip buffer après corruption.

Quram’s DNG Opcode Interpreter Bugs

Les fichiers DNG incorporent trois listes d’opcodes appliquées à différentes étapes du décodage. Quram reproduit l’API d’Adobe, mais son handler Stage-3 pour DeltaPerColumn (opcode ID 11) fait confiance aux bounds de plane fournis par l’attaquant.

Bounds de plane défaillants dans DeltaPerColumn

  • Les attaquants positionnent plane=5125 et planes=5123 alors que les images Stage-3 n’exposent que les planes 0–2 (RGB).
  • Quram calcule opcode_last_plane = image_planes + opcode_planes au lieu de plane + count, et ne vérifie jamais si la plage résultante de planes tient dans l’image.
  • La boucle écrit donc un delta dans raw_pixel_buffer[plane_index] avec un offset totalement contrôlé (par ex. plane 5125 ⇒ offset 5125 * 2 bytes/pixel = 0x2800). Chaque opcode ajoute une valeur float 16 bits (0x6666) à l’emplacement ciblé, produisant une primitive d’add heap OOB précise.

Transformer des incréments en écritures arbitraires

  • L’exploit corrompt d’abord QuramDngImage.bottom/right du Stage-3 en utilisant 480 opérations DeltaPerColumn malformées afin que les opcodes futurs considèrent d’énormes coordonnées comme in-bounds.
  • Les opcodes MapTable (opcode 7) sont alors dirigés vers ces bounds factices. En utilisant une table de substitution composée de zéros complets ou un DeltaPerColumn avec des deltas -Inf, l’attaquant met à zéro n’importe quelle région, puis applique des deltas additionnels pour écrire des valeurs exactes.
  • Puisque les paramètres des opcodes vivent dans les metadata DNG, le payload peut encoder des centaines de milliers d’écritures sans toucher directement la mémoire du processus.

Heap Shaping Under Scudo

Scudo bucketise les allocations par taille. Quram alloue par hasard les objets suivants avec des tailles de chunk identiques de 0x30 bytes, ils atterrissent donc dans la même région (espacement 0x40 bytes sur le heap) :

  • des descriptors QuramDngImage pour Stage 1/2/3
  • QuramDngOpcodeTrimBounds et opcodes vendor Unknown (ID ≥14, incluant ID 23)

L’exploit ordonne les allocations pour placer les chunks de façon déterministe :

  1. Les opcodes Stage-1 Unknown(23) (20 000 entrées) sprayent des chunks 0x30 qui seront plus tard freed.
  2. Stage-2 libère ces opcodes et place un nouveau QuramDngImage dans la région libérée.
  3. 240 entrées Stage-2 Unknown(23) sont freed, et le Stage-3 alloue immédiatement son QuramDngImage plus un nouveau raw pixel buffer de la même taille, réutilisant ces emplacements.
  4. Un TrimBounds crafté s’exécute en premier dans la liste 3 et alloue encore un raw pixel buffer avant de free l’état Stage-2, garantissant l’adjacence “raw pixel buffer ➜ QuramDngImage”.
  5. 640 TrimBounds supplémentaires sont marqués minVersion=1.4.0.1 pour que le dispatcher les saute, mais leurs objets backing restent alloués et deviennent plus tard des cibles primitives.

Cette chorégraphie place le raw buffer du Stage-3 immédiatement avant le QuramDngImage Stage-3, si bien que l’overflow basé sur les planes inverse des champs à l’intérieur du descriptor au lieu de crasher de l’état aléatoire.

Reuse des opcodes vendor “Unknown” comme blobs de données

Samsung laisse le bit haute positionné dans les IDs d’opcodes vendor (par ex. ID 23), ce qui ordonne à l’interpréteur d’allouer la structure mais de sauter son exécution. L’exploit abuse de ces objets dormants comme heaps contrôlés par l’attaquant :

  • Les entrées Unknown(23) des listes d’opcodes 1 et 2 servent de scratchpads contigus pour stocker des octets de payload (JOP chain à offset 0xf000 et une commande shell à 0x10000 relative au raw buffer).
  • Parce que l’interpréteur traite toujours chaque objet comme un opcode quand la liste 3 est processée, prendre le contrôle de la vtable d’un objet suffit plus tard à commencer l’exécution des données de l’attaquant.

Construction de faux objets MapTable & contournement de l’ASLR

Les objets MapTable sont plus grands que TrimBounds, mais une fois la corruption de layout effectuée, le parser lit volontiers des paramètres supplémentaires hors-bornes :

  1. Utiliser la primitive d’écriture linéaire pour écraser partiellement un pointeur de vtable TrimBounds avec une table de substitution MapTable craftée qui mappe les 2 bytes bas d’une vtable TrimBounds voisine vers la vtable MapTable. Seuls les octets bas diffèrent entre les builds Quram supportés, donc une unique table de lookup 64K peut couvrir sept versions firmware et chaque slide ASLR de 4 KB.
  2. Patcher le reste des champs TrimBounds (top/left/width/planes) afin que l’objet se comporte comme un MapTable valide quand il sera exécuté plus tard.
  3. Exécuter l’opcode factice sur une mémoire nulle. Parce que le pointeur de table de substitution référence en réalité la vtable d’un autre opcode, les octets de sortie deviennent des low-order addresses leaked depuis libimagecodec.quram.so ou son GOT.
  4. Appliquer des passes MapTable additionnelles pour convertir ces fuites de deux octets en offsets vers des gadgets tels que __ink_jpeg_enc_process_image+64, QURAMWINK_Read_IO2+124, qpng_check_IHDR+624, et l’entrée __system_property_get de libc. Les attaquants reconstruisent ainsi des adresses complètes à l’intérieur de leur région sprayée d’opcodes sans API native de divulgation mémoire.

Déclencher la transition JOP ➜ system()

Une fois les pointeurs de gadget et la commande shell placés dans le spray d’opcodes :

  1. Une dernière vague d’écritures DeltaPerColumn ajoute 0x0100 à l’offset 0x22 du QuramDngImage Stage-3, décalant son pointeur de raw buffer de 0x10000 de sorte qu’il référence maintenant la chaîne de commande de l’attaquant.
  2. L’interpréteur commence à exécuter la queue de 1040 opcodes Unknown(23). La première entrée corrompue a sa vtable remplacée par la table forgée à l’offset 0xf000, donc QuramDngOpcode::aboutToApply résout qpng_read_data (la 4ᵉ entrée) depuis la fausse table.
  3. Les gadgets enchaînés effectuent : chargement du pointeur QuramDngImage, addition de 0x20 pour pointer vers le pointeur raw buffer, déréférencement, copie du résultat dans x19/x0, puis saut via des slots GOT réécrits vers system. Parce que le pointeur raw buffer vaut maintenant la chaîne de l’attaquant, le gadget final exécute system(<shell command>) à l’intérieur de com.samsung.ipservice.

Notes sur les variantes d’allocateur

Deux familles de payload existent : une ajustée pour jemalloc, l’autre pour scudo. Elles diffèrent dans l’ordre des blocks d’opcode pour obtenir l’adjacence mais partagent les mêmes primitives logiques (bug DeltaPerColumn ➜ MapTable zero/write ➜ fake vtable ➜ JOP). La quarantaine désactivée de Scudo rend la réutilisation du freelist 0x30-byte déterministe, tandis que jemalloc s’appuie sur le contrôle des size-class via le tile/subIFD sizing.

Références

Tip

Apprenez et pratiquez le hacking AWS :HackTricks Training AWS Red Team Expert (ARTE)
Apprenez et pratiquez le hacking GCP : HackTricks Training GCP Red Team Expert (GRTE) Apprenez et pratiquez le hacking Azure : HackTricks Training Azure Red Team Expert (AzRTE)

Soutenir HackTricks