Last updated at Wed, 26 Jul 2017 13:50:28 GMT
If you weren't already aware, Rapid7 is offering a bounty for exploits that target a bunch of hand-selected, patched vulnerabilities. There are two lists to choose from, the Top 5 and the Top 25 . An exploit for an issue in the Top 5 list will receive a $500 bounty and one from the Top 25 list will fetch a $100 bounty. In addition to a monetary reward, a successful participant also gets to join the elite group of people that have contributed to Metasploit over the years. Their work will be immortally assimilated into the Framework, under BSD license, for all to see.
Despite the low value of the reward, I saw this as an opportunity to make a little extra cash and take a look a fairly challenging bug. I selected CVE-2011-0657 from the Top 5 due to my previous experience with the DNS protocol. After I claimed the bug, and checked that my name was safely in the table of players, I immediately began procrastinating.
Later that day, Jon Butler (@securitea) tweeted to the effect that he had been working on the bug. I replied, letting him know I was willing to collaborate and share the cash and glory. After discussing some logistics, Jon sent me his commented IDB of the old version of DNSAPI.dll from Windows 7 and a PoC based on Scapy. When I opened the IDB, Jon already had it pointed at the “_Dns_Ip4ReverseNameToAddress_A” function. It was well commented, but I quickly invoked the Hex-Rays decompiler and started analyzing the function. You can find the HTML output here. You probably want to keep it open in a new tab while you continue reading.
After doing some input validation, the string preceding “.in-addr.arpa” is copied into a local stack buffer on line 23. Inspecting the constraints showed that it isn't possible to cause a buffer overflow at this point.
I read on and noticed that it was processing the local stack buffer in reverse. It starts with “v_suffix” on line 26 and looks to see if it points at a ‘.' character. If the value ever points at the beginning of the buffer, processing is halted and the “v_return” value is written to the output “a_ret” pointer on line 49. This seems all well and good, or is it?
After looking for a few more minutes, I came to a realization. Here is an excerpt from the chat log with Jon.
(5:33:22 PM) jduck: hexrays shows two nested loops
(5:33:48 PM) jduck: while (1) { while (1) { --endptr; .... } ... --endptr; }
(5:33:55 PM) jduck: so it could double decrement
(5:34:11 PM) jduck: then the if == begin will never catch it
(5:34:39 PM) Jon Butler: hmm
(5:34:43 PM) jduck: 0.in-addr.arpa == trigger
(5:34:53 PM) Jon Butler: i'll test it
(5:35:45 PM) Jon Butler: no crash
A skilled auditor may notice my error here. I thought for sure that would crash the service, but it didn't. So I thought some more...
(5:35:54 PM) jduck: im running thru it in my head hehe
(5:36:02 PM) Jon Butler: yeah, its all good
(5:36:06 PM) Jon Butler: cant hurt to try
(5:36:13 PM) jduck: maybe .0.in-addr.arpa ?
(5:36:16 PM) Jon Butler: i was thinking lots of dots might do it as well
(5:36:20 PM) jduck: with a preceding period
Now at this point, I had some doubt that this was the bug at all and changed the subject of our conversation before Jon got a chance to test with this input. Silly me. Also, Jon was having some issues getting a debugger going attached to the service.
(5:24:47 PM) Jon Butler: also, protip: dont atatch windbg to the DNS client then wait while windbg tries to resolve microsoft.com to get symbols
Jon and I spent the rest of Tuesday evening and most of Wednesday evening flailing every which way except the right direction. Jon battled the symbol resolution problem while I went off on a tangent trying to trigger the bug in XP. By Wednesday evening (late night Wednesday for Jon), he had solved the symbol issue and began stepping through the code to gain a better understanding. We threw several ideas back and forth, but none of them lead to a crash. Eventually, time got the better of us and we called it a day.
NOTE: In order to work around the symbol issue, its possible to use the “symchk” executable to download the symbols for the “dnscache” service process before attaching to it. Once downloaded, set the _NT_SYMBOL_PATH variable to point to *ONLY* the local symbol directory, and voila.
Thursday, Jon came online and we continued reviewing the changed functions within the XP DNSAPI.dll. We were hoping that they might give us some insight that we didn't get before. On Jon's recommendation, I asked HD about the Windows XP vector. It went something like this:
19:54 <@jduck> will rapid7 give $500 for the local xp exploit?
19:54 <@hdm> jduck: sure if its a remote on windows 7
So I abandoned my efforts trying to trigger the bug via the LPC on XP, and diverted my attention back to Windows 7. I started by going back through the changes (still using XP binaries) one at a time, hoping to eliminate any that weren't security related. I found some changes related to locking, but it's unclear if that was related. After I went through all of these changes, and didn't find any glaring issues, I went back to diffing the Windows 7 binaries. I grabbed fresh copies of the DLLs, grabbed fresh copies of their symbols, created fresh IDBs and BinDiff'd them. To my surprise, there was were only four changed functions!
After getting my Windows 7 VM going, working around the symbol resolution issue, I started playing around sending inputs. I read the IPv6 version, ”_Dns_Ip6ReverseNameToAddress_A”,
and spent a couple of hours sending various inputs. Finally, I got a crash!
Unfortunately, it was only a 0xc00000fd exception. The human-readable description of this exception code, which irks one of my pet peeves, is often displayed as “Stack Overflow”. This is not the kind of crash you want to see when developing an exploit since this kind of crash is rarely exploitable. In this particular case, there is no exception handler, so it simply kills the process. The service is set to restart automatically twice, and reset counts after one day, but that isn't terribly helpful (try: sc qfailure dnscache).
Let's take another look at the decompiler output for the Ip4 version. Consider an input string of “.0.in-addr.arpa”. On the first iteration, a ‘0' will be found, so “v_suffix” will simply be decremented. On the second iteration, a ‘.' character is found on line 33. Next, it is overwritten with a NUL byte on line 38 and re-incremented. The “strtoul” function is called on line 40 and the return value from it is merged into ultimate return value on line 43. Since “v_suffix” does not point to the beginning of the buffer, it will be decremented on line 47. Note that after decrementing the pointer here, it will point at the beginning of the buffer (the first ‘.' character). The next statement that is executed is “--v_suffix;” on line 32. At this point, the pointer has escaped the bounds of the local buffer, and will never again have the chance to point to the beginning. If no ‘.' character is found before the beginning of the stack is reached, the 0xc00000fd exception will be raised when the guard page at the top of the stack is accessed.
Even though I managed to crash the process, I wasn't 100% sure that this was the reason Microsoft released an update. I didn't see anything interesting in the other changed functions. It seemed unlikely that anything good could come from this since there was no return address or function pointer on the stack before the function.
My first thought was to assume that I could control the data above the buffer on the stack. I hypothesized that I could do this via some deeper call stack that would occur in a preceding function call. Perhaps controlling this data would allow passing an input string that was longer than the function originally allowed. That would violate assumptions made by the programmers, and could lead to further corruption. So I created a WinDbg script that would put more valid-ish strings into the stack above (lower addresses) the buffer.
First I tested with the Ip4 variant, but it didn't yield anything fun. Then, I tried some things with the Ip6 version, which writes one byte at a time for each pair of nibbles encountered (ex. “a.b.”). It will write up to 16 bytes (the size of the destination buffer passed in, likely a struct in6_addr). I double-checked and concluded that it wasn't possible to cause a buffer overflow this way.
Although I didn't get an awesome crash from this experiment, I found that it was possible to prevent a crash from occurring this way. In one instance, an already-used return address on the stack contained a ‘.' character and prevented the crash. Being able to force this type of behavior is certainly advantageous, so I wrote this down for later.
Slightly disappointed with these results, I took a look at the Ip4 version's stack frame.
Just before the data in “v_buf”, we find the pointer “v_out_ptr”. After a brief look over, it seemed the best next-step would be to try to corrupt this pointer and cause the “v_result” value to be written somewhere unexpected. If the pointer happened to contain a ‘.' character, it would get replaced by a NUL byte. That is, if “v_out_ptr” was 0x00132e40, it would then become 0x00130040. It is possible for this to happen one of two ways. First, we would need to find some way to control the length of preceding function calls stack frames (ex. via “alloca”). This is often a long tedious path, for which not many good tools exist. The other option means crossing our fingers and hoping ASLR gives us a lucky value. I love rare cases where a mitigation contributes to exploitability!
NOTE: Although it's not visible in the decompiler output, the “v_out_ptr” is read from the stack immediately before writing the output value. This is one of the reasons why the decompiler can be misleading when doing exploit development.
Initially, I tried a few experiments using the Ip6 version. Unfortunately, the Ip6 version has far more strict handling of the return from “strtoul”. If a zero is returned (ex. a string like “z.”), or if the value is greater than 15, the loop terminates and nothing is written. So I went to check the situation using the Ip4 version. It is a bit more lenient in that it accepts zero return values, as you can see on line 41. However, if we fail that conditional the function returns zero and no write occurs. In fact, only way to get the Ip4 function to write to “v_out_ptr” is when “v_suffix” points at the start of the buffer (line 44). Ugh, strict constraints or impossibilities, not a good feeling.
Finally, on Saturday, I caved in and decided to reach out to Neel Mehta. As the original discoverer of the vulnerability, I figured he had a unique perspective on the issue. After exchanging several emails, Neel confirmed that I had nailed the root cause and offered several promising ideas for where to go next.
The first idea was to use TCP based LLMNR resolution. I looked at my Windows 7 SP0 machine and it wasn't listening on TCP port 5355. Bummer. Further googling led to an old TechNet arcticle that states “TCP-based LLMNR messages are not supported in Windows Vista”. Even if Windows 7 supports this feature, RFC4795 says TCP resolution is only used when the server has a reply that is too long for UDP. In this situation, similar to traditional DNS, the truncation (TC) bit is set in the flags section. Although it may be possible to construct a serious of queries and/or spoofed responses in order to elicit a truncated response, this was not investigated. This could be considered an exercise for the reader, should you be so inclined.
The second idea that Neel conveyed centered around the additional registers that are pushed onto the stack within the course of the functions executing. Looking at push instructions in the function shows that esi, edi, and ebx are pushed to the stack (in that order). These registers are later restored prior to returning to the calling function, “Dns_StringToDnsAddrEx”. After returning, the ebx register is checked against the value 0x17. The edi register is passed to one of the “RtlIpv6StringtoAddressEx” functions (ANSI or UNICODE). The esi register is passed as the destination argument to one of two ‘‘bzero(dst, 0x40)” calls. Unfortunately, none of this looked particularly promising.
The third idea that Neel proposed was to investigate the interaction between regular DNS queries and these functions. It turns out that the calling function is called from “DnsGetProxyInfoPrivate” which is exported along with “DnsGetProxyInformation”. We made no further effort to investigate this avenue. Perhaps another exercise for the reader :-)
With Saturday winding down, I decided to put together a quick trigger-fuzzer to test if random luck would lead to anything sexy. I ran it for an hour or so, but quickly got tired of looking at 0xc00000fd exception after 0xc00000fd exception. My hope had started to run out and my batteries needed recharging, so I crashed.
Sunday, Jon and I went back and forth discussing whether or not the issue was exploitable at all. We recapped our findings, but ultimately came to the conclusion that there was no way we could write a reliable exploit in time to qualify for the bounty. I had previously said I'd conduct a few more experiments in the debugger to see if corrupting the other stack-saved registers led to any nice crashes in the parent function. I set a break point in the processing loop of each of the vulnerable functions and fired off some trigger queries. Each time the breakpoint was hit, I wrote a ‘.' character to a byte offset in the saved register area and continuing execution. Out of all 16 bytes, only one led to a different crash. This was the saved esi value, which was subequently used in the bzero operation.
Similar to the “v_out_ptr” value, this value was a stack pointer that points to a the output area of “Dns_StringToDnsAddrEx”. If it happened to contain a ‘.' character, it would get modified to point to an address higher on the stack. This really isn't much help since we're already higher than any data that could affect code flow (return addreses, etc). This path seemed like a dead end. Having fulfilled my promise to try this experiment, I readied myself to admit defeat.
Prior to formally giving up on the bounty, Jon suggested I email Neel one last time to ask if he managed to obtain code execution from this vulnerability. Neel replied stating he hadn't. He decided to stop work on the bug once Microsoft agreed that the issue should be rated Critical. He reiterated that he believes it's possible to exploit this bug, but agreed that it was definitely more challenging than most bugs.
Although I want to believe that the bug is exploitable, I simply can't see a way. Jon and I have folded. I would love to say this bug is unequivocally not exploitable, but as we have seen in the past this probably isn't wise. Regardless, it seems to me, and I believe the facts show, that this bug is challenging enough that it's not possible to write a reliable exploit leveraging it in one week.
Despite my opinion, there are still some avenues left unexplored for those that are inclined to push forward on this bug. If you wish to continue where we left off or just play with bug, our technical notes are available and a DoS Metasploit module has been added to the tree. If you do push the analysis envelope forward on this bug, we hope you will contribute your findings back to the community. Good luck and happy exploiting to you all!