前段时间我某次逆向一个程序,发现有一个处于无敌的进程组件用火绒剑都杀不死,进程一直悬挂在进程列表,这意味着内核ZwTerminateProcess都无法杀死这个进程,当时立刻吓我一跳,第一时间我判断能做到这种事情的只有rootkit和bookit技术,难道我这种老司机还能翻车被恶意软件感染了,当时立刻重装系统,并且启用了secureboot,不过现在我找到了答案,让我们一探究竟。

复现场景

notmyfault64.exe可以非常适合复现这个场景,读者可以这里下载, 运行即可获得一个无法杀死的进程:

vmware_kQeKX7gqUN.jpg

点击huang,然后尝试使用火绒剑杀死这个进程:

vmware_vKASADXwHs.png

这确实很反常识,内核居然有杀不死的进程:

vmware_BUszlUVAjX.png

我快速逆向发现这个工具的驱动使用了KeWaitForSingleObject,这意味着线程一直在内核模式等待一个无法发生的事件,非常有意思。

进一步测试

这玩意有没有潜力用于恶意软件开发呢?我编写了如下驱动代码测试:

#include <ntddk.h>


//#define IOCTL_Device_Function CTL_CODE(DeviceType, Function, Method, Access)
#define IOCTL_Device_Usermode CTL_CODE(FILE_DEVICE_UNKNOWN, 0x008, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define IOCTL_Device_Usermode_APC CTL_CODE(FILE_DEVICE_UNKNOWN, 0x009, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define IOCTL_Device_Kernelmode CTL_CODE(FILE_DEVICE_UNKNOWN, 0x010, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define IOCTL_Device_Kernelmode_APC CTL_CODE(FILE_DEVICE_UNKNOWN, 0x020, METHOD_BUFFERED, FILE_ANY_ACCESS)

void unloadriver(PDRIVER_OBJECT driver_object) {
	UNREFERENCED_PARAMETER(driver_object);
}

NTSTATUS dispatch_function(
	_In_ struct _DEVICE_OBJECT* DeviceObject,
	_Inout_ struct _IRP* Irp
) {
	Irp->IoStatus.Status = STATUS_UNSUCCESSFUL;
	DbgPrint("Good to in the dispatch ruteam");
	UNREFERENCED_PARAMETER(DeviceObject);
	PIO_STACK_LOCATION stack  = IoGetCurrentIrpStackLocation(Irp);

	KEVENT event = { 0 };

	switch (stack->Parameters.DeviceIoControl.IoControlCode) {
		case IOCTL_Device_Usermode:
			KeInitializeEvent(&event, SynchronizationEvent, 0);
			KeWaitForSingleObject(&event, UserRequest, UserMode, 0, nullptr);
			break;
		case IOCTL_Device_Usermode_APC:
			KeInitializeEvent(&event, SynchronizationEvent, 0);
			KeWaitForSingleObject(&event, UserRequest, UserMode, 1, nullptr);
			break;
		case IOCTL_Device_Kernelmode:
			KeInitializeEvent(&event, SynchronizationEvent, 0);
			KeWaitForSingleObject(&event, UserRequest, KernelMode, 0, nullptr);
			break;
		case IOCTL_Device_Kernelmode_APC:
			KeInitializeEvent(&event, SynchronizationEvent, 0);
			KeWaitForSingleObject(&event, UserRequest, KernelMode, 1, nullptr);
			break;
		default:
			DbgPrint("no macth any ioctl");
			return -1;
	}

	IofCompleteRequest(Irp, IO_NO_INCREMENT);
	return STATUS_SUCCESS;
}

NTSTATUS default_dispatch(
	PDEVICE_OBJECT DeviceObject,
	PIRP Irp
)
{
	UNREFERENCED_PARAMETER(DeviceObject);

	Irp->IoStatus.Status = STATUS_SUCCESS;
	Irp->IoStatus.Information = 0;
	IoCompleteRequest(Irp, IO_NO_INCREMENT);

	return STATUS_SUCCESS;
}

