
Microsoft HEIF Extension (msheif_store.dll) OOB-read
WIC를 통해 조작된 HEIF 이미지를 디코딩할 때, msheif 코덱은 1바이트 소스 버퍼를 할당하지만 이후 CopyPixels 중에 해당 버퍼에서 매우 많은 양의 픽셀 데이터를 복사하려고 시도합니다. 복사는 stride × ROI 높이에서 파생된 크기로 memmove를 사용하며, 소스 버퍼의 실제 길이를 검증하지 않습니다. 이로 인해 범위 초과 읽기와 즉각적인 액세스 위반(충돌)이 발생합니다. 실제로 악성 HEIF를 열거나 미리 보면 HEIF Image Extensions를 통해 경로를 지정하는 모든 WIC 소비자에서 서비스 거부가 발생합니다.
명명 참고: roi_height와 같은 변수 이름은 가독성을 위해 디스어셈블리에서 추론되었으며, 공급업체 기호 이름을 반영하지 않습니다.
| 제품 | Microsoft HEIF Image Extensions |
|---|---|
| 모듈 | msheif_store.dll |
| 운영체제 | Windows 11 |
| 확장 버전 | 1.2.22.0 (Microsoft.HEIFImageExtension_1.2.22.0_x64__8wekyb3d8bbwe\x64\msheif_store.dll) |
a. 취약점 상세 설명
이 버그는 이미지/ROI 너비가 잘못 구문 분석될 때(예: 0x01000100 = 16,777,472) stride와 최종 복사 크기가 증가하여 독립적으로 악화됩니다. 그러나 핵심 취약점은 소스 길이가 계산된 복사 길이에 대해 확인되지 않는다는 것입니다.
(7de0.78e4): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
msheif_store!DllCanUnloadNow+0x228b1d:
00007ffd`841bc45d c5fe6f02 vmovdqu ymm0,ymmword ptr [rdx] ds:000002b0`65be6ff0=c0
0:000> !heap -p -a rdx
address 000002b065be6ff0 found in
_DPH_HEAP_ROOT @ 2b05bed1000
in busy allocation ( DPH_HEAP_BLOCK: UserAddr UserSize - VirtAddr VirtSize)
2b062c2f2d8: 2b065be6ff0 1 - 2b065be6000 2000
...
00007ffe75b90563 MFPlat!operator new+0x0000000000000023
00007ffe75b83d36 MFPlat!MFCreateMemoryBuffer+0x0000000000000056
00007ffd840157b1 msheif_store!DllCanUnloadNow+0x0000000000081e71
...
0:000> r r8
r8=0000000001000100
b. 입력에서의 코드 흐름
0:000> k
# Child-SP RetAddr Call Site
00 0000005d`6a9aed98 00007ffe`6fdefcc2 msheif_store!DllCanUnloadNow+0x228b21
01 0000005d`6a9aeda0 00007ffe`6fcb3adc msheif_store!DllCanUnloadNow+0x18c382
02 0000005d`6a9aee00 00007ffe`6fcb4513 msheif_store!DllCanUnloadNow+0x5019c
03 0000005d`6a9aeef0 00007ffe`6fcb96da msheif_store!DllCanUnloadNow+0x50bd3
04 0000005d`6a9af0a0 00007fff`318551d5 msheif_store!DllCanUnloadNow+0x55d9a
05 0000005d`6a9af1a0 00007fff`318082eb windowscodecs!CPyramidBase::CopyPixels+0xf5
06 0000005d`6a9af260 00007fff`31857e52 windowscodecs!CFormatConverter::CopyPixels+0x22b
07 0000005d`6a9af3e0 00007fff`31857e52 windowscodecs!CFormatConverterResolver::CopyPixels+0xc2
08 0000005d`6a9af480 00007ff6`15fb0f5d heif_decoder!HEIF_LoadBitmapFromMemory+0x34d [C:\\\\Users\\\\test\\\\source\\\\repos\\\\heif_decoder\\\\heif_decoder\\\\heif_decoder.cpp @ 256]
09 0000005d`6a9af670 00007ff6`15fb1754 heif_decoder!HEIF_FuzzMemory+0x9b [C:\\\\Users\\\\test\\\\source\\\\repos\\\\heif_decoder\\\\heif_decoder\\\\heif_decoder.cpp @ 357]
0a 0000005d`6a9af830 00007ff6`15fb3039 heif_decoder!wmain+0x394 [C:\\\\Users\\\\test\\\\source\\\\repos\\\\heif_decoder\\\\heif_decoder\\\\heif_decoder.cpp @ 524]
0b 0000005d`6a9afaa0 00007ff6`15fb2ee2 heif_decoder!invoke_main+0x39 [D:\\\\a\\\\_work\\\\1\\\\s\\\\src\\\\vctools\\\\crt\\\\vcstartup\\\\src\\\\startup\\\\exe_common.inl @ 91]
0c 0000005d`6a9afaf0 00007ff6`15fb2d9e heif_decoder!__scrt_common_main_seh+0x132 [D:\\\\a\\\\_work\\\\1\\\\s\\\\src\\\\vctools\\\\crt\\\\vcstartup\\\\src\\\\startup\\\\exe_common.inl @ 288]
0d 0000005d`6a9afb60 00007ff6`15fb30ce heif_decoder!__scrt_common_main+0xe [D:\\\\a\\\\_work\\\\1\\\\s\\\\src\\\\vctools\\\\crt\\\\vcstartup\\\\src\\\\startup\\\\exe_common.inl @ 331]
0e 0000005d`6a9afb90 00007fff`3847e8d7 heif_decoder!wmainCRTStartup+0xe [D:\\\\a\\\\_work\\\\1\\\\s\\\\src\\\\vctools\\\\crt\\\\vcstartup\\\\src\\\\startup\\\\exe_wmain.cpp @ 17]
0f 0000005d`6a9afbc0 00007fff`3939c34c KERNEL32!BaseThreadInitThunk+0x17
10 0000005d`6a9afbf0 00000000`00000000 ntdll!RtlUserThreadStart+0x2c
해석된 내부 호출(명확성을 위해 이름 변경):
CWICHeifDecoderFrame::CopyPixels
→ CWICHeifBitmapSourceBase::CopyPixels_Transform
→ CWICHeifBitmapSourceBase::CopyPixels_Base
→ sub_7FFD8411FC40 // memmove 수행
→ memmove // AV (src에서 vmovdqu 로드)
c. 버퍼 크기
CHEIFItemInfoEntry_GetDataSize가 조기 "성공 크기 0" 경로를 취하도록 유발하는 조작된 HEIF(예: sub_7FFD8408B898이 실패하고 함수가 sizeBuffer = 0으로 성공을 반환하는 경우).
소스 버퍼: MFCreateMemoryBuffer(0)을 통해 할당됨 ⇒ 1바이트.
복사 크기: copy_size = stride * abs(roi_height)로 파생됨. 잘못 구문 분석된 너비(예: 0x01000100)와 일반적인 BPP로 인해 stride가 엄청나게 커져 매우 큰 copy_size가 생성됨.
취약한 호출:
__int64 sub_7FFD8411FC40(__m128i* dst, __int64 a2, __m128i* src, int a4,
unsigned int stride, unsigned int abs_roi_height)
{
if (src && dst) {
if (stride == a2 && stride == a4) {
// 'src'에 최소 (stride * abs_roi_height) 바이트가 있는지 검증하지 않음
memmove(dst, src, (size_t)abs_roi_height * stride); // 여기서 AV 발생
}
}
}
소스가 1바이트인 이유(할당 경로):
__int64 __fastcall CHEIFStreamReader_ReadItemData(__int64 *a1, __int64 *a2, IMFMediaBuffer **a3)
{
ppBuffer = 0i64;
if ( v7(a2) )
{
*cbMaxLength = 0i64;
result = (*(*a2 + 0xA0))(a2, cbMaxLength); // CHEIFItemInfoEntry_GetDataSize
if ( result < 0 )
{
goto LABEL_10;
}
if ( *cbMaxLength > 0xC800000ui64 )
{
result = 0xC00D36BE;
goto LABEL_23;
}
// GetDataSize가 조기 성공을 반환한 경우 cbMaxLength가 여전히 0일 수 있음
result = MFCreateMemoryBuffer(cbMaxLength[0], &ppBuffer);
if ( result < 0 )
{
goto LABEL_34;
}
...
}
__int64 __fastcall CHEIFItemInfoEntry_GetDataSize(__int64 a1, unsigned __int64 *sizeBuffer)
{
if ( sizeBuffer )
{
*sizeBuffer = 0i64;
DataLocationInfo = (*(*v5 + 0x78i64))(v5, 0x6D657461i64, &v32);
if ( DataLocationInfo >= 0 )
{
v13 = v32;
v14 = *(v32 + 0xF0);
if ( v14 )
{
if ( !sub_7FFD8408B898((v14 + 0xC8), (a1 - 0x78), &v33, v31) )
{
// sizeBuffer를 업데이트하지 않고 성공(0/S_OK) 반환
// 호출자는 이를 길이=0으로 처리 → MFCreateMemoryBuffer(0)
DataLocationInfo = 0;
goto LABEL_62;
}
v17 = v33;
DataLocationInfo = CHEIFItemInfoEntry_GetDataLocationInfo(v5, v13, *(v33 + 16), &v29, &v30);
if ( DataLocationInfo >= 0 )
{
for ( i = 0; ; ++i )
{
if ( i >= *v17 )
{
*sizeBuffer = v6;
goto LABEL_62;
}
v21 = *(*(v17 + 8) + 24i64 * i + 16);
}
if ( v21 + v6 < v6 )
break;
v6 += v21;
DataLocationInfo = 0;
}
...
...
LABEL_62:
sub_7FFD83F91AEC();
return DataLocationInfo;
}
d. 제안된 수정
CopyPixels 경로에서 소스 버퍼에 최소 stride * abs(roi_height) 바이트가 있는지(그리고 src_stride/src_height가 요청된 영역을 충족하거나 초과하는지) 검증합니다. 그렇지 않으면 WINCODEC_ERR_INSUFFICIENTBUFFER(또는 이에 상응하는 값)로 실패합니다.
e. 분석 방법론(기호 재구성)
호출 흐름을 검토 가능하게 만들기 위해, 코덱 자체의 로깅 문자열에서 IDA의 내부 함수 이름을 재구성했습니다.
목표. 주요 루틴(...CopyPixels_Base, ...CopyPixels_Transform, CHEIFStreamReader::ReadItemData 등)에 대해 사람이 읽을 수 있는 함수 이름을 식별하여 충돌 스택을 디컴파일된 코드와 일치시킵니다.
절차:
로깅 도우미 찾기: 충돌 지점 주변 호출자의 디컴파일러 보기에서 함수 이름과 유사한 상수 문자열 인수(예: "CWICHeifBitmapSourceBase::CopyPixels_Base")로 호출되는 작은 도우미를 찾습니다.
호출자 수집: 해당 도우미에 대한 xref를 사용하여 모든 호출자 함수를 나열합니다.
이름 추출: 각 호출자에 대해 디컴파일된 보기를 열고 도우미의 두 번째 인수(문자열 리터럴)를 읽습니다. 이 문자열은 일관되게 의도된 함수 이름을 포함합니다.
호출자 이름 바꾸기: 각 호출자에서 N(편집 → 이름 바꾸기)을 누르고 함수 이름을 복구된 문자열(예: CWICHeifBitmapSourceBase::CopyPixels_Base)로 설정합니다.
전체 충돌 체인에 의미 있는 이름이 생길 때까지 인접 프레임에 대해 반복합니다.
검증. 이름을 바꾼 후 호출 사이트를 다시 실행하고 인수 유형/동작이 레이블과 일치하는지 확인합니다(예: ...CopyPixels_Base는 stride/ROI 계산과 최종 memmove 호출이 발생하는 위치임).
전제 조건 : Microsoft Store의 HEIF Image Extensions가 설치/활성화되어 있어야 함(HEIC/HEIF 디코딩에 필요).
재현 단계
첨부된 PoC 파일(../access_violation_0000xxxxxxxxx461_00000xxxxxxxx6B0_1.heif)을 다운로드하여 로컬 폴더에 저장합니다.
HEIF Image Extensions가 설치되어 있는지 확인합니다(Microsoft Store를 열고 "HEIF Image Extensions" 검색 → 설치).
Microsoft Photos로 PoC 파일을 엽니다(Photos가 기본값인 경우 파일을 두 번 클릭하거나 마우스 오른쪽 버튼 클릭 → 연결 프로그램 → Photos).
소프트웨어 다운로드 링크
HEIF Image Extensions (Microsoft Store) https://apps.microsoft.com/detail/9pmmsr1cgpwg?hl=en-US&gl=US