Building up our Skills and Speed for the Future WannaCry Attacks
By Mitja Kolsek, 0patch Team
Like many other stories of the past week, mine begins with this tweet.
I think @natashenka and I just discovered the worst Windows remote code exec in recent memory. This is crazy bad. Report on the way. 🔥🔥🔥— Tavis Ormandy (@taviso) May 6, 2017
Natalie Silvanovich and Tavis Ormandy of Google Project Zero found a pretty nasty bug in Microsoft Malware Protection Engine, allowing an attacker to execute arbitrary code as LocalSystem on any Windows computer running any Microsoft anti-malware product such as Security Essentials or Windows Defender by simply having that computer access a malicious file. Attack vectors were abundant, from emailing the file or sending it via any other channel like Skype or Messenger, to having it hosted on a malicious web site or uploading it to an IIS web server.
Unlike many other stories of the past week, mine is not about how Natalie and Tavis found this bug, how they reported it to Microsoft or how the fact that they found and reported it was made known to the public. Rather, it is about the bug itself, its root cause, and - of course - about writing a micropatch for it.
But first: why would we want to write a micropatch for a vulnerability that would quickly get automatically fixed on all Windows computers anyway? As you may know, Microsoft was super fast in fixing this bug and made an update available literally over the weekend. Furthermore, the Malware Protection Engine is implemented as a dynamic-load library mpengine.dll, and Microsoft designed their anti-malware products smartly enough to not require a computer restart - the old DLL is simply unloaded, and the new one loaded.
So why write a micropatch? Well, not every computer gets updated automatically: while automatic application of updates is configured by default, admins can change that if they want to control what gets applied when. And enterprise admins like to have such control, allowing them to test new code before deploying it to computers throughout their organization. Just imagine the updated mpengine.dll having a flaw that prevented users from accessing legitimate files.
Another reason for writing this micropatch was to learn, as we haven't patched a security product before - and one can expect to stumble upon something new here (and stumble I did, as you will see). The final reason was to teach, to share some knowledge with those of you who want to analyze vulnerabilities yourselves and learn how to write micropatches.
The first step in analyzing a vulnerability is to reproduce its exploitation. The Project Zero report provides a downloadable proof-of-concept file, which has a .zip extension, but is really an HTML-lookalike file that comprises a tiny exploit bit and a lot of random HTML content that makes sure the engine processes the file.
Reproducing on 64-bit Windows 8.1 was trivial - just downloading and saving the file was enough to make the Windows Defender service crash, instantly turning from this:
After the crash, the Application Event Log contained an Error event about this crash, revealing the crashing module being mpengine.dll, and the crash location being at offset 0x21745a. (You will find a different crash address in Google's report because they were working on a 32-bit computer.)
Note that I was using mpengine.dll version 1.1.13701.0, which is the last vulnerable version before the fixed 1.1.13704.0. It is always good to do your analysis on the last vulnerable version in order to minimize the difference with the fixed version - you will thank yourself when diffing these versions.
With the bug successfully reproduced, the path was clear towards analysis. Here, the Google report was a great start, as Natalie and Tavis have clearly gained substantial understanding of what goes on in the crash case. The most important detail for me was that it was a type confusion error, specifically with some function expecting a string object but getting a number object (which resulted in calling a string vtable function where there really was no vtable).
This was important because when a bug is a type confusion error, a typical fix is to add the missing check for the correct type. And such a fix is usually easy to recognize when observing the difference between vulnerable and fixed code.
Which brings us to IDA. The image below shows the function that crashed - the exact access violation location was the mov rax, [rcx] instruction (see the red box) at address 0x75A31745A, which is at offset 0x21745a from mpengine.dll's default base address.
So I went on to diff the two versions of mpengine.dll, the vulnerable 1.1.13701.0 and the patched 1.1.13704.0. There were 38440 matched functions, which, in scientific terms, is an awful lot. What I could do with these results was compare the above crashing function between the two versions. If I was lucky, the patch would be there and I could go home early.
Nope. Both functions are logically identical, which means that the flaw (and the patch) is somewhere higher on the call stack. At this point one could diff all functions that call this function, but there are about 50 of them - and if all of those turned out to be identical as well, such approach could turn into an exponential mission impossible. (Not to mention that IDA may not see all callers.)
Now about the call stack: you will notice that I haven't used a debugger up to this point, and the reason is that Windows Defender is a protected service and as such tries very hard to protect itself from tampering. You cannot attach a debugger to a protected process, even if you're a local administrator. And it's not easy to un-protect a protected service either: while its protected status is defined by the LaunchProtected registry value (in our case under HKLM\SYSTEM\CurrentControlSet\Services\WinDefend), you cannot change that value for Windows Defender while Windows Defender is running as it prevents you from "attacking" it.
Fortunately, we have a way to stop Windows Defender - by crashing it with the PoC. So what I did was crash Windows Defender, rename its LaunchProtected registry value, restarted the computer (the protected status of services is read only at system startup), then configured Windows Error Reporting to generate dump files for crashing processes. (I only created the LocalDumps key and the DumpFolder value containing "C:\dumps" in it.)
After crashing Windows Defender again, I got its mini dump file in C:\dumps, and it contained a full call stack for the access violation. I was only interested in locations from mpengine.dll:
The top one we already know - it's the access violation location in the crashing function that we diffed just moments ago. So I proceeded with the second address, FreeSigFiles+0x12046f, and located it in IDA. It was, as expected, after a call to the crashing function. I then took the address of the function containing that address, and viewed the diff with its patched version.
This is clearly the patch I was looking for. It was simple, it did exactly what I was expecting it to do, and it was in the code path of our crash.
My goal at this point was to create a micropatch that would inject the same patch logic into the vulnerable version of mpengine.dll. In a perfect world, I could use literally the same code that I found in the patched version, and inject it in the same place - but in this world a compiler likes to use different registers and different implementation of the same logic in two subsequent builds. So I had to re-implement the patch logic from the original patch.
Let's look at the vulnerable function in IDA.
The above image shows the vulnerable function and a good location for injecting our code. The location is selected so that it allows us to jump from our patchlet code directly to the function epilogue (the lowest block of code).
Here is the patch code for 64-bit mpengine.dll version 1.1.13701.0:
What this single-patchlet patch, inserted at the shown point in code, does is - just as the original patch - call GetTypeOf on the object (whose address is in register r9) and see if its type code is 4 (string). If it is, it continues execution of original code where it was injected . Otherwise, it sets the return code (register al) to 0 and jumps to function epilogue.
Note that in order to avoid any negative side effects, I had to (1) review the GetTypeOf function to see which registers it may taint and whether that could impact the code after our injected patch (it taints rdx and rcx, but rdx holds nothing valuable at our injection point), and then (2) store rcx on the stack before calling GetTypeOf function because rcx holds some value that is still being used after our injected patch.
I also wrote the same patch for the last vulnerable 32-bit version of mpengine.dll. If you have 0patch Agent installed, patches ZP-271 and ZP-272 should already be downloaded to your computer, waiting for any occurrence of the vulnerable mpengine.dll getting loaded.
The Irony of Protected Services
To restore the original system configuration, I turned Windows Defender back to a protected service, and... shoot, the patch stopped getting applied. It quickly became clear that we can't inject our loader into the protected Windows Defender because only binaries signed by Microsoft are allowed to get loaded. (It's a bit more complex than that but close enough.) This is Windows Defender protecting itself against local malware - even with admin privileges - trying to compromise it.
The irony is that a Windows anti-malware protection prevents our security product from fixing a vulnerability in Windows Defender, while the exploit for the same vulnerability can freely execute arbitrary code in Windows Defender. (Hmm, perhaps we should leverage this exploit to get our code running inside Windows Defender and thereby fix it.)
So while we're exploring options for extending our reach towards patching protected services, patches ZP-271 and ZP-272 for Malware Protection Engine will only get applied on Windows 7 and Windows Vista, which don't have protected services.
Experimenting with Micropatches for CVE-2017-0290
If you want to experiment with these micropatches, you'll need two things:
- A 32-bit or 64-bit Windows 7 computer running Windows Defender. While you could also do the testing on newer Windows versions, you would have to un-protect the Windows Defender service in order to proceed.
- Vulnerable mpengine.dll. If your Windows Defender doesn't happen to have this exact version (unlikely, due to automatic updates), you can get it here:
Then browse to C:\ProgramData\Microsoft\Windows Defender (folder permissions don't originally allow you to open it so Windows will ask you for elevation, after which it will add your account to the folder ACL). Open folder Definition Updates, and notice one or more subfolders with GUID-like names starting with curly braces. Open each of these folders to find the one containing mpengine.dll. Rename the existing mpengine.dll into something else, then save the vulnerable mpengine.dll there.
Start the Windows Defender service.
Download the proof-of-concept file and store it in some empty temporary folder.
Launch the Windows Defender console via the Control Panel, and Custom-Scan the above folder. Notice that Windows Defender service crashes.
Now install 0patch Agent on the computer. If you don't already have it, download a free copy and register it with your free 0patch account.
Finally, restart the Windows Defender service and re-scan the temporary folder. This time, you'll see an "Exploit Attempt Blocked" popup instead of Windows Defender crashing.
If you want to build our patches yourself, you can download their source code and build them using 0patch Agent for Developers.
While this vulnerability has already been automatically fixed on most computers, it turned out to be an interesting learning experience to micropatch it. I hope this post will help future micropatchers jump-start their research.
While I was writing this post, the world got pierced by the WannaCry ransomware worm exploiting a known vulnerability that had an official patch available for Windows operating systems which Microsoft supported at the time. Many hospitals and other critical infrastructure components were taken offline, partly also because they were stuck with unsupported OSs such as Windows XP. They have very rational and complex reasons for being on such outdated systems in 2017, and undoubtedly they will have similar reasons next year and the year after. One of our goals with 0patch is to provide protection for such end-of-support systems while users scramble to update them (and have the same problem almost immediately afterwards). Defending against modern attackers will require rapid response, and this exercise with CVE-2017-0290 - although likely of low value to users - was an example of building up skills and speed. The world will need a lot of 3rd-party patch developers though, so all existing and prospective security researchers are warmly welcome to join us.