extern "C"
NTSTATUS DriverEntry(PDRIVER_OBJECT object , PUNICODE_STRING Regstring) {

	UNREFERENCED_PARAMETER(Regstring);
	UNICODE_STRING devname;
	RtlInitUnicodeString(&devname, L"\\Device\\killmyifyoucan");

	PDEVICE_OBJECT deviceobject;

	IoCreateDevice(object, 0, &devname, FILE_DEVICE_UNKNOWN, 0, 1, &deviceobject);

	UNICODE_STRING symlink;
	RtlInitUnicodeString(&symlink,L"\\DosDevices\\killmeifyoucan");
	IoCreateSymbolicLink(&symlink,&devname);

	object->DriverUnload = unloadriver;
	object->MajorFunction[IRP_MJ_CREATE] = default_dispatch;
	object->MajorFunction[IRP_MJ_CLOSE] = default_dispatch;
	object->MajorFunction[IRP_MJ_DEVICE_CONTROL] = dispatch_function;

	return 0;
}

客户端:

#include <Windows.h>
#include <stdio.h>

#define IOCTL_Device_Usermode CTL_CODE(FILE_DEVICE_UNKNOWN, 0x008, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define IOCTL_Device_Usermode_APC CTL_CODE(FILE_DEVICE_UNKNOWN, 0x009, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define IOCTL_Device_Kernelmode CTL_CODE(FILE_DEVICE_UNKNOWN, 0x010, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define IOCTL_Device_Kernelmode_APC CTL_CODE(FILE_DEVICE_UNKNOWN, 0x020, METHOD_BUFFERED, FILE_ANY_ACCESS)


int main()
{
    HANDLE hDevice = CreateFileW(
        L"\\\\.\\killmeifyoucan",
        GENERIC_READ | GENERIC_WRITE,
        0,
        nullptr,
        OPEN_EXISTING,
        FILE_ATTRIBUTE_NORMAL,
        nullptr);

    if (hDevice == INVALID_HANDLE_VALUE)
    {
        printf("CreateFile failed: %lu\n", GetLastError());
        getchar();
        return 1;
    }
    printf("Device opened.\n");

    DWORD bytesReturned = 0;
    BOOL ret = DeviceIoControl(
        hDevice,
        IOCTL_Device_Kernelmode_APC,
        nullptr,
        0,
        nullptr,
        0,
        &bytesReturned,
        nullptr);

    printf("DeviceIoControl ret=%d gle=%lu\n",ret,GetLastError());
    

    return 0;
}

很遗憾,并没有,实际上被ZwTerminateProcess选中的进程,除了这个挂起的线程以外全被清空了,我们再也无法创建新的线程在这个进程里面。

vmware_0bYjIpwGiS.png

如果设置可以被打断那么这个线程立刻就会被kill掉,实际上毫无用处,不过有意思的是无论设置的KernelMode和usermode,都无法被终结,实际上我们对这个线程毫无办法,只能重启系统来恢复

底层原理

只有被驱动设置了取消历程的IRP可以被取消,进程管理器会等待与线程有关的I/O全部处理取消,才会删除这个线程。系统即使是在内核,系统无法强制取消IRP,IRP随时可能返回,可能损坏系统内存,系统如果直接把线程删除,IRP返回的数据将放到一个随机的地址,这将瞬间崩溃windows

非常有意思的情况,虽然对我毫无用处,但加深了我对线程、进程和I/O的理解:

  • 线程和I / O是强相关的,基本上直接绑定
  • 进程只是一个容器,被kill实际上系统需要先把线程全部释放掉才能清除进程
  • 线程有内核模式和用户模式,线程作为载体只是平等的执行用户态和内核态的代码,任何线程都能执行驱动的代码,驱动的代码有机会发生在任意上下文,任意线程任意进程。

结论

如果下次再遇到无法结束的进程,很可能只是驱动BUG导致的,我们大可放心,毕竟如果真的感染高级的恶意代码,很可能压根无法察觉;而且PG经过几轮迭代在现代windows 11已经非常激进,对抗PG的成本远远超过找一个固件漏洞载入bootkit,更别说还有hypergraud这种硬件监控pg的完整性。

让GPT给我生成的window 11娘的告警图,都2026年了,Ai抽卡生成的图片总是三四个手,笑:

Pasted image 20260704191708.png