
Prueba de concepto y arnés de reproducción instrumentado para CVE-2026-28609, una escritura fuera de límites en MatroskaExtractor de Android, alcanzable mediante un archivo WebM manipulado con una pista de audio PCM big-endian.
Prueba de concepto, arnés de reproducción instrumentado y análisis técnico para CVE-2026-28609, una escritura fuera de límites en el MatroskaExtractor de Android, alcanzable mediante un archivo WebM manipulado con una pista de audio PCM big-endian.
frameworks/av/media/module/extractors/mkv/MatroskaExtractor.cppMatroskaSource::read() en el MatroskaExtractor de Android contiene un
bucle de intercambio de bytes PCM big-endian que convierte un puntero a datos
de trama en uint16_t * antes de sumar frame->range_offset(), un
desplazamiento en bytes. Dado que la aritmética de punteros de C sobre un
uint16_t * escala el desplazamiento por sizeof(uint16_t) = 2, el puntero
resultante queda 2 * range_offset bytes más allá del inicio del búfer, y no
range_offset bytes.
Cuando range_offset > 0, el bucle lee y escribe uno o más bytes más allá
del final del MediaBuffer de la trama. En un dispositivo sin ASan, la
lectura excesiva se realiza sin errores de forma silenciosa y la escritura
excesiva corrompe el byte inmediatamente posterior al búfer.
La línea vulnerable:
// 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]);
}
La corrección upstream convierte el puntero a uint8_t * antes de aplicar
el desplazamiento:
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 disparador funcional y reproducible para CVE-2026-28609, verificado bajo AddressSanitizer en un dispositivo Android 14 real. El repositorio incluye:
range_offset distinto de cero.dlopen,
invoca GETEXTRACTORDEF y lee fotogramas del archivo.El resultado, en un dispositivo vulnerable:
==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 rama vulnerable requiere que se cumplan todas las siguientes condiciones:
| Condición | Origen |
|---|---|
| La pista es PCM | mType == PCM |
| Big-endian | AMEDIAFORMAT_KEY_PCM_BIG_ENDIAN == 1 |
| Muestras de 16 bits | AMEDIAFORMAT_KEY_BITS_PER_SAMPLE == 16 |
El frame tiene range_offset distinto de cero | Establecido por set_range(offset, ...) |
Las tres primeras se satisfacen declarando la pista con el ID de códec
A_PCM/INT/BIG y una profundidad de bits de 16. La cuarta es la interesante.
range_offset se establece a un valor distinto de cero solo en
MatroskaSource::setWebmBlockCryptoInfo(), que se invoca desde
readBlock() bajo la condición:
if (err == OK && mExtractor->mIsWebm && trackInfo->mEncrypted) {
err = setWebmBlockCryptoInfo(mbuf);
}
Por lo tanto, el archivo desencadenante debe cumplir:
mIsWebm — el elemento DocType de EBML debe ser "webm", no
"matroska".mEncrypted — la pista debe declarar ContentEncodings con
ContentEncodingType = 1 (Encryption) y un ContentEncKeyID.0x1) sea 0, lo que indica que la trama
no está cifrada pero sí codificada en contenido. Esto toma la rama else en
setWebmBlockCryptoInfo, que llama a set_range(1, len - 1).Tras el strip, range_offset = 1 para cada trama. La rama vulnerable
entonces calcula (uint16_t *)data + 1, lo que avanza 2 bytes,
y el bucle lee/escribe los bytes [2, 2 + range_length) — un byte
más allá del final de la asignación de 65 bytes (trama de 64 bytes + 1 byte
de señal).
+---------+------------------------------------+
| 0x00 | 64 bytes of frame data (0xAA...) |
+---------+------------------------------------+
signal PCM payload
El byte de señal 0x00 es eliminado por set_range(1, 64), dejando una
trama de 64 bytes dentro de una asignación de 65 bytes. La aritmética de
punteros vulnerable entonces escribe en el desplazamiento de byte 2 hasta 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
Dependencias upstream, clonadas por el usuario antes de la compilación:
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 en PATH (Linux, o adb.exe desde 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 ../..