Last week, I took some time to check the mitigation for the EoP vulnerability that I described in the last post. In this short blog post I’ll describe how Malwarebytes v22.214.171.12439 implemented a failed mititgation and how I exploited it again with the same attack vector.
As in the last post, I’m assuming Windows 7/10 x86 but x64 is also affected. The final PoC is here.
Let’s recap the issue. The Anti-Exploit driver of Malwarebytes v126.96.36.19911 doesn’t check the input buffer properly and uses a DWORD at the offset 0x290 to call _wcslwr.
Let x be the value of the DWORD and p be the address of the input buffer. Then the argument to _wcslwr is: p + 2 * x + 0x29A.
If we assume x = 0x40000000, the argument almost always points to an unmapped region in the userspace. In such cases, _wcslwr causes a recoverable exception and creates a crash file under C:\, which can be redirected to an arbitrary location.
My previous PoC dropped a bind shell DLL on C:\Windows\system32\msfte.dll to achieve EoP.
Let’s see how Malwarebytes patched the driver. They just added an extra layer of defence, i.e. sub_402144.
sub_402144 is such a small function. A decompiled version of it is shown below.
sub_402144 imposes a few more constraints on the input buffer. Let x, p be as defined above, y be a DWORD at the offset 0x294 of the buffer and L be the size of the buffer. Then the following three conditions must be satisfied.
- L = 2 * z + 0x29E
- WORD at (p + 2 * x + 0x298) = 0
- WORD at (p + 2 * z + 0x29A) = 0
where z = x + y. If we assume x = 0x40000000, y = 0x40000311 and L = 0x8c0, the first condition is satisfied. The last one is trivial as p + 2 * z + 0x29A points to the tail of the input buffer considering the first one.
As for the second condition, note that p + 2 * x + 0x298 points to a WORD in the userspace and p + 2 * x + 0x29A is the argument to _wcslwr. If we fill unmapped regions of the userspace with readonly copies of “\x00\x00\x41\x00” with something like heap spraying, then the second condition is satisfied.
Below is an example of the code that performs readonly heap spraying.
def heap_spray(): VirtualAlloc = windll.kernel32.VirtualAlloc VirtualProtect = windll.kernel32.VirtualProtect buff = "\x00\x00\x41\x00" * 0x4000 PAGE_READONLY = 0x2 PAGE_READWRITE = 0x4 MEM_COMMIT = 0x00001000 MEM_RESERVE = 0x00002000 oldProtect = c_int(0) for i in xrange(0x7800): addr = VirtualAlloc(0, 0x10000, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE) if not addr: break memmove(addr, buff, 0x10000) result = VirtualProtect(addr, 0x10000, PAGE_READONLY, byref(oldProtect)) #if not result: # print "failed to change memory protection" #print "0x%08x" % addr
With these settings, sub_402144 returns 1 without causing any exception or BSOD in most cases. Eventually _wcslwr is called, which causes an exception as it tries to convert a readonly upper case string \x41\x00 (“A”) to \x61\x00 (“a”).
This is the same situation in which I exploited the driver in the last post.
To summarize, the payload I used in my PoC looks like below before it’s obfuscated. The dotted parts are filled with nulls.