Out-of-bounds read information disclosure vulnerability in Microsoft Windows GDI+ EMR_STRETCHDIBITS record
This article, the first 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-21915) exists when the Windows GDI+
component improperly discloses the contents of its 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 EMR_STRETCHDIBITS
record can result in a read past the end of an allocated buffer and disclose 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.1110 of gdi32full.dll
and version 10.0.19041.1151 of gdiplus.dll
.
The below is the exception output and the relevant excerpt of the call stack from WinDbg after a crash detected in the MRBDIB::vInit()
function.
10:000> g
2(1fd0.848): Access violation - code c0000005 (first chance)
3First chance exceptions are reported before any exception handling.
4This exception may be expected and handled.
5eax=087254c4 ebx=08746df0 ecx=001ae4c4 edx=001c0098 esi=08577000 edi=087589c4
6eip=75a3fc8e esp=00daed20 ebp=00daed44 iopl=0 nv up ei pl nz na pe cy
7cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010207
8ucrtbase!memcpy+0x4e:
975a3fc8e f3a4 rep movs byte ptr es:[edi],byte ptr [esi]
100:000> kn
11 # ChildEBP RetAddr
1200 00daed24 76861859 ucrtbase!memcpy+0x4e
1301 00daed44 76861a0b gdi32full!MRBDIB::vInit+0x79
1402 00daedf8 76874d24 gdi32full!MF_AnyDIBits+0x16d
1503 00daeee4 76a871a3 gdi32full!StretchDIBitsImpl+0x134
1604 00daef24 70505ed5 GDI32!StretchDIBits+0x43
1705 00daf088 70505646 gdiplus!MfEnumState::OutputDIB+0x886
1806 00daf0e8 705051e9 gdiplus!EmfEnumState::StretchDIBits+0x10b
1907 00daf0fc 70528a0d gdiplus!EmfEnumState::ProcessRecord+0x79
2008 00daf120 70548eec gdiplus!GdipPlayMetafileRecordCallback+0xdd
2109 00daf14c 76863945 gdiplus!EnumEmfDownLevel+0x6c
220a 00daf220 7685cb4c gdi32full!bInternalPlayEMF+0x855
230b 00daf234 76a8462b gdi32full!EnumEnhMetaFile+0x2c
240c 00daf254 70509332 GDI32!EnumEnhMetaFileStub+0x2b
250d 00daf2a8 7050683c gdiplus!MetafilePlayer::EnumerateEmfRecords+0xc8
260e 00daf360 7050ae0c gdiplus!GpGraphics::EnumEmf+0x464
270f 00daf4d0 70518211 gdiplus!GpMetafile::EnumerateForPlayback+0x651
2810 00daf628 7052942f gdiplus!GpGraphics::DrawImage+0x541
2911 00daf694 70548d36 gdiplus!GpGraphics::DrawImage+0x61
3012 00daf6f8 70548c07 gdiplus!GdipDrawImage+0x116
3113 00daf718 008b12e6 gdiplus!GdipDrawImageI+0x37
3214 (Inline) -------- Harness!Gdiplus::Graphics::DrawImage+0x18
33...
340:000> !msec.exploitable
35Exploitability Classification: PROBABLY_EXPLOITABLE
36Recommended Bug Title: Probably Exploitable - Read Access Violation on Block Data Move starting at ucrtbase!memcpy+0x000000000000004e (Hash=0x4e667e2b.0x3c6f1875)
37
38This is a read access violation in a block data move, and is therefore classified as probably exploitable.
Crash analysis
At first glance, it seems a large value 0x1c0098
is passed to memcpy()
as the Size
parameter by the MRBDIB::vInit()
function, as shown by the following pseudocode:
1void MRBDIB::vInit(...)
2{
3 if ( Size ) // 0x1c0098
4 {
5 if ( srcBmci->bmciHeader.bcSize == 0xc )
6 {
7 ...
8 }
9 else
10 {
11 memcpy(dstBmci, srcBmci, Size); // Crash here!
12 ...
13 }
14 ...
15 }
16 ...
17}
Note that the MRBDIB::vInit()
function checks only that the Size
parameter has been provided. When the bcSize
member of the BITMAPCOREHEADER
1 structure is not set to the minimum 0xC
size of a header, it will call memcpy()
with the provided Size
parameter.
Replaying the execution flow using the Time Travel Debugging capabilities of WinDbg Preview reveals how the value of the Size
parameter is calculated in the bMetaGetDIBInfo()
function which is called by MF_AnyDIBits()
before MRBDIB::vInit()
.
The above hypothesis of the Size
variable containing the incorrect value can be confirmed – after capturing a time travel trace of the test harness and the crashing sample file – by setting a memory access breakpoint on the memory address of the Size
variable using the ba w4 0x59ee50
command and running back to the last point of memory access of this variable using the g-
command. We can also add the variables to the Watch
window like (int*)(0x59ee50)
to observe how they change during the execution.
Further analysis showed that the value 0x1c0098
comes from the &size
parameter which is set by the bMetaGetDIBInfo()
function based on the value of the biClrUsed
member of the BITMAPINFOHEADER
2 structure, as shown by the following pseudocode:
1int bMetaGetDIBInfo(HDC hdc, 0, LPBITMAPINFO lpbmi, int &size, int &sizeImage, UINT ColorUse, int cLines, 0)
2{
3 ...
4 biBitCount = lpbmi->bmiHeader.biBitCount;
5 if (biBitCount == 16 || biBitCount == 32)
6 {
7 ...
8 }
9 else if (biBitCount != 24 &&
10 lpbmi->bmiHeader.biCompression != BI_JPEG &&
11 lpbmi->bmiHeader.biCompression != BI_PNG)
12 {
13 biClrUsed = lpbmi->bmiHeader.biClrUsed; // 0x7001c
14 if (biClrUsed)
15 {
16 // 0x1c0070 = 4 * 0x7001c
17 colorTableSize = sizeof(RGBQUAD) * biClrUsed;
18 goto LABEL_28;
19 }
20 if (biBitCount < 16)
21 {
22 colorTableSize = cbBits * (1 << biBitCount);
23LABEL_28:
24 // 0x28 + 0x1c0070 = 0x1c0098
25 biSize += colorTableSize;
26 goto LABEL_12;
27 }
28 }
29LABEL_12:
30 ...
31 biWidth = lpbmi->bmiHeader.biWidth;
32 if ( biWidth >= 0 )
33 res = CJSCAN(biWidth,
34 lpbmi->bmiHeader.biPlanes,
35 lpbmi->bmiHeader.biBitCount,
36 &cbBits);
37 if ( res )
38 {
39 if ( !cLines )
40 {
41 cLines = lpbmi->bmiHeader.biHeight;
42 if ( cLines < 0 )
43 cLines = -cLines;
44 }
45 if ( ULongLongToULong(cbBits * cLines, cbBits * cLines >> 32) >= 0 )
46 {
47 biSizeImage = cbBits;
48LABEL_21:
49 *size = biSize; // 0x1c0098
50 *sizeImage = biSizeImage; // 0x8b80
51 return 1;
52 }
53 }
54 return 0;
55}
Root cause analysis
Based on the Windows GDI documentation, the bmiColors
member of the BITMAPINFO
3 structure is used for bitmaps that do not use the full color range, e.g. pixels in an 8-bit bitmap can only have 256
possible color values. The color table stored in the bmiColors
member consists of an array of RGBQUAD
4 values. Full 24-bit bitmaps do not have a color table, hence they do not have a bmiColors
member. The biClrUsed
member of the BITMAPINFOHEADER
structure specifies the number of color indices in the color table that are actually used by the bitmap. The size of the color table is calculated as sizeof(RGBQUAD) * biClrUsed
.
A quick search for the little-endian byte sequence 1C 00 07
in the sample file that triggered the crash shows that the value of the biClrUsed
member is directly controllable by the value at offset 2664h
in the sample file:
12644h: 28 00 00 00 F8 00 00 00 90 00 00 00 01 00 08 00 (...ΓΈ...........
22654h: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
32664h: 1C 00 07 00 00 00 00 00 00 00 00 00 ............
The following is the human readable representation of the specially crafted BITMAPINFOHEADER
structure, as also shown by the 010 Editor after applying the EMF
binary template.
1typedef struct tagBITMAPINFOHEADER {
2 DWORD biSize = 0x28;
3 LONG biWidth = 0xf8;
4 LONG biHeight = 0x90;
5 WORD biPlanes = 0x1;
6 WORD biBitCount = 0x8;
7 DWORD biCompression = 0x0;
8 DWORD biSizeImage = 0x0;
9 LONG biXPelsPerMeter = 0x0;
10 LONG biYPelsPerMeter = 0x0;
11 DWORD biClrUsed = 0x7001c;
12 DWORD biClrImportant = 0x0;
13} BITMAPINFOHEADER;
There are two members of the BITMAPINFOHEADER
structure which are particularly interesting for us:
- The
biBitCount
member determines the number of bits that define each pixel and the maximum number of colors in the bitmap. - The
biClrUsed
member determines the number of color indexes in the color table that are actually used by the bitmap.
According to the documentation, the system has the following assumptions:
- If
biCompression
equalsBI_RGB
and the bitmap uses8
bpp or less, the bitmap has a color table immediately following theBITMAPINFOHEADER
structure. - If
biBitCount
is8
the bitmap has a maximum of256
colors, and thebmiColors
member ofBITMAPINFO
contains up to256
entries. - If
biClrUsed
is nonzero and thebiBitCount
member is less than16
, thebiClrUsed
member specifies the actual number of colors the graphics engine or device driver accesses.
Based on the above, it seems that the root cause of the vulnerability is that the value of the biClrUsed
member should not be greater than 256
as this maximum possible value is already determined by the biBitCount
member.
The call stack also tells us that the vulnerability can be triggered by an EMR_STRETCHDIBITS
5 record containing a specially crafted BITMAPINFOHEADER
structure with a large biClrUsed
value that may lead to memory corruption in the MRBDIB::vInit()
function and could cause memcpy()
to read memory out-of-bounds.
Patch analysis
We already know the root cause, however, we do not know how this issue was fixed in version 10.0.19041.1706 of gdiplus.dll
. Diffing the patched and the vulnerable DLLs using the BinDiff plugin available for IDA Pro, Binary Ninja or Ghidra, we can identify what changes have been made to the patched file.
The Matched Functions
subview displays the pairs of functions that are associated with each other. We can filter the results with the stretch
keyword for functions related to the EMR_STRETCHDIBITS
record. The EmfEnumState::StretchDIBits()
function shows only 41% similarity with 97% confidence. Let’s examine the changes in this function in the BinDiff graph GUI.
The red nodes indicate basic blocks to which the comparison algorithms were unable to find equivalents. Let’s zoom in on the first red block on the left hand side that have been inserted between the two other green blocks. The new block added a CALL
instruction to a new function at 0x10080d08
that can be found among the unmatched functions:
It seems that the exploitation of this vulnerability is facilitated by a flaw in the original GetBitmapFromRecord()
function, which is supposed to check that an EMR
record is sufficiently large to fully contain the bitmap data, and is called at the beginning of the EMR_STRETCHDIBITS
record handler function EmfEnumState::StretchDIBits()
before any EMF
parsing actually takes place.
The vulnerability was fixed by the new GetBitmapFromRecordEx()
function that contains additional validation logic to check the value of the biClrUsed
member of the BITMAPINFOHEADER
structure, as shown by the below pseudocode:
1if ( GetDibNumPalEntries(
2 0,
3 biSize,
4 srcBmi->bmiHeader.biBitCount,
5 srcBmi->bmiHeader.biCompression,
6 srcBmi->bmiHeader.biClrUsed,
7 &numPalEntries) )
8{
9 if ( ULongLongToULong(4 * numPalEntries, v18) >= 0 )
10 {
11 if (...)
12 {
13 // Number of colors used is larger than size of color table.
14 if ( srcBmi->bmiHeader.biClrUsed <= numPalEntries )
15 {
16 dstBmi = srcBmi;
17 iUsageSrc = DIB_RGB_COLORS;
18 goto LABEL_15;
19 }
20 // Allocate new memory for BITMAPINFO.
21 dstBmi = HeapAlloc(GpRuntime::GpMemHeap, 0, dwBytes);
22 if ( dstBmi )
23 {
24 // Copy BITMAPINFO to the new memory area.
25 memcpy(dstBmi, srcBmi, dwBytes);
26 // Reset biClrUsed member to maximum allowed value.
27 dstBmi->bmiHeader.biClrUsed = numPalEntries;
28 // The color table consists of an array of 16-bit indexes.
29 iUsageSrc = DIB_PAL_COLORS;
30LABEL_15:
31 BitmapInfoPointer::Attach(pBmi, dstBmi, iUsageSrc);
32 return 1;
33 }
34 }
35 }
36}
If the GetBitmapFromRecordEx()
function encounters a large biClrUsed
value, it will copy the affected BITMAPINFO
structure to a new memory area and reset the biClrUsed
member of the BITMAPINFOHEADER
structure to the maximum allowed value based on the number of entries in the color palette determined by the GetDibNumPalEntries()
function.
Timeline
⬅️ 2021-09-29: Reported issue to MSRC.
➡️ 2021-09-29: MSRC opened case 67754.
➡️ 2021-09-30: MSRC confirmed the vulnerability.
⬅️ 2021-12-03: Requested status update.
➡️ 2021-12-03: MSRC provided January 2022 PT as target date.
➡️ 2021-12-22: MSRC assigned CVE-2022-21915 and confirmed target date.
➡️ 2022-01-11: Coordinated public release of advisory.
Bibliography
- CVE-2022-21915 - Security Update Guide - Microsoft - Windows GDI+ Information Disclosure Vulnerability
- VMware Workstation ThinPrint EMR_STRETCHDIBITS Out-Of-Bounds Read Information Disclosure Vulnerability
- Adobe Acrobat Pro DC ImageConversion EMF EMR_STRETCHDIBITS Parsing Out-Of-Bounds Read Information Disclosure Vulnerability
- Adobe Acrobat Pro DC ImageConversion EMF EMR_STRETCHDIBITS cySrc Parsing Heap-based Buffer Overflow Remote Code Execution Vulnerability
- Issue 951: GDI: Insufficient bounds check on GDI32!ConvertDxArray
- Issue 824: Microsoft GDI+ out-of-bounds write due to invalid pointer arithmetic in DecodeCompressedRLEBitmap
- Issue 729: Windows gdi32.dll multiple issues in the EMF COMMENT_MULTIFORMATS record handling
- Issue 722: Windows gdi32.dll multiple issues in the EMF CREATECOLORSPACEW record handling
- FireFox 2.0.0.11 and Opera 9.50 beta Remote Memory Information Leak
- Time Travel Debugging - Sample App Walkthrough