Back to Posts

Share this post

Reverse Engineering Terminator aka Zemana AntiMalware/AntiLogger Driver

Posted by: voidsec

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:

  • Zemana AntiMalware, zamguard64.sys
  • Zemana AntiLogger, 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 😊

Advisories and CVEs

  • CVE-2023-36205: Zemana AntiMalware (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.
  • CVE-2023-36204: Zemana AntiMalware (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.

Zam64.sys

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.

DeviceIoControlHandler

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 filters
  • 0x80002008: Check driver dispatch routines
  • 0x8000200c: Fix driver dispatch routines
  • 0x80002010: Register a process as authenticated
  • 0x80002014: SCSI read
  • 0x80002018: SCSI write
  • 0x8000201c: Open physical drive
  • 0x80002020: Get kernel image information
  • 0X80002024: Dump miniport information
  • 0x80002028: Fix critical kernel functions
  • 0x8000202c: Delete file
  • 0x80002030: Enumerate processes
  • 0x80002034: Enumerate process modules
  • 0x80002038: Create a registry key
  • 0x8000203c: Delete a registry key
  • 0x80002044: Save miniport fix
  • 0x80002048: Terminate process
  • 0x8000204c: Open process
  • 0x80002050: Block unsafe DLL
  • 0x80002054: Get driver protocol
  • 0x80002058: Delete a value
  • 0x8000205c: Query directory file
  • 0x80002060: Enable ZAM Guard
  • 0x80002064: Disable ZAM Guard
  • 0x80002080: Send system information
  • 0x80002084: Open a thread
  • 0x80002088: Set authenticated process last beat
  • 0x8000208c: Enabled Real-Time protection
  • 0x80002090: Disabled Real-Time protection
  • 0x80002094: Get Real-Time protection status

I’ve highlighted all the interesting functionalities (from an offensive standpoint) that an attacker can abuse to perform operations well beyond her privileges.

The Allowlist Mechanism

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:

  • Allowlist enabled/disabled flag (in red; 1 means enabled)
  • Allowlist entry count (in violet)

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:

  • Session ID (in yellow)
  • PID (in orange)
  • Image Name (in green)

Bypassing the Allowlist

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.

PoC
unsigned int pid = GetCurrentProcessId();
DeviceIoControl(hDevice, 0x80002010, &pid, sizeof(pid), NULL, 0, NULL, NULL)

Terminating a Process

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

PoC is equally astonishing:

unsigned int pid = 1234; //target PID
DeviceIoControl(hDevice, 0x80002048, &pid, sizeof(pid), NULL, 0, NULL, NULL))

Unrestricted SCSI Read/Write

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 device
  • OffsetHigh: 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)

Enable/Disable ZAM Guard & Real-Time Protection

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 Guard
  • 0x80002064: Disable ZAM Guard
  • 0x8000208c: Enabled Real-Time protection
  • 0x80002090: Disabled Real-Time protection

Local Privilege Escalation

IOCTL 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:

  1. Register the “exploit” process to the driver to be added to the allowlist via IOCTL 0x80002010.
  2. Retrieve the PID of any privileged process (running as NT AUTHORITY\SYSTEM).
  3. Create a buffer with the PID of the privileged process.
  4. Submit the created buffer via the 0x8000204C IOCTL to retrieve a handle to the target process.
  5. Allocate executable memory to the target process and load any arbitrary code (shellcode) that would then be executed in the privileged context.
  6. Using the CreateRemoteThread() API, start the just allocated shellcode.
  7. WIN WIN WIN

Video PoC and Exploit

The full LPE and Arbitrary SCSI read/write exploits can be found on GitHub.

Wrapping up

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:

  • It can terminate processes: which is perfect for killing AV/EDR/XDR and other security products.
  • It can grant privilege escalation capabilities: helpful for persistence.
  • It has arbitrary raw SCSI disk Read/Write access that can be leveraged to perform full disk encryption.

I sincerely hope Microsoft will add this driver to the blocklist, as it’s easy to weaponise for nastiest attacks.

References

Back to Posts