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

CVE-2022-34728

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

TL;DR

An information disclosure vulnerability (CVE-2022-34728) 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_SETPIXELV 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 processing a specially crafted EMF metafile may lead to memory corruption in the ModifyRecordColor() function and cause a subsequent call to memcpy() to read memory out-of-bounds.

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

 10:000> g
 2(16d4.d34): Access violation - code c0000005 (first chance)
 3First chance exceptions are reported before any exception handling.
 4This exception may be expected and handled.
 5eax=00000008 ebx=00000002 ecx=07ff2720 edx=00000008 esi=0809eff8 edi=07ff2720
 6eip=711045e0 esp=009df3a4 ebp=009df3b8 iopl=0         nv up ei pl zr na pe nc
 7cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010246
 8gdiplus!EmfEnumState::ModifyRecordColor+0x22:
 9711045e0 8b3430          mov     esi,dword ptr [eax+esi] ds:002b:0809f000=????????
100:000> kb
11 # ChildEBP RetAddr      Args to Child              
1200 009df3b8 71165417     00000002 00000003 07ff2720 gdiplus!EmfEnumState::ModifyRecordColor+0x22
1301 009df3dc 71127c5d     0000000f 00000008 0809eff8 gdiplus!EmfEnumState::ProcessRecord+0x61157
1402 009df400 7114812c     0000000f 00000000 00000008 gdiplus!GdipPlayMetafileRecordCallback+0xdd
1503 009df42c 76373945     8c010a6b 080a0fe8 0809eff0 gdiplus!EnumEmfDownLevel+0x6c
1604 009df500 7636cb4c     711480c0 07fefcb0 009df574 gdi32full!bInternalPlayEMF+0x855
1705 009df514 76f7450b     8c010a6b eb460a5d 711480c0 gdi32full!EnumEnhMetaFile+0x2c
1806 009df534 711085a2     8c010a6b eb460a5d 711480c0 GDI32!EnumEnhMetaFileStub+0x2b
1907 009df588 71105aac     8c010a6b eb460a5d 009df62c gdiplus!MetafilePlayer::EnumerateEmfRecords+0xc8
2008 009df640 7110a07c     07fefcb0 eb460a5d 009df788 gdiplus!GpGraphics::EnumEmf+0x464
2109 009df7b0 71117461     009df7f4 009df7f4 00000002 gdiplus!GpMetafile::EnumerateForPlayback+0x651
220a 009df908 7112867f     07fb9f28 009df938 009df948 gdiplus!GpGraphics::DrawImage+0x541
230b 009df970 711976a8     07fb9f28 009df998 009df9a8 gdiplus!GpGraphics::DrawImage+0x61
240c 009df9d0 711988bf     00000064 00000064 00000064 gdiplus!GpMetafile::GetBitmap+0x1c0
250d 009df9e4 7117c8a5     00000064 00000064 07fbbff0 gdiplus!GpMetafile::GetThumbnail+0x2f
260e 009dfa0c 005d128b     07fb9f28 00000064 00000064 gdiplus!GdipGetImageThumbnail+0x65
27...

Crash analysis

The following analysis is based on Microsoft Windows 10 Professional (x86) using version 10.0.19041.1706 of gdiplus.dll.

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

 1int EmfEnumState::ProcessRecord(int Type, uint Size, ...)
 2{
 3  ...
 4  if ( *(this + 9) ) {
 5    ...
 6  } else {
 7    nSize = Size;   // 0x10 = 0x8
 8  }
 9  switch ( Type ) { // 0xF
10    ...
11    case 0xF:
12      EmfEnumState::ModifyRecordColor(this, ColorAdjustTypeBrush, ColorAdjustTypePen);
13    ...
14  }
15}

It seems ModifyRecordColor() has the same purpose as the SetPixelV() function which sets the pixel at the specified coordinates to the closest approximation of the specified color.

 1void EmfEnumState::ModifyRecordColor(int this, int ColorAdjustTypeBrush, int ColorAdjustTypePen)
 2{
 3  pixel = *(this + 0x5C);
 4  if ( pixel ) {
 5    if ( *(this + 0x60) >= (4 * ColorAdjustTypeBrush) ) { // 0x10 >= 0x8
 6      oldPalEntry = *(4 * ColorAdjustTypeBrush + pixel);  // Crash here!
 7      newPalEntry = MfEnumState::ModifyColor(this, oldPalEntry, ColorAdjustTypePen);
 8      if ( newPalEntry != oldPalEntry ) {
 9        if ( EmfEnumState::CreateCopyOfCurrentRecord(this, Size) )
10          *(*(v3 + 0x98) + 4 * ColorAdjustTypeBrush + 8) = newPalEntry;
11      }
12    }
13  }
14}

