Out-of-bounds read information disclosure vulnerability in Microsoft Windows GDI+ EMR_BITBLT record

CVE-2022-29112

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 BITMAPINFO1 structure and the biClrUsed member of the BITMAPINFOHEADER2 structure.

The EMRBITBLT3 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 RGBQUAD4. 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.

Time Travel Debugging in WinDbg Preview

Time Travel Debugging in WinDbg Preview

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 changed functions in the patched binary

The changed functions in the patched binary

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.

Zoomed-in on the changed parts of the patched function

Zoomed-in on the changed parts of the patched function

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