Source: https://bugs.chromium.org/p/project-zero/issues/detail?id=826


The GDI+ library can handle bitmaps originating from untrusted sources through a variety of attack vectors, like EMF files, which may embed bitmaps in records such as EMR_PLGBLT, EMR_BITBLT, EMR_STRETCHBLT, EMR_STRETCHDIBITS etc.

In a simplified scheme of things, let's introduce the following symbols, as they are calculated by GDI+ (all arithmetic is performed on signed 32-bit types):

columns = abs(biHeight)
bytes_per_row_signed = biWidth * (((biPlanes * biBitCount + 31) & 0xFFFFFFE0) / 8)

While the gdiplus!ValidateBitmapInfo attempts to validate the correctness of the bitmap headers to some degree, it also fills out portions of a structure which is later used to display the bitmap or perform any other operations on the image. One of them is a pointer to the first row of pixels, calculated depending on the signedness of the biHeight field, which indicates if the bitmap is encoded upside-down or not. This is illustrated by the following pseudo-code snippet:

  --- cut ---
  if (biHeight > 0) {
    first_row = &pixels_buffer[bytes_per_row_signed * (biHeight - 1)];
  } else {
    first_row = pixels_buffer;
  }
  --- cut ---

Even though there are some dependencies between the various fields that must be met, the attacker still has almost full control over the values of both bytes_per_row_signed and biHeight. If the bytes_per_row_signed variable holds a negative value and biHeight is larger than 1, then we can get the first_row pointer to point at a nearly arbitrary location relative to the address of pixels_buffer.

The exploitation of this bug is additionally facilitated by a flaw in the gdiplus!GetBitmapFromRecord function, which is supposed to check that the EMF record is sufficiently large to fully contain the bitmap data, and is called at the beginning of the BMP-related EMF record handlers, before any BMP parsing actually takes place. The most interesting expression is as follows:

  --- cut ---
  if (record_length - bitmap_data_offset >= GetDibBitsSize(&header)) {
    return TRUE;
  }
  return FALSE;
  --- cut ---

The above check appears to be effective at a first glance, but it turns out that the GetDibBitsSize() function returns 0 if there are any problems detected in the headers, including invalid values in specific fields (biWidth, biHeight, ...), integer overflows etc. As a result, contrary to intuition, a malformed header will cause the above check to automatically pass, opening up the potential for bugs such as the one discussed in this report further in the bitmap handling code.

A poc.emf file is attached. It has been confirmed to crash both x86 and x64 builds of a test EMF viewer written in C++, and Microsoft Office 2013. It uses an EMR_PLGBLT record with a malformed, embedded bitmap and the following fields:

  biWidth    = 0x30000000
  biHeight   = 0x00000002
  biPlanes   = 0x0001
  biBitCount = 0x0008

The above combination of values will lead to GetDibBitsSize() returning 0, bytes_per_row_signed holding a negative value, and the first_row pointer addressing an invalid address lower than the actual buffer:

  --- cut ---
  (4144.1e30): Access violation - code c0000005 (first chance)
  First chance exceptions are reported before any exception handling.
  This exception may be expected and handled.
  eax=f046faf4 ebx=0000fdec ecx=00003e72 edx=00000000 esi=f046012c edi=07c7d624
  eip=75969b60 esp=0034ec88 ebp=0034ec90 iopl=0         nv up ei pl nz ac pe nc
  cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00210216
  msvcrt!memcpy+0x5a:
  75969b60 f3a5            rep movs dword ptr es:[edi],dword ptr [esi]
  0:000> kb
  ChildEBP RetAddr  Args to Child
  0034ec90 6b0a5bd3 07c7d624 f046012c 0000f9c8 msvcrt!memcpy+0x5a
  0034ecb0 6b09780d 07c7d1e0 f046012c 20000000 gdiplus!EmfPlusCommentStream::Write+0x9e
  0034f584 6b098180 07c7d1e0 00000002 08be4cd8 gdiplus!CopyOnWriteBitmap::GetData+0x3f3
  0034f59c 6b0a6029 07c7d1e0 00000002 08be4cd8 gdiplus!GpBitmap::GetData+0x1c
  0034f5b4 6b0a8a55 00000005 08be4cd8 00000000 gdiplus!MetafileRecorder::WriteObject+0x49
  0034f5d8 6b0a7814 07c7badc 0034f730 07c90d28 gdiplus!MetafileRecorder::RecordObject+0x57
  0034f720 6b0a453d 0034f7f8 08be4cd8 00000000 gdiplus!MetafileRecorder::RecordDrawImage+0x93
  0034f818 6b0a4838 08be4cd8 00000000 00000000 gdiplus!GpGraphics::DrawImage+0x1f0
  0034f87c 6b0c205d 08be4cd8 0034f918 00000003 gdiplus!GpGraphics::DrawImage+0x66
  0034f96c 6b0c7ed1 0000004f 07c94cb0 0000a67c gdiplus!CEmfPlusEnumState::PlgBlt+0x264
  0034f980 6b0986ca 0000004f 0000a67c 00460074 gdiplus!CEmfPlusEnumState::ProcessRecord+0xe7
  0034f99c 6b098862 0000004f 00000000 0000a67c gdiplus!GdipPlayMetafileRecordCallback+0x6c
  0034f9c4 773955ec 7021208b 05d56ff8 00460074 gdiplus!EnumEmfDownLevel+0x6e
  0034fa50 6b09aa36 7021208b 403581b3 6b0987f4 GDI32!bInternalPlayEMF+0x6a3
  0034fa88 6b09d199 7021208b 5e461f1b 0134faf4 gdiplus!MetafilePlayer::EnumerateEmfRecords+0x104
  0034fb30 6b09f455 00000000 5e461f1b 0034fc58 gdiplus!GpGraphics::EnumEmf+0x391
  0034fc90 6b0a4742 00000000 42901225 42901d0b gdiplus!GpMetafile::EnumerateForPlayback+0x7b9
  0034fd8c 6b0a47c6 07c75f28 00000000 00000000 gdiplus!GpGraphics::DrawImage+0x3f5
  0034fdf0 6b09c792 07c75f28 0034fe64 0034fe64 gdiplus!GpGraphics::DrawImage+0x51
  0034fe28 6b09ea7a 07c75f28 0034fe64 00000005 gdiplus!GpGraphics::DrawMetafileSplit+0x1f
  0034fe7c 6b09f4d5 07c71d28 0034ff08 00000000 gdiplus!GpMetafile::ConvertToEmfPlus+0x1c1
  0034fea0 6b074f71 07c71d28 0034ff08 00000005 gdiplus!GpMetafile::ConvertToEmfPlus+0x1d
  0034fedc 010c117e 07c71d28 07c75f28 0034ff08 gdiplus!GdipConvertToEmfPlus+0xbf
  ...
  --- cut ---

The above analysis was performed using the gdiplus.dll file found in C:\Windows\winsxs\x86_microsoft.windows.gdiplus_6595b64144ccf1df_1.1.7601.23407_none_5c02a2f5a011f9be\GdiPlus.dll on a fully patched Windows 7 64-bit operating system (md5sum c861ee277cd4e2d914740000161956ef).


Proof of Concept:
https://gitlab.com/exploit-database/exploitdb-bin-sploits/-/raw/main/bin-sploits/40256.zip