Out-of-bounds read information disclosure vulnerability in Microsoft Windows GDI+ EMR_STRETCHDIBITS record (again)

CVE-2022-38006

This article, the seventh 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-38006) exists when the Windows GDI+ component improperly handles objects in 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 BITMAPINFOHEADER in an EMR_STRETCHDIBITS 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.1706 of gdiplus.dll.

It seems that processing an EMR_STRETCHDIBITS record containing a specially crafted BITMAPINFOHEADER structure may lead to memory corruption in the MfEnumState::OutputDIB() function and could cause memcpy() to read memory out-of-bounds and trigger an access violation exception.

The below is the relevant excerpt of the crash analysis from WinDbg when processing an EMF metafile.

 10:000> g
 2(aac.300): Access violation - code c0000005 (first chance)
 3First chance exceptions are reported before any exception handling.
 4This exception may be expected and handled.
 5eax=073ff006 ebx=073fefd8 ecx=00000001 edx=00000002 esi=073ff000 edi=07409ff0
 6eip=76f98d4a esp=0020f29c ebp=0020f2a4 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:
 976f98d4a f3a5            rep movs dword ptr es:[edi],dword ptr [esi]
100:000> kn
11 # ChildEBP RetAddr      
1200 0020f2a4 6c8e4ed5     msvcrt!memcpy+0x5a
1301 0020f3e4 6c8e47d8     gdiplus!MfEnumState::OutputDIB+0x6ad
1402 0020f448 6c8e433f     gdiplus!EmfEnumState::StretchDIBits+0x13d
1503 0020f464 6c907c5d     gdiplus!EmfEnumState::ProcessRecord+0x7f
1604 0020f488 6c92812c     gdiplus!GdipPlayMetafileRecordCallback+0xdd
1705 0020f4b4 750f3945     gdiplus!EnumEmfDownLevel+0x6c
1806 0020f588 750ecb4c     gdi32full!bInternalPlayEMF+0x855
1907 0020f59c 7714450b     gdi32full!EnumEnhMetaFile+0x2c
2008 0020f5bc 6c8e85a2     GDI32!EnumEnhMetaFileStub+0x2b
2109 0020f610 6c8e5b86     gdiplus!MetafilePlayer::EnumerateEmfRecords+0xc8
220a 0020f6c8 6c8ea07c     gdiplus!GpGraphics::EnumEmf+0x53e
230b 0020f838 6c8f7461     gdiplus!GpMetafile::EnumerateForPlayback+0x651
240c 0020f990 6c90867f     gdiplus!GpGraphics::DrawImage+0x541
250d 0020f9fc 6c927f76     gdiplus!GpGraphics::DrawImage+0x61
260e 0020fa60 6c927e47     gdiplus!GdipDrawImage+0x116
270f 0020fa80 0027136a     gdiplus!GdipDrawImageI+0x37
2810 (Inline) --------     GDI!Gdiplus::Graphics::DrawImage+0x18
29...snip...
300:000> !msec.exploitable
31Exploitability Classification: PROBABLY_EXPLOITABLE
32Recommended Bug Title: Probably Exploitable - Read Access Violation on Block Data Move starting at msvcrt!memcpy+0x000000000000005a (Hash=0x725b1b07.0x62204670)
33
34This is a read access violation in a block data move, and is therefore classified as probably exploitable.

Crash analysis

The following analysis is based on Windows 10 Professional using GdiPlus.dll version 10.0.19041.1706.

When processing EMF records, the execution flow reaches the EmfEnumState::ProcessRecord() function within GdiPlus.dll to handle each record. For EMR_STRETCHDIBITS records execution continues with the EmfEnumState::StretchDIBits() function.

The below is the hexidecimal representation of the BITMAPINFOHEADER structure provided with the affected EMR_STRETCHDIBITS record:

1069Ch: 28 00 00 00 A5 0E 00 00 00 00 00 00 01 00 20 00  (...¥......... . 
206ACh: 00 00 00 00 22 22 22 22 22 22 22 22 22 22 22 22  ...."""""""""""" 
306BCh: 22 22 22 22 22 20 02 22                          """"" ."

The following is the human readable representation of the BITMAPINFOHEADER shown above.

 1typedef struct tagBITMAPINFOHEADER {
 2  DWORD   biSize          = 0x28;
 3  LONG    biWidth         = 0xea5;
 4  LONG    biHeight        = 0x0;
 5  WORD    biPlanes        = 0x1;
 6  WORD    biBitCount      = 0x20;       // Number of bits per pixel. 
 7  DWORD   biCompression   = 0x0;        // Type of compression for a compressed bottom-up bitmap.
 8  DWORD   biSizeImage     = 0x22222222;
 9  LONG    biXPelsPerMeter = 0x22222222;
10  LONG    biYPelsPerMeter = 0x22222222;
11  DWORD   biClrUsed       = 0x22222222; // Number of 2-bytes color indexes in the color table.
12  DWORD   biClrImportant  = 0x22022022;
13} BITMAPINFOHEADER;

At first glance, it seems that the value passed to the memcpy() function by the MfEnumState::OutputDIB() function as the Size parameter is larger than the bitmap, as the below pseudocode shows:

 1void MfEnumState::OutputDIB(...) {
 2...snip...
 3          if ( !GetDibNumPalEntries(
 4            1,                       // flag?
 5            biSize,                  // 0x28
 6            Src->biBitCount,         // 0x20
 7            Src->biCompression,      // 0x0
 8            Src->biClrUsed,          // 0x22222222
 9            &palEntries) )
10            goto FAIL;
11          if ( Src & 3 || iUsage ) { // DIB_PAL_COLORS
12            palSize = sizeof(WORD) * (iUsage == DIB_RGB_COLORS) + 2;
13            if ( UIntMult(palEntries, palSize, &dwBytes) < 0 )       // 0x03 * 0x02 = 0x06
14              goto FAIL;
15            if ( UIntMult(palEntries, sizeof(DWORD), &palSize) < 0 ) // 0x03 * 0x04 = 0x0c
16              goto FAIL;
17            if ( SizeTAdd(dwBytes, Src->biSize, &Size) < 0 )         // 0x06 + 0x28 = 0x2e
18              goto FAIL;
19            if ( SizeTAdd(palSize, Src->biSize, &dwBytes) < 0 )      // 0x0c + 0x28 = 0x34
20              goto FAIL;
21            Dst = GpMallocEx(dwBytes, 8); // 0x34
22            if ( !Dst )
23              goto FAIL;
24            memcpy(Dst, Src, Size); // Crash here!
25          }
26...snip...
27}

Root cause analysis

Following the execution flow in WinDbg reveals how the offending value of the Size parameter is calculated. It seems the MfEnumState::OutputDIB() function assumes that bmiColors consists of three DWORD color masks and that it also contains 16-bit indexes at the same time.?

The GpMallocEx() function will allocate a buffer large enough to hold the 0x28 bytes bitmap and the 0xc bytes color masks, while the memcpy() function will try to copy the 0x28 bytes bitmap and the 0x6 bytes indices. However, it turnes out that the source bitmap stored in memory is only 0x28 bytes, hence memcpy() will read 0x6 bytes past the allocated buffer.

Further analysis showed that the culprit seems to be the GetDibNumPalEntries() function which is used several times during the process. It is also called by GetBitmapFromRecordEx() in the EmfEnumState::StretchDIBits() record handler function before MfEnumState::OutputDIB() to determine the size of the bitmap. However, the return value of the GetDibNumPalEntries() function depends on the first flag parameter, as the following pseudocode shows:

 1int GetDibNumPalEntries(int flag, uint biSize, int biBitCount, uint biCompression, uint biClrUsed, uint *palEntries)
 2{
 3  nSize = biSize;
 4  // This will override the biCompression parameter.
 5  if ( (biBitCount == 0x10 || biBitCount == 0x20) && flag )
 6    biCompression = BI_BITFIELDS;
 7  ...
 8  palEntries = 0;
 9  if ( biCompression ) {
10    if ( biCompression-- ) {
11      if ( biCompression-- ) {
12        if ( biCompression != 1 || biBitCount != 0x10 && biBitCount != 0x20 )
13          return 0;
14        // The color table consists of three DWORD color masks.
15        biClrUsed = nSize > 0x28 ? 0 : 3;
16        palEntries = biClrUsed;
17        goto LABEL_11;
18      }
19      ...
20      return 0;
21    }
22  }
23  if ( biBitCount == 1 || biBitCount == 4 || biBitCount == 8 )
24    goto LABEL_18;
25  if ( biBitCount != 0x18 && biBitCount != 0x10 && biBitCount != 0x20 )
26    return 0;
27LABEL_11:
28  if ( biClrUsed && biClrUsed <= palEntries )
29    palEntries = biClrUsed;
30  if ( palEntries > 0x100 )
31    palEntries = 256;
32  result = 1;
33  return result;
34}

If the flag is 1 the GetDibNumPalEntries() function assumes/enforces BI_BITFIELDS compression, which means that there are three DWORD color masks and hence returns 3, otherwise the return value is 0. Note that the value of flag varies during the execution process, as the following call tree summarizes:

1EmfEnumState::StretchDIBits()
2├── GetBitmapFromRecordEx()
3|   └── GetDibNumPalEntries(0, ...) -> 0
4└── MfEnumState::OutputDIB()
5    └── GetDibNumPalEntries(0, ...) -> 0
6    └── GetDibNumPalEntries(1, ...) -> 3
7    └── GetDibNumPalEntries(1, ...) -> 3

The GetBitmapFromRecordEx() function will only extract the 0x28 bytes bitmap from the record, while the MfEnumState::OutputDIB() function will falsely assume that there are additional color masks (or 2-byte indices?) provided with the bitmap.

Note that the crash can be avoided by setting the biCompression member of the BITMAPINFOHEADER structure at offset 0x6ac in the sample file to BI_BITFIELDS, which also suggests that the root cause may be enforcing BI_BITFIELDS compression on a BI_RGB bitmap.

The bug has been reproduced on a fully patched Windows 10 64-bit with a 32-bit PoC program, but the 64-bit build of gdiplus.dll might be also affected. Note that PageHeap is required to reproduce the crash.

Patch analysis

Timeline

⬅️ 2022-05-28: Reported issue to MSRC.
➡️ 2022-05-31: MSRC opened case 72140.
⬅️ 2022-06-15: Requested status update.
➡️ 2022-06-17: MSRC still investigates the issue.
⬅️ 2022-06-20: Reuploaded attachment, just to be sure.
➡️ 2022-06-21: MSRC confirmed the vulnerability.
➡️ 2022-09-13: Coordinated public release of advisory.

Bibliography