Out-of-bounds read information disclosure vulnerability in Microsoft Windows GDI+ EMR_SETDIBITSTODEVICE record
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_SETDIBITSTODEVICE
1 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.
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 BITMAPINFOHEADER
2 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 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
- CVE-2022-21904 - Security Update Guide - Microsoft - Windows GDI+ Information Disclosure Vulnerability
- Windows GDI SetDIBitsToDevice - Information Disclosure
- ZDI-20-647: Microsoft Windows EMF EMR_SETDIBITSTODEVICE Out-Of-Bounds Read Information Disclosure Vulnerability
- Issue 992: Windows gdi32.dll heap-based out-of-bounds reads / memory disclosure in EMR_SETDIBITSTODEVICE and possibly other records
- Analyzing CVE-2017-0190: WMF Flaws Can Lead to Data Theft, Code Execution
- Issue 757: Windows gdi32.dll heap-based out-of-bounds reads / memory disclosure in multiple DIB-related EMF record handlers
- Secunia: Yet Another Windows GDI Story
- Time Travel Debugging - Sample App Walkthrough