Out-of-bounds read information disclosure vulnerability in Microsoft Windows GDI+ EMR_STRETCHDIBITS record (again)
This article, the seventh in a seven-part series on vulnerabilities found via fuzzing the Graphics Device Interface (GDI) of Microsoft Windows, is about an information disclosure vulnerability in the EMR_STRETCHDIBITS
enhanced metafile record. For other articles in the series click here.
TL;DR
An information disclosure vulnerability (CVE-2022-38006) exists when the Windows GDI+
component improperly handles objects in memory.
This vulnerability allows remote attackers to disclose sensitive information on affected installations of Microsoft Windows. User interaction is required to exploit this vulnerability in that the target must visit a malicious page or open a malicious file.
The specific flaw exists within the processing of EMF
metafiles in GdiPlus.dll
. A specially crafted BITMAPINFOHEADER
in an EMR_STRETCHDIBITS
record can result in a read past the end of an allocated buffer and disclose initialized or uninitialized heap memory. An attacker can leverage this in conjunction with other vulnerabilities to execute code in the context of the current process.
Description
The following analysis is based on Microsoft Windows 10 Professional (x86) using version 10.0.19041.1706 of gdiplus.dll
.
It seems that processing an EMR_STRETCHDIBITS
record containing a specially crafted BITMAPINFOHEADER
structure may lead to memory corruption in the MfEnumState::OutputDIB()
function and could cause memcpy()
to read memory out-of-bounds and trigger an access violation exception.
The below is the relevant excerpt of the crash analysis from WinDbg
when processing an EMF
metafile.
10:000> g
2(aac.300): Access violation - code c0000005 (first chance)
3First chance exceptions are reported before any exception handling.
4This exception may be expected and handled.
5eax=073ff006 ebx=073fefd8 ecx=00000001 edx=00000002 esi=073ff000 edi=07409ff0
6eip=76f98d4a esp=0020f29c ebp=0020f2a4 iopl=0 nv up ei pl nz na pe nc
7cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010206
8msvcrt!memcpy+0x5a:
976f98d4a f3a5 rep movs dword ptr es:[edi],dword ptr [esi]
100:000> kn
11 # ChildEBP RetAddr
1200 0020f2a4 6c8e4ed5 msvcrt!memcpy+0x5a
1301 0020f3e4 6c8e47d8 gdiplus!MfEnumState::OutputDIB+0x6ad
1402 0020f448 6c8e433f gdiplus!EmfEnumState::StretchDIBits+0x13d
1503 0020f464 6c907c5d gdiplus!EmfEnumState::ProcessRecord+0x7f
1604 0020f488 6c92812c gdiplus!GdipPlayMetafileRecordCallback+0xdd
1705 0020f4b4 750f3945 gdiplus!EnumEmfDownLevel+0x6c
1806 0020f588 750ecb4c gdi32full!bInternalPlayEMF+0x855
1907 0020f59c 7714450b gdi32full!EnumEnhMetaFile+0x2c
2008 0020f5bc 6c8e85a2 GDI32!EnumEnhMetaFileStub+0x2b
2109 0020f610 6c8e5b86 gdiplus!MetafilePlayer::EnumerateEmfRecords+0xc8
220a 0020f6c8 6c8ea07c gdiplus!GpGraphics::EnumEmf+0x53e
230b 0020f838 6c8f7461 gdiplus!GpMetafile::EnumerateForPlayback+0x651
240c 0020f990 6c90867f gdiplus!GpGraphics::DrawImage+0x541
250d 0020f9fc 6c927f76 gdiplus!GpGraphics::DrawImage+0x61
260e 0020fa60 6c927e47 gdiplus!GdipDrawImage+0x116
270f 0020fa80 0027136a gdiplus!GdipDrawImageI+0x37
2810 (Inline) -------- GDI!Gdiplus::Graphics::DrawImage+0x18
29...snip...
300:000> !msec.exploitable
31Exploitability Classification: PROBABLY_EXPLOITABLE
32Recommended Bug Title: Probably Exploitable - Read Access Violation on Block Data Move starting at msvcrt!memcpy+0x000000000000005a (Hash=0x725b1b07.0x62204670)
33
34This is a read access violation in a block data move, and is therefore classified as probably exploitable.
Crash analysis
The following analysis is based on Windows 10 Professional using GdiPlus.dll
version 10.0.19041.1706.
When processing EMF
records, the execution flow reaches the EmfEnumState::ProcessRecord()
function within GdiPlus.dll
to handle each record. For EMR_STRETCHDIBITS
records execution continues with the EmfEnumState::StretchDIBits()
function.
The below is the hexidecimal representation of the BITMAPINFOHEADER
structure provided with the affected EMR_STRETCHDIBITS
record:
1069Ch: 28 00 00 00 A5 0E 00 00 00 00 00 00 01 00 20 00 (...¥......... .
206ACh: 00 00 00 00 22 22 22 22 22 22 22 22 22 22 22 22 ....""""""""""""
306BCh: 22 22 22 22 22 20 02 22 """"" ."
The following is the human readable representation of the BITMAPINFOHEADER
shown above.
1typedef struct tagBITMAPINFOHEADER {
2 DWORD biSize = 0x28;
3 LONG biWidth = 0xea5;
4 LONG biHeight = 0x0;
5 WORD biPlanes = 0x1;
6 WORD biBitCount = 0x20; // Number of bits per pixel.
7 DWORD biCompression = 0x0; // Type of compression for a compressed bottom-up bitmap.
8 DWORD biSizeImage = 0x22222222;
9 LONG biXPelsPerMeter = 0x22222222;
10 LONG biYPelsPerMeter = 0x22222222;
11 DWORD biClrUsed = 0x22222222; // Number of 2-bytes color indexes in the color table.
12 DWORD biClrImportant = 0x22022022;
13} BITMAPINFOHEADER;
At first glance, it seems that the value passed to the memcpy()
function by the MfEnumState::OutputDIB()
function as the Size
parameter is larger than the bitmap, as the below pseudocode shows:
1void MfEnumState::OutputDIB(...) {
2...snip...
3 if ( !GetDibNumPalEntries(
4 1, // flag?
5 biSize, // 0x28
6 Src->biBitCount, // 0x20
7 Src->biCompression, // 0x0
8 Src->biClrUsed, // 0x22222222
9 &palEntries) )
10 goto FAIL;
11 if ( Src & 3 || iUsage ) { // DIB_PAL_COLORS
12 palSize = sizeof(WORD) * (iUsage == DIB_RGB_COLORS) + 2;
13 if ( UIntMult(palEntries, palSize, &dwBytes) < 0 ) // 0x03 * 0x02 = 0x06
14 goto FAIL;
15 if ( UIntMult(palEntries, sizeof(DWORD), &palSize) < 0 ) // 0x03 * 0x04 = 0x0c
16 goto FAIL;
17 if ( SizeTAdd(dwBytes, Src->biSize, &Size) < 0 ) // 0x06 + 0x28 = 0x2e
18 goto FAIL;
19 if ( SizeTAdd(palSize, Src->biSize, &dwBytes) < 0 ) // 0x0c + 0x28 = 0x34
20 goto FAIL;
21 Dst = GpMallocEx(dwBytes, 8); // 0x34
22 if ( !Dst )
23 goto FAIL;
24 memcpy(Dst, Src, Size); // Crash here!
25 }
26...snip...
27}
Root cause analysis
Following the execution flow in WinDbg
reveals how the offending value of the Size
parameter is calculated. It seems the MfEnumState::OutputDIB()
function assumes that bmiColors
consists of three DWORD color masks and that it also contains 16-bit indexes at the same time.?
The GpMallocEx()
function will allocate a buffer large enough to hold the 0x28
bytes bitmap and the 0xc
bytes color masks, while the memcpy()
function will try to copy the 0x28
bytes bitmap and the 0x6
bytes indices. However, it turnes out that the source bitmap stored in memory is only 0x28
bytes, hence memcpy()
will read 0x6
bytes past the allocated buffer.
Further analysis showed that the culprit seems to be the GetDibNumPalEntries()
function which is used several times during the process. It is also called by GetBitmapFromRecordEx()
in the EmfEnumState::StretchDIBits()
record handler function before MfEnumState::OutputDIB()
to determine the size of the bitmap. However, the return value of the GetDibNumPalEntries()
function depends on the first flag
parameter, as the following pseudocode shows:
1int GetDibNumPalEntries(int flag, uint biSize, int biBitCount, uint biCompression, uint biClrUsed, uint *palEntries)
2{
3 nSize = biSize;
4 // This will override the biCompression parameter.
5 if ( (biBitCount == 0x10 || biBitCount == 0x20) && flag )
6 biCompression = BI_BITFIELDS;
7 ...
8 palEntries = 0;
9 if ( biCompression ) {
10 if ( biCompression-- ) {
11 if ( biCompression-- ) {
12 if ( biCompression != 1 || biBitCount != 0x10 && biBitCount != 0x20 )
13 return 0;
14 // The color table consists of three DWORD color masks.
15 biClrUsed = nSize > 0x28 ? 0 : 3;
16 palEntries = biClrUsed;
17 goto LABEL_11;
18 }
19 ...
20 return 0;
21 }
22 }
23 if ( biBitCount == 1 || biBitCount == 4 || biBitCount == 8 )
24 goto LABEL_18;
25 if ( biBitCount != 0x18 && biBitCount != 0x10 && biBitCount != 0x20 )
26 return 0;
27LABEL_11:
28 if ( biClrUsed && biClrUsed <= palEntries )
29 palEntries = biClrUsed;
30 if ( palEntries > 0x100 )
31 palEntries = 256;
32 result = 1;
33 return result;
34}
If the flag
is 1
the GetDibNumPalEntries()
function assumes/enforces BI_BITFIELDS
compression, which means that there are three DWORD
color masks and hence returns 3
, otherwise the return value is 0
. Note that the value of flag
varies during the execution process, as the following call tree summarizes:
1EmfEnumState::StretchDIBits()
2├── GetBitmapFromRecordEx()
3| └── GetDibNumPalEntries(0, ...) -> 0
4└── MfEnumState::OutputDIB()
5 └── GetDibNumPalEntries(0, ...) -> 0
6 └── GetDibNumPalEntries(1, ...) -> 3
7 └── GetDibNumPalEntries(1, ...) -> 3
The GetBitmapFromRecordEx()
function will only extract the 0x28
bytes bitmap from the record, while the MfEnumState::OutputDIB()
function will falsely assume that there are additional color masks (or 2-byte indices?) provided with the bitmap.
Note that the crash can be avoided by setting the biCompression
member of the BITMAPINFOHEADER
structure at offset 0x6ac
in the sample file to BI_BITFIELDS
, which also suggests that the root cause may be enforcing BI_BITFIELDS
compression on a BI_RGB
bitmap.
The bug has been reproduced on a fully patched Windows 10 64-bit with a 32-bit PoC program, but the 64-bit build of gdiplus.dll
might be also affected. Note that PageHeap
is required to reproduce the crash.
Patch analysis
Timeline
⬅️ 2022-05-28: Reported issue to MSRC.
➡️ 2022-05-31: MSRC opened case 72140.
⬅️ 2022-06-15: Requested status update.
➡️ 2022-06-17: MSRC still investigates the issue.
⬅️ 2022-06-20: Reuploaded attachment, just to be sure.
➡️ 2022-06-21: MSRC confirmed the vulnerability.
➡️ 2022-09-13: Coordinated public release of advisory.