Sfilter的实现原理

简介

Windows 文件过滤驱动的原理主要基于操作系统的 I/O 管道,利用过滤驱动程序来监控、修改或控制文件系统的 I/O 操作。所有的 I/O 操作(如打开文件、读写文件、关闭文件等)都通过 IRP 来处理。当用户模式程序发出 I/O 请求时,操作系统生成相应的 IRP 并将其发送到适当的设备驱动程序。

回调函数 : 在 IRP 请求处理中,涉及的函数通常被称为 回调函数。这些回调函数是在驱动程序中定义的,用于处理特定类型的 IRP 请求。回调函数在特定的事件发生时被调用。这也就是为什么三环程序明明没有任何跳转就可以去执行注册的回调函数,就是发送了IRP请求,所以注册的回调函数会被执行

控制设备:DriverEntry中创建,接收自己客户端的IRP

过滤设备:绑定的时候创建,在设备栈上接收其他R3程序的IRP

驱动和设备的关系:

设备对象是驱动对象的具体实例:驱动对象定义驱动程序的属性和行为(例如处理各种IRP的函数),设备对象是驱动控制的具体设备,实际接收和处理IO请求。一个驱动对象可以管理多个设备对象,每个设备对象代表一个独立的硬件或虚拟设备。

创建过滤驱动代码

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
// 定义 DEVICE_EXTENSION 结构,用于存储设备特定信息
typedef struct _DEVICE_EXTENSION {
PDEVICE_OBJECT NextDevice; // 指向下层设备对象
} DEVICE_EXTENSION, *PDEVICE_EXTENSION;

// IRP分发例程
NTSTATUS DispatchPassThrough(PDEVICE_OBJECT DeviceObject, PIRP Irp) {
PDEVICE_EXTENSION deviceExtension = (PDEVICE_EXTENSION)DeviceObject->DeviceExtension;
IoSkipCurrentIrpStackLocation(Irp);
return IoCallDriver(deviceExtension->NextDevice, Irp);
}

// 卸载驱动
void UnloadDriver(PDRIVER_OBJECT DriverObject) {
PDEVICE_OBJECT deviceObject = DriverObject->DeviceObject;

PDEVICE_EXTENSION deviceExtension = (PDEVICE_EXTENSION)deviceObject->DeviceExtension;
if (deviceExtension->NextDevice)
{
IoDetachDevice(deviceExtension->NextDevice);
}
PDEVICE_OBJECT nextDevice = deviceObject->NextDevice;
IoDeleteDevice(deviceObject);
deviceObject = nextDevice;

KdPrint(("FilterDriver: Driver Unloaded.\n"));
}

// 驱动入口
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
UNICODE_STRING targetDevice = RTL_CONSTANT_STRING(L"\\Device\\TestDevice");
PDEVICE_OBJECT filterDevice = NULL;
PDEVICE_EXTENSION deviceExtension;
NTSTATUS status;

KdPrint(("FilterDriver: Driver Loaded.\n"));

// 创建过滤设备对象
status = IoCreateDevice(
DriverObject,
sizeof(DEVICE_EXTENSION), // 分配扩展
NULL,
FILE_DEVICE_UNKNOWN,
0,
FALSE,
&filterDevice
);

if (!NT_SUCCESS(status))
{
KdPrint(("FilterDriver: Failed to create filter device.\n"));
return status;
}

// 初始化设备扩展并设置为设备对象的扩展
deviceExtension = (PDEVICE_EXTENSION)filterDevice->DeviceExtension;

// 设置驱动的分发例程
for (int i = 0; i <= IRP_MJ_MAXIMUM_FUNCTION; i++)
{
DriverObject->MajorFunction[i] = DispatchPassThrough;
}

// 设置驱动卸载例程
DriverObject->DriverUnload = UnloadDriver;

// 获取目标设备并附加
status = IoAttachDevice(filterDevice, &targetDevice, &deviceExtension->NextDevice);
if (!NT_SUCCESS(status))
{
KdPrint(("FilterDriver: Failed to attach to target device.\n"));
IoDeleteDevice(filterDevice);
return status;
}

KdPrint(("FilterDriver: Attached to /Device/TestDevice successfully.\n"));
return STATUS_SUCCESS;
}

注意的是要创建一下设备扩展

1
2
3
4
typedef struct _DEVICE_EXTENSION 
{
PDEVICE_OBJECT NextDevice; // 指向下层设备对象
} DEVICE_EXTENSION, *PDEVICE_EXTENSION;

然后在这里可以获取目标设备对象指针

1
status = IoAttachDevice(filterDevice, &targetDevice, &deviceExtension->NextDevice);

然后在过滤驱动中就可以把这个IRP往下分发,NextDevice就来自DEVICE_EXTENSION

1
2
3
4
5
6
7
// IRP分发例程
NTSTATUS DispatchPassThrough(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
PDEVICE_EXTENSION deviceExtension = (PDEVICE_EC++XTENSION)DeviceObject->DeviceExtension;
IoSkipCurrentIrpStackLocation(Irp);
return IoCallDriver(deviceExtension->NextDevice, Irp);
}

