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

CVE-2022-26934

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

TL;DR

An information disclosure vulnerability (CVE-2022-26934) 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_CREATEDIBPATTERNBRUSHPT 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.1110 of gdi32full.dll and 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 MfEnumState::ModifyDib() function.

 10:000> g
 2(2138.16ec): Access violation - code c0000005 (first chance)
 3First chance exceptions are reported before any exception handling.
 4This exception may be expected and handled.
 5eax=081db073 ebx=00000002 ecx=0000001d edx=00000000 esi=081dafff edi=082d28a0
 6eip=75ff8d4a esp=012ff0e8 ebp=012ff0f0 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:
 975ff8d4a f3a5            rep movs dword ptr es:[edi],dword ptr [esi]
100:000> kb
11 # ChildEBP RetAddr  Args to Child              
1200 012ff0f0 6eb1592c 082d27f0 081daf4f 00000124 msvcrt!memcpy+0x5a
1301 012ff124 6eac784e 00000001 081daf23 00000000 gdiplus!MfEnumState::ModifyDib+0x6e
1402 012ff164 6eb13bfb 081daf23 00000000 012ff194 gdiplus!EmfEnumState::CreateModifiedDib+0x61920
1503 012ff198 6eac7608 082d2720 6ea65140 0000005e gdiplus!EmfEnumState::CreateDibPatternBrushPt+0x126
1604 012ff1ac 6ea889dd 0000005e 00000184 081dae64 gdiplus!EmfEnumState::ProcessRecord+0x624c8
1705 012ff1d0 6eaa8ecc 0000005e 00000000 00000184 gdiplus!GdipPlayMetafileRecordCallback+0xdd
1806 012ff1fc 75c54b25 cc011116 081dc800 081dae5c gdiplus!EnumEmfDownLevel+0x6c
1907 012ff2d0 75c4dd2c 6eaa8e60 082cfcb0 012ff344 gdi32full!bInternalPlayEMF+0x855
2008 012ff2e4 76b6462b cc011116 1c4610b6 6eaa8e60 gdi32full!EnumEnhMetaFile+0x2c
2109 012ff304 6ea69302 cc011116 1c4610b6 6eaa8e60 GDI32!EnumEnhMetaFileStub+0x2b
220a 012ff358 6ea6680c cc011116 1c4610b6 012ff3fc gdiplus!MetafilePlayer::EnumerateEmfRecords+0xc8
230b 012ff410 6ea6addc 082cfcb0 1c4610b6 012ff558 gdiplus!GpGraphics::EnumEmf+0x464
240c 012ff580 6ea781e1 012ff5c4 012ff5c4 00000002 gdiplus!GpMetafile::EnumerateForPlayback+0x651
250d 012ff6d8 6ea893ff 08295f28 012ff708 012ff718 gdiplus!GpGraphics::DrawImage+0x541
260e 012ff744 6eaf9898 08295f28 012ff76c 012ff77c gdiplus!GpGraphics::DrawImage+0x61
270f 012ff7a4 6eafaaaf 00000064 00000064 00000064 gdiplus!GpMetafile::GetBitmap+0x1c0
2810 012ff7b8 6eadea95 00000064 00000064 0829bff0 gdiplus!GpMetafile::GetThumbnail+0x2f
2911 012ff7e0 002b128b 08295f28 00000064 00000064 gdiplus!GdipGetImageThumbnail+0x65
30...
310:000> !msec.exploitable
32Exploitability Classification: PROBABLY_EXPLOITABLE
33Recommended Bug Title: Probably Exploitable - Read Access Violation on Block Data Move starting at msvcrt!memcpy+0x000000000000005a (Hash=0x4b166071.0x5026552b)
34
35This is a read access violation in a block data move, and is therefore classified as probably exploitable.

Crash analysis

Further analysis revealed that the MfEnumState::ModifyDib() function will read past the BITMAPINFO1 structure, due to an invalid biWidth value that specifies the width of the bitmap, in pixels.

The EMRCREATEDIBPATTERNBRUSHPT2 structure contains members for the CreateDIBPatternBrushPt enhanced metafile record, as described in the Windows GDI documentation. The below is the raw EMR record, that defines a pattern brush for graphics operations, starting at offset 6Ch in the crash sample:

1006Ch: 5E 00 00 00 8C 01 00 00 64 00 00 00 01 00 00 00  ^...Œ...d.......
2007Ch: C7 00 00 00 BF 00 00 00 64 00 00 00 64 00 00 00  Ç...¿...d...d...

