
Prova de conceito e harness de reprodução instrumentado para CVE-2026-28609, uma escrita fora dos limites no MatroskaExtractor do Android, alcançável por meio de um arquivo WebM criado com uma faixa de áudio PCM big-endian.
Prova de conceito, harness de reprodução instrumentado e análise técnica para CVE-2026-28609, uma gravação fora dos limites no MatroskaExtractor do Android, alcançável por meio de um arquivo WebM especialmente construído com uma faixa de áudio PCM big-endian.
frameworks/av/media/module/extractors/mkv/MatroskaExtractor.cppMatroskaSource::read() no MatroskaExtractor do Android contém um
loop de troca de bytes PCM big-endian que converte um ponteiro de dados
de frame para uint16_t * antes de adicionar frame->range_offset(), um deslocamento em bytes.
Como a aritmética de ponteiros em C sobre um uint16_t * escala o deslocamento por
sizeof(uint16_t) = 2, o ponteiro resultante fica 2 * range_offset
bytes além do início do buffer, e não range_offset bytes.
Quando range_offset > 0, o loop lê e grava um ou mais bytes
além do fim do MediaBuffer do frame. Em um dispositivo sem ASan,
a leitura excessiva é bem-sucedida silenciosamente e a gravação excessiva corrompe o byte
imediatamente após o buffer.
A linha vulnerável:
// 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]);
}
A correção upstream converte o ponteiro para uint8_t * antes de aplicar
o deslocamento:
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]);
}
Um gatilho funcional e reproduzível para a CVE-2026-28609, verificado sob AddressSanitizer em um dispositivo Android 14 real. O repositório inclui:
range_offset diferente de
zero.dlopen,
invoca GETEXTRACTORDEF e lê frames do arquivo.O resultado, em um dispositivo vulnerável:
==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
O ramo vulnerável exige que todas as condições a seguir sejam verdadeiras:
| Condição | Origem |
|---|---|
| A faixa é PCM | mType == PCM |
| Big-endian | AMEDIAFORMAT_KEY_PCM_BIG_ENDIAN == 1 |
| Amostras de 16 bits | AMEDIAFORMAT_KEY_BITS_PER_SAMPLE == 16 |
O frame tem range_offset diferente de zero | Definido por set_range(offset, ...) |
As três primeiras são satisfeitas ao declarar a faixa com o ID de codec
A_PCM/INT/BIG e profundidade de bits 16. A quarta é a interessante.
range_offset é definido com um valor diferente de zero apenas em
MatroskaSource::setWebmBlockCryptoInfo(), que é chamado a partir de
readBlock() sob a condição:
if (err == OK && mExtractor->mIsWebm && trackInfo->mEncrypted) {
err = setWebmBlockCryptoInfo(mbuf);
}
Portanto, o arquivo de gatilho deve satisfazer:
mIsWebm — o elemento EBML DocType deve ser "webm", não
"matroska".mEncrypted — a faixa deve declarar ContentEncodings com
ContentEncodingType = 1 (Encryption) e um ContentEncKeyID.0x1) seja 0, indicando que o quadro
não está criptografado, mas possui codificação de conteúdo. Isso segue o ramo else em
setWebmBlockCryptoInfo, que chama set_range(1, len - 1).Após a remoção, range_offset = 1 para cada quadro. O ramo vulnerável
então calcula (uint16_t *)data + 1, o que avança 2 bytes,
e o loop lê/escreve os bytes [2, 2 + range_length) — um byte
além do fim da alocação de 65 bytes (quadro de 64 bytes + 1 byte de
sinal).
+---------+------------------------------------+
| 0x00 | 64 bytes of frame data (0xAA...) |
+---------+------------------------------------+
signal PCM payload
O byte de sinal 0x00 é removido por set_range(1, 64), deixando um
frame de 64 bytes dentro de uma alocação de 65 bytes. A aritmética de
ponteiro vulnerável então escreve no deslocamento de byte 2 até 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
Dependências upstream, clonadas pelo usuário antes da compilação:
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 no PATH (Linux, ou adb.exe do 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 ../..
Se sua árvore AOSP usa o layout modular do extrator
(av/media/module/extractors/mkv/), nenhum ajuste adicional é necessário.
Se ela usa o layout mais antigo (av/media/libstagefright/matroska/),
consulte os scripts de build para a variável MKV.