分发IRP函数

过滤驱动有一个很重要的点,就是判断是来自自己设备的IRP,还是来自其他客户端的IRP,如果是自己的IRP,就自己处理,一定不能往下分发IRP,如果是拦截来的IRP,就可以选择是否往下发

如何判断是来自自己客户端的还是其他客户端的IRP呢?直接将传进来的PDEVICE_OBJECT DeviceObject和在DriverEntry创建的设备对象做对比(做成一个全局变量),如果相同,就是自己客户端的IRP,如果不是,那么就是别人客户端发来的IRP,说明需要拦截处理,并传递给下一个IRP栈链的下一个设备

Sfliter框架监控移动存储设备原理

比较知名的文件过滤框架有Filemon,Slifter,Minifilter

其中Filemon和Sfliter虽然已经不是主流,但是仍然可以学习

Filemon只监控固定磁盘,对于移动硬盘,U盘无法感知

Sfilter可以监控到移动硬盘,U盘

那么Sfilter是如何监控到移动硬盘,U盘这类的呢?毕竟这些卷设备是后来在任何时间随时插入设备的,并不是在一开始就可以绑定过滤驱动

这里一共涉及到两次创建过滤驱动设备对象

设备对象是用来处理IRP请求的具体实例,而具体的IRP请求处理函数是写在驱动里面的
也就是说,两次创建的设备对象的IRP处理函数是共享的,但是并不冲突,因为第一次创建的设备对象只用到IRP_MJ_FILE_SYSTEM_CONTROL,第二次创建的设备对象用使用其他的IRP请求函数

第一次创建过滤驱动设备对象:

第一次创建设备对象是为了获取到文件系统设备对象

首先注册一个 注册文件系统变更回调 IoRegisterFsRegistrationChange, 以便在文件系统驱动(如 NTFS)加载时,过滤驱动能够收到通知。

一旦我们进入这个回调,我们可以从这个回调的参数VOID MyFsNotification(PDEVICE_OBJECT DeviceObject, BOOLEAN FsActive) DeviceObject拿到对应的文件系统设备对象

此时,我们需要注册一个过滤驱动设备对象,用我们在DriverEntry保存的驱动对象,注册过滤驱动设备对象,让这个过滤驱动设备对象使用我们自己的驱动注册的IRP例程处理,在这里是IRP_MJ_FILE_SYSTEM_CONTROL,具体来说还要再细分是否是 IRP_MN_MOUNT_VOLUME

第二次创建过滤驱动设备对象:

值得注意的是,我们在IRP_MJ_FILE_SYSTEM_CONTROL处理函数里面,我们第二次创建的过滤驱动设备对象将要附加到卷设备对象上,所以在IRP_MJ_FILE_SYSTEM_CONTROL处理函数,我们的目标是获取到卷设备对象,但是一开始进去这个IRP处理拿到的DeviceObject是还没初始化好的卷设备对象,我们需要注册一个完成例程函数,并将IRP下发到底层的驱动设备进行对卷设备的初始化,然后在完成例程函数里面,拿到初始化好的卷设备对象,拿到之后,才是正式第二次创建过滤驱动设备对象进行附加到卷设备对象

一些实现的细节

关于FastIO

Fast I/O 是一种绕过常规 I/O 请求路径的快速数据访问机制,它允许文件系统直接完成一些常用操作(如读取、写入等),而不经过驱动的 IRP 调度路径。对于文件系统过滤驱动来说,这样可能导致无法截获或处理所有的 I/O 操作。

Sfliter会禁用FastIO,要求全部数据都走IRP请求,确保监控到对于文件的全部操作

Skip和Copy的区别

关于IoCopyCurrentIrpStackLocationToNextIoSkipCurrentIrpStackLocation(Irp)的区别

每一层驱动会有自己独立的 IRP 堆栈位置(称为 “IRP stack location”)。当一个过滤驱动拦截到 IRP 请求时,它实际上并不是“接管”了别人的 IRP,而是“共享”了这个 IRP。

当过滤驱动调用 IoSkipCurrentIrpStackLocation(Irp) 时,它跳过的是 当前堆栈位置 的作用,而 不是跳过下层驱动的堆栈位置。实际上,它只是告知系统当前驱动无需对堆栈位置做任何更改或检查,直接将 IRP 传递给下一层驱动即可。

当不需要设置完成例程函数的时候,调用IoSkipCurrentIrpStackLocation(Irp)就够了

但是如果设置了完成例程函数,那么就需要调用IoCopyCurrentIrpStackLocationToNext,将完成例程函数和当前IRP堆栈上下文保存在下一层驱动的IRP堆栈,记住,这里不是覆盖下一层的驱动IRP堆栈,而是把信息保存在下一层驱动IRP堆栈