The following is the human readable representation of the above EMRCREATEDIBPATTERNBRUSHPT record:

 1typedef struct tagEMRCREATEDIBPATTERNBRUSHPT {
 2  DWORD iType   = 0x5e;  // EMRCREATEDIBPATTERNBRUSHPT
 3  DWORD nSize   = 0x18c; // Size of the record, in bytes.
 4  DWORD ihBrush = 0x64;  // Index of brush in handle table.
 5  DWORD iUsage  = 0x1;   // DIB_PAL_COLORS
 6  DWORD offBmi  = 0xc7;  // Offset from the start of this record to the DIB header.
 7  DWORD cbBmi   = 0xbf;  // Size of the DIB header.
 8  DWORD offBits = 0x64;  // Offset to bitmap bits.
 9  DWORD cbBits  = 0x64;  // Size of bitmap bits.
10} EMRCREATEDIBPATTERNBRUSHPT;

DIB_PAL_COLORS means that the color table consists of an array of 16-bit indexes into the LogPalette object that is currently defined in the playback device context.

The BITMAPINFOHEADER3 structure contains information about the dimensions and color format of a DIB. The BITMAPINFO structure is followed by the bitmap bits that form a packed DIB The below is the raw structure that contains information about the dimensions and color format of a DIB, starting at offset 133h in the provided crash sample:

