by Mitja Kolsek, the 0patch Team
- Day 0 (05/08/18) - ZDI reports the vulnerability to Microsoft
- Day 135 (09/20/18) - ZDI publishes vulnerability details and PoC, no official fix available
- Day 136 (09/21/18) - Micropatches for all supported Windows versions created, distributed and applied on all users' computers
- Day 154 (10/09/18) - Microsoft issues an official fix
- Day 157 (10/12/18) - Official fix found to be incomplete, further micropatches created and distributed, Microsoft notified.
Last Thursday, Zero Day Initiative published details of an unpatched remotely exploitable vulnerability in all Windows versions (discovered by Lucas Leong) due to Microsoft missing their 120-day fixing window. We immediately tested ZDI's proof-of-concept and found the following:
- Jet is only supported in 32-bit, which means that a 64-bit application tricked into accessing a malformed data source file will not be exploitable. Indeed, double-clicking ZDI's poc.js on 64-bit Windows results in an error message; in order to launch poc.js on a 64bit machine one needs to use the 32-bit wscript.exe by launching c:\windows\SysWOW64\wscript.exe poc.js.
- A more realistic attack could probably be conceived using a malicious Office document referencing an external malformed Jet data source. We haven't investigated that, however, as our job is not to write exploits but micropatches. (Resourceful attackers will soon reveal their weaponization ideas anyway.)
As usually, we started our analysis from the closest observable point of failure and worked backward to the vulnerable code. Ideally, the "closest observable point of failure" is a process crash, and in this case, ZDI's PoC indeed causes a crash in wscript.exe due to an attempt to write past the allocated memory block. So their PoC was perfect for us. (Not surprisingly, it's easier for us to work with a crash case than a full blown calc-popping exploit.) Here's how the crash looks like in WinDbg, with Page Heap enabled and invalid memory access in function TblPage::CreateIndexes:
For anyone with some mileage in reverse engineering, the instruction causing the exception indicates that the code is accessing an indexed element in an array containing 4-byte elements, whereby the array starts at ecx+574h and the index is in eax (which contains 2300h). And it's clear that the index is beyond the array's limits.
From a patcher's perspective, this raises the following questions:
1) Where does this index come from? (And does it affect anything else, which would cause a micropatch to simply shift the problem further down the code and prompting another micropatch for that?)
2) Does the array have a fixed size? (Which would mean a simple hard-coded index validation micropatch could suffice.)
In tracking the index value 2300h, we found that eax was read from address esi+24h a few instructions back. We then used !heap to find out who allocated it and set an access breakpoint to see who wrote this value there. That turned out to be function Index::Restore, which is called a couple of instruction earlier in TblPage::CreateIndexes. What happens there is value 2300h is copied to the location we're watching with our access breakpoint from address pointed to by edx. And edx at that moment points to the memory-mapped copy of the malformed data source file.
So we found out that the malformed value 2300h comes directly from the PoC file group1, specifically at offset 1257h. Furthermore, looking around the code for additional uses of this user-provided index, we found one case immediately after the crash location, so whatever we'd do we'd need to make sure that this second use wouldn't also cause problems.
An obvious solution to fixing both uses of the malformed index was to check the index immediately after it gets copied from the memory-mapped file (i.e., right after the call to Index::Restore), and setting it to some safe value in case it was out of bounds. But in order to check the bounds we'd need to know the bounds.
Using !heap on ecx at the point of crash we inspected who allocated the memory block from which the PoC has escaped, and how large that block was. We found it to be a fixed-size block of 778h bytes, most likely a C++ object containing a fixed-size array of 4-byte elements. But how many elements? Let's calculate: the array begins at offset 574h in the memory block, and ends at most at offset 778h-1. The difference between the two means there is at most 204h bytes in the array, and dividing that by 4 (bytes) we get 81h elements, meaning that valid index values are between 00h and (at most) 80h.
Note that we say "at most 80h" - at this point we can't know whether the entire space between offset 574h and the end of the memory block is reserved for the array (although it surely seems so), we just calculated the bounds for preventing the offending instruction from escaping the memory block. And we'll be okay with this for now: we'll prevent an exploit from overwriting heap management data or other objects allocated on the heap, which will take most (if not all) of the leverage from the attacker.
So how should the micropatch look like? It would have to be injected right after the call to Index::Restore and check that the user-supplied value that was written to [esi+24h] doesn't exceed 80h. If it did exceed 80h, the patch would have to correct the value at [esi+24h], say by overwriting it with 0 (which is a valid index). And here is the source code of this micropatch:
We initially wrote this micropatch for Windows 7, and had a candidate ready just 7 hours after ZDI had published their PoC. After attempting to port it to other supported Windows versions, we noticed that almost all of them have the exact same version of msrd3x40.dll - which meant the same micropatch would apply to all these systems. The only Windows version with a different msrd3x40.dll was Windows 10: peculiarly, both DLLs had the same version and exactly the same size, but plenty of small differences between the two (including the link timestamp). The code was exactly the same and in the same place though (probably just a re-build), so we could actually use the exact same source code for the micropatch, just a different file hash.
These two micropatches for a published 0day were then issued less than 24 hours after the 0day was dropped, and distributed to our users' computers within 60 minutes, where they were automatically applied to any running process with vulnerable msrd3x40.dll loaded. Which nicely demonstrates the speed, simplicity and user-friendliness of micropatching when it comes to fixing vulnerabilities. In fact one of our goals with 0patch is to make vulnerability patching so fast that attackers won't even manage to develop a reliable exploit for a public vulnerability, much less launch a campaign with it, before the vulnerability is already patched on most users' computers. What a goal, huh?
How Can You Get These Micropatches, And What To Do Next?
These micropatches are completely free for everyone, and can be obtained by installing and registering 0patch Agent from https://0patch.com. They will prevent exploitation of this 0day as long as you have the latest Windows updates applied (we did not port them to older Windows updates, but if you have a need for that you can contact us at firstname.lastname@example.org).
Once Microsoft issues their official fix (likely on October Patch Tuesday), you will simply apply that update and these micropatches will automatically get obsoleted without you having to do anything else.