
Analyse approfondie et preuve de concept pour CVE-2026-27280, une écriture hors limites dans dng_render_task::ProcessArea du SDK Adobe DNG, accessible via l'API de décodage d'images d'Android, avec cause racine, évaluation de l'exploitabilité et analyse des écarts RCE.
dng_render_task::ProcessArea (Adobe DNG SDK / Android)Atteint depuis un fichier DNG ordinaire via l'API publique de décodage d'images d'Android, et
reproduit sur un Galaxy S25 en utilisant le /system/lib64/libdng_sdk.so du téléphone lui-même.
DNG_REQUIRE(srcArea.Size() == dstArea.Size()) + indexation srcArea)#12 AImageDecoder_decodeImage /system/lib64/libjnigraphics.so
#11 android::ImageDecoder::decode /system/lib64/libhwui.so
#09 SkCodec::getPixels /system/lib64/libhwui.so
#08 SkRawCodec::onGetPixels /system/lib64/libhwui.so
#07 dng_render::Render() /system/lib64/libdng_sdk.so
#06 SkDngHost::PerformAreaTask /system/lib64/libhwui.so
#00 dng_render_task::ProcessArea /system/lib64/libdng_sdk.so <-- vulnérable
ProcessArea mappe un tampon temporaire de la taille d'une tuile sur srcArea mais l'indexe avec les
coordonnées de dstArea. Comme srcArea = dstArea + fSrcOffset et que fSrcOffset est le
DefaultCropOrigin du fichier — non nul dans tous les DNG d'appareils photo réels — le pointeur de
destination est déplacé hors de l'allocation de fSrcOffset * rowStep * pixelSize, et la relecture du masque
y écrit. C'est un déplacement, pas un débordement d'entier, donc aucune allocation énorme n'est nécessaire :
c'est pourquoi c'est atteignable sur un téléphone.
01-vulnerability/ les versions vulnérable et corrigée de dng_render.cpp, et le diff 2471->2502
02-poc/ générateurs, chargeurs, sondes (sources) + binaires arm64 précompilés
03-samples/ les fichiers DNG
04-evidence/ journaux ASAN, tombstones de l'appareil, sorties des sondes
05-analysis/ cause racine, générateur, mesures des voisins, analyse des écarts pour la RCE
Le travail qui va au-delà du verdict antérieur : il réfute deux de ses affirmations par
mesure, comble l'écart « un pointeur corrompu est-il utilisé », et identifie précisément pourquoi la
chaîne s'arrête. Lisez 06-rce-lab/README.md en premier ; les fichiers evidence/*.md sont les
comptes-rendus par expérience, et src/ + apk/ sont les harnais reproductibles.
adb push 02-poc/bin-arm64/poc_aimg 03-samples/trigger_natural_croporigin.dng /data/local/tmp/
adb shell "cd /data/local/tmp && chmod 755 poc_aimg && ./poc_aimg trigger_natural_croporigin.dng"
# -> Segmentation fault, trames tombstone dans libdng_sdk.so
La reconstruction des chargeurs arm64 nécessite les propres drapeaux dng_sdk d'Android (depuis Android.bp), sinon
les mises en page des structures ne correspondront pas à la bibliothèque embarquée :
-DUNIX_ENV=1 -DqDNGBigEndian=0 -DqDNGThreadSafe=1 -DqDNGUseLibJPEG=1 \
-DqDNGUseXMP=0 -DqDNGValidateTarget=1 -DqDNGUsingSanitizer=1 -DqLinux=1 -DqAndroid=1
Cela correspond à CVE-2026-27280 (DNG SDK 1.7.1 2471 et antérieurs, CWE-787, CVSS 3.1 7.8 ÉLEVÉE), publié le 2026-03-10 — correspondance par plage de versions et classe, non confirmé par Adobe nommant la fonction.
Prouvé, sur le vrai chemin de décodage Android, sur un appareil patché au 2026-01-01 :
écriture hors limites avec destination choisie par l'attaquant (granularité de 4 octets, dans les deux
directions, indépendante de l'ASLR) ; atteignable depuis un DNG simple via AImageDecoder,
démontré en tuant Samsung Gallery dans son chargeur de vignettes ; corruption silencieuse et déterministe
des voisins ; un pointeur corrompu atteignant free() (N1 clos) ; un
heap-spray exprimé par le format DNG (ImageStats × SubIFD) ; l'écriture visant
ces objets pulvérisés, 6/10 contre 0/10.
Pas une RCE, sur le chemin Android, et pour une raison racine : le code peut écrire une seule
valeur, 0x3F800000. Tout en découle — l'écriture est contiguë (pas de pas
sur les en-têtes Scudo), il n'y a pas de zéro à écrire (pas de corruption de longueur/référence), et la
réécriture partielle préservant les balises a exactement un décalage de cible fixe. Une valeur est
un seul tir contre une réservation aléatoire de 256 Mo, et aucun volume de spray ni aucune
concurrence n'ajoute de tirs. Sur le SDK de bureau, l'ensemble inscriptible est de 256 valeurs → 256 tirs
sur une bande d'environ 67 Mo, c'est pourquoi le 7.8 est correct là-bas.
Retiré du verdict antérieur (05-analysis/05-exploitability-verdict.md),
falsifié par la mesure : « un décodeur réel n'offre aucune primitive de spray » et « le
chevauchement n'est pas orientable depuis l'intérieur d'un seul décodage ». Le bloc n'est pas PAC, BTI ou les
sommes de contrôle de Scudo — c'est la valeur unique inscriptible, une propriété du code que la
correction 2502 d'Adobe élimine à la racine en indexant avec srcArea.
Aucun exploit fonctionnel n'a été construit, et aucun n'est publié ici.
| fichier | ce qu'il fait |
|---|
trigger_natural_croporigin.dng | géométrie naturelle (DefaultCropOrigin = 8,8) ; plante AImageDecoder |
benign_reference.dng | même fichier avec origine 0 — se décode correctement, utilisé pour sonder le tas |
silent_corruption.dng | petite classe de taille : écrit hors limites sans planter (5/5) |
far_write_crash.dng | grand déplacement : défaut visible |
| fichier | contenu |
|---|
01-root-cause.md | le bug, la correction, la forme de l'écriture |
02-dng-generator.md | comment le fichier déclencheur est construit, les deux paramètres de réglage, les pièges |
03-neighbours.md | chiffres de contrôlabilité, ce qui se trouve à côté du tampon, les atténuations |
04-what-is-missing-for-rce.md | ce qui est prouvé (P1-P8), ce qui ne l'est pas (N1-N3), les étapes restantes |
05-exploitability-verdict.md | le verdict antérieur — remplacé par 06-rce-lab, conservé pour la trace |
| fichier | contenu |
|---|
EXP1-stride-vs-scudo.md | pas vs en-têtes Scudo : possible dans l'abstrait, impossible sur le chemin Android (planes==1) |
EXP2-N1-corrupted-pointer-used.md | un pointeur corrompu atteint free() — N1 clos |
EXP2-real-process.md | l'écriture dans le vrai processus Samsung Gallery, rien d'artificiel |
EXP-imagestats-spray.md | le heap-spray que le verdict antérieur jugeait impossible, exprimé par le format DNG |
EXP-aimed-spray.md | viser l'écriture sur des objets pulvérisés : 6/10 contre 0/10 |
EXP-small-class-in-app.md | pointeurs de code à portée dans un vrai processus d'application |
EXP-band-commit.md / EXP-concurrency.md | pourquoi la validation de la cible échoue : une valeur inscriptible = un seul tir |
EXP-write-read-mismatch.md | un second défaut + l'audit complet de la source de ProcessArea |
EXP-value-1.0f.md | la route du 1.0f comme donnée, fermée structurellement |