by Mitja Kolsek, the 0patch Team
Timeline
- 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.
Introduction
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.
- Obviously, getting a user to launch a .js file is not a convincing attack scenario (such file could already do anything within user's privileges). The good news for attackers is that this attack can be mounted via Internet Explorer, especially since even on 64-bit Windows, Internet Explorer rendering processes are 32-bit. On the upside, we were unable to get the exploit working from a web site because - at least on IE11 - the security setting "Access data sources across domains" is disabled in Internet and Intranet zone, which resulted in a JavaScript error. Launching a malicious poc.html from a local drive (or USB disk) does work, however, whereby the accompanying data source file can be in a shared folder and doesn't need to be delivered with poc.html. Nevertheless, the user then has to press the "Allow blocked content" button, which amounts to a considerable level of social engineering required to execute the attack via Internet Explorer.
- 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.)
Vulnerability Analysis
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:
eax=00002300 ebx=00000002 ecx=00834918 edx=00000000 esi=008360a8 edi=0024d64c
eip=6b881234 esp=0024d4c0 ebp=07ac427c iopl=0 nv up ei pl nz ac po nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010212
msrd3x40!DllRegisterServer+0x1b644:
6b881234 89b48174050000 mov dword ptr [ecx+eax*4+574h],esi ds:002b:0083da8c=????????
eip=6b881234 esp=0024d4c0 ebp=07ac427c iopl=0 nv up ei pl nz ac po nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010212
msrd3x40!DllRegisterServer+0x1b644:
6b881234 89b48174050000 mov dword ptr [ecx+eax*4+574h],esi ds:002b:0083da8c=????????
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.
The Micropatch
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:
; Patch for VULN-4112 in msrd3x40.dll version 4.0.9801.0 32bit
MODULE_PATH "..\AffectedModules\msrd3x40.dll_4.0.9801.0_32bit\msrd3x40.dll"
PATCH_ID 1000010
PATCH_FORMAT_VER 2
VULN_ID 4112
PLATFORM win32
patchlet_start
PATCHLET_ID 1
PATCHLET_TYPE 2
PATCHLET_OFFSET 0x4118C
code_start
cmp dword [esi+24h], 0x80 ; is the index lower than or equal to 0x80?
jbe pass ; if not, we let it pass
; (jbe means unsigned compare, we don't want to allow
; values like 0x80002300 to be considered negative)
mov dword [esi+24h], 0 ; otherwise we set it to 0
call PIT_ExploitBlocked ; and show the Exploit Blocked popup
pass:
code_end
patchlet_end
MODULE_PATH "..\AffectedModules\msrd3x40.dll_4.0.9801.0_32bit\msrd3x40.dll"
PATCH_ID 1000010
PATCH_FORMAT_VER 2
VULN_ID 4112
PLATFORM win32
patchlet_start
PATCHLET_ID 1
PATCHLET_TYPE 2
PATCHLET_OFFSET 0x4118C
code_start
cmp dword [esi+24h], 0x80 ; is the index lower than or equal to 0x80?
jbe pass ; if not, we let it pass
; (jbe means unsigned compare, we don't want to allow
; values like 0x80002300 to be considered negative)
mov dword [esi+24h], 0 ; otherwise we set it to 0
call PIT_ExploitBlocked ; and show the Exploit Blocked popup
pass:
code_end
patchlet_end
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 support@0patch.com).
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.
Cheers!
@mkolsek
@0patch