Arbitrary read information disclosure vulnerability in Microsoft Windows GDI+ EMR_STARTDOC record

CVE-2022-35837

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

TL;DR

An information disclosure vulnerability (CVE-2022-35837) exists when the Windows GDI+ component improperly discloses memory information.

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_STARTDOC metafile record may lead to several arbitrary memory read operations in the StartDocWImpl() function and e.g. could cause __wcsicmp() to read memory out-of-bounds. 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.

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

 10:000> g
 2(2044.2448): Access violation - code c0000005 (first chance)
 3First chance exceptions are reported before any exception handling.
 4This exception may be expected and handled.
 5eax=00000024 ebx=07a38f10 ecx=00000065 edx=00000000 esi=7630e3b0 edi=210000e6
 6eip=76a2e86a esp=009bee00 ebp=009bee08 iopl=0         nv up ei pl nz ac po nc
 7cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010212
 8ucrtbase!__ascii_wcsicmp+0x1b:
 976a2e86a 0fb717          movzx   edx,word ptr [edi]       ds:002b:210000e6=????
100:000> kn
11 # ChildEBP RetAddr
1200 009bee08 76a04393 ucrtbase!__ascii_wcsicmp+0x1b
1301 009bee44 76a10c65 ucrtbase!__crt_state_management::wrapped_invoke<...>+0x2b
1402 009bee50 76349f87 ucrtbase!_o__wcsicmp+0x15
1503 009bef88 75e05112 gdi32full!StartDocWImpl+0x31547
1604 009bef9c 76377c81 GDI32!StartDocW+0x22
1705 009befd4 76326969 gdi32full!MRSTARTDOC::bPlay+0x81
1806 009bf060 755a44ad gdi32full!PlayEnhMetaFileRecord+0x59
1907 009bf078 755a4309 gdiplus!EmfEnumState::PlayRecord+0x2d
2008 009bf094 755c7c5d gdiplus!EmfEnumState::ProcessRecord+0x49
2109 009bf0b8 755e812c gdiplus!GdipPlayMetafileRecordCallback+0xdd
220a 009bf0e4 76323945 gdiplus!EnumEmfDownLevel+0x6c
230b 009bf1b8 7631cb4c gdi32full!bInternalPlayEMF+0x855
240c 009bf1cc 75e0450b gdi32full!EnumEnhMetaFile+0x2c
250d 009bf1ec 755a85a2 GDI32!EnumEnhMetaFileStub+0x2b
260e 009bf240 755a5aac gdiplus!MetafilePlayer::EnumerateEmfRecords+0xc8
270f 009bf2f8 755aa07c gdiplus!GpGraphics::EnumEmf+0x464
2810 009bf468 755b7461 gdiplus!GpMetafile::EnumerateForPlayback+0x651
2911 009bf5c0 755c867f gdiplus!GpGraphics::DrawImage+0x541
3012 009bf62c 755e7f76 gdiplus!GpGraphics::DrawImage+0x61
3113 009bf690 755e7e47 gdiplus!GdipDrawImage+0x116
3214 009bf6b0 00c0136a gdiplus!GdipDrawImageI+0x37
33...

Crash analysis

Further analysis indicates that the culprit might be the StartDocWImpl() function that performs a string comparison on user controlled data. As the following pseudocode shows, the second parameter of the string comparison function __wcsicmp() can be directly controlled.

 1int StartDocWImpl(HDC hdc, _DOCINFOW *lpdi)
 2{
 3  ...
 4  if ( lpdi )
 5  {
 6    qmemcpy(&Dst, lpdi, 0x14);
 7    if ( Dst != 0xc )
 8    {
 9      Dst = 0x14;
10      Src = 0;
11      ...
12      if ( lpdi->cbSize == 0x14 &&
13           lpdi->lpszDatatype &&
14           lpdi->fwType <= 1 &&
15           __wcsicmp(L"emf", lpdi->lpszDatatype) ) // Crash here!
16      {
17        ...
18      }
19    ...
20    }
21  }
22  ...
23}

