By Luka Treiber, 0patch Team.
I've been fishing around the web for my next target to patch when something got caught into my net. Then along came Mitja and asked:
M:What's the catch?
-Well, zero...
M:Oh, that's a shame...
-Yeah, but not if it's a zero-day
As you've probably noticed, the last Patch Tuesday didn't make it. Consequently a number of 0-days are getting published, with CVE-2017-0038 being the first one on the list. But don't worry, every cloud has a silver lining. I had some free time last week to look into the matter and as a result I can give you the very first 0patch for a 0-day.
CVE-2017-0038 is a bug in EMF image format parsing logic that does not adequately check image dimensions specified in the image file being parsed against the amount of pixels provided by that file. If image dimensions are large enough the parser is tricked into reading memory contents beyond the memory-mapped EMF file being parsed. An attacker could use this vulnerability to steal sensitive data that an application holds in memory or as an aid in other exploits when ASLR needs to be defeated.
I have to kindly thank Mateusz Jurczyk of Google Project Zero for a terse and accurate report that allowed me to quickly grasp what the bug was about and jump on to 0patching it.
Mateusz's PoC worked as advertised - by launching an instance of IE 11 and dropping poc.emf onto it, this is the result I got:
The image had to be zoomed in to the max in order to see the intimidating heap data leaking through the colorful pixels. And it did leak consistently: reloading the PoC resulted in a different image on each reload.
In order to see the pixel values I put poc.emf onto an HTML canvas and printed the pixel values with JavaScript. Diffing the results after consecutive PoC reloads showed that the only pixel that didn't change was the one in the lower left corner (if you look really closely at the screenshot above you will find a red smudge right there).
In other words:
FF 33 33 FF
This is consistent with the report (one only needs to look closely enough). So the only pixel that originates from the actual data provided by the EMF file is that one (represented by the 4 bytes) - everything else is heap data read from out of bounds of the image pixel data.
Now let's go on to patching. Before we do that we need to understand where the vulnerable code is. Luckily the next thing that the report conveniently reveals is the name of the vulnerable function MRSETDIBITSTODEVICE::bPlay. This is important because the PoC produces no crash or exception that could be caught by a debugger for an in-depth analysis. With that piece of info in hand we can attach WinDbg to iexplore.exe and set a breakpoint
bp MRSETDIBITSTODEVICE::bPlay
According to the report this is where all the trouble is. It says cbBitsSrc is not checked against "the expected number of bitmap bytes" - the number which is calculated from cxSrc and cySrc image dimensions.
Here we need a little help from MSDN:
- cxSrc
Width of the source rectangle, in logical units.
- cySrc
Height of the source rectangle, in logical units.
- cbBitsSrc
Size of source bitmap bits.
After feeding WinDbg with symbols for EMRSETDIBITSTODEVICE type (explanation follows later) the explanation from the report finally got clear.
When processing the PoC EMF file, the EMRSETDIBITSTODEVICE structure (detailed in the lower right pane of the screenshot above) passed to the MRSETDIBITSTODEVICE::Play function as the first argument (rcx register that gets stored to rbx, marked in blue) contains cbBitsSrc set to 4, which does not match the 0x10 * 0x10 image dimensions defined by cxSrc and cySrc (these amount to 256 pixels or 1024 bytes).
So to fix the flaw a check has to be added before any data gets stolen. What principally needs to be done is adding a check whether cbBitsSrc is smaller than cxSrc * cySrc * 4 before any image pixels get copied around. An appropriate spot seems to be right before the first series of checks that validate the EMRSETDIBITSTODEVICE attributes (see cmp - jg pairs in the above code screenshot that end with a jmp to function exit in case any of the checks fail). However, selecting the exact patch location is not just about where to logically fit the modifications that we want to apply. The way patching works is that a jump is made from original to patch code and then back. At the patch location in original code a jmp instruction is written (consuming 5 bytes of original instruction space) that redirects execution to a memory block designated for patch instructions. The original instructions that were replaced by the jmp instruction are relocated to the end of that patch code block followed by a final jmp instruction directing execution back to original code right after the patch location. So there are some additional requirements that need to be met:
- instructions at the patch location need to be relocatable (e.g., no short jumps);
- at the patch location there must be either a single instruction or several instructions that belong to the same execution flow (cross-references must not point anywhere in-between them).
It might seem desirable to select 000007fe`feeae3b8 as patch location at the first glance, but test rax,rax takes only 3 bytes so patching would also eat into the jne (which is not relocatable) because of the said 5-byte requirement. At the currently selected location, however, patching affects two 3-byte instructions - mov rsi,rax and test rax,rax - which are both relocatable.
Having selected the patch location we can finally write the actual patch. Below is the content of a .0pp patch file ready to be compiled with 0patch Builder that is included in 0patch Agent for Developers - usage of which we already discussed in a previous post.
At the beginning of the code_start section there is the cxSrc * cySrc * 4 calculation that gets stored in rcx. Both multiplications are followed by an overflow check so we don't end up with a product that seems within bounds but was produced by overlong factors. In case invalid image dimensions are detected, a call to a special function of our 0patch Agent is made in order to display an "Exploit Blocked" popup, and most importantly the rax register is set to 0 so that the following test rax,rax instruction can set the jump condition for the pink jne (resulting in the function exiting).
;target platform: Windows 7 x64
;
RUN_CMD C:\Program Files\Internet Explorer\iexplore.exe C:\0patch\Patches\ZP-gdi32\poc.emf
MODULE_PATH "C:\Windows\System32\gdi32.dll"
PATCH_ID 259
PATCH_FORMAT_VER 2
VULN_ID 2135
PLATFORM win64
patchlet_start
PATCHLET_ID 1
PATCHLET_TYPE 2
PATCHLET_OFFSET 0x0004e3b5
;NOTE: We checked that rcx is free to use.
; We don't care if rsi gets spoiled by block_it because in that case
; it will not be used by anyone any more
code_start
imul ecx, dword[rbx+28h], 04h ; cxSrc * 4 (each pixel is represented by 4 bytes)
jc block_it ; if overflow
imul ecx, dword[rbx+2ch] ; * cySrc
jc block_it ; if overflow
cmp ecx,dword[rbx+3ch] ; cbBitsSrc < cxSrc * cySrc * 4
jbe skip
block_it:
call PIT_ExploitBlocked ; show "Exploit Blocked" popup
xor rax,rax ; setting jump condition for the original jne after the patch
skip:
code_end
patchlet_end
;
RUN_CMD C:\Program Files\Internet Explorer\iexplore.exe C:\0patch\Patches\ZP-gdi32\poc.emf
MODULE_PATH "C:\Windows\System32\gdi32.dll"
PATCH_ID 259
PATCH_FORMAT_VER 2
VULN_ID 2135
PLATFORM win64
patchlet_start
PATCHLET_ID 1
PATCHLET_TYPE 2
PATCHLET_OFFSET 0x0004e3b5
;NOTE: We checked that rcx is free to use.
; We don't care if rsi gets spoiled by block_it because in that case
; it will not be used by anyone any more
code_start
imul ecx, dword[rbx+28h], 04h ; cxSrc * 4 (each pixel is represented by 4 bytes)
jc block_it ; if overflow
imul ecx, dword[rbx+2ch] ; * cySrc
jc block_it ; if overflow
cmp ecx,dword[rbx+3ch] ; cbBitsSrc < cxSrc * cySrc * 4
jbe skip
block_it:
call PIT_ExploitBlocked ; show "Exploit Blocked" popup
xor rax,rax ; setting jump condition for the original jne after the patch
skip:
code_end
patchlet_end
By the way, the EMRSETDIBITSTODEVICE structure member offsets relative to rbx were obtained by dumping type structure using WinDbg's dt command:
0:034> dt symbollib!EMRSETDIBITSTODEVICE
+0x000 emr : tagEMR
+0x008 rclBounds : _RECTL
+0x018 xDest : Int4B
+0x01c yDest : Int4B
+0x020 xSrc : Int4B
+0x024 ySrc : Int4B
+0x028 cxSrc : Int4B
+0x02c cySrc : Int4B
+0x030 offBmiSrc : Uint4B
+0x034 cbBmiSrc : Uint4B
+0x038 offBitsSrc : Uint4B
+0x03c cbBitsSrc : Uint4B
+0x040 iUsageSrc : Uint4B
+0x044 iStartScan : Uint4B
+0x048 cScans : Uint4B
+0x000 emr : tagEMR
+0x008 rclBounds : _RECTL
+0x018 xDest : Int4B
+0x01c yDest : Int4B
+0x020 xSrc : Int4B
+0x024 ySrc : Int4B
+0x028 cxSrc : Int4B
+0x02c cySrc : Int4B
+0x030 offBmiSrc : Uint4B
+0x034 cbBmiSrc : Uint4B
+0x038 offBitsSrc : Uint4B
+0x03c cbBitsSrc : Uint4B
+0x040 iUsageSrc : Uint4B
+0x044 iStartScan : Uint4B
+0x048 cScans : Uint4B
But since the type information is not available in WinDbg by just loading symbols from Microsoft's Symbol Server, the following workaround was needed. I compiled a bare-bone DLL project with only EMRSETDIBITSTODEVICE x; instance declaration as custom source. Then just before launching iexplore.exe I added the DLL to the AppInit_DLLs registry value so it got loaded into the process (thanks to this trick).
After compiling this .0pp file with 0patch Builder, the patch gets applied and instead of the rainbow image an empty image is displayed in the browser and an "Exploit Blocked" popup shows up.
Here is a video I captured that summarizes the described process.
So this is my first 0-day fixed. While not the most severe issue, I get shivers thinking that instead of the rainbow image, a malicious page could steal credentials to my online banking account or grab a photo of me after last night's party from my browser's memory.
If you have 0patch Agent installed, patches ZP-258 through ZP-264 should already be present on your machine. If not, you can download a free copy of 0patch Agent to protect yourself from exploits against the presented issue while waiting for Microsoft's official fix. Note that when Microsoft’s update fixes this issue, it will replace the vulnerable gdi32.dll and our patch will automatically stop getting applied as it is strictly tied to the vulnerable version of the DLL. We have deployed this patch for the following platforms: Windows 10 64bit, Windows 8.1 64bit, Windows 7 64bit and Windows 7 32bit.
If you would like to write patches like this yourself, do not hesitate to download our 0patch Agent for Developers and give it a try (we made the .0pp files available for download so you can build them yourself). We'll also happily accept any feedback.
Now onward to writing the next 0-day 0patch.