Out-of-bounds read information disclosure vulnerability in Microsoft Windows GDI+ EMR_BITBLT record
This article, the forth 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_BITBLT
enhanced metafile record. For other articles in the series click here.
TL;DR
An information disclosure vulnerability (CVE-2022-29112) 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_BITBLT
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.1348 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 ValidateBitmapInfo()
function.
10:000> g
210d0.20): Access violation - code c0000005 (first chance)
3First chance exceptions are reported before any exception handling.
4This exception may be expected and handled.
5eax=000000d0 ebx=0089f0c8 ecx=ffd0d0d0 edx=07ab9000 esi=0000000a edi=00000012
6eip=6b422373 esp=0089f038 ebp=0089f060 iopl=0 nv up ei ng nz na po cy
7cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010283
8gdiplus!ValidateBitmapInfo+0x17a:
96b422373 0fb64a02 movzx ecx,byte ptr [edx+2] ds:002b:07ab9002=??
100:000> kn
11 # ChildEBP RetAddr
1200 0089f060 6b4220a0 gdiplus!ValidateBitmapInfo+0x17a
1301 0089f4ac 6b406390 gdiplus!CopyOnWriteBitmap::CopyOnWriteBitmap+0x88
1402 0089f4cc 6b40635b gdiplus!CopyOnWriteBitmap::Create+0x25
1503 0089f4dc 6b405bc5 gdiplus!GpBitmap::GpBitmap+0x3a
1604 0089f61c 6b4b3960 gdiplus!MfEnumState::OutputDIB+0x5a6
1705 0089f6b8 6b4675e4 gdiplus!EmfEnumState::BitBlt+0x2a0
1806 0089f6cc 6b4289dd gdiplus!EmfEnumState::ProcessRecord+0x624a4
1907 0089f6f0 6b448ecc gdiplus!GdipPlayMetafileRecordCallback+0xdd
2008 0089f71c 767f4b25 gdiplus!EnumEmfDownLevel+0x6c
2109 0089f7f0 767edd2c gdi32full!bInternalPlayEMF+0x855
220a 0089f804 769c462b gdi32full!EnumEnhMetaFile+0x2c
230b 0089f824 6b409302 GDI32!EnumEnhMetaFileStub+0x2b
240c 0089f878 6b4068e6 gdiplus!MetafilePlayer::EnumerateEmfRecords+0xc8
250d 0089f930 6b40addc gdiplus!GpGraphics::EnumEmf+0x53e
260e 0089faa0 6b4181e1 gdiplus!GpMetafile::EnumerateForPlayback+0x651
270f 0089fbf8 6b4293ff gdiplus!GpGraphics::DrawImage+0x541
2810 0089fc64 6b448d16 gdiplus!GpGraphics::DrawImage+0x61
2911 0089fcc8 6b448be7 gdiplus!GdipDrawImage+0x116
3012 0089fce8 009912e6 gdiplus!GdipDrawImageI+0x37
3113 (Inline) -------- Harness!Gdiplus::Graphics::DrawImage+0x18
32...
330:000> !msec.exploitable
34Exploitability Classification: UNKNOWN
35Recommended Bug Title: Read Access Violation starting at gdiplus!ValidateBitmapInfo+0x000000000000017a (Hash=0x7d4a575b.0x76056535)
Crash analysis
Further analysis revealed that this issue might be also related to the bmiColors
member of the BITMAPINFO
1 structure and the biClrUsed
member of the BITMAPINFOHEADER
2 structure.
The EMRBITBLT
3 structure contains members for the BitBlt
enhanced metafile record, as described in the Windows GDI documentation. The below is the raw EMR
record, that specifies a block transfer of pixels from a source bitmap to a destination rectangle, starting at offset 6Ch
in the crash sample:
1006Ch: 4C 00 00 00 8C 01 00 00 64 00 00 01 00 00 00 00 L...Œ...d.......
2007Ch: B1 00 00 02 C7 00 00 00 64 00 00 00 64 00 00 00 ±...Ç...d...d...
3008Ch: 64 00 00 00 64 08 00 00 20 00 CC 00 E8 03 8E 00 d...d... .Ì.è.Ž.
4009Ch: C9 03 00 00 00 10 00 00 6A 6B 6C 6D 6E 75 06 00 É.......jklmnu..
500ACh: 4C 00 00 00 8C E4 FF 00 64 00 00 01 00 00 00 00 L...Œäÿ.d.......
600BCh: B1 00 00 02 C7 00 00 00 64 00 00 00 64 00 00 00 ±...Ç...d...d...
700CCh: 64 00 00 00 64 08 00 00 20 00 CC 00 E8 03 8E 00 d...d... .Ì.è.Ž.
800DCh: C9 03 00 00 00 10 00 00 FF E1 00 00 00 20 00 00 É.......ÿá... ..
900ECh: 00 00 80 3F 00 C0 79 C4 00 C0 79 C4 FF FF FF 00 ..€?.ÀyÄ.ÀyÄÿÿÿ.
1000FCh: 00 00 00 00 64 00 00 00 28 00 00 FF E1 00 00 00 ....d...(..ÿá...
11010Ch: 20 00 00 00 00 80 3F 00 C0 79 C4 00 C0 79 C4 FF ....€?.ÀyÄ.ÀyÄÿ
12011Ch: FF FF 00 00 00 00 00 64 00 05 00 28 00 00 00 80 ÿÿ.....d...(...€
13012Ch: 00 00 00 00 01 00 00 28 00 00 00 64 00 00 00 FF .......(...d...ÿ
14013Ch: FF 00 00 01 00 08 00 01 00 00 00 00 00 00 00 05 ÿ...............
15014Ch: 00 00 00 00 00 00 09 12 00 00 00 00 00 00 00 17 ................
16015Ch: 01 02 03 ...
The following is the human readable representation of the above EMRBITBLT
record:
1typedef struct tagEMRBITBLT {
2 DWORD iType = 0x4c; // EMR_BITBLT
3 DWORD nSize = 0x18c; // Size of the record.
4 RECTL rclBounds;
5 LONG xDest = 0x64;
6 LONG yDest = 0x64;
7 LONG cxDest = 0x64;
8 LONG cyDest = 0x864;
9 DWORD dwRop = 0xcc0020;
10 LONG xSrc = 0x8e03e8;
11 LONG ySrc = 0x3c9;
12 XFORM xformSrc;
13 COLORREF bkColorSrc = 0x0;
14 DWORD iUsageSrc = 0x20000b1;
15 DWORD offBmiSrc = 0xc7;
16 DWORD cbBmiSrc = 0x64;
17 DWORD offBitsSrc = 0x64;
18 DWORD cbBitsSrc = 0x64;
19} EMRBITBLT;
Based on the Windows GDI documentation, the BITMAPINFO
structure defines the dimensions and color information for a DIB
.
1typedef struct tagBITMAPINFO {
2 BITMAPINFOHEADER bmiHeader;
3 RGBQUAD bmiColors[1];
4} BITMAPINFO;
The bmiColors
member contains an array of RGBQUAD
4. The number of entries in the array depends on the values of the biBitCount
and biClrUsed
members of the BITMAPINFOHEADER
structure. The biClrUsed
member is directly controllable via the value at offset 153h
in our sample file:
10133h: 28 00 00 00 64 00 00 00 FF FF 00 00 01 00 08 00 (...d...ÿÿ......
20143h: 01 00 00 00 00 00 00 00 05 00 00 00 00 00 00 09 ................
30153h: 12 00 00 00 00 00 00 00 17 01 02 03 ............
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 = 0x64;
4 LONG biHeight = 0xFFFF;
5 WORD biPlanes = 0x1;
6 WORD biBitCount = 0x8;
7 DWORD biCompression = 0x1; // BI_RLE8
8 DWORD biSizeImage = 0x0;
9 LONG biXPelsPerMeter = 0x5;
10 LONG biYPelsPerMeter = 0x9000000;
11 DWORD biClrUsed = 0x12;
12 DWORD biClrImportant = 0x0;
13} BITMAPINFOHEADER;
The biBitCount
value 0x8
indicates that the bitmap has a maximum of 256
colors and the bmiColors
member of BITMAPINFO
contains up to 256
entries.
The value of the biClrUsed
is the number of colors the graphics engine or device driver accesses, while biClrImportant
specifies that all colors are required.
The crashing code in the ValidateBitmapInfo()
supposed to convert RGB
bitmap colors to a palette of ARGB
color values and will simply try to loop through the bmiColors
member of the BITMAPINFO
structure based on the value of the biClrUsed
member, eventually attempting to read memory past the actual array.
1int ValidateBitmapInfo(...)
2{
3 ...
4 if ( nColors )
5 {
6 ...
7 pltEntry = palette->Entries;
8 if ( &palette->Entries[nColors] >= palette->Entries ? nColors >> 2 : 0 )
9 {
10 i = 0;
11 clrsUsed = &palette->Entries[nColors] >= palette->Entries ? nColors >> 2 : 0;
12 do {
13 blue = bmiColor->rgbBlue;
14 arg = (bmiColor->rgbGreen | (((bmiColor->rgbRed | 0xFFFFFF00) << 8)) << 8; // Crash here!
15 ++bmiColor;
16 ++i;
17 pltEntry->argb = blue | arg;
18 ++pltEntry;
19 ...
20 }
21 while ( i < clrsUsed );
22 }
23 }
24 ...
25}
Root cause analysis
It seems ValidateBitmapInfo()
fails to detect when the biClrUsed
member value does not correspond to the number of actual entries set in the bmiColors
member value of the BITMAPINFO
structure.
The following excerpt of the check right before the crashing code shows that the function only verifies that the biClrUsed
member value is smaller than the number of color entries indicated by the biBitCount
member value and sets the number of elements in the Entries
array of the ColorPalette
to the value provided.
1int ValidateBitmapInfo(...)
2{
3 ...
4 palette->Count = 1 << biBitCount
5 nColors = 1 << (1 << biBitCount);
6 biClrUsed = bmi->biClrUsed;
7 if ( biClrUsed && biClrUsed < nColors )
8 {
9 palette->Count = biClrUsed;
10 nColors = biClrUsed;
11 }
12 ...
13}
But why is the bmiColors
array turns out to be smaller than the number of colors?
Replaying the execution flow using the Time Travel Debugging capabilities of WinDbg Preview reveals how the size of the source bitmap was calculated in the MfEnumState::OutputDIB()
function before ValidateBitmapInfo()
.
The above hypothesis of the bmiColors
being truncated 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 source BITMAPINFO
structure using the ba w4 0x1ec55fb0
command and running back to the last point of memory access of this variable using the g-
command.
Further analysis revealed that the MfEnumState::OutputDIB()
function miscalculates the size of the BITMAPINFO
structure due to the corrupted value of the iUsageSrc
field of the EMR_BITBLT
record. As shown by the following pseudo code, the size of the color palette entries will be only 0x2
bytes, hence the total size of the palette of 12
colors will be only 0x24
bytes. This will result in only 0x4c
bytes as the overall size of the bitmap, which is smaller than the actual size.
1void MfEnumState::OutputDIB(...)
2{
3 ...
4 entrySize = 2 * (iUsageSrc == 0) + 2; // 0x2 * (0x20000b1 == 0x0) + 0x2 = 0x2
5 if ( palEntries <= 0xFFFFFFFF / entrySize )
6 {
7 palSize = palEntries * entrySize; // 0x12 * 0x2 = 0x24
8 Size = biSize + palSize; // 0x28 + 0x24 = 0x4c
9 if ( Size >= palSize )
10 {
11 Dst = GpMallocEx(Size, 0); // 0x4c
12 palEntries = Dst;
13 if ( Dst )
14 {
15 memcpy(Dst, Src, Size);
16 }
17 }
18 }
19 ...
20}
Based on the above, it seems that the root cause of the vulnerability is that the value of the iUsageSrc
field of the EMR_BITBLT
record should be DIB_RGB_COLORS
, so the color entry size would be the correct 0x4
bytes.
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. The MfEnumState::OutputDIB()
function shows only 82% similarity with 98% confidence. Let’s examine the changes in this function in the BinDiff graph GUI. Note that green blocks are identical, while yellow blocks contain some different instructions between functions and the grey nodes indicate basic blocks containing newly added code.
The vulnerability was fixed by introducing a check at the beginning of the MfEnumState::OutputDIB()
function, immediately after checking the size of the bitmap, to ensure that the value of the iUsageSrc
field of the EMR_BITBLT
record is either DIB_RGB_COLORS
or DIB_PAL_COLORS
. You can see that if either check fails, the function will jump to the function epilogue at 0x10025272
and return.
Furthermore, you may have noticed a new function call added to the patched binary. Microsoft has also started to log unusual usage patterns of GDI+
with the TraceLoggingUnsupportedGdiPlusUsage()
function, as shown by the below pseudocode:
1void MfEnumState::OutputDIB(...)
2{
3 ...
4 if ( biSize < 0x28; )
5 return;
6 if ( iUsageSrc && iUsageSrc != 1 ) // 0x20000b1
7 TraceLogging::TraceLoggingUnsupportedGdiPlusUsage(1, iUsageSrc, 0);
8 return;
9 ...
10}
Timeline
⬅️ 2021-11-26: Reported issue to MSRC.
➡️ 2021-11-29: MSRC opened case 68706.
➡️ 2021-12-16: MSRC closed the case as a low severity DoS.
➡️ 2022-02-09: MSRC reactivated the case after an internal review.
⬅️ 2022-02-09: Indicated details have been kept confidential.
➡️ 2022-02-14: MSRC confirmed severity has been raised to important ID.
➡️ 2022-03-02: MSRC requested May 2022 PT as target date.
⬅️ 2022-03-02: Accepted target date.
➡️ 2022-05-10: MSRC assigned CVE-2022-29112.
➡️ 2022-05-10: Coordinated public release of advisory.
Bibliography
- CVE-2022-29112 - Security Update Guide - Microsoft - Windows GDI+ Information Disclosure Vulnerability
- Issue 826: Microsoft GDI+ out-of-bounds reads due to invalid pointer arithmetic in ValidateBitmapInfo
- Adobe Acrobat and Reader Out-of-bounds Write (APSB17-36: CVE-2017-16407)
- OpenOffice EMF File EMR_BITBLT Record Integer Overflow (CVE-2007-5746)