The following is the human readable form of the affected EMR_STARTDOC metafile record that triggers the read access violation:

1typedef struct {
2  DWORD iType = 0x0000006b;
3  DWORD	nSize = 0x000000cc;
4  DOCINFO lpdi;
5} EMR_STARTDOC;

The DOCINFO structure contains the input and output file names and other information used by the StartDoc() function, which starts a print job.

1typedef struct _DOCINFOW {
2  int     cbSize       = 0x00000014;
3  LPCWSTR lpszDocName  = 0x0000001e;
4  LPCWSTR lpszOutput   = 0x000000b4;
5  LPCWSTR lpszDatatype = 0x210000e6; // Address passed to __wcsicmp()
6  DWORD   fwType       = 0x00000001;
7} DOCINFOW, *LPDOCINFOW;

Root cause analysis

Further analysis revealed multiple arbitrary read primitives and that the ANSI version of the StartDoc() function is also affected. As the below pseudocode shows, the lpszDocName and lpszOutput members of the DOCINFO structure can be also abused to read arbitrary memory locations.

 1int StartDocA(HDC hdc, const DOCINFOA *lpdi)
 2{
 3  hdca = hdc;
 4  v11.cbSize = 20;
 5  v11.lpszDocName = 0;
 6  v11.lpszOutput = 0;
 7  v11.lpszDatatype = 0;
 8  v11.fwType = 0;
 9  if ( lpdi )
10  {
11    v2 = lpdi->lpszDocName;
12    if ( v2 ) // 1st crash here!
13    {
14      v3 = lpdi->lpszDocName;
15      do
16        v4 = *v3++;
17      while ( v4 );
18      v5 = v3 - (v2 + 1) + 1;
19      if ( v5 > 260 )
20        goto LABEL_11;
21      v11.lpszDocName = &UnicodeString;
22      RtlMultiByteToUnicodeN(&UnicodeString, 0x208u, 0, v2, v5);
23    }
24    v6 = lpdi->lpszOutput;
25    if ( v6 ) // 2nd crash here!
26    {
27      v7 = lpdi->lpszOutput;
28      do
29        v8 = *v7++;
30      while ( v8 );
31      v9 = v7 - (v6 + 1) + 1;
32      if ( v9 > 260 )
33      {
34LABEL_11:
35        GdiSetLastError(206);
36        return -1;
37      }
38      v11.lpszOutput = &v14;
39      RtlMultiByteToUnicodeN(&v14, 0x208u, 0, v6, v9);
40    }
41    ms_exc.registration.TryLevel = 0;
42    if ( lpdi->cbSize == 20 && lpdi->lpszDatatype && lpdi->fwType <= 1 ) {
43      if ( !__stricmp("emf", lpdi->lpszDatatype) ) // 3rd crash here!
44      {
45        v11.lpszDatatype = L"EMF";
46      } 
47      else
48      {
49        RtlMultiByteToUnicodeN(&v13, 0x208u, 0, lpdi->lpszDatatype, strlen(lpdi->lpszDatatype) + 1);
50        v11.lpszDatatype = &v13;
51      }
52    }
53    ms_exc.registration.TryLevel = -2;
54  }
55  return StartDocW(hdca, &v11);
56}

Proof of concept

The type of information that could be disclosed if an attacker successfully exploited these arbitrary read primitives might be memory layout. The following PoC program demonstrates that these arbitrary read primitives may provide an oracle that an attacker could exploit to scan the process memory address space and defeat memory protections such as ASLR.

Console output of the PoC

Console output of the PoC

The program will scan the process memory via the lpszDocName member of the DOCINFO structure starting from 0x00010000 until the StartDoc() function returns without errors, which means it hit the first valid base address. 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 gdi32full.dll might be also affected.

Patch analysis

Timeline

⬅️ 2022-05-16: Reported issue to MSRC.
➡️ 2022-05-19: MSRC opened case 71988.
➡️ 2022-05-27: MSRC confirmed the vulnerability.
➡️ 2022-09-13: Coordinated public release of advisory.

Bibliography