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

CVE-2022-21904

This article, the second 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_SETDIBITSTODEVICE enhanced metafile record. For other articles in the series click here.

TL;DR

An information disclosure vulnerability (CVE-2022-21904) 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 when converting metafiles from EMF format to WMF. A specially crafted EMR_SETDIBITSTODEVICE record can result in a read past the end of an allocated buffer and disclose small portions of 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 DoSetDIBitsToDevice() function.

 10:000> g
 2(1614.f34): Access violation - code c0000005 (first chance)
 3First chance exceptions are reported before any exception handling.
 4This exception may be expected and handled.
 5eax=083fc002 ebx=00000000 ecx=00000000 edx=00000002 esi=083fc000 edi=083fdff8
 6eip=774d8e88 esp=0135fb5c ebp=0135fb64 iopl=0         nv up ei pl nz na po nc
 7cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010202
 8msvcrt!memcpy+0x198:
 9774d8e88 8a06            mov     al,byte ptr [esi]          ds:002b:083fc000=??
100:000> kn
11 # ChildEBP RetAddr  
1200 0135fb64 6e04d10c msvcrt!memcpy+0x198
1301 0135fbb0 6e04b2c1 gdiplus!DoSetDIBitsToDevice+0x60
1402 0135fc2c 6dfa0cb0 gdiplus!bHandleSetDIBitsToDevice+0xd1
1503 0135fc50 6dfa0f07 gdiplus!bParseWin32Metafile+0xb9
1604 0135fc68 6dfa1273 gdiplus!GdipConvertEmfToWmf+0x91
1705 0135fca4 6dfa130e gdiplus!GdipGetWinMetaFileBitsEx+0xfe
1806 0135fd54 6dfa1664 gdiplus!ConvertEmfToPlaceableWmf+0x5f
1907 0135fd6c 00b9123f gdiplus!GdipEmfToWmfBits+0x24
20...
210:000> !msec.exploitable
22Exploitability Classification: UNKNOWN
23Recommended Bug Title: Read Access Violation starting at msvcrt!memcpy+0x0000000000000198 (Hash=0x6a334444.0x7b280c43)

Crash analysis

The Metafile::EmfToWmfBits() method converts an enhanced-format metafile to a Windows Metafile Format (WMF) metafile and stores the converted records in a specified buffer. Calling this method on the sample EMF file will trigger a reproduceable out-of-bounds read access violation.

It seems the value 0x2b is being passed to memcpy() as the Size parameter by the DoSetDIBitsToDevice() function, as shown by the following pseudocode:

 1int DoSetDIBitsToDevice(...)
 2{
 3  if ( Size ) // 0x2b
 4  {
 5    if ( Size >= 0x28 )
 6    {
 7      Dst = LocalAlloc(0, Size);
 8      if ( Dst )
 9      {
10          memcpy(Dst, Src, Size); // Crash here!
11          ...
12      }
13      ...
14    }
15    else
16    {
17      ...
18    }
19  }
20}

A DIB is a format used to define device-independent bitmaps in various color resolutions. Its main purpose is to allow bitmaps to be moved from one device to another. A DIB consists of two parts: the bits themselves and a header that describes the format of the bits. The header contains the color format, a color table, and the size of the bitmap.

A DIB is normally transported in metafiles, usually using the StretchDIBits() function that moves a rectangle from the DIB to a rectangle on a destination surface, stretching or compressing as necessary, while the SetDIBitsToDevice() function sets a DIB directly to the output surface.

