Reading Time: 9 minutes
Recently, a threat actor (TA) known as SpyBot posted a tool, on a Russian hacking forum, that can terminate any antivirus/Endpoint Detection & Response (EDR/XDR) software. IMHO, all the hype behind this announcement was utterly unjustified as it is just another instance of the well-known Bring Your Own Vulnerable Driver (BYOVD) attack technique: where a legitimate signed driver is dropped on victims’ machine and later used to disable security solutions and/or deliver additional payloads.
This technique requires administrative privileges and User Account Controls (UAC) acceptance in order to function properly, and it is not one of the stealthiest. On top of that, if the attacker is already a local admin on a machine, no security boundary can’t be crossed as that’s always a GAME OVER; the possibilities are unlimited from that attack perspective.
While I’ve seen a lot of material from the defensive community (they were fast on this one) about the detection mechanism, IOCs, prevention policies and intelligence, I feel some other, perhaps more interesting vulnerable code paths in this driver were not explored nor discussed.
Zemana has two lines of products:
zamguard64.sys
zam64.sys
Despite the different names, the drivers are the same (they also share the same hash); let’s dive into the Zemana’s (zam64.sys
) driver.
Note: if you like to explore the reverse-engineered driver or just follow along with this blog post, IDA’s DB, as well as the vulnerable driver, are present on GitHub 😊
zamguard64.sys
, zamguard32.sys
) v. <= 3.2.28 and Zemana AntiLogger (zam64.sys
, zam32.sys
) v. <= 2.74.204.664 are affected by an Incorrect Access Control vulnerability where IOCTL 0x8000204C
allow a non-privileged user to open a handle to any privileged process running on the machine. A non-privileged user can open a handle to the \.\ZemanaAntiMalware
device, register within the driver using IOCTL 0x80002010
and send the IOCTL mentioned above to get a handle to any privileged process. Attackers could exploit this issue by injecting arbitrary code in the context of the privileged process to achieve local privilege escalation up to NT AUTHORITY\SYSTEM
.zamguard64.sys
, zamguard32.sys
) v. <= 3.2.28 and Zemana AntiLogger (zam64.sys
, zam32.sys
) v. <= 2.74.204.664 are affected by an Incorrect Access Control vulnerability where IOCTLs 0x80002014
and 0x80002018
respectively grant unrestricted disk read/write capabilities. A non-privileged user can open a handle to the \.\ZemanaAntiMalware
device, register within the driver using IOCTL 0x80002010
and send the IOCTLs mentioned above to disclose sensitive files on the system or escalate privileges by overwriting the boot sector or critical code in the pagefile.The file is a C++ binary compiled for the x64 architecture. It does not contain symbols, but luckily it’s not obfuscated. The driver implements most of the AV functionalities and has some interesting capabilities.
The driver registers the following DeviceName: \Device\ZemanaAntiMalware
; it fails to set an appropriate security descriptor.
As can be seen, by the snippet below, it creates a default security descriptor; then it doesn’t use it, passing a NULL
pointer to the RtlSetDaclSecurityDescriptor()
function.
SecurityDescriptor = 0i64; [--Truncated--] default_SecurityDescriptor = FltBuildDefaultSecurityDescriptor(&SecurityDescriptor, 0x1F0001u); RtlSetDaclSecurityDescriptor(SecurityDescriptor, 1u, 0i64, 0); ObjectAttributes.RootDirectory = 0i64; ObjectAttributes.SecurityQualityOfService = 0i64; ObjectAttributes.ObjectName = &v5; ObjectAttributes.SecurityDescriptor = SecurityDescriptor; ObjectAttributes.Length = 48; ObjectAttributes.Attributes = 576; [--Truncated--]
As a result, every registered user on the machine, disregarding its privilege, is allowed to communicate with the driver.
As soon as I started to reverse engineer it, I noted that the function named DnsPrint_RpcZoneInfo()
is a “custom wrapper” for the DbgPrint()
function.
DnsPrint_RpcZoneInfo(7, (__int64)"Main.c", 231, "DriverEntry", 0xC0000001, "Can not allocate early boot pattern");
Without diving too much into the uninteresting internals of the function, we can see how the filename (Main.c
) and function name (DriverEntry
) are present in the debug messages.
I created an IDA script to recover and automatically rename most of the binary’s unnamed functions. Leveraging the information stored within the debug messages to quickly gain insight regarding binary’s capabilities and aiding future reverse engineering efforts.
Here is the complete list of all the driver’s exposed functionalities (via IOCTLs), as defined in the DeviceIoControlHandler()
routine:
0x80002004
: Create a file bypass filters0x80002008
: Check driver dispatch routines0x8000200c
: Fix driver dispatch routines0x80002010
: Register a process as authenticated0x80002014
: SCSI read0x80002018
: SCSI write0x8000201c
: Open physical drive0x80002020
: Get kernel image information0X80002024
: Dump miniport information0x80002028
: Fix critical kernel functions0x8000202c
: Delete file0x80002030
: Enumerate processes0x80002034
: Enumerate process modules0x80002038
: Create a registry key0x8000203c
: Delete a registry key0x80002044
: Save miniport fix0x80002048
: Terminate process0x8000204c
: Open process0x80002050
: Block unsafe DLL0x80002054
: Get driver protocol0x80002058
: Delete a value0x8000205c
: Query directory file0x80002060
: Enable ZAM Guard0x80002064
: Disable ZAM Guard0x80002080
: Send system information0x80002084
: Open a thread0x80002088
: Set authenticated process last beat0x8000208c
: Enabled Real-Time protection0x80002090
: Disabled Real-Time protection0x80002094
: Get Real-Time protection statusI’ve highlighted all the interesting functionalities (from an offensive standpoint) that an attacker can abuse to perform operations well beyond her privileges.
Immediately after some variables initialisation and sanity checks, the driver performs an interesting operation:
if ( IOCTL != 0x80002010 ) { if ( IOCTL + 0x7FFFDFAC > 0x10 || (v10 = 0x11001, !_bittest(&v10, IOCTL + 0x7FFFDFAC)) ) { if ( (unsigned int)check_allowlist_enabled() && !(unsigned int)ZmnAuthIsRegisteredProcessId(CurrentPID, 1) ) { return_status = STATUS_ACCESS_DENIED; Logger( 7, "Main.c", 482, "DeviceIoControlHandler", STATUS_ACCESS_DENIED, "ProcessID %d is not authorised to send IOCTLs ", CurrentPID); goto exit; } } }
Unless the received IOCTL code is 0x80002010
, the check_whitelist_enabled()
and ZmnAuthIsRegisteredProcessId()
functions are called.
The first function checks a global variable, determining if the allowlist is enabled. The ZmnAuthIsRegisteredProcessId()
function “consumes” the PID of the process issuing the IOCTL request as a parameter. It checks if the received PID is among the allowed ones in the allowlist data structure. If not, the function returns 0, and the “Access Denied” message is returned by the driver to the process issuing the IOCTL request.
Before the allowlist, there is a global data structure, checked by the check_whitelist_enabled()
function, maintaining the status of the AuthenticationManager
:
The allowlist array can contain up to 100 entries (each element’s size is of 0x980
bytes). Each entries in the allowlist (in blue) contain the following information:
Investigating the IOCTL code 0x80002010
, we’ll discover that the ZmnAuthRegisterProcess()
function is called.
The function takes as a parameter a PID of a running process. If it wasn’t already registered within the AuthenticationManager
, it retrieves the session ID
and the Image Name
of the process and adds an entry in the allowlist.
Here the driver fails to verify if the process issuing the IOCTL request is already present in the allowlist, granting any process to become “trusted” and capable of issuing privileged commands to the driver.
unsigned int pid = GetCurrentProcessId(); DeviceIoControl(hDevice, 0x80002010, &pid, sizeof(pid), NULL, 0, NULL, NULL)
Let’s explore the functionality abused by the Spybot TA to terminate AV and EDR processes. IOCTL code 0x80002048
calls the ZmnPhTerminateProcessById()
function, passing the PID of a running process as a parameter.
The function itself is nothing special; it checks that the process being terminated is not a critical process, then it retrieves a handle to the process and calls the ZwTerminateProcess()
API to kill the target process.
__int64 __fastcall ZmnPhTerminateProcessById(unsigned int PID, int a2) { NTSTATUS status; // ebx unsigned int pid; // [rsp+30h] [rbp-28h] int v11; // [rsp+60h] [rbp+8h] BYREF HANDLE ProcessHandle; // [rsp+78h] [rbp+20h] BYREF ProcessHandle = 0i64; v11 = 0; status = STATUS_UNSUCCESSFUL; Timeout.QuadPart = 0xFFFFFFFFFF676980ui64; if ( HlpIsCriticalSystemProcess(PID, &v11) && v11 ) { [--Truncated--] return status; } status = ZmnPhOpenProcess(&ProcessHandle, PID, 1u, 1); if ( status >= 0 ) { status = ZwTerminateProcess(ProcessHandle, 0); [--Truncated--]
All the hype and fuss regarding Spybot’s “groundbreaking” exploit lies in the above lines of code.
PoC is equally astonishing:
unsigned int pid = 1234; //target PID DeviceIoControl(hDevice, 0x80002048, &pid, sizeof(pid), NULL, 0, NULL, NULL))
IOCTLs 0x80002014
and 0x80002018
respectively expose unrestricted disk read/write capabilities. The expected buffer structure is somewhat complex and mimics Microsoft’s SCSI_REQUEST_BLOCK
structure.
Here is what it looks like:
typedef struct _SCSI_buffer { ULONG32 DiskNumber; UCHAR Padding; UCHAR PathId; UCHAR TargetId; UCHAR Lun; ULONG32 OffsetHigh; ULONG32 OffsetLow; ULONG32 Length; ULONG32 DataTransferLength; } SCSI_ACCESS
DiskNumber
: specify the disk to access via \\GLOBAL??\\PhysicalDrive[N]
symbolic link (where [N]
is the DiskNumber
). Symbolic link target: \Device\Harddisk[N]\DR[N]
e.g. PhysicalDrive0 -> \Device\Harddisk0\DR0
Padding
: nothing interesting here, just come padding.PathId
: the SCSI port or bus for the request.TargetId
: target controller or device on the bus.Lun
: the logical unit number of the deviceOffsetHigh
: must be set to 0 to pass the check in function ZmnIoScsiReadWriteDisk()
OffsetLow
: sector logical block addressing (LBA).Length
Count
: number of bytes to read/write (DataTransferLength
)Simply issuing the following IOCTL codes (after being registered as a trusted process) allow any process to affect the global status of the Zemana AntiMalware/AntiLogger Real-Time protection and ZAM Guard.
0x80002060
: Enable ZAM Guard0x80002064
: Disable ZAM Guard0x8000208c
: Enabled Real-Time protection0x80002090
: Disabled Real-Time protectionIOCTL 0x8000204C
retrieves a handle to any running process, including privileged ones. That’s perfect for achieving a complete privilege escalation from any low-privileged user to NT AUTHORITY\SYSTEM
. It is enough to:
0x80002010
.NT AUTHORITY\SYSTEM
).0x8000204C
IOCTL to retrieve a handle to the target process.CreateRemoteThread()
API, start the just allocated shellcode.The full LPE and Arbitrary SCSI read/write exploits can be found on GitHub.
If the TA had been more thorough with their analysis, they would have discovered that the Zemana driver was the perfect ransomware kit starter-pack, and they would have employed it rather than just sold it:
I sincerely hope Microsoft will add this driver to the blocklist, as it’s easy to weaponise for nastiest attacks.