On October 25th, I found a bug in the Anti-Exploit driver of Malwarebytes v22.214.171.12411 that caused BSOD and finally exploited it to achieve an EoP from a standard user to SYSTEM. In short, it’s a combination of incomplete input validations and an insecure manner of accessing a file. In this blog post, I’ll walk through the process of how I exploited the bug.
Systems affected include Windows 7 SP1 x86/64 and Windows 10 x86/64. In this post I’m assuming Windows 7/10 x86 as I wrote a complete exploit only for those systems. However, I think you can easily port it to x64 systems if needed.
The first thing I did was to identify kernel mode drivers used by Malwarebytes and identify attack surfaces exposed. Using DriverView and WinObj, I found out the Anti-Exploit driver (mbae.sys) was accessible by a normal user through the device name “\\.\ESProtectionDriver”.
As can be seen from the picture above, there are quite a few drivers from Malwarebytes with fancy names, but I didn’t look into the other drivers as I lost my interests a bit after having exploited the first target.
The interesting part of the code in mbae.sys is the dispatch routine for the IRP major function code IRP_MJ_DEVICE_CONTROL. Identifying the routine is obvious, because it’s directly referenced from the DriverEntry. The following is the graph overview of the function sub_40338C that processes DeviceIoControl codes.
The blue boxes are where DeviceIoControl codes are compared with some constants. Each of the yellow boxes calls the handler for a DeviceIoControl code. The red boxes validates the input buffer (SystemBuffer).
From the blue boxes, 5 different DeviceIoControl codes can be identified: 0x22E000, 0x22E004, 0x22E008, 0x22E00C, 0x22E010. These codes means the driver uses the method of Buffered IO, though it’s not very relevant here.
The validation in the red boxes obviously hinders attacks mindlessly using generic IOCTL fuzzers. The input buffer needs a DWORD size field, a 0x14 bytes hash field, and obfuscated data field like below:
The obfuscated data is deobfuscated in the validation process (the bottom red box).
Then I looked at the handlers for DeviceIoControl codes. Some of the handlers has try/except blocks and in each of the except blocks it creates a crash file under C:\, meaning if I could make the exception handler called in a controlled manner (no BSOD), I could create an arbitrary file at an arbitrary location by redirecting the file write.
My initial attack was simply capturing and replaying legit DeviceIoControl requests made by the userland code of Malwarebytes.
WinDBG and IDA revealed the uses of some the DeviceIoControl codes: 0x22E000 is used for an InjectionRequest (whatever it means) and called when the Anti-Exploit protection is turned on. 0x22E004 and 0x22E008 are for an UninjectionRequest and an AllowUnloadRequest respectively, which are called when a user turns the Anti-Exploit protection off.
Luckily enough, I successfully got a BSOD by replaying an UninjectionRequest a few times. No fuzzing was needed.
Next I stepped into the root cause of the bug.
After some experiments, I learned that an exception could be caused by one of the two calls of _wcslwr as shown below. Here edi points to the first byte of the SystemBuffer.
_wcslwr can fail. For example, if the argument of _wcslwr points to an unmapped or read-only page or a sequence of bytes that’s not terminated by a double nulls ‘\x00\x00’, an exception happens. If the location where the exception happened was in the Kernel Space, it leads to a BSOD.
The strategy is to always make an exception happen in the User Space.
On the first call of _wcslwr, eax points to the offset 0x298 of the SystemBuffer. So if an exception happens here by accessing past the end of the SystemBuffer, it definitely leads to a BSOD. This can be avoided by putting a double nulls ‘\x00\x00’ at the offset 0x298.
The next call of _wcslwr is more interesting (I mean, controllable). The DWORD at the offset 0x290 of the SystemBuffer is doubled and added to edi (the address of SystemBuffer). In my exploit, I put 0x40000000 at the offset 0x290 so the argument of _wcslwr points to the User Space. The DWORD at the offset 0x290 may be any value other then 0x00000000.
The layout of the payload (before being obfuscated) is shown below.
Sometimes it happens that no exception occurs, but it does no harm.
I made some analysis on the deobfuscation scheme to send an obfuscated payload. Simply put, it’s just a triple XORs.
The first step of deobfuscation is as simple as XORing the data part with the hash part extended to the length of the data by repetitions. A hash of the data is computed at this point and compared with the hash in the SystemBuffer. If they match, each byte of the data is XORed with the first byte (LSB) of the thread ID and the offset (mod 0x100) of the byte in the data.
The steps of obfuscation is the same because of the commutativity of XORs.
The fact that the thread ID is involved makes the hash and the data parts less predictable, but actually we can make the entire payload static by using a thread whose LSB of the thread ID is constant, like always 0. Then the correct hash can be obtained from memory using a debugger. Reversing the (SHA-1 like) hashing algorithm wasn’t necessary.
The idea of exploitation is to redirect the location of the crash file using a symbolic link to another location like \GLOBAL??\C:\Windows\system32\msfte.dll, then overwrite it with a fake DLL. The DLL is supposed to be loaded by a process with SYSTEM priviledge.
This techninque is based on the fact that the device map can be changed per process like “chroot” thing in *nix OS.
The shortcoming of this method is that we cannot predictively trigger the load of msfte.dll. We could quickly find a better place to put a payload DLL and hijack DLL loads by running a Process Monitor as SYSTEM.
Below is a successfull run of the PoC.
- 2018.10.25: Vunlerability detected.
- 2018.10.30: PoC shared with Malwarebytes.
- 2019.02.02: Full disclosure.