Microsoft Windows - 'gdi32.dll' Heap Buffer Overflow in ExtEscape() Triggerable via EMR_EXTESCAPE EMF Record (MS16-055)

EDB-ID:

39834




Platform:

Multiple

Date:

2016-05-17


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

Two of the escape codes supported by the public ExtEscape() API are POSTSCRIPT_IDENTIFY and POSTSCRIPT_INJECTION, which are only processed if the Device Context is associated with a printer. In the code responsible for handling the two escape codes, we can find the following constructs:

--- cut ---
.text:7DAE3E9F                 mov     ecx, [ebp+cjInput]
.text:7DAE3EA2                 lea     eax, [ecx+1Ah]
.text:7DAE3EA5                 add     ecx, 17h
.text:7DAE3EA8                 cmp     eax, ecx
.text:7DAE3EAA                 jb      loc_7DAD19AD
.text:7DAE3EB0                 and     eax, 0FFFFFFFCh
.text:7DAE3EB3                 mov     [ebp+Size], eax
.text:7DAE3EB9                 push    [ebp+Size]      ; Size
.text:7DAE3EBF                 mov     eax, large fs:18h
.text:7DAE3EC5                 mov     eax, [eax+30h]
.text:7DAE3EC8                 push    0               ; Flags
.text:7DAE3ECA                 push    dword ptr [eax+18h] ; HeapHandle
.text:7DAE3ECD                 call    ds:__imp__RtlAllocateHeap@12 ; RtlAllocateHeap(x,x,x)
...
.text:7DAE3EEF                 mov     eax, [ebp+cjInput]
.text:7DAE3EF2                 push    eax             ; Size
.text:7DAE3EF3                 mov     [esi+10h], eax
.text:7DAE3EF6                 lea     eax, [esi+14h]
.text:7DAE3EF9                 push    edi             ; Src
.text:7DAE3EFA                 push    eax             ; Dst
.text:7DAE3EFB                 call    _memcpy
--- cut ---

which can be translated to the following C-like pseudocode (assuming 32-bit wide types):

--- cut ---
if (cjInput + 26 > cjInput > 23) {
  buffer = Allocate((cjInput + 26) & ~4);
  ...
  memcpy(buffer + 20, lpInData, cjInput);
  ...
}
--- cut ---

From the code snippet shown above, it is clear that while it checks for a possible integer overflow between cjInput+23 and cjInput+26, it does not check if the "+23" part overflows the 32-bit type or not. As a consequence, if cjInput is set to anywhere between -23 and -1, a small heap-based buffer will be allocated (<30 bytes) and the function will try to copy ~4GB of data into it, leading to an obvious buffer overflow condition.

Under normal circumstances, the problem can only be triggered with an unusually large value of the cjInput parameter, which is unlikely to be used by a programmer. However, EMF (Enhanced Windows Metafile) files can act as remote proxy for DrawEscape() (via EMR_DRAWESCAPE) and ExtEscape() (via EMR_EXTESCAPE) calls. Interestingly, the corresponding MRESCAPE::bCheckRecord() record verification routine doesn't ensure that the cjInput value is valid (i.e. that enough input data is actually present in the record). As a result, a specially crafted EMF file can pass any controlled value as cjInput, thus potentially /lying/ to ExtEscape() about the number of input bytes. Lack of cjInput sanitization in MRESCAPE::bCheckRecord() is therefore believed to be the culprit of the bug (regardless of the integer overflow in ExtEscape()).

While this is just one example of what an arbitrary cjInput parameter passed to DrawEscape() / ExtEscape() may lead to, we suspect that it could also have other security implications, e.g. if any of the functions trust cjInput and read beyond the record buffer, and then use the data in such a way that it is possible to retrieve it back in the client (like Internet Explorer), then it could be used as a memory disclosure primitive.

As previously mentioned, the bug only reproduces when the destination HDC is associated with a printer. After a brief search I haven't found a vector to achieve this using existing Windows client applications supporting the EMF format (such as IE), so I've developed a short dedicated program to demonstrate the problem (poc.cc), which boils down to the following API calls:

--- cut ---
HDC hdc = CreateDC("WINSPOOL", "Fax", NULL, NULL);
HENHMETAFILE hemf = GetEnhMetaFile("poc.emf");

RECT rect = {0, 0, 100, 100};
PlayEnhMetaFile(hdc, hemf, &rect);
--- cut ---

Upon compiling it and starting with the enclosed poc.emf file in the current working directory, the expected crash is generated in memcpy():

--- cut ---
(353c.fa4): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=003300e7 ebx=004ffbe8 ecx=3ffffc39 edx=00000003 esi=00331000 edi=00500c1c
eip=779823a3 esp=0028fb34 ebp=0028fb3c iopl=0         nv up ei pl nz na po nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010202
ntdll!memcpy+0x33:
779823a3 f3a5            rep movs dword ptr es:[edi],dword ptr [esi]

0:000> kb
ChildEBP RetAddr  Args to Child              
0028fb3c 771a3f00 004ffd04 003300e8 ffffffff ntdll!memcpy+0x33
0028fd98 771c3fa9 bc21881a 00001015 ffffffff GDI32!ExtEscape+0x431
0028fdbc 77194e17 bc21881a 004f9588 00000004 GDI32!MRESCAPE::bPlay+0x32
0028fe34 7719ca93 bc21881a 004f9588 003300d8 GDI32!PlayEnhMetaFileRecord+0x2c5
0028febc 7719caf2 bc21881a 423d5f3a 00000000 GDI32!bInternalPlayEMF+0x66b
0028fed8 00401479 bc21881a b6467a1d 0028fef8 GDI32!PlayEnhMetaFile+0x32
WARNING: Stack unwind information not available. Following frames may be wrong.
0028ff18 004010fd 0028ff28 75949e34 7efde000 image00400000+0x1479
0028ff94 77999882 7efde000 4f2b9f18 00000000 image00400000+0x10fd
0028ffd4 77999855 00401280 7efde000 00000000 ntdll!__RtlUserThreadStart+0x70
0028ffec 00000000 00401280 7efde000 00000000 ntdll!_RtlUserThreadStart+0x1b
--- cut ---

The bug has been reproduced on a fully patched Windows 7 64-bit with a 32-bit POC program, but the 64-bit build of gdi32.dll also seems to be affected.


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