10133h: 28 00 00 00 01 09 00 00 01 00 00 00 01 00 01 00  (............... 
20143h: 00 00 00 00 04 00 00 00 00 00 00 00 00 00 00 00  ................ 
30153h: 00 00 00 00 00 00 00 00                          ........

The following is the human readable representation of the above BITMAPINFOHEADER structure:

 1typedef struct tagBITMAPINFOHEADER {
 2  DWORD biSize          = 0x28;
 3  LONG  biWidth         = 0x901; // Width of the bitmap, in pixels.
 4  LONG  biHeight        = 0x1;
 5  WORD  biPlanes        = 0x1;
 6  WORD  biBitCount      = 0x1;
 7  DWORD biCompression   = 0x0;   // BI_RGB, uncompressed.
 8  DWORD biSizeImage     = 0x4;   // Size of the image, in bytes.
 9  LONG  biXPelsPerMeter = 0x0;
10  LONG  biYPelsPerMeter = 0x0;
11  DWORD biClrUsed       = 0x0;
12  DWORD biClrImportant  = 0x0;
13} BITMAPINFOHEADER;

In the attached crash sample, MfEnumState::ModifyDib() will call memcpy() with a 0x124 passed as size. This value depends on the size of the DIB and it is calculated from the biWidth member of the BITMAPINFOHEADER by the GetDibBitsSize() function which returns 0, if there are any problems detected in the headers, including invalid values in specific fields (biWidth, biHeight, …).

 1int GetDibBitsSize(BITMAPINFOHEADER *pbmi, DWORD *biSizeImage) {
 2  lSrcStride = 0;
 3  if ( pbmi->biSize >= 0x28 && pbmi->biWidth > 0 && pbmi->biPlanes == 1 )
 4  {
 5    biCompression = pbmi->biCompression;
 6    if ( biCompression && biCompression != BI_BITFIELDS && biCompression != 10 )
 7    {
 8      *biSizeImage = pbmi->biSizeImage;
 9      return 1;
10    }
11    biHeight = pbmi->biHeight;
12    if ( biHeight < 0 )
13      biHeight = -biHeight;
14    if ( GetDibByteWidth(pbmi->biWidth, 1, pbmi->biBitCount, &lSrcStride) >= 0
15      && UIntMult(lSrcStride, biHeight, &biSize, v6, nSize) >= 0 )
16    {
17      *biSizeImage = biSize; // 0x124
18      return 1;
19    }
20  }
21  *biSizeImage = 0;
22  return 0;
23}

Root cause analysis

The GetDibByteWidth() function calculates the stride of the image. The stride is the number of bytes from one row of pixels in memory to the next row of pixels in memory. If padding bytes are present, the stride is wider than the width of the image. For uncompressed RGB formats as specified by biCompression = 0x0, the minimum stride is always the image width in bytes, rounded up to the nearest DWORD, according to the following formula:

1((((biWidth * biBitCount) + 0x1F) & 0x1FFFFFFC) >> 0x3) = stride

Substituting the proper values into the above formula will give us the value of the size parameter of the memcpy() call that triggers the access violation.

1((((0x901 * 0x1) + 0x1F) & 0x1FFFFFFC) >> 0x3) = 0x124

The GetBitmapFromRecord() function ensures that the EMF record is sufficiently large to fully contain the bitmap data, and is called before any bitmap parsing actually takes place in the CreateDibPatternBrushPt() record handler function.

 1int GetBitmapFromRecord(...)
 2{
 3  result = 0;
 4  if (nSize >= 0x2C && offBmi <= nSize - 0x2C && offBits <= nSize)
 5  {
 6    ...
 7    if (GetDibNumPalEntries(0, *&offBmi[cbBits], biBitCount, biCompression, v12, &biSize)) // biSize = 0x2
 8    {
 9      if ( UIntMult(4u, biSize, &cbBits, v13, v15) >= 0      // cbBits = 0x8
10        && SizeTAdd(cbBits, **pbmi, &cbBits, v14, v16) >= 0  // cbBits = 0x30
11        && cbBits <= nSize - offBmi                          // 0x30 <= 0x18C - 0xC7   -> OK
12        && GetDibBitsSize(*pbmi, &biSize)                    // biSize = 0x124
13        && nSize - offBmi >= **pbmi                          // 0x18C - 0xC7 >= 0x28   -> OK
14        && nSize - offBits >= biSize )                       // 0x18C - 0x64 >= 0x124  -> OK
15      {
16        result = 1;
17      }
18    }
19  }
20  return result;
21}

However, the EmfEnumState::CreateModifiedDib() function does not sufficiently check that the source bitmap is valid before calling the MfEnumState::ModifyDib() function which performs a bit-block transfer of the color data and copies the source rectangle directly to the destination rectangle as specified by the SRCCOPY raster-operation code.

 1int EmfEnumState::CreateModifiedDib(...) {
 2  result = 0;
 3  if ( Src->biSize < 0x28
 4    || !GetDibNumPalEntries(0, Src->biSize, Src->biBitCount, Src->biCompression, Src->biClrUsed, &biSize)
 5    || !GetDibBitsSize(Src, &Size)
 6    || Size <= 0 )
 7  {
 8    return result;
 9  }
10
11  if ( biSize != 2 || rop == SRCCOPY || *(&Src->biSize + 0x28) || *(&Src->biWidth + 0x28) != 0xFFFFFF )
12  {
13    v9 = *a4;
14    dstSize = MfEnumState::GetModifiedDibSize(Src, biSize, Size, a4);
15    if ( dstSize > 0 )
16    {
17      if ( MfEnumState::CreateRecordToModify(dstSize) )
18      {
19        Dst = *(this + 0x98);
20        MfEnumState::ModifyDib(this, Src, v9, Src, a3, Dst, biSize, Size, 1); // Crash here!
21      }
22    }
23    return Dst;
24  }
25
26  return 0;
27}

The MfEnumState::ModifyDib() function will try to copy 292 bytes, however, the real size of our buffer is only 176 bytes, hence the memcpy() function will copy additional 29 bytes past the buffer. The crash can be only reproduced with PageHeap enabled, otherwise execution continues with the record handler function CreateDIBPatternBrushPt().

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 EmfEnumState::CreateDibPatternBrushPt() function shows only 94% similarity with 99% confidence. Let’s examine the changes in this function in the BinDiff graph GUI. Note that 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 merging GetBitmapFromRecord() into the EmfEnumState::CreateDibPatternBrushPt() function and calling the IsValidBitmapRecordSize() function at 0x1007e698 to verify the size of the record before moving on to EmfEnumState::CreateModifiedDib(), as shown by the below pseudocode:

 1HBRUSH EmfEnumState::CreateDibPatternBrushPt()
 2{
 3  if ( GetDibNumPalEntries(...) )
 4  {
 5    if ( GetDibBitsSize(...) )
 6    {
 7      if ( a4 )
 8      {
 9        ...
10        if ( IsValidBitmapRecordSize(...) )
11        {
12          lpPackedDIB = EmfEnumState::CreateModifiedDib(Src, 0, &iUsage, SRCCOPY);
13          if ( lpPackedDIB )
14          {
15            return CreateDIBPatternBrushPt(lpPackedDIB, iUsage);
16          }
17        }
18      }
19    }
20  }
21}

Timeline

⬅️ 2021-12-29: Reported issue to MSRC.
➡️ 2021-12-29: MSRC opened case 69345.
➡️ 2022-01-13: MSRC confirmed the vulnerability.
➡️ 2022-05-10: MSRC assigned CVE-2022-26934.
➡️ 2022-05-10: Coordinated public release of advisory.

Bibliography