完成例程本来就是设置在当前堆栈的下一层堆栈里,这相当于是一个规范,也可以用实际的IRP的返回来理解。在完成例程里,根据返回不同的状态值,IRP的控制流可能会发生相应的变化,这样,下层堆栈执行完成例程后,会将IRP的控制权交付给本层堆栈。从这个意义上讲,完成例程,只能放在下层堆栈,实际上,设计也是这样的。 2,拷贝当前堆栈的内容到下层堆栈,只是为了保证执行环境一样。

最下层的驱动如果设置了完成例程函数,它如何调用Iocopycurrentirpstacklocationtonext?

最下层驱动实际上不需要也不应该调用IoCopyCurrentIrpStackLocationToNext。这是因为在驱动程序的堆栈中,最下层驱动是直接与硬件交互的层级,它通常负责完成I/O请求,并且不需要将IRP传递给更下一层的驱动程序。

IoSkipCurrentIrpStackLocation(Irp):无设置完成例程

1
2
3
4
5
6
7
8
9
NTSTATUS status;
// 假设 CurrentDeviceObject 是当前驱动的设备对象
// 和 NextDeviceObject 是下一层驱动的设备对象

// 跳过当前的 IRP 堆栈位置
IoSkipCurrentIrpStackLocation(Irp);

// 调用下一层驱动的 IRP 处理
status = IoCallDriver(NextDeviceObject, Irp);

Iocopycurrentirpstacklocationtonext(Irp):设置了完成例程

1
2
3
4
5
6
7
8
9
10
11
12
IoCopyCurrentIrpStackLocationToNext(Irp);

// 设置完成例程
IoSetCompletionRoutine(Irp, CompletionRoutine, NULL, TRUE, TRUE, TRUE);

// 先将 IRP 发送给目标设备
PDEVICE_EXTENSION deviceExtension = (PDEVICE_EXTENSION)DeviceObject->DeviceExtension;

NTSTATUS status = IoCallDriver(deviceExtension->TargetDeviceObject, Irp);

// 返回状态
return status;

还有配合事件对象的:

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
// 完成例程函数
NTSTATUS MyCompletionRoutine(PDEVICE_OBJECT DeviceObject, PIRP Irp, PVOID Context)
{
UNREFERENCED_PARAMETER(DeviceObject);

// 获取传入的事件对象指针
PKEVENT event = (PKEVENT)Context;

// 唤醒等待的线程
KeSetEvent(event, IO_NO_INCREMENT, FALSE);

// 返回 STATUS_MORE_PROCESSING_REQUIRED 需要对 IRP 的进一步处理
return STATUS_MORE_PROCESSING_REQUIRED;
}


NTSTATUS MySynchronousIoRequest(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
KEVENT event;
NTSTATUS status;

// 初始化一个事件对象
KeInitializeEvent(&event, NotificationEvent, FALSE);

// 复制当前 IRP 堆栈位置到下一个位置
IoCopyCurrentIrpStackLocationToNext(Irp);

// 设置完成例程,并传入事件对象指针作为上下文
IoSetCompletionRoutine(Irp, MyCompletionRoutine, &event, TRUE, TRUE, TRUE);

// 将 IRP 发送到下层驱动
status = IoCallDriver(DeviceObject, Irp);

if (status == STATUS_PENDING)
{
// 如果 IRP 是异步处理的,等待事件被唤醒
KeWaitForSingleObject(&event, Executive, KernelMode, FALSE, NULL);

// 获取 IRP 完成后的状态
status = Irp->IoStatus.Status;
}

// 返回最终的操作状态
return status;
}

这里解释一下:

status = IoCallDriver(DeviceObject, Irp);当调用这个代码的时候,有两种情况

如果下层驱动同步完成,即直接处理并返回成功状态(如 STATUS_SUCCESS),那么 IoCallDriver 会立即返回,随后 MySynchronousIoRequest 函数继续执行接下来的代码。

如果下层驱动异步完成,即下层驱动返回 STATUS_PENDINGIoCallDriver 也会立即返回 STATUS_PENDING,然后 MySynchronousIoRequest 会调用 KeWaitForSingleObject 等待事件对象被信号化。

驱动和完成例程函数的返回状态

驱动需要返回状态,包括但不限于 STATUS_SUCCESS STATUS_PENDING STATUS_UNSUCCESSFUL STATUS_ACCESS_DENIEDSTATUS_BUFFER_TOO_SMALLSTATUS_INVALID_PARAMETER

完成例程函数也需要返回状态,一般就两种,STATUS_SUCCESSSTATUS_MORE_PROCESSING_REQUIRED

STATUS_MORE_PROCESSING_REQUIRED 确保了 IRP 不会被系统自动完成,从而允许调用代码继续访问和操作该 IRP。如果在完成例程中返回 STATUS_SUCCESS,那么 IRP 会被系统标记为已完成,资源可能被释放,从而导致调用代码无法再安全地访问该 IRP

所以返回STATUS_MORE_PROCESSING_REQUIRED意味着重新获得IRP的控制权,可以重修选择下发还是终结IRP