Reverse Engineering Zemana AntiMalware
How to reverse it?
BYOVD (Bring Your Own Vulnerable Driver): a powerful technique to obtain kernel privileges.
BYOVD is an attack technique in which an attacker deploys a legitimate but vulnerable driver on a compromised machine, and then exploits this vulnerability to gain kernel-level privileges. With such access, an attacker can perform various malicious actions: hiding malware, dumping credentials, or disabling EDR (Endpoint Detection and Response) solutions.
For example, in February 2020, a RobbinHood ransomware campaign was observed, where the attacker used a driver signed by a motherboard manufacturer to disable EDRs.
Since then, other similar campaigns have been documented, including:
- a BlackByte campaign exploiting a graphics driver,
- a BYOVD campaign using a signed Windows driver,
- several incidents involving AuKill, a tool using an outdated Process Explorer driver to neutralize EDR protections.
Technical analysis: checking for the presence of ZwTerminateProcess with CFF Explorer
One of the functions used in this type of attack is ZwTerminateProcess. This function, present in ntoskrnl.exe
(the Windows kernel), allows forcefully terminating a process from kernel mode.
Before attempting any exploitation, we can verify if this function is accessible from a given driver using CFF Explorer, a static analysis tool for PE (Portable Executable) files. By inspecting the import table, we see that ZwTerminateProcess is referenced in ntoskrnl.exe
. This means that if a driver has sufficient privileges, it can directly call this function to terminate any process on the system, including those protected by mechanisms like Protected Process Light (PPL).
Now, we open the driver in IDA.
The goal is to identify the function associated with the IRP_MJ_DEVICE_CONTROL
request, which is called when a user-mode program sends an IOCTL command via the DeviceIoControl function.
Windows drivers use a DRIVER_OBJECT
structure containing a table of function pointers, called MajorFunction
. Each entry in this table corresponds to a standard operation ( e.g., IRP_MJ_CREATE
, IRP_MJ_CLOSE
, IRP_MJ_READ
, IRP_MJ_WRITE
, or in our case, IRP_MJ_DEVICE_CONTROL
.)
This is usually done in the DriverEntry
routine. We look for an instruction like :
DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = FunctionAddress;
Note: on Windows, Zw* functions are generally wrappers for system calls. When these functions are called from a kernel context, they can directly interact with internal OS structures.
- Index 13 corresponds to a function executed when a DeviceIoControl request is sent from user mode to unload the driver. In other words, this function allows triggering the unload of the driver via an IOCTL command.
- Index 28 corresponds to the real handler for IOCTL commands. This function manages the main logic for interactions between user mode and the driver. A security mechanism is implemented : the driver maintains a whitelist (allow list) of processes considered legitimate and authorized to interact with it.
However, by sending an IOCTL request containing a PID (process ID) as a parameter, it is possible to manually add your own process to this allow list. This operation allows bypassing the security measure by pretending to be a trusted process. Once added to the whitelist, the process can trigger various driver functionalities, including attempting to terminate a target process via an IOCTL command, which is precisely the case in our scenario.
In more detail, the sub_140009BE0
function consumes the PID of the process sending the IOCTL request. It checks whether this PID is present in the allow list data structure. If it is not, the function returns 0, meaning the request is rejected. Otherwise, it authorizes the execution of the requested operations.
We now focus on the ZwTerminateProcess
function, located in the Import Table section of the driver.
Analyzing the function that contains ZwTerminateProcess, we find the function sub_1400127B0
:
sub_1400127B0
is called with 4 arguments:
- a pointer to
ProcessHandle
(typeHANDLE*
), if the call to ZwOpenProcess succeeds, the driver will use this handle in further actions like ZwTerminateProcess. a1
, previously identified as the PID of the target process, this value is assigned to the UniqueProcess field inside the CLIENT_ID structure and tells ZwOpenProcess which process to open a handle for.- a flag, this specifies the type of access we want for the process handle. For example, if we want to terminate the process, we would pass
PROCESS_TERMINATE (0x0001)
. This value is passed to ZwOpenProcess. a4
, this is a control flag that influences how the OBJECT_ATTRIBUTES structure is populated
So sub_1400127B0
appears to be a wrapper function around ZwOpenProcess
, taking a PID as a parameter and returning a handle in ProcessHandle
.
if (status >= 0)
Follows the NTSTATUS convention: a positive return indicates success.
If the handle is successfully obtained, the driver directly calls ZwTerminateProcess
.
Then it checks the return value with two conditions :
- Either the error code is negative (standard failure)
- Or the code is
0xC000010A
, which corresponds toSTATUS_PROCESS_IS_TERMINATING
.
So, this function tries to:
- Open a handle to a process using its PID.
- Terminate the process via
ZwTerminateProcess
. - If that fails due to the process already exiting (STATUS_PROCESS_IS_TERMINATING), and if requested, it waits for the process to terminate cleanly using ZwWaitForSingleObject.
In practice, the process works as follows:
- From user-mode, an IOCTL is sent with a PID as a parameter.
- The driver checks if this PID is in the allow list.
- If the PID is authorized, the
sub_1400127B0
function is called to obtain a handle on the process. - Then,
ZwTerminateProcess
is invoked with this handle to try to terminate the process.
Once the exact IOCTL code used for the termination operation is identified, it can be used in our own program.
Lastly, we need to locate the symbolic link exposed by the driver to be able to open a handle from userland. This symbolic link is usually defined via IoCreateSymbolicLink
in the DriverEntry
.
Once all these elements are gathered:
Now we can design a small program that:
- Opens a handle to the driver
- Constructs an IOCTL request with the targeted PID (e.g.
MsMpEng.exe
) - Sends this request to the driver via
DeviceIoControl
The driver will accept the request and execute the call to ZwTerminateProcess
.
Source
- (Sophos, 2025), https://news.sophos.com/en-us/2024/03/04/itll-be-back-attackers-still-abusing-terminator-tool-and-variants/
- (Reddit, 2025), https://www.reddit.com/r/crowdstrike/comments/13wjrgn/20230531_situational_awareness_spyboy_defense/
- (Voidsec, 2025), https://voidsec.com/reverse-engineering-terminator-aka-zemana-antimalware-antilogger-driver/
- (CETP - Altered Security, 2025) @Saad Ahla