Monday, January 27, 2020

Malformed PE Header Kernel Denial Of Service

This post is about a bug in the Windows Kernel that i recently discovered and reported to Microsoft. It lies in code responsible for parsing PE executables.

The "nt!MiCreateImageFileMap" function is prone to a vulnerability where the "e_lfanew" field of the "_IMAGE_DOS_HEADER" structure is not properly checked against the "SizeOfImage" field of the "_IMAGE_OPTIONAL_HEADER" structure. The bug is due to the kernel assuming two contradictive things, the first is that the "e_lfanew" field is an offset into the "on-disk" file and the second is that it is an offset into the "in-memory" executable image.

The function reads (I/O Read) the "_IMAGE_NT_HEADERS" structure as long as data is within the file size, not bearing in mind that PE executables support file overlays. So, while the function reads and parses PE headers "successfully!". Any subsequent call to the "nt!RtlImageNtHeader" or "nt!RtlImageNtHeaderEx" function on the PE Header of the executable image's memory will end up reading beyond memory boundaries (In other words, a file offset is being used to reference memory).

For example, when the "MiCreateImageFileMap" function returns, it calls the "nt!MiValidateImageHeader" function, which calls the "nt!MiMapImageInSystemCache" function to a map number of PTEs corresponding to "SizeOfImage" (total size of PE in memory) and then passes this memory to the "nt!SeValidateImageHeader" function which calls into the Code Integrity driver (CI.dll) functions CI!CiValidateImageHeader, CI!CipFixImageType, nt!RtlImageNtHeaderRtlImageNtHeader feteches e_lfanew again and accesses memory.

So, let's have a look at MiCreateImageFileMap

1) It first starts with a call to "FsRtlGetFileSize()"  function to get the file size of the input executable. If it is is above 0xFFFFFFFF bytes, the call fails. Now, we know the input executable must be below 4GB.

2) It then calls the "IoPageRead" function to read one page representing the input executable's DOS header and hopefully the NT file headers, from disk into memory, if it was not already cached in memory. Then, as expected, it checks to see whether the "e_magic" field is set to "MZ" (0x5A4D).

3) It then makes sure "e_lfanew" plus sizeof(nt!_IMAGE_NT_HEADERS64) does not overflow and does not exceed the input file size on disk.

4) If the data at "e_lfanew" does not fit into one memory page, then function proceeds with allocating two pages and reattempts reading again.

5) Then, it calculates the size and address of _IMAGE_NT_HEADERS structure in memory.

6) At this point, the "MiCreateImageFileMap" function proceeds with parsing the in-memory NT headers by calling  "MiVerifyImageHeader".

 7) The call to "MiCreateImageFileMap" will then proceed to building a   _CONTROL_AREA structure, where the section headers are also parsed via calling the "MiParseImageSectionHeaders" function.

Now let's move to another function, "MiValidateImageHeader". Its prototype looks like below.
It uses the control area structure created previously by "MiCreateImageFileMap" to map the executable header and sections into kernel system cache.

It then calls the "SeValidateImageHeader" function to validate the code integrity of the executable. SeValidateImageHeader itself calls into CI.dll, where "RtlImageNtHeaderEx" is finally called. See, Call stack in image below.

Now, let's move to, "RtlImageNtHeaderEx". Its prototype looks like below.

Its first argument is the executable image's base in memory. It simply finds the location of NT headers in memory by using the "e_lfanew" field.

So, if we modify the "e_lfanew" field to be located in the PE overlay i.e. beyond SizeOfImage, we can have a valid memory image where, at offset 0x3C, we have a value that represents an offset beyond the boundary of image memory in kernel system cache. Moreover, if we set the "IMAGE_DLLCHARACTERISTICS_FORCE_INTEGRITY" characteristic, we force the "RtlImageNtHeaderEx" to use this file offset as a memory offset, crashing the kernel.

Here is the crash call stack.

By the way, "MiCreateImageFileMap" improperly sanitizing "e_lfanew" against "SizeOfImage" also led to a memory corruption bug in the "nt!MiRelocateImage" function. I will explain that in the next blog post.

You can find POC here. Password: POCpoc@123

The bug was assigned CVE-2019-1391.

You can follow me @waleedassar