
Proof-of-Concept und instrumentierte Reproduktions-Harness für CVE-2026-28609, einen Out-of-Bounds-Schreibvorgang in Androids MatroskaExtractor, der über eine manipulierte WebM-Datei mit einer Big-Endian-PCM-Audiospur erreichbar ist.
Proof-of-Concept, instrumentierte Reproduktions-Harness und technischer Write-up für CVE-2026-28609, einen Out-of-Bounds-Write in Androids MatroskaExtractor, der über eine manipulierte WebM-Datei mit einer Big-Endian-PCM-Audiospur erreichbar ist.
frameworks/av/media/module/extractors/mkv/MatroskaExtractor.cppMatroskaSource::read() in Androids MatroskaExtractor enthält eine
PCM-Big-Endian-Byte-Swap-Schleife, die einen Frame-Datenzeiger auf
uint16_t * castet, bevor frame->range_offset() addiert wird, ein Byte-Offset.
Da die C-Zeigerarithmetik auf einem uint16_t * den Offset mit
sizeof(uint16_t) = 2 skaliert, liegt der resultierende Zeiger 2 * range_offset
Bytes hinter dem Anfang des Puffers, nicht range_offset Bytes.
Wenn range_offset > 0, liest und schreibt die Schleife ein oder mehrere Bytes
über das Ende des MediaBuffer des Frames hinaus. Auf einem Gerät ohne ASan
gelingt der Über-Lesevorgang stillschweigend und der Über-Schreibvorgang beschädigt das Byte
unmittelbar nach dem Puffer.
Die verwundbare Zeile:
// 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]);
}
Der Upstream-Fix castet den Zeiger vor dem Anwenden des Offsets zu 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]);
}
Einen funktionierenden, reproduzierbaren Trigger für CVE-2026-28609, verifiziert unter AddressSanitizer auf einem echten Android-14-Gerät. Das Repository enthält:
range_offset treibt.dlopen lädt,
GETEXTRACTORDEF aufruft und Frames aus der Datei liest.Das Ergebnis auf einem verwundbaren Gerät:
==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
Der verwundbare Zweig erfordert, dass alle folgenden Bedingungen erfüllt sind:
| Bedingung | Quelle |
|---|---|
| Track ist PCM | mType == PCM |
| Big-Endian | AMEDIAFORMAT_KEY_PCM_BIG_ENDIAN == 1 |
| 16-Bit-Samples | AMEDIAFORMAT_KEY_BITS_PER_SAMPLE == 16 |
Frame hat einen range_offset ungleich Null | Gesetzt durch set_range(offset, ...) |
Die ersten drei werden erfüllt, indem der Track mit der Codec-ID
A_PCM/INT/BIG und einer Bittiefe von 16 deklariert wird. Die vierte ist die interessante.
range_offset wird nur in
MatroskaSource::setWebmBlockCryptoInfo() auf einen Wert ungleich Null gesetzt, welche aus
readBlock() unter folgender Bedingung aufgerufen wird:
if (err == OK && mExtractor->mIsWebm && trackInfo->mEncrypted) {
err = setWebmBlockCryptoInfo(mbuf);
}
Daher muss die Trigger-Datei Folgendes erfüllen:
mIsWebm — das EBML-DocType-Element muss "webm" sein, nicht
"matroska".mEncrypted — der Track muss ContentEncodings mit
ContentEncodingType = 1 (Verschlüsselung) und eine ContentEncKeyID deklarieren.0x1) 0 ist, was anzeigt, dass der Frame
unverschlüsselt, aber content-encoded ist. Dies nimmt den else-Zweig in
setWebmBlockCryptoInfo, der set_range(1, len - 1) aufruft.Nach dem Strip gilt range_offset = 1 für jeden Frame. Der verwundbare
Zweig berechnet dann (uint16_t *)data + 1, was um 2 Bytes vorrückt,
und die Schleife liest/schreibt Bytes [2, 2 + range_length) — ein Byte
über das Ende der 65-Byte-Allokation hinaus (64-Byte-Frame + 1-Byte
Signal).
+---------+------------------------------------+
| 0x00 | 64 bytes of frame data (0xAA...) |
+---------+------------------------------------+
signal PCM payload
Das Signalbyte 0x00 wird durch set_range(1, 64) entfernt, wodurch ein
64-Byte-Frame innerhalb einer 65-Byte-Zuweisung verbleibt. Die verwundbare Pointer-
Arithmetik schreibt dann an Byte-Offset 2 bis 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
Vorgelagerte Abhängigkeiten, die vom Benutzer vor dem Build geklont werden:
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 im PATH (Linux oder adb.exe aus 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 ../..