The Delicate Art of Remote Checks – A Glance Into MS15-034
April 15th, 2015
Recently, the research team posted a testing script for the MS15-034 vulnerability to pastebin for the greater community to test. We received some feedback about how exactly we figured out how to check, and remote checks in general.
By definition a remote check is a piece of code that allows the user to discern a vulnerability by actually exercising the code in a patch. These types of checks became popular during the era of worms, as a way to reliably determine exploitability in circumstances where a server’s banner was not enough information to discern a patched status. The exact process is somewhat difficult to capture as years of patch analysis at eEye (acquired by BeyondTrust in 2012) provides some measure of intuition.
One generally starts with a binary diff, and this circumstance is no different. Let’s examine the changes to HTTP.sys on Windows 7.
The notable takeaway from this is that every function name contains ‘range’. This is pretty exciting for us because it is reminiscent of a vulnerability in Apache HTTPd when handling the ‘Range’ header (see RFC2616 Section 14.35).
The conjecture that we are indeed dealing with the range header is easy enough to verify. If we take a look at the caller of HTTP!UlpParseRange in IDA we see the following:
Generally now, since our goal is to audit this vulnerability, we will set up a kernel debugger on an affected system. In our case it will be a Windows 7 SP1 VMWare target. We can prepare this target in the debugger for testing by setting breakpoints on all changed functions.
In this case, our breakpoints will trigger a callstack to be printed and will continue execution. Since we already had the Apache RangeDos trigger laying around, we simply ran that against the VM as it does test quite a few conditions as a bonus. We get the following:
So the standard Apache RangeDos script indeed hits one of the patched functions. Let’s take a closer look at HTTP!UlpParseRange:
The old code seems to be manipulating some kind of large integer.
And the new code looks like it’s using a call to HTTP!RtlULongLongAdd to check for integer overflow. Note, this is not the standard 3 parameter function, but a 5 parameter implementation in HTTP.sys. We can see if we get an error (like STATUS_INTEGER_OVERFLOW), we return 0xC000000D – STATUS_INVALID_PARAMETER.
After trimming a POC down to hit this function, it ends up being pretty simple.
Now, we can breakpoint the top of the changed block in the unpatched variant and get some feedback.
This looks great. EAX is 31337 (our upper range) and EDI is 1337 (our lower). In the old code, then it looks like if our lower range is 0, we end up subtracting nothing from the upper range. We then add one to it. Seems like if our upper range was massive, we could add one to it to flip it over to 0. This is more clear in the HexRays output
*(_QWORD *)v18 = __PAIR__(v22, v23) – __PAIR__(v21, v20) + 1;
We can see that our upper range is now huge. Let’s check out the addition.
We can see that EAX is predictably small now. Now that we have some indication of what the pre-patch block is doing, let’s look at the patch again.
Presumably here if we try the same shenanigans, we will cause an error to be returned. What is interesting is that this error is returned in many circumstances (as indicated by the degree of the node). It is our feeling that this may be key to checking for the patch. Let us revert our attention back to the pre-patched scenario.
One of many ways to get the unpatched function to return STATUS_INVALID_PARAMETER is to cause the highlighted check to fail below.
This is easy enough to do with a debugger. If we do this we get an interesting response:
Contrast this to the response from the same script if the STATUS_INVALID_PARAMETER return value is not forced:
This is the core change that allows us to check for patched vs unpatched.
tl;dr – the failure of the call to HTTP!RtlULongLongAdd in the patched function will cause STATUS_INVALID_PARAMETER to be returned from HTTP!UlpParseRange, which in turn will cause an “Invalid Header” response to be sent to the client. In the unpatched scenario, HTTP!UlpParseRange will return 0, causing a different error message to be sent back to the client “Requested Range Not Satisfiable”.