During the conversion process the system will parse the metafile and call the bHandleSetDIBitsToDevice() handler function to process the EMR_SETDIBITSTODEVICE1 record found in the metafile. The function ensures that the provided EMF metafile record is a valid bitmap record by calling IsValidEnhMetaRecordBitmapEx() before the actual DIB setting operation is performed.

 1int bHandleSetDIBitsToDevice(tagEMRSETDIBITSTODEVICE *emr, int hdc)
 2{
 3  result = 0;
 4  BmiSrc = 0;
 5  cbBmiSrc = emr->cbBmiSrc; // 0x2b
 6  xDest = emr->xDest;
 7  yDest = emr->yDest;
 8  xSrc = emr->xSrc;
 9  ySrc = emr->ySrc;
10  wSrc = emr->cxSrc;
11  hSrc = emr->cySrc;
12  offBitsSrc = emr->offBitsSrc;
13  iStartScan = emr->iStartScan;
14  biHeight = emr->cScans;
15  cbBitsSrc = emr->cbBitsSrc;
16  iUsageSrc = emr->iUsageSrc;
17  if ( IsValidEnhMetaRecordBitmapEx(
18        *(hdc + 0x1E8),
19        emr,
20        1,
21        emr->offBmiSrc,
22        emr->cbBmiSrc, // 0x2b
23        emr->offBitsSrc,
24        cbBitsSrc,
25        iUsageSrc,
26        emr->cScans,
27        &BmiSrc) )
28  {
29    result = DoSetDIBitsToDevice(
30      hdc,
31      xDest,
32      yDest,
33      xSrc,
34      ySrc,
35      wSrc,
36      hSrc,
37      iUsageSrc,
38      iStartScan,
39      biHeight,
40      BmiSrc,
41      cbBmiSrc, // 0x2b
42      emr + offBitsSrc,
43      cbBitsSrc);
44  }
45  BitmapInfoPtr::~BitmapInfoPtr(&Src);
46  return result;
47}

Note that IsValidEnhMetaRecordBitmapEx() prepares the source bitmap header and passes it to the function performing the actual DIB setting operation as the BmiSrc parameter. The cbBmiSrc member value in the EMR metafile record structure is also passed as a parameter to the DIB function and subsequently used as the Size parameter to perform the memory copy operation on the BmiSrc buffer containing the source bitmap header.

EMR records in 010 Editor

EMR records in 010 Editor

A quick search for the byte 0x2b in the sample file that triggered the crash shows that the cbBmiSrc member value is directly controllable by the value at offset 694h in the sample file:

10660h: 50 00 00 00 9C 15 00 00 B1 00 00 00 94 FD FF FF  P...œ...±...”ýÿÿ 
20670h: DC 00 00 00 BD FD FF FF B1 00 00 00 94 FD FF FF  Ü...½ýÿÿ±...”ýÿÿ 
30680h: 00 00 00 00 00 00 00 00 2B 00 00 00 29 00 00 00  ........+...)... 
40690h: 50 00 00 00 2B 00 00 00 78 00 00 00 24 15 00 00  P...+...x...$... 
506A0h: 00 00 00 00 20 00 CC 00 2B 00 00 00 29 00 00 00  .... .Ì.+...)... 
606B0h: 28 00 00 00 0B 00 00 00 FF FF FF FF 01 00 18 00  (.......ÿÿÿÿ.... 
706C0h: 00 00 00 00 24 15 00 00 13 0B 9F 72 D5 9F 74 D7  ....$.....ŸrÕŸt× 
806D0h: 9F 75 D8 A0 76 D9 A0 77 D9 A0 77 D9              ŸuØ vÙ wÙ wÙ

The following is the human readable representation of the specially crafted EMR_SETDIBITSTODEVICE structure, as also shown by the 010 Editor after applying the EMF binary template.

 1typedef struct tagEMRSETDIBITSTODEVICE {
 2  DWORD iType      = 0x50;   // EMR_SETDIBITSTODEVICE
 3  DWORD nSize      = 0x159c; // Size of the record.
 4  RECTL rclBounds;
 5  LONG  xDest      = 0xb1;
 6  LONG  yDest      = 0xfffffd94;
 7  LONG  xSrc       = 0x0;
 8  LONG  ySrc       = 0x0;
 9  LONG  cxSrc      = 0x2b;
10  LONG  cySrc      = 0x29;
11  DWORD offBmiSrc  = 0x50;   // Offset to the source BITMAPINFO structure.
12  DWORD cbBmiSrc   = 0x2b;   // Size of the source BITMAPINFO structure.
13  DWORD offBitsSrc = 0x78;
14  DWORD cbBitsSrc  = 0x1524;
15  DWORD iUsageSrc  = 0x0;
16  DWORD iStartScan = 0xcc0020;
17  DWORD cScans     = 0x2b;
18} EMRSETDIBITSTODEVICE;

Root cause analysis

It seems bCheckBitmapInfoHeader() uses the biSize member value in the BITMAPINFOHEADER2 structure of the source bitmap to allocate memory for a new BITMAPINFOHEADER structure, instead the cbBmiSrc member value in the EMR_SETDIBITSTODEVICE record structure passed to it as a function parameter all the way down the following call tree:

1bHandleSetDIBitsToDevice()
2└── IsValidEnhMetaRecordBitmapEx()
3    └── bCheckBitmap()
4        └── bCheckBitmapInfo()
5            └── bCheckBitmapInfoHeader()
6                └── LocalAlloc(0, biSize)

This will result in a out-of-bounds read (CWE-128) past the buffer holding the soruce BITMAPINFO structure, as shown in the below pseudocode, due to the fact that the allocated buffer is only biSize=0x28 which is smaller than the size cbBmiSrc=0x2b of the structure as specified in the metafile record.

 1int bCheckBitmapInfoHeader(...)
 2{
 3  if ( cbBmiSrc >= 0xC )
 4  {
 5    srcBmi = (emr + offBmiSrc);
 6    biSize = *(emr + offBmiSrc);
 7    if ( cbBmiSrc >= biSize
 8      && CalculateColorTableSize(...)
 9      && (!UsageSrc || ULongAdd(...) >= 0 && (biSize = v13, cbBmiSrc >= v13) )
10    {
11      ...
12      dstBmi = LocalAlloc(0, biSize); // 0x28
13      if ( dstBmi )
14      {
15        memcpy(dstBmi, srcBmi, biSize);
16        dstBmi->bmiHeader.biClrUsed = colorTableSize;
17        LOBYTE(UsageSrc) = DIB_PAL_COLORS;
18        BitmapInfoPtr::Attach(bmiPtr, dstBmi, UsageSrc);
19        return 1;
20      }
21    }
22  }
23  return 0;
24}

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. We can filter the results with the check keyword for functions related to the validation process. The bCheckBitmapInfoHeader() function shows 95% similarity with 98% confidence. Let’s examine the changes in this function in the BinDiff graph GUI.

 1int bCheckBitmapInfoHeader(...)
 2{
 3  if ( cbBmiSrc >= 0xC )
 4  {
 5    srcBmi = (emr + offBmiSrc);
 6    biSize = *(emr + offBmiSrc);
 7    if ( cbBmiSrc >= biSize
 8      && CalculateColorTableSize(...)
 9      && (!UsageSrc || ULongAdd(...) >= 0 && cbBmiSrc >= v12) )
10    {
11      ...
12      dstBmi = LocalAlloc(0, cbBmiSrc); // 0x2b
13      if ( dstBmi )
14      {
15        memcpy(dstBmi, srcBmi, cbBmiSrc);
16        dstBmi->bmiHeader.biClrUsed = colorTableSize;
17        LOBYTE(UsageSrc) = DIB_PAL_COLORS;
18        BitmapInfoPtr::Attach(bmiPtr, dstBmi, UsageSrc);
19        return 1;
20      }
21    }
22  }
23  return 0;
24}

As the above pseudocode shows, the patched bCheckBitmapInfoHeader() function now allocates a buffer according to the size specified in the cbBmiSrc member value of the EMR_SETDIBITSTODEVICE record.

Timeline

⬅️ 2021-09-09: Reported issue to MSRC.
➡️ 2021-09-11: MSRC opened case 67387.
➡️ 2021-09-25: MSRC requested additional information.
⬅️ 2021-09-25: Provided additional information.
➡️ 2021-09-28: MSRC indicated the case is pending review.
➡️ 2021-10-01: MSRC confirmed the vulnerability.
➡️ 2021-12-22: MSRC assigned CVE-2022-21904 and confirmed target date.
➡️ 2022-01-11: Coordinated public release of advisory.

Bibliography