In this blog post, I’ll be describing two bugs I found inside the MalwareFox AntiMalware drivers (zam32.sys/zam64.sys) that allow a non-privileged process to “authenticate” itself with the driver and issue special IOCTLs leading to privilege escalation.
This process of registration or authentication is used by the driver to know which processes to trust when receiving a device control request. Normally, these processes should be the antimalware’s own processes.
A process that is authenticated by the driver can send special IOCTLs that cannot be sent by other non-authenticated processes. These special IOCTLs can be used to:
- Enable/Disable real-time protection
- Read/Write to raw disk
- Create full access user-mode process handles
Registered processes are stored in an array located in the data section of the driver. In zam64.sys, each element of the array has 0x980 bytes and the maximum number of elements is 100. An element contains information on the process such as its PID, its session id and the name of the image file name from the EPROCESS structure.
During the run-time of the anti-virus only a single process is registered with the driver, and that is MalwareFox’s own process ZAM.exe, which runs within session 1. There’s also a ZAM.exe process running as a Windows service but it doesn’t seem to be registered.
Figure 1 – MalwareFox’s entry in the registration array
So, by registering our process with the driver, we enable the god-mode capability to send special IOCTLs and basically make use of them to escalate privileges on the system.
I have found two ways to do so.
CVE-2018-6593: Register the process by connecting to the mini-filter communication port:
As shown in the figure below, a default security descriptor is built for the mini-filter communication port allowing access only to SYSTEM and the administrators. But right after that, RtlSetDaclSecurityDescriptor is called with a NULL DACL pointer. This leads to the DACL pointer, that was setup by FltBuildDefaultSecurityDescriptor, being overwritten with NULL. As a result, everyone has access to the object.
In addition, the maximum number of connections allowed to the port are 100 even though only a single connection appears to be needed by the Antimalware.
And here’s what it looks like under Windbg.
The interesting thing here is that when a process connects to the port, the driver automatically registers it as a trusted process in the array we saw above. The figure below displays the first and second entries in the array when our process (exploit.exe) is connected to the port.
Our process is now registered and can send special IOCTLs as it pleases.
It turns out, the developers zeroed to DACL pointer of the port’s security descriptor because their own process (ZAM.exe) doesn’t run with administrator privileges and turns at medium IL. This of course isn’t an excuse to disable all access checks and from everything we saw until now this is probably an anti-virus you don’t want on your machine; and this is not all!
CVE-2018-6606: Registering the process by sending IOCTL 0x80002010
It turns out there’s a straightforward way to register a process as trusted. Send IOCTL 0x80002010 with a process id of your choice and voilà the process with the PID you supplied is now registered and fully trusted by the driver!!!
What the driver fails to do here is check if the requestor process itself is a registered process. It does check for this when a process sends a special IOCTL, but it fails to do so if the process wants to register another process as trusted; rendering all other checks useless.
Thus, all we need to do to register our process is send IOCTL 0x80002010 with our process’s PID.
We can now send any special IOCTLs we want, and we’ll be using IOCTL 0x8000204C to elevate privileges.
MalwareFox seems to need user-mode full access handles to processes. And since we saw how its usermode process lacks the necessary privileges, it delegates this task of opening handles to its driver.
IOCTL 0x8000204C is a special IOCTL that must be sent by a registered process to the driver. The requestor simply provides a PID as an input and gets a full access handle as an output from kernel-mode; how cool is that for us ?
We use this opportunity to open a full access handle to winlogon.exe, inject a cmd.exe shellcode and then create a remote thread.
A video demonstrating the first bug (CVE-2018-6593):
Source code for CVE-2018-6593: https://goo.gl/yrxLfW
Source code for CVE-2018-6606: https://goo.gl/YtsYEo