
Эксплойт-доказательство концепции и инструментированный стенд для воспроизведения CVE-2026-28609 — записи за пределами границ в MatroskaExtractor Android, достижимой через специально сформированный файл WebM с аудиодорожкой PCM с прямым порядком байтов.
Proof-of-concept, инструментированный стенд для воспроизведения и техническое описание CVE-2026-28609 — записи за границами буфера в Android-компоненте MatroskaExtractor, достижимой через специально сформированный файл WebM с аудиодорожкой PCM в формате big-endian.
frameworks/av/media/module/extractors/mkv/MatroskaExtractor.cppMatroskaSource::read() в Android-компоненте MatroskaExtractor содержит
цикл побайтовой перестановки для PCM big-endian, который приводит указатель
на данные кадра к типу uint16_t * до прибавления frame->range_offset() —
смещения в байтах. Поскольку арифметика указателей в C для типа uint16_t *
масштабирует смещение на sizeof(uint16_t) = 2, результирующий указатель
оказывается на 2 * range_offset байт дальше начала буфера, а не на
range_offset байт.
Когда range_offset > 0, цикл читает и записывает один или несколько байт
за концом MediaBuffer кадра. На устройстве без ASan чтение за границами
проходит незаметно, а запись за границами повреждает байт, непосредственно
следующий за буфером.
Уязвимая строка:
// 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]);
}
Исправление в upstream приводит указатель к uint8_t * перед применением
смещения:
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]);
}
Рабочий, воспроизводимый триггер для CVE-2026-28609, проверенный под AddressSanitizer на реальном устройстве Android 14. Репозиторий включает:
range_offset.dlopen,
вызывает GETEXTRACTORDEF и читает кадры из файла.Результат на уязвимом устройстве:
==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
Уязвимая ветка требует одновременного выполнения всех следующих условий:
| Условие | Источник |
|---|---|
| Дорожка имеет формат PCM | mType == PCM |
| Big-endian | AMEDIAFORMAT_KEY_PCM_BIG_ENDIAN == 1 |
| 16-битные сэмплы | AMEDIAFORMAT_KEY_BITS_PER_SAMPLE == 16 |
У фрейма ненулевой range_offset | Устанавливается через set_range(offset, ...) |
Первые три условия выполняются объявлением дорожки с кодеком
A_PCM/INT/BIG и глубиной 16 бит. Четвёртое — самое интересное.
range_offset устанавливается в ненулевое значение только в
MatroskaSource::setWebmBlockCryptoInfo(), которая вызывается из
readBlock() при условии:
if (err == OK && mExtractor->mIsWebm && trackInfo->mEncrypted) {
err = setWebmBlockCryptoInfo(mbuf);
}
Следовательно, файл-триггер должен удовлетворять условиям:
mIsWebm — элемент EBML DocType должен быть "webm", а не
"matroska".mEncrypted — дорожка должна объявлять ContentEncodings с
ContentEncodingType = 1 (шифрование) и ContentEncKeyID.0x1) равен 0, что указывает на то, что кадр
не зашифрован, но имеет кодирование содержимого. Это приводит к ветке else в
setWebmBlockCryptoInfo, которая вызывает set_range(1, len - 1).После удаления сигнального байта range_offset = 1 для каждого кадра. Уязвимая
ветка затем вычисляет (uint16_t *)data + 1, что сдвигает указатель на 2 байта,
и цикл читает/записывает байты [2, 2 + range_length) — на один байт
за пределами 65-байтового выделения (64-байтовый кадр + 1-байтовый
сигнальный байт).
+---------+------------------------------------+
| 0x00 | 64 bytes of frame data (0xAA...) |
+---------+------------------------------------+
signal PCM payload
Байт сигнала 0x00 удаляется с помощью set_range(1, 64), оставляя
64-байтовый фрейм внутри 65-байтового выделения. Уязвимая арифметика
указателей затем выполняет запись по байтовым смещениям со 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
Зависимости вышестоящего уровня, клонируемые пользователем перед сборкой:
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 в PATH (Linux или adb.exe из 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 ../..
Если ваше дерево AOSP использует модульную структуру извлечения
(av/media/module/extractors/mkv/), дополнительная настройка не требуется.
Если используется более старая структура (av/media/libstagefright/matroska/),
см. скрипты сборки для переменной MKV.