The following is the human readable form of the affected EMRSETPIXELV1 structure which defines the color of the pixel at the specified logical coordinates and triggers the read access violation:

1typedef struct tagEMRSETPIXELV {
2  DWORD    iType        = 0x0000000f; // 0x04 bytes
3  DWORD    nSize;       = 0x00000010; // 0x04 bytes
4  POINTL   ptlPixel {
5    DWORD x             = 0x00000001; // 0x04 bytes
6    DWORD y             = 0x0000000e; // 0x04 bytes
7  };
8  COLORREF crColor      = 0x00000010; // 0x04 bytes
9} EMRSETPIXELV;                       // 0x14 bytes

Root cause analysis

It seems ModifyRecordColor() assumes that the minimum size of the data within the EMR_SETPIXELV2 record is only 0x8 bytes, while the ptlPixel member is an 0x8 bytes POINTL structure and the crColor member is another 0x4 bytes COLORREF3 structure, which all together require 0xC bytes.

Due to the fact that the nBytes field of the EMR_HEADER record is set to 0x100, the GetEmf() function will only allocate 256 bytes for the metafile and cut off the crColor field of the EMR_SETPIXELV record, which still seems to be valid as the value of the nSize field is 0x4 bytes smaller anyway.

As the below pseudocode shows, the EnumEmfDownLevel() function checks only that the value of the nSize member is larger than 0x8 bytes, which would indicate that there are additional fields in the record.

 1int EnumEmfDownLevel(...) {
 2  if ( !emr || mr->nSize < 8 || ... || IsEmfPlusRecord(emr) )
 3    goto LABEL_13;
 4  Size = nSize - 8;
 5  Type = emr->iType;
 6  Header = emr->dParm;
 7  if ( nSize - 8 <= 0 ) {
 8    Size = 0;
 9    Header = 0;
10  }
11  ...
12  if ( !GdipPlayMetafileRecordCallback(Type, 0, Size, Header, ...) ) {
13    ...
14    result = 0;
15  } else {
16LABEL_13:
17    result = 1;
18  }
19  return result;
20}

Proof of concept

An attacker who successfully exploited the vulnerability could leak small parts of initialized or uninitialized heap memory of the process, due to the fact that the ModifyColor() function will pick up the next 0x4 bytes followed by the ptlPixel field of the record. The attacker can only leak 0x3 bytes, because the high-order byte will be set to 0x0 during the operation.

As the below excerpt of the memory shows, the attacker could leak the bytes 0x77, 0x66 and 0x3A as F2 will be set to 0x0.

10113AF88  0F 00 00 00 10 00 00 00  01 00 00 00 0E 00 00 00  ................
20113AF98  77 66 3A F2 AF 7C 00 08  14 0C 46 29 00 00 00 00  wf:=ยป|....F)....

The ModifyRecordColor() function will call EmfEnumState::CreateCopyOfCurrentRecord() to create a copy of the EMR_SETPIXELV record at 0x02F20788 and then set the crColor value of the new record located at 0x02F20798 to 0x003A6677.

102F20788  0F 00 00 00 10 00 00 00  01 00 00 00 0E 00 00 00  ................
202F20798  77 66 3A 00 00 00 00 00  00 00 00 00 00 00 00 00  wf:.............

The following is the human readable form of the above EMRSETPIXELV structure which contains the leaked bytes in the crColor member:

1typedef struct tagEMRSETPIXELV {
2  DWORD    iType        = 0x0000000f;
3  DWORD    nSize;       = 0x00000010;
4  POINTL   ptlPixel {
5    DWORD  x            = 0x00000001;
6    DWORD  y            = 0x0000000e;
7  };
8  COLORREF crColor      = 0x003A6677;
9} EMRSETPIXELV;

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-20: Reported issue to MSRC.
➡️ 2022-05-23: MSRC opened case 72035.
➡️ 2022-06-02: MSRC confirmed the vulnerability.
➡️ 2022-09-13: Coordinated public release of advisory.

Bibliography