
Preuve de concept et harnais de reproduction instrumenté pour CVE-2026-28609, une écriture hors limites dans MatroskaExtractor d'Android, exploitable via un fichier WebM conçu avec une piste audio PCM en big-endian.
Preuve de concept, harnais de reproduction instrumenté et analyse technique pour CVE-2026-28609, une écriture hors limites dans le MatroskaExtractor d'Android, atteignable via un fichier WebM forgé contenant une piste audio PCM en big-endian.
frameworks/av/media/module/extractors/mkv/MatroskaExtractor.cppMatroskaSource::read() dans le MatroskaExtractor d'Android contient
une boucle d'inversion d'octets PCM big-endian qui transtype un pointeur
vers les données de trame en uint16_t * avant d'ajouter
frame->range_offset(), un décalage en octets.
Comme l'arithmétique de pointeurs en C sur un uint16_t * multiplie le
décalage par sizeof(uint16_t) = 2, le pointeur résultant se trouve à
2 * range_offset octets après le début du tampon, et non à
range_offset octets.
Lorsque range_offset > 0, la boucle lit et écrit un ou plusieurs octets
au-delà de la fin du MediaBuffer de la trame. Sur un appareil sans ASan,
la lecture excédentaire réussit silencieusement et l'écriture excédentaire
corrompt l'octet situé immédiatement après le tampon.
La ligne vulnérable :
// MatroskaExtractor.cpp:1105 (pre-fix)
uint16_t *dstData = (uint16_t *)frame->data() + frame->range_offset();
uint16_t *srcData = (uint16_t *)frame->data() + frame->range_offset();
for (size_t i = 0; i < frame->range_length() / 2; i++) {
dstData[i] = ntohs(srcData[i]);
}
Le correctif amont convertit le pointeur en uint8_t * avant d'appliquer
le décalage :
uint16_t *data = (uint16_t *)((uint8_t *)frame->data() + frame->range_offset());
for (size_t i = 0; i < frame->range_length() / 2; i++) {
data[i] = ntohs(data[i]);
}
Un déclencheur fonctionnel et reproductible pour CVE-2026-28609, vérifié sous AddressSanitizer sur un véritable appareil Android 14. Le dépôt comprend :
range_offset non nul.dlopen,
invoque GETEXTRACTORDEF et lit les trames depuis le fichier.Le résultat, sur un appareil vulnérable :
==14927==ERROR: AddressSanitizer: heap-buffer-overflow
READ of size 2 at 0x003c61ab3960 thread T0
#0 ... MatroskaSource::read(...) MatroskaExtractor.cpp:1113
0x003c61ab3961 is located 0 bytes after 65-byte region
La branche vulnérable nécessite que toutes les conditions suivantes soient remplies :
| Condition | Source |
|---|---|
| La piste est PCM | mType == PCM |
| Big-endian | AMEDIAFORMAT_KEY_PCM_BIG_ENDIAN == 1 |
| Échantillons 16 bits | AMEDIAFORMAT_KEY_BITS_PER_SAMPLE == 16 |
La trame a un range_offset non nul | Défini par set_range(offset, ...) |
Les trois premières sont satisfaites en déclarant la piste avec l'ID de codec
A_PCM/INT/BIG et une profondeur de bits de 16. La quatrième est la plus intéressante.
range_offset n'est défini à une valeur non nulle que dans
MatroskaSource::setWebmBlockCryptoInfo(), qui est appelée depuis
readBlock() sous la condition :
if (err == OK && mExtractor->mIsWebm && trackInfo->mEncrypted) {
err = setWebmBlockCryptoInfo(mbuf);
}
Par conséquent, le fichier déclencheur doit satisfaire :
mIsWebm — l'élément EBML DocType doit être "webm", et non
"matroska".mEncrypted — la piste doit déclarer ContentEncodings avec
ContentEncodingType = 1 (Encryption) et un ContentEncKeyID.0x1) vaut 0, indiquant que l'image
n'est pas chiffrée mais encodée au niveau du contenu. Cela emprunte la branche else dans
setWebmBlockCryptoInfo, qui appelle set_range(1, len - 1).Après le strip, range_offset = 1 pour chaque image. La branche
vulnérable calcule alors (uint16_t *)data + 1, ce qui avance de 2 octets,
et la boucle lit/écrit les octets [2, 2 + range_length) — un octet
au-delà de la fin de l'allocation de 65 octets (image de 64 octets + 1 octet
de signal).
+---------+------------------------------------+
| 0x00 | 64 bytes of frame data (0xAA...) |
+---------+------------------------------------+
signal PCM payload
L'octet de signal 0x00 est supprimé par set_range(1, 64), laissant une trame de 64 octets dans une allocation de 65 octets. L'arithmétique de pointeur vulnérable écrit ensuite à l'offset d'octet 2 à 65.
.
├── README.md
├── LICENSE
├── .gitignore
│
├── exploit/
│ └── generator.py WebM generator + verifier
│
├── harness/
│ └── harness_c_abi.cpp dlopen-based trigger harness
│
└── scripts/
├── build.conf API level, sanitizer, RTTI flags
├── include_dirs.conf.sample Include roots template
├── build_foundation.sh Build the foundation archive
├── build_plugin.sh Build the extractor plugin
├── build_harness.sh Build the harness
└── run.sh End-to-end build + push + run
Dépendances en amont, clonées par l'utilisateur avant la compilation :
av/ frameworks/av (AOSP)
libwebm/ external/libwebm (mkvparser)
flac/ external/flac
aosp-includes/ system/core, system/logging, system/libbase,
frameworks/native — header trees only
aosp-includes/libs/ libstagefright_foundation.so, libmedia.so,
libutils.so, libbinder.so, libcutils.so,
libbase.so, libmediandk.so, libstagefright_flacdec.so
— pulled from the target device
bash, python3, make$ANDROID_NDK_HOMEadb dans le PATH (Linux, ou adb.exe depuis WSL)mkdir -p deps && cd deps
# AOSP frameworks/av (contains the vulnerable extractor)
git clone --depth 1 -b android-14.0.0_r1 \
https://android.googlesource.com/platform/frameworks/av av
# libwebm (mkvparser)
git clone --depth 1 \
https://android.googlesource.com/platform/external/libwebm libwebm
# libFLAC
git clone --depth 1 \
https://android.googlesource.com/platform/external/flac flac
# AOSP header trees (no full checkout required)
mkdir -p aosp-includes
cd aosp-includes
for m in core libbase logging native; do
git clone --depth 1 \
"https://android.googlesource.com/platform/system/$m" "$m" 2>/dev/null || true
done
cd ../..