2007-11-26 11:53:52 阅读(78)
WINDOWS文件过滤系统驱动开发,可用于硬盘还原,防病毒,文件安全防护,文件加密等诸多领域。而掌握核心层的理论及实践,对于成为一名优秀的开发人员不可或缺。
WINDOWS文件过滤系统驱动开发的两个经典例子,Filemon与SFilter,初学者在经过一定的理论积累后,对此两个例子代码的研究分析,会是步入驱动开发殿堂的重要一步,相信一定的理论积累以及贯穿剖析理解此两个例程后,就有能力开始进行文件过滤系统驱动开发的实际工作了。
对于SFilter例子的讲解,楚狂人的教程已经比较流行,而Filemon例子也许因框架结构相对明晰,易于剖析理解,无人贴出教程,本人在剖析Filemon的过程中积累的一些笔记资料,陆续贴出希望对初学者有所帮助,并通过和大家的交流而互相提高。
Filemon学习笔记 第一篇:
Filemon的大致架构为,在此驱动程序中,创建了两类设备对象。
一类设备对象用于和Filemon对应的exe程序通信,以接收用户输入信息,比如挂接或监控哪个分区,是否要挂接,是否要监控,监控何种操作等。此设备对象只创建了一个,在驱动程序的入口函数DriverEntry中。此类设备对象一般称为控制设备对象,并有名字,以方便应用层与其通信操作。
第二类设备对象用于挂接到所须监控的分区,比如c:,d:或e:,f:,以便拦截到引应用层对该分区所执行的读,写等操作。此类设备对象为安全起见,一般不予命名,可根据须监控多少分区而创建一个或多个。
驱动入口函数大致如下:
双击代码全选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
120
121
122
123
124
125
126
127
NTSTATUS
DriverEntry(
IN PDRIVER_OBJECT DriverObject,
IN PUNICODE_STRING RegistryPath
)
{
NTSTATUS ntStatus;
PDEVICE_OBJECT guiDevice;
WCHAR deviceNameBuffer[] = L"DeviceFilemon";
UNICODE_STRING deviceNameUnicodeString;
WCHAR deviceLinkBuffer[] = L"DosDevicesFilemon";
UNICODE_STRING deviceLinkUnicodeString;
ULONG i;
DbgPrint (("Filemon.SYS: entering DriverEntry
"));
FilemonDriver = DriverObject;
//
// Setup the device name
//
RtlInitUnicodeString (&deviceNameUnicodeString,
deviceNameBuffer );
//
// Create the device used for GUI communications
//此设备对象用来和用户交互信息
ntStatus = IoCreateDevice ( DriverObject,
sizeof(HOOK_EXTENSION),
&deviceNameUnicodeString,
FILE_DEVICE_FILEMON,
0,
TRUE,
&guiDevice );
//
// If successful, make a symbolic link that allows for the device
// object's access from Win32 programs
//
if(NT_SUCCESS(ntStatus)) {
//
// Mark this as our GUI device
//
((PHOOK_EXTENSION) guiDevice->DeviceExtension)->Type = GUIINTERFACE;
//
// Create a symbolic link that the GUI can specify to gain access
// to this driver/device
//
RtlInitUnicodeString (&deviceLinkUnicodeString,
deviceLinkBuffer );
ntStatus = IoCreateSymbolicLink (&deviceLinkUnicodeString,
&deviceNameUnicodeString );
if(!NT_SUCCESS(ntStatus)) {
DbgPrint (("Filemon.SYS: IoCreateSymbolicLink failed
"));
IoDeleteDevice( guiDevice );
return ntStatus;
}
//
// Create dispatch points for all routines that must be handled.
// All entry points are registered since we might filter a
// file system that processes all of them.
//
for( i = 0; i <= IRP_MJ_MAXIMUM_FUNCTION; i++ ) {
DriverObject->MajorFunction[i] = FilemonDispatch;
}
#if DBG
//
// Driver unload is only set if we are debugging Filemon. This is
// because unloading a filter is not really safe - threads could
// be in our fastio routines (or about to enter them), for example,
// and there is no way to tell. When debugging, we can risk the
// occasional unload crash as a trade-off for not having to
// reboot as often.
//
// DriverObject->DriverUnload = FilemonUnload;
#endif // DBG
//
// Set up the Fast I/O dispatch table
//
DriverObject->FastIoDispatch = &FastIOHook;
} else {
//
// If something went wrong, cleanup the device object and don't load
//
DbgPrint(("Filemon: Failed to create our device!
"));
return ntStatus;
}
//
// Initialize the name hash table
//
for(i = 0; i < NUMHASH; i++ ) HashTable[i] = NULL;
//
// Find the process name offset
//
ProcessNameOffset = FilemonGetProcessNameOffset();//为了得到当前进程名字
//
// Initialize the synchronization objects
//
#if DBG
KeInitializeSpinLock( &CountMutex );
#endif
ExInitializeFastMutex( &LogMutex );
ExInitializeResourceLite( &FilterResource );
ExInitializeResourceLite( &HashResource );
//
// Initialize a lookaside for file names
//
ExInitializeNPagedLookasideList( &FullPathLookaside, NULL, NULL,
0, MAXPATHLEN, 'mliF', 256 );
//
// Allocate the first output buffer
//
CurrentLog = ExAllocatePool( NonPagedPool, sizeof(*CurrentLog) );
if( !CurrentLog ) {
//
// Oops - we can't do anything without at least one buffer
//
IoDeleteSymbolicLink( &deviceLinkUnicodeString );
IoDeleteDevice( guiDevice );
return STATUS_INSUFFICIENT_RESOURCES;
}
//
// Set the buffer pointer to the start of the buffer just allocated
//
CurrentLog->Len = 0;
CurrentLog->Next = NULL;
NumLog = 1;
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
ntStatus = IoCreateDevice ( DriverObject,
sizeof(HOOK_EXTENSION),
&deviceNameUnicodeString,
FILE_DEVICE_FILEMON,
0,
TRUE,
&guiDevice );
//
// If successful, make a symbolic link that allows for the device
// object's access from Win32 programs
//
if(NT_SUCCESS(ntStatus)) {
//
// Mark this as our GUI device
//
((PHOOK_EXTENSION) guiDevice->DeviceExtension)->Type = GUIINTERFACE;
//
// Create a symbolic link that the GUI can specify to gain access
// to this driver/device
//
RtlInitUnicodeString (&deviceLinkUnicodeString,
deviceLinkBuffer );
ntStatus = IoCreateSymbolicLink (&deviceLinkUnicodeString,
&deviceNameUnicodeString );
if(!NT_SUCCESS(ntStatus)) {
DbgPrint (("Filemon.SYS: IoCreateSymbolicLink failed
"));
IoDeleteDevice( guiDevice );
return ntStatus;
}
上面代码完成的功能为创建了用于与应用层交互的控制设备对象,名字在参数&deviceNameUnicodeString,中。设备对象创建成功后又调用IoCreateSymbolicLink创建了一个符号连接,以便于应用层交互。 在入口点函数DriverEntry代码中,还有一处代码: ProcessNameOffset = FilemonGetProcessNameOffset();//为了得到当前进程名字。此函数体如下:双击代码全选1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ULONG
FilemonGetProcessNameOffset(
VOID
)
{
PEPROCESS curproc;
int i;
curproc = PsGetCurrentProcess();//调用PsGetCurrentProcess取得KPEB基址
//然后搜索KPEB,得到ProcessName相对KPEB的偏移量
// Scan for 12KB, hoping the KPEB never grows that big!
//
for( i = 0; i < 3*PAGE_SIZE; i++ ) {
if( !strncmp( SYSNAME, (PCHAR) curproc + i, strlen(SYSNAME) )) {
return i;
}
}
//
// Name not found - oh, well
//
return 0;
这个函数通过查找KPEB (Kernel Process Environment Block),取得进程名,GetProcessNameOffset主要是调用PsGetCurrentProcess取得KPEB基址,然后搜索KPEB,得到ProcessName相对KPEB的偏移量,存放在全局变量ProcessNameOffset中,得到此偏移量的作用是:无论当前进程为哪个,其名字在KPEB中的偏移量不变,所以都可以通过此偏移量得到。而在入口点函数DriverEntry执行时,当前进程必为系统进程,所以在此函数中方便地根据系统进程名SYSNAME(#define SYSNAME "System")得到此偏移量。
分发函数剖析:
在入口点函数中,通过代码:
双击代码全选1
2
3
for( i = 0; i <= IRP_MJ_MAXIMUM_FUNCTION; i++ ) {
DriverObject->MajorFunction[i] = FilemonDispatch;
}
简单地把各个分发例程设置成了FilemonDispatch; 然后我们追踪其函数体:双击代码全选1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
NTSTATUS
FilemonDispatch(
IN PDEVICE_OBJECT DeviceObject,
IN PIRP Irp
)
{
//
// Determine if its a request from the GUI to us, or one that is
// directed at a file system driver that we've hooked
//
if( ((PHOOK_EXTENSION) DeviceObject->DeviceExtension)->Type == GUIINTERFACE ) {
return FilemonDeviceRoutine( DeviceObject, Irp );
} else {
return FilemonHookRoutine( DeviceObject, Irp );
}
}
函数体先判断需要处理IRP包的设备对象的类型,看是属于控制设备对象,还是属于用于挂接并监控文件读写操作的过滤设备对象。如果是属于后者 则进入:FilemonHookRoutine( DeviceObject, Irp )
此函数是拦截文件操作的中心,在其中获得了被操作的文件名字,并且根据操作类型,在
switch( currentIrpStack->MajorFunction ) {
}
中针对不同的MajorFunction,打印出相关操作信息。
因此函数体太长 不再全部列出。
其函数体总体框架为:得到被操作的文件名字,打印相关操作信息,然后下发IRP到底层驱动。
在下发IRP到底层驱动处理前,本层驱动必须负责设置下层IO堆栈的内容。这样下一层驱动调用IoGetCurrentIrpStackLocation()时能得到相应的数据。
设置下层IO堆栈的内容,一般用两个函数来实现:
IoCopyCurrentIrpStackLocationToNext( Irp )
此函数一般用在本驱动设置了完成例程时调用,把本层IO _STACK_LOCATION 中的参数copy到下层,但与完成例程相关的参数信息例外。因为本驱动设置的完成例程只对本层驱动有效。
IoSkipCurrentIrpStackLocationToNext(Irp)
此函数的作用是:直接把本层驱动IO堆栈的内容设置为下层驱动IO堆栈指针的指向。因两层驱动IO堆栈的内容完全一致,省却copy过程。
而在Filemon的处理中,它用了一个特别的办法,没有调用此两个函数,FilemonHookRoutine函数体里面有三句代码:
双击代码全选1
2
3
4
PIO_STACK_LOCATION currentIrpStack = IoGetCurrentIrpStackLocation(Irp);
PIO_STACK_LOCATION nextIrpStack = IoGetNextIrpStackLocation(Irp);
*nextIrpStack = *currentIrpStack;//此步设置了下层驱动的IO_STACK_LOCATION
直接设置了下层驱动IO堆栈的值。
在FilemonHookRoutine函数里,用一个宏实现了复杂的获得拦截到的被操作文件的名字:双击代码全选1
2
3
if( FilterOn && hookExt->Hooked ) {
GETPATHNAME( createPath );
}
GETPATHNAME( createPath )宏展开为:双击代码全选1
2
3
4
5
6
7
#define GETPATHNAME(_IsCreate)
fullPathName = ExAllocateFromNPagedLookasideList( &FullPathLookaside );
if( fullPathName ) {
FilemonGetFullPath( _IsCreate, FileObject, hookExt, fullPathName );
} else {
fullPathName = InsufficientResources;
}
在函数:FilemonGetFullPath( _IsCreate, FileObject, hookExt, fullPathName )中实现了获得被操作的文件名字,此函数代码较多,判断条件复杂,理解起来比较麻烦,下面重点讲解。
对函数FilemonGetFullPath的理解关键在于理顺结构,
此函数的功能就是获得文件名字,获得文件名字一般在三种状态下:
一:在打开文件请求中,但在打开文件前。
二:在打开文件请求中,但在打开文件后,通过在本层驱动中设置完成例程。在完成例程中获得。
三:在过滤到读写等操作时。
而在此函数中,它包含了第一种和第三种方法,通过一些烦琐的条件判断,先判断出目前是处于什么状态中,然后根据不同状态采取不同方法。
先分析当在第一种条件下,此函数的处理方法,可以精炼为如下:
双击代码全选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
VOID
FilemonGetFullPath(
BOOLEAN createPath,
PFILE_OBJECT fileObject,
PHOOK_EXTENSION hookExt,
PCHAR fullPathName
)
{
ULONG pathLen, prefixLen, slashes;
PCHAR pathOffset, ptr;
BOOLEAN gotPath;
PFILE_OBJECT relatedFileObject;
ANSI_STRING fileName;
ANSI_STRING relatedName;
UNICODE_STRING fullUniName;
prefixLen = 2; // "C:"
if( !fileObject ) {
sprintf( fullPathName, "%C:", hookExt->LogicalDrive );
return;
}
//
// Initialize variables
//
fileName.Buffer = NULL;
relatedName.Buffer = NULL;
gotPath = FALSE;
if( !fileObject->FileName.Buffer)
{
sprintf( fullPathName, "%C:", hookExt->LogicalDrive);
return;
}else
DbgPrint("fileOjec->FileName:%s",fileObject->FileName);
if( !NT_SUCCESS( RtlUnicodeStringToAnsiString( &fileName, &fileObject->FileName, TRUE ))) {
sprintf( fullPathName, "%C: <Out of Memory>", hookExt->LogicalDrive );
return;
}
pathLen = fileName.Length + prefixLen;
relatedFileObject = fileObject->RelatedFileObject;
//
// Only look at related file object if this is a relative name
//
if( fileObject->FileName.Buffer[0] != L'' &&
relatedFileObject && relatedFileObject->FileName.Length ) {
DbgPrint("relatedFileObject filename : %s",relatedFileObject->FileName);
if( !NT_SUCCESS( RtlUnicodeStringToAnsiString( &relatedName, &relatedFileObject->FileName, TRUE ))) {
sprintf( fullPathName, "%C: <Out of Memory>", hookExt->LogicalDrive );
RtlFreeAnsiString( &fileName );
return;
}
pathLen += relatedName.Length+1;
}
if( fileObject->DeviceObject->DeviceType != FILE_DEVICE_NETWORK_FILE_SYSTEM ) {
sprintf( fullPathName, "%C:", hookExt->LogicalDrive );
}
if( pathLen >= MAXPATHLEN ) {
strcat( fullPathName, " <Name Too Long>" );
} else {
//
// Now we can build the path name
//
fullPathName[ pathLen ] = 0;
pathOffset = fullPathName + pathLen - fileName.Length;
memcpy( pathOffset, fileName.Buffer, fileName.Length + 1 );
if( fileObject->FileName.Buffer[0] != L'' &&
relatedFileObject && relatedFileObject->FileName.Length ) {
//
// Copy the component, adding a slash separator
//
*(pathOffset - 1) = '';
pathOffset -= relatedName.Length + 1;
memcpy( pathOffset, relatedName.Buffer, relatedName.Length );
//
// If we've got to slashes at the front zap one
//
if( pathLen > 3 && fullPathName[2] == '' && fullPathName[3] == '' ) {
strcpy( fullPathName + 2, fullPathName + 3 );
}
}
}
}
上面的精简后的函数代码为只考虑目前处于第一种情况,即打开文件请求中,但文件尚未打开时。
在此时,文件的名字信息存储在文件对象fileObject->FileName,与fileObject->RelatedFileObject->FileName, FileObject->FileName 是RelatedObject 的相对路径,通过对两者的解析组合出文件名字。
而在FilemonGetFullPath 函数体中的另一段代码:
双击代码全选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
FilemonGetFullPath
{
…………………..
…………………..
…………………..
if( !gotPath && !createPath ) {
fileNameInfo = (PFILE_NAME_INFORMATION) ExAllocatePool( NonPagedPool,
MAXPATHLEN*sizeof(WCHAR) );
if( fileNameInfo &&
FilemonQueryFile(hookExt->FileSystem, fileObject, FileNameInformation,
fileNameInfo, (MAXPATHLEN - prefixLen - 1)*sizeof(WCHAR) )) {
fullUniName.Length = (SHORT) fileNameInfo->FileNameLength;
fullUniName.Buffer = fileNameInfo->FileName;
if( NT_SUCCESS( RtlUnicodeStringToAnsiString( &fileName, &fullUniName, TRUE ))) {
fullPathName[ fileName.Length + prefixLen ] = 0;
if( hookExt->Type == NPFS ) {
strcpy( fullPathName, NAMED_PIPE_PREFIX );
} else if( hookExt->Type == MSFS ) {
strcpy( fullPathName, MAIL_SLOT_PREFIX );
} else if( fileObject->DeviceObject->DeviceType != FILE_DEVICE_NETWORK_FILE_SYSTEM ) {
sprintf( fullPathName, "%C:", hookExt->LogicalDrive );
} else {
//
// No prefix for network devices
//
}
memcpy( &fullPathName[prefixLen], fileName.Buffer, fileName.Length );
gotPath = TRUE;
RtlFreeAnsiString( &fileName );
fileName.Buffer = NULL;
}
}
if( fileNameInfo ) ExFreePool( fileNameInfo );
}
…………………
…………………
…………………
}
上面这段代码是处理另外一种情况,即是在其他读写请求中,自己根据拦截到的fileObject构建IRP,下发到底层,以此来查询文件名信息。整个过程还是易于理解的。
在理清这两种脉络后,再剖析此整个函数,就很容易理解整个函数代码了。
代码中对 MajorFunction == IRP_MJ_CREATE_NAMED_PIPE
MajorFunction == IRP_MJ_CREATE_MAILSLOT 的判断是为了辨别对拦截到的进程间的两种通信方式:命名管道与邮槽的处理。
周末来了,祝大家周末愉快。余下的整理后再帖,希望和大家多交流。
连城碧 QQ 276265852 MSN:haochao0000@hotmail.com
本文示例代码或素材下载