Windows Kernel ATMFD.DLL NamedEscape 0x250D pool corruption due to malformed .MMM metrics CVE-2018-0788 The OpenType ATMFD.DLL kernel-mode font driver on Windows has an undocumented "escape" interface, handled by the standard DrvEscape and DrvFontManagement functions implemented by the module. The interface is very similar to Buffered IOCTL in nature, and handles 13 different operation codes in the numerical range of 0x2502 to 0x2514. It is accessible to user-mode applications through an exported (but not documented) gdi32!NamedEscape function, which internally invokes the NtGdiExtEscape syscall. It is difficult to understand the functionality and design of the various escape codes based on the ATMFD.DLL image alone, as no debug symbols are provided for it on the Microsoft Symbol Server. However, such symbols are available for the ATMLIB.DLL user-mode client library which uses the interface, and more importantly for fontdrvhost.exe, the sandboxed user-mode font driver on Windows 10, which shares most of its code with ATMFD. These two sources of information are invaluable in reverse-engineering the NamedEscape code area. All symbols referenced in this report were originally found in fontdrvhost.pdb, but can also be applied to the corresponding code in ATMFD. The problem discussed in this report is caused by the fact that ATMFD lacks any sort of sanitization of the font metrics found in .MMM files (Multiple Master Metrics); and specifically the "first char" and "last char" fields. The equivalent .PFM files (Type-1 font metrics) have the 8-bit dfFirstChar and dfLastChar fields at offsets 95 and 96, and the internal ValidatePFMPointers() function verifies that dfLastChar >= dfFirstChar: --- cut --- .text:000217C8 mov esi, _pfmBase .text:000217CE lea ebx, [esi+75h] .text:000217D1 movzx edi, byte ptr [esi+dfLastChar] .text:000217D5 movzx eax, byte ptr [esi+dfFirstChar] .text:000217D9 sub edi, eax .text:000217DB inc edi .text:000217DC cmp edi, ecx .text:000217DE jle loc_218CB --- cut --- If dfLastChar is not greater or equal to dfFirstChar, the font loading is aborted. However, there is no adequate ValidateMMMPointers() function, and so an attacker can set these fields (at offsets 170 and 172) to arbitrary values in .MMM files, including scenarios where dfFirstChar > dfLastChar. One function that makes use of the saved first/last char indexes is MakePFM(), called by BDMakePFM(), which is the handler function for escape code 0x250D. The routine wrongly assumes that dfFirstChar may never be greater than dfLastChar, and so it uses the "dfLastChar - dfFirstChar" expression in a number of places to determine the number of characters in the font. The result is then used for calculating buffer sizes and offsets in memory. When dfFirstChar is greater than dfLastChar, the result of "dfLastChar - dfFirstChar" is negative, causing MakePFM() to miscalculate the required size of the output buffer and offsets within it. This could in turn lead to various types of pool corruption, such as continuous buffer overflow, buffer underflow, or even a non-continuous overflow with a controlled offset relative to the output buffer. The last mentioned primitive can be achieved by overwriting an offset stored within the generated PFM structures, that in theory should not be overwritten during the subsequent loops that use it, but it becomes possible with the negative offsets in play. Two examples of system bugchecks generated on Windows 7 32-bit are shown below. The first one is caused by an invalid memory write with a partially controlled index (the "cccc" part in 0x00cccc84), while the second one is due to an attempt to write to a memory region before the allocated output buffer (note the negative 0xfffffeea offset). It is advised to enable the Special Pools mechanism for win32k.sys to reliably reproduce the crashes. --- cut --- PAGE_FAULT_IN_NONPAGED_AREA (50) Invalid system memory was referenced. This cannot be protected by try-except. Typically the address is just plain bad or it is pointing at freed memory. Arguments: Arg1: ffd90cda, memory referenced. Arg2: 00000001, value 0 = read operation, 1 = write operation. Arg3: 94c52169, If non-zero, the instruction address which referenced the bad memory address. Arg4: 00000000, (reserved) [...] TRAP_FRAME: 9ba7c61c -- (.trap 0xffffffff9ba7c61c) ErrCode = 00000002 eax=00000120 ebx=00330000 ecx=ff0c4054 edx=00cccc84 esi=ff0c4054 edi=00000000 eip=94c52169 esp=9ba7c690 ebp=9ba7c754 iopl=0 nv up ei ng nz na po nc cs=0008 ss=0010 ds=0023 es=0023 fs=0030 gs=0000 efl=00010282 ATMFD+0x12169: 94c52169 88441102 mov byte ptr [ecx+edx+2],al ds:0023:ffd90cda=?? Resetting default scope LAST_CONTROL_TRANSFER: from 8292129b to 828a7aa8 STACK_TEXT: 9ba7c17c 8292129b 00000003 9cbc9f46 00000065 nt!RtlpBreakWithStatusInstruction 9ba7c1cc 82921d99 00000003 829919d0 0000001f nt!KiBugCheckDebugBreak+0x1c 9ba7c590 828ad8dd 00000050 ffd90cda 00000001 nt!KeBugCheck2+0x68b 9ba7c604 82886b18 00000001 ffd90cda 00000000 nt!MmAccessFault+0xbd 9ba7c604 94c52169 00000001 ffd90cda 00000000 nt!KiTrap0E+0xdc WARNING: Stack unwind information not available. Following frames may be wrong. 9ba7c754 94c42178 ff0c4042 00000006 ff0c4054 ATMFD+0x12169 9ba7c7a0 94c44738 0f6e8917 00000000 0000250d ATMFD+0x2178 9ba7c7d4 94c44878 0000250d 0f6e86cf 00000000 ATMFD+0x4738 9ba7c80c 94f34653 00000000 00000000 0000250d ATMFD+0x4878 9ba7c838 94f346b3 00000000 00000000 0000250d win32k!atmfdFontManagement+0x49 9ba7c85c 94e758ab 00000000 0000250d 001000d4 win32k!atmfdEscape+0x1e 9ba7c8a0 94f2fa4b 00000000 0000250d 001000d4 win32k!PDEVOBJ::Escape+0x39 9ba7caec 94e6ebb4 9ba7cb70 0000250d 001000d4 win32k!GreNamedEscape+0x14c 9ba7cc0c 82883936 00000000 0037fd20 00000009 win32k!NtGdiExtEscape+0x32c 9ba7cc0c 77196c74 00000000 0037fd20 00000009 nt!KiSystemServicePostCall 001ffacc 00000000 00000000 00000000 00000000 ntdll!KiFastSystemCallRet --- cut --- --- cut --- PAGE_FAULT_IN_NONPAGED_AREA (50) Invalid system memory was referenced. This cannot be protected by try-except. Typically the address is just plain bad or it is pointing at freed memory. Arguments: Arg1: ff056f3e, memory referenced. Arg2: 00000001, value 0 = read operation, 1 = write operation. Arg3: 94d32115, If non-zero, the instruction address which referenced the bad memory address. Arg4: 00000000, (reserved) [...] TRAP_FRAME: 891a461c -- (.trap 0xffffffff891a461c) ErrCode = 00000002 eax=fffffeea ebx=000000ff ecx=fea4cccc edx=ff057054 esi=ff057054 edi=00000000 eip=94d32115 esp=891a4690 ebp=891a4754 iopl=0 nv up ei pl nz ac po cy cs=0008 ss=0010 ds=0023 es=0023 fs=0030 gs=0000 efl=00010213 ATMFD+0x12115: 94d32115 66890c02 mov word ptr [edx+eax],cx ds:0023:ff056f3e=???? Resetting default scope LAST_CONTROL_TRANSFER: from 8292029b to 828a6aa8 STACK_TEXT: 891a417c 8292029b 00000003 1bf25859 00000065 nt!RtlpBreakWithStatusInstruction 891a41cc 82920d99 00000003 829909d0 0000001f nt!KiBugCheckDebugBreak+0x1c 891a4590 828ac8dd 00000050 ff056f3e 00000001 nt!KeBugCheck2+0x68b 891a4604 82885b18 00000001 ff056f3e 00000000 nt!MmAccessFault+0xbd 891a4604 94d32115 00000001 ff056f3e 00000000 nt!KiTrap0E+0xdc WARNING: Stack unwind information not available. Following frames may be wrong. 891a4754 94d22178 ff057042 00000006 ff057054 ATMFD+0x12115 891a47a0 94d24738 1dce23b6 00000000 0000250d ATMFD+0x2178 891a47d4 94d24878 0000250d 1dce2c6e 00000000 ATMFD+0x4738 891a480c 94f84653 00000000 00000000 0000250d ATMFD+0x4878 891a4838 94f846b3 00000000 00000000 0000250d win32k!atmfdFontManagement+0x49 891a485c 94ec58ab 00000000 0000250d 001000d4 win32k!atmfdEscape+0x1e 891a48a0 94f7fa4b 00000000 0000250d 001000d4 win32k!PDEVOBJ::Escape+0x39 891a4aec 94ebebb4 891a4b70 0000250d 001000d4 win32k!GreNamedEscape+0x14c 891a4c0c 82882936 00000000 003bfd20 00000009 win32k!NtGdiExtEscape+0x32c 891a4c0c 77456c74 00000000 003bfd20 00000009 nt!KiSystemServicePostCall 002ff890 00000000 00000000 00000000 00000000 ntdll!KiFastSystemCallRet --- cut --- Exploitation of the vulnerability could allow local attackers to execute arbitrary code in the context of ring-0 and thus elevate their privileges in the operating system. This bug is subject to a 90 day disclosure deadline. After 90 days elapse or a patch has been made broadly available, the bug report will become visible to the public. Found by: mjurczyk