对于用户的文件操作请求,Windows 用户层中对文件的各种操作映射到微过滤驱动中就转化为类型为Create,Read,Write 和Close 等的I/O 操作,因此只要对这些操作的内容进行过滤处理,即可达到透明加解密的目的。首先在IRP_MJ_CREATE 中查询目标文件是否是监控文件类型,创建一个StreamContext 并附着在文件对象上,并在StreamContext 中设置加解密标志。在正常的文件读写编辑过程中,在IRP_MJ_READ 后操作对读取的内容进行解密处理,在IRP_MJ_WRITE 预操作中对写入的内容进行加密处理,由此对文件读取实现了加解密处理。由于Windows 系统自带的缓存机制会导致文件明文泄露,因此还需要在操作中实时清理系统缓存。
Minifilter驱动程序的入口点仍然是DriverEntry函数,DriverEntry函数的原型是
NTSTATUS DriverEntry (
__in PDRIVER_OBJECT DriverObject,
__in PUNICODE_STRING RegistryPath
);
DriverObject在驱动加载时候由系统创建,RegisterPath是Minifilter在注册表中的键值,对于Minifilter而言,DriverEntry函数最主要的功能是实现过滤管理器的注册,即调用FltRegisterFilter函数,该函数原型为:
NTSTATUS FltRegisterFilter(
IN PDRIVER_OBJECT Driver,
IN CONST FLT_REGISTRATION *Registration,
OUT PFLT_FILTER *RetFilter
);
FltRegisterFilter 是本驱动对象的指针,Registration 是一个指向FLT_REGISTRATION 结构体的指针,它包含了Minifilter 处理I/O 操作的一系列回调例程的指针,RetFilter 是接受输出参数,即过滤器指针,这个指针被用来作为后续需要的所有回调例程的输入参数。FLT_REGISTRATION 为Filter Manager 提供了Minifilter 驱动需要的回调例程的信息,Minifilter 就是使用这些回调例程来过滤I/O 请求的。在本系统中,我们的Registration定义如下:
CONST FLT_REGISTRATION FilterRegistration = {
sizeof( FLT_REGISTRATION ), // Size
FLT_REGISTRATION_VERSION, // Version
0, // Flags
Context_Array, // Context
Dispatch_Array, // Operation callbacks
DriverExit, // FilterUnload
SetupInstance, // InstanceSetup
NULL, // InstanceQueryTeardown
NULL, // InstanceTeardownStart
NULL, // InstanceTeardownComplete
NULL, // GenerateFileName
NULL, // GenerateDestinationFileName
NULL // NormalizeNameComponent
};
Context_Array用来注册上下文管理函数,Dispatch_Array是注册处理IRP请求时候的回调函数,是整个MiniFilter的核心。
Context_Array定义:
CONST FLT_CONTEXT_REGISTRATION Context_Array[] = {
{ FLT_VOLUME_CONTEXT, 0, CleanupContext, sizeof(VOLUME_CONTEXT), CONTEXT_TAG },
{ FLT_STREAM_CONTEXT, 0, CleanupContext, STREAM_CONTEXT_SIZE, STREAM_CONTEXT_TAG },
{ FLT_CONTEXT_END }
};
CleanupContext函数是Context清理函数,用来释放上下文信息。
Dispatch_Array定义:
CONST FLT_OPERATION_REGISTRATION Dispatch_Array[] = {
{ IRP_MJ_CREATE, FLTFL_OPERATION_REGISTRATION_SKIP_PAGING_IO,
PreCreate, PostCreate },
{ IRP_MJ_CLEANUP, FLTFL_OPERATION_REGISTRATION_SKIP_PAGING_IO,
PreCleanup, NULL },
{ IRP_MJ_CLOSE, 0, PreClose, NULL },
{ IRP_MJ_QUERY_INFORMATION, 0, PreQueryInfo, PostQueryInfo },
{ IRP_MJ_SET_INFORMATION, 0, PreSetInfo, PostSetInfo },
{ IRP_MJ_DIRECTORY_CONTROL, 0, PreDirCtrl, PostDirCtrl },
{ IRP_MJ_READ, 0, PreRead, PostRead },
{ IRP_MJ_WRITE, 0, PreWrite, PostWrite },
{ IRP_MJ_OPERATION_END }
};
驱动程序设计完毕之后,安装到用户PC上的方法有函数注册与Inf文件注册这两大类方法,这里我们选择的是使用Inf文件在注册表中进行注册,这部分也说明在我们的文件透明加解密驱动在系统的设备栈中所处的位置。
;;;
;;; SYSTE
;;;
;;;
;;; Copyright (c) 2001, Microsoft Corporation
;;;
[Version]
signature = "$Windows NT$"
Class = "ActivityMonitor" ;This is determined by the work this filter driver does
ClassGuid = {b86dff51-a31e-4bac-b3cf-e8cfe75c9fc2} ;This value is determined by the Class
Provider = %Msft%
DriverVer = 06/16/2007,1.0.0.3
CatalogFile = SYSTE.cat
[DestinationDirs]
DefaultDestDir = 12
MiniFilter.DriverFiles = 12 ;%windir%\system32\drivers
;;
;; Default install sections
;;
[DefaultInstall]
OptionDesc = %ServiceDescription%
CopyFiles = MiniFilter.DriverFiles
[DefaultInstall.Services]
AddService = %ServiceName%,,MiniFilter.Service
;;
;; Default uninstall sections
;;
[DefaultUninstall]
DelFiles = MiniFilter.DriverFiles
[DefaultUninstall.Services]
DelService = SYSTE,0x200 ;Ensure service is stopped before deleting
;
; Services Section
;
[MiniFilter.Service]
DisplayName = %ServiceName%
Description = %ServiceDescription%
ServiceBinary = %12%\%DriverName%.sys ;%windir%\system32\drivers\
Dependencies = "FltMgr"
ServiceType = 2 ;SERVICE_FILE_SYSTEM_DRIVER
;StartType = 0 ;SERVICE_BOOT_START
StartType = 3 ;SERVICE_DEMAND_START
ErrorControl = 1 ;SERVICE_ERROR_NORMAL
LoadOrderGroup = "FSFilter Encryption"
AddReg = MiniFilter.AddRegistry
;
; Registry Modifications
;
[MiniFilter.AddRegistry]
HKR,"Instances","DefaultInstance",0x00000000,%Instance1.Name%
HKR,"Instances\"%Instance1.Name%,"Altitude",0x00000000,%Instance1.Altitude%
HKR,"Instances\"%Instance1.Name%,"Flags",0x00010001,%Instance1.Flags%
;
; Copy Files
;
[MiniFilter.DriverFiles]
%DriverName%.sys
[SourceDisksFiles]
SYSTE.sys = 1,,
[SourceDisksNames]
1 = %DiskId1%,,,
;;
;; String Section
;;
[Strings]
Msft = "Microsoft Corporation"
ServiceDescription = "encryption SYSTE minfilter Driver"
ServiceName = "SYSTE"
DriverName = "SYSTE"
DiskId1 = "SYSTE Device Installation Disk"
;Instances specific information.
Instance1.Name = "SYSTE Instance"
Instance1.Altitude = "141000"
Instance1.Flags = 0x0 ; allow automatic attachments
在安装驱动时候将此INF文件与驱动程序SYS文件放置于同一目录下右击进行安装即可。
进程名获取
文件透明加密驱动时根据进程名来区分是否为机密进程,在文件过滤驱动中不仅要获得用户要打开的文件的绝对路径,而且要获取用户使用的进程的名。
在实际的操作系统中,可能会出现有黑客利用仿冒进程或者DLL注入的方式伪装或者劫持机密进程从而非法操作机密文件,对于这一模块,我们会交给应用层程序通过对可执行文件的完整性判断、加载模块的完整性判断来甄别是否有恶意进程伪装机密进程。
在Windows内部对于每一个进程维护了EPROCESS结构,这个结构体保存有进程的名字,但是Windows没有公开此进程的结构,我们可以采取偏移位置查询的方法来获取进程名。驱动程序的入口程序DriverEntry函数是在“System”进程中被执行,那么我们可以在EPROCESS中搜索到System的偏移位置,从而获取到进程名这一变量在EPROCESS中的偏移位置。
ULONG Ps_GetProcessNameOffset ( VOID ){
PEPROCESS curproc = NULL ;
int i = 0 ;
curproc = PsGetCurrentProcess() ;
for (i=0; i<3*PAGE_SIZE; i++)
{
if (!strncmp("System", (PCHAR)curproc+i, strlen("System")))
{ return i ;}
}
return 0 ;}
机密进程链表存储
驱动中进程的结构体信息我们使用了iPROCESS_INFO结构体存储,结构体定义如下:
typedef struct _iPROCESS_INFO{
CHAR szProcessName[16] ; //存放进程名
BOOLEAN bMonitor ; //是否为机密进程
WCHAR wsszRelatedExt[64][6] ; //这里确定相关联文件的后缀名
LIST_ENTRY ProcessList ; //LIST_ENTRY入口
}iPROCESS_INFO,*PiPROCESS_INFO ;
其中wsszRelatedExt数组信息我们根据表中的机密进程与之相对应的文件后缀名来填写。机密进程由管理员在安装透明加解密驱动的时候指定,使用ExInterlockedInsertTailList函数动态添加到全局链表中。
不同于传统的应用层与驱动层使用DeviceIoControl函数进行异步/同步通信,Minifilter有内建支持的API供给开发者使用。通信机制类似于socket或者管道(pipe)通信,称为通信端口,应用层与驱动层根据唯一的端口标识符端口号(Unicode String)来进行连接的建立。其关键函数声明如下:
NTSTATUS
FltCreateCommunicationPort(
IN PFLT_FILTER Filter,
OUT PHANDLE PortHandle,
IN POBJECT_ATTRIBUTES ObjectAttributes,
IN PVOID ServerPortCookie OPTIONAL,
IN PFLT_CONNECT_NOTIFY ConnectNotifyCallback,
IN PFLT_DISCONNECT_NOTIFY DisconnectNotifyCallback,
IN PFLT_MESSAGE_NOTIFY MessageNotifyCallback,
IN ULONG MaxConnections
);
其中Filter为微过滤驱动器的句柄,ObjectAttributes为根据端口名生成的POBJECT_ATTRIBUTES对象,ConnectNotifyCallback为用户态与内核态建立连接时内核会调用的回调函数。DisconnectNotifyCallback为用户态与内核态断开连接时内核会调用的回调函数。MessageNotifyCallback为用户态与内核态传送数据时内核会调用的回调函数。
在应用层用户使用FilterConnectCommunicationPort函数初始化通信端口接口并通过FilterSendMessage函数向驱动层传递消息。二者之间基于公用的定义传输结构的文件来进行命令判断,目前定义的IOCTL(控制命令)有如下几种:
#define IOCTL_GET_ALL_PROCESS_INFO 0x00000001
#define IOCTL_GET_PROCESS_COUNT 0x00000002
#define IOCTL_SET_PROCESS_MONITOR 0x00000003
#define IOCTL_ADD_PROCESS_INFO 0x00000004
#define IOCTL_DEL_PROCESS_INFO 0x00000005
#define IOCTL_SET_FILEKEY_INFO 0x00000006
#define IOCTL_SET_MONITOR 0x00000007
#define IOCTL_SET_KEYLIST 0x00000008
#define IOCTL_GET_MONITOR 0x00000009
Windows操作系统为用户提供了巧妙的缓存机制,即只要一个文件被以缓冲方式打开过,则其内容的全部或者一部分就已经保存在了内存里。一般来说,一个文件无论有多少个进程在访问,都只有一份文件缓冲。一般的应用层的读写请求都是普通的读写请求(不带IRP_PAGING_IO/IRP_SYNCHRONOUS_PAGING_IO/IRP_NOCACHE),这种请求的特点是文件系统会直接调用CcCopyRead和CcCopyWrite函数完成,这两个函数会直接从缓冲中读数据,如果缓冲中没有,才会转换为分页读写请求。而如果系统中已经存在了该文件的文件缓冲,那么应用层的读写请求往往会被转化为Fast IO请求。
对于IO操作,Irp机制是最基本、默认的处理机制。Irp机制可以用于同步的、异步的、cached或者noncached IO操作。当遇到“缺页中断”时,Memory Manager也会通过发送相应的Irp包给文件系统来处理。 而 FastIO 的设计初衷则是用来处理快速的、同步的、并且“on cached files”的IO操作。当进行 FastIO 操作时,所需处理的数据是直接在用户buffer和系统缓存中进行传输的,而不是通过文件系统和存储器驱动栈(storage driver stack)。事实上存储器驱动并不使用FastIO机制。当需要处理的数据已经存在于系统缓存,则采用FastIO机制的读写操作立刻就可以完成。否则,系统会产生一个缺页中断,这会导致系统发送相应的IRP包来完成用户所需的读写操作。通常发生这种情况(指:所需的数据不在系统缓存的情况)的时候,FastIO函数会返回FALSE,或者一直等到缺页中端响应函数把所需的数据都加载到系统缓存中。(注:如果FastIO处理函数返回FALSE,那么调用者必须自己创建相应Irp来完成所需的处理。)
缓冲文件读写
缓冲管理器使用文件映射来缓冲文件数据,文件的缓冲初始化是通过文件系统驱动对缓冲管理器的一个调用(CcInitializeCacheMap)完成。收到这样一个请求的时候,缓冲管理器调用 VMM(Virtual Memory Manager)来创建表示文件映射的段对象,也就是整个文件数据内容。接下来,当一个进程试图访问属于这个文件的数据时,缓冲管理器动态映射这个文件到系统虚拟地址空间中为他保留的虚拟地址范围的适当地方。但由于保留给缓冲管理器的地址范围是固定的,所以缓冲管理器可能需要丢弃一些以前的文件缓冲数据。
图3.3.1-1 对缓冲文件进行读请求的操作流程
缓冲文件的写请求与读请求类似,这里不再赘述。
文件缓冲的清理
对于Windows用户而言,缓冲机制可以调高系统的文件读写效率从而获取更好地用户体验,但是因为缓冲的数据即是存放在内存中,是以明文形式存储的,黑客可以通过恶意程序非法访问这段内存而导致机密文件的外泄。在初步的解决方案中,我们采取的是即时清除缓存的方法,即将用户所有的读写请求强制转化为分页读写请求。这种方案的思路借鉴了楚狂人在寒江独钓中提到的SFILTER的文件缓冲处理方案,基本步骤如下:
(1)当有普通进程打开机密文件时,文件缓冲为密文,并且不允许机密进程打开这个文件。
(2)当有机密进程打开一个文件时候,文件缓冲为明文,并且不允许普通进程打开这个文件。
(3)二者切换时候,中间清除文件缓冲。
关于文件缓冲的清理本驱动放置在了IRP_MJ_CLEANUP的预操作回调中,首先使用 FltGetFileNameInformation函数获取当前文件绝对路径,然后根据文件名判断是否为机密文件。对于非机密文件,安全起见我们也是默认清除其缓存,而对于机密文件,也是需要强制清除其缓存,但是excel文件是一个特例。如果操作excel文件的缓存,会出发写磁盘的操作而导致部分excel文件的数据被加密,最终使得加密标识被破坏而文件被损坏无法再次打开。
对于文件缓冲清除的具体操作由Cc_ClearFileCache()函数完成,函数声明如下:
VOID Cc_ClearFileCache(
__in PFILE_OBJECT FileObject,
__in BOOLEAN bIsFlushCache,
__in PLARGE_INTEGER FileOffset,
__in ULONG Length) ;
本函数中,首先需要获取文件对象对应的FCB,即FSRTL_COMMON_FCB_HEADER结构体对象: Fcb = (PFSRTL_COMMON_FCB_HEADER)FileObject->FsContext ;
然后使用ExAcquireResourceExclusiveLite()函数获取到Fcb->Resource或者 Fcb->PagingIoResource这个资源锁,这里可以使用true循环,可能需要多次操作才能获取到。获取到资源锁之后,依次调用CcFlushCache、MmFlushImageSection、CcPurgeCacheSection这三个函数实现对于缓冲的清除,这一点和传统的文件过滤驱动的方式是一样的。
在实际操作中,如果有用户同时在编辑机密文件的时候不小心使用非机密进程导致文件缓冲被清除,会造成用户的信息流失,整个驱动程序的稳定性与兼容性也不是很好。并且频繁地清除缓冲,也会对系统的性能造成较大的影响。
双缓冲机制
缓冲管理器必需维护每个文件的缓冲信息,这个信息用缓冲位图来维护,对于每个文件,缓冲管理器分配一个共享缓冲位图(Shared Cache Map)结构来保存这个文件和这个文件相关联的其他信息的映射位图。这个共享缓冲位图结构在这个文件缓冲被文件系统驱动或者网络重定向请求初始化的时候分配。此外,共享缓冲位图对每个文件是唯一的,因此只有在首次为文件建立缓冲的时候分配,每次用一个特定的文件对象发出一个初始化缓冲的时候,缓冲管理器就分配一个私有的缓冲位图(Priviate Cache Map)机构。缓冲管理器用这个结构来标记缓冲已使用,其中还包含缓冲管理器用于预读控制的信息和其他的数据。需要特别说明的是,共享缓冲位图结构和私有缓冲位图的结构都是由缓冲管理器分配和维护的。为了创建两个缓冲区,必需利用缓冲管理器向文件系统驱动提供的接口,接口可以分为四类即文件操作接口:用来初始化文件缓冲区,刷新缓冲数据到磁盘,修改文件大小,清除缓冲数据等。
在本驱动程序中,借鉴了Windows WDK中提供的例程:Swap Buffer,即对于同一机密文件而言为机密进程与非机密进程创建了两个缓冲区(非机密进程访问的系统分配的缓冲区,机密进程访问的是我们申请的附加在文件上下文中的缓冲区),并且控制仅有机密进程可以访问明文缓冲区,这部分的具体实现方法的介绍请参照3.6数据加解密模块。
在楚狂人编写的寒江独钓中提到的基于sfilter的透明驱动开发中,对于如何标记一个特定的文件(一个文件在多次打开时会产生多个文件对象)是使用FCB来实现的。但是无论是FastFat还是NTFS,Microsoft对于FCB的结构与相应操作都未解密,这就给我们使用FCB作为文件标识符带来了一定的难度。而在Minifilter框架中,我们使用Stream Context来代替FCB,Stream Context结构体中的RefCount参数用来为文件对象计数。当RefCount计数器归零的时候,我们就需要向文件尾写入加密标识、清除文件上下文并且清空缓冲。
STREAM_CONTEXT结构体定义
在本系统中我们默认会在IRP_MJ_CREATE的后操作回调函数中进行文件上下文的处理:
FLT_POSTOP_CALLBACK_STATUS
PostCreate (
__inout PFLT_CALLBACK_DATA Data,
__in PCFLT_RELATED_OBJECTS FltObjects,
__inout_opt PVOID CompletionContext,
__in FLT_POST_OPERATION_FLAGS Flags
);
我们自定义的Stream Context结构体如下:
typedef struct _STREAM_CONTEXT {
//文件名
UNICODE_STRING FileName;
//卷名
WCHAR wszVolumeName[64] ;
//文件密钥HASH值
UCHAR szKeyHash[HASH_SIZE] ;
//文件流计数器,当技术值为时表示所有文件对象都已被关闭
//仅当值为时可以写入加密标识以及清除文件缓冲.
LONG RefCount;
//文件大小(用户查询可得到的大小)
LARGE_INTEGER FileValidLength ;
//文件实际物理大小
LARGE_INTEGER FileSize ;
//文件加密标识长度
ULONG uTrailLength ;
//文件权限
ULONG uAccess ;
//标志位
BOOLEAN bIsFileCrypt ;
//标识文件是否加密
BOOLEAN bEncryptOnWrite ; //当文件已经加密或者已经加入文件加密表时置为TRUE
BOOLEAN bDecryptOnRead ; //当未加密的机密文档第一次读写时置为TRUE
BOOLEAN bHasWriteData ; //判断是否可以写入文件标识
BOOLEAN bFirstWriteNotFromBeg ; //判断是否需要移位写入
BOOLEAN bHasPPTWriteData ; //对于PPT格式文件,用户保存当前修改但是没有关闭文件对象时候置为TRUE
//锁定正在读写的Stream Context
PERESOURCE Resource;
//自旋锁判断当前IRQL
KSPIN_LOCK Resource1 ;
} STREAM_CONTEXT, *PSTREAM_CONTEXT;
STREAM_CONTEXT操作
Windows会为每一个文件对象维护一个Stream Context结构体,我们可以根据回调数据使用如下方式查找已经存在的Stream Context结构体:
status = FltGetStreamContext(
Data->Iopb->TargetInstance,
Data->Iopb->TargetFileObject,&streamContext );
如果文件对象对应的Stream Context并不存在,那么可以使用如下方式为文件对象创建一个Stream Context结构体:
status = FltSetStreamContext( Data->Iopb->TargetInstance,
Data->Iopb->TargetFileObject,
FLT_SET_CONTEXT_KEEP_IF_EXISTS,
streamContext,
&oldStreamContext );
对于一个文件对象的Stream Context结构体的操作主要是IRP_MJ_CREATE的后回调函数与IRP_MJ_CLOSE的预函数函数中。
在PostCreate()函数中,主要是对用户打开的文件对象的判断。如果用户是新创建的文件,那么我们需要为此文件对象建立一个Stream Context结构体并且开启RefCount计数。如果用户打开的是一个已经打开的文件(即已经有该文件的文件对象存在),那么我们需要查找到该文件对象对应的Stream Context结构体并且将RefCount计数器加1。
在PreClose()函数中,每当驱动收到一个IRP_MJ_CLOSE消息,会自动对此文件对象的Stream Context结构体中的RefCount进行判断,如果RefCount计数归零,那么自动调用File_WriteFileFlag()开始向文件尾写入加密标识。
加密标识保存位置
对于磁盘上已经创建的文件,机密进程第一次打开时,必须要对文件进行判断:
在本驱动设计中,由于对于机密文档的处理时采取客户机向服务器申请密钥的方式,为了增强安全性,我们假设用户只有在获取到权限的情况下(即在服务器认证之后)编辑的文档属于机密文档,对于在加密驱动运行期间打开的已存在的机密文档,将无法正常显示。用户只可以在获得服务器许可的情况下编辑/创建/修改机密文件。为了用户可以间断性地对加密文件进行编辑,我们需要为机密文件建立加密标识以区分该文件是否被加密,同时也是保存了该文件的完整性校验码与加解密密钥种子。
加密标识存放的位置,总体来说有如下选择:
l 在卷目录下建立一个隐藏文件,保存当前目录下其他文件的加密标识。但是实际操作中我们发现这样的设置非常麻烦并且稳定性很差。当加密文件被移动时(包括如拷贝、压缩打包、网络发送等),加密标识非常容易丢失。
l Windows NT 6.0架构采用的NTFS的文件系统允许我们在文件中添加额外的流信息,而不会干预文件的本身内容。但是目前企业中仍存在着大量的Windows XP操作系统,它采用的是Fast Fat文件系统另外现在大部分的U盘也是使用的Fat32文件系统,如果我们将加密标识添加在NTFS流信息中,一旦用户使用U盘拷贝文件等就会导致文件的损坏。
l 在文件内容中保存加密标识:这种解决方案不仅可以兼容各个文件系统,而且方便了文件的移动。在本驱动中,我们选择将加密标识添加在文件尾中,这样,操作起来较为方便。
加密标识结构体定义
typedef struct _FILE_FLAG{
//为加密文件确定一个唯一的GUID值来分辨是否为加密文件
UCHAR FileFlagHeader[FILE_GUID_LENGTH] ;
//系统分配每一个文件都有自己的KEY值,FileKeyHash以SHA1标准对于KEY值做HASH
UCHAR FileKeyHash[HASH_SIZE] ;
//对于用户查询文件信息时返回文件的实际大小(略去File_Flag大小)
LONGLONG FileValidLength ;
//预留结构
UCHAR Reserved[SECTOR_SIZE-HASH_SIZE-FILE_GUID_LENGTH-8] ;
}FILE_FLAG,*PFILE_FLAG;
每一次用户打开文件的时候,驱动程序都会读取加密标识并进行判断。而当机密文件的所有文件对象(即计数器值RefCount归零)的时候驱动程序将会自动更新加密标识并写入到文件尾。
加密标识的读写
在驱动中,使用File_WriteFileFlag与File_ReadFileFlag这两个函数分别进行加密标识的写入与读取。
在File_WriteFileFlag函数中,首先需要获取文件的当前实际大小。因为本驱动选择的是将加密标识放置在文件尾,在用户打开文件(即内存中存在文件对象)的时间周期内可以认为对于机密文件而言加密标识是不存在的。因此,直接调用Minifilter提供的FltQueryInformationFile函数获取文件的FILE_STANDARD_INFORMATION信息,就可以获取文件的实际大小。
调用ExAllocatePoolWithTag函数为FileFlag申请内存周期之后设置文件的写入偏移,即文件加密标识写入到文件中的首地址:
FileSize.QuadPart = FileSize.QuadPart + (SECTOR_SIZE - FileSize.QuadPart % SECTOR_SIZE) + FILE_FLAG_LENGTH ;
在这里,我们假定分页内存中页表的大小为512字节,因此,文件的正文内容就补全为512的整数倍。最后,使用File_ReadWriteFile函数将封装好的FileFlag结构体按照偏移写入到文件尾部。
File_ReadFileFlag函数的原理与File_WriteFileFlag类似,只是在用户第一次打开文件(即文件上下文被第一次创建的时候),从FileSize.QuadPart-FILE_FLAG_LENGTH位置处开始读入FILE_FLAG_LENGTH大小的内容。
在驱动程序中设置有全局变量:PFILE_FLAG g_psFileFlag。在DriverEntry函数中会调用File_InitFileFlag()函数为全局文件加密标识分配内存并填入GUID。这样每次读写文件时候通过对文件尾部的加密标识与全局加密标识的对比就可以知道文件是否已经被加密。
加密算法与密钥分配
在加密算法的选择中,为了保证对于文件内容具有较快的加解密速度,我们考虑选取流加/解密的方式。而在流加/解密算法中,RC4算法简单易于使用软件实现并且具有较高的效率,不会对系统的性能造成较大的影响。RC4加密算法是大名鼎鼎的RSA三人组中的头号人物Ron Rivest在1987年设计的密钥长度可变的流加密算法簇。之所以称其为簇,是由于其核心部分的S-box长度可为任意,但一般为256字节。该算法的速度可以达到DES加密的10倍左右。RC4算法的原理很简单,包括初始化算法和伪随机子密码生成算法两大部分。其加解密的基本步骤展示如下:
void __stdcall rc4_init(UCHAR *s, PUCHAR key, ULONG Len) //初始化函数
{
int i =0, j = 0;
UCHAR k[256] = {0};
UCHAR tmp = 0;
for(i=0;i<256;i++)
{
s[i]=i;
k[i]=key[i%Len];
}
for (i=0; i<256; i++)
{
j=(j+s[i]+k[i])%256;
tmp = s[i];
s[i] = s[j]; //交换s[i]和s[j]
s[j] = tmp;
}
}
void __stdcall rc4_crypt(UCHAR *s, PUCHAR Data, ULONG Len) //加解密
{
int i = 0, j = 0, t = 0;
UCHAR k = 0;
UCHAR tmp;
for(k=0;k { i=(i+1)%256; j=(j+s[i])%256; tmp = s[i]; s[i] = s[j]; //交换s[x]和s[y] s[j] = tmp; t=(s[i]+s[j])%256; Data[k] ^= s[t]; } }
文件加解密过程
对于机密文件的加解密,主要是在IRP_MJ_READ与IRP_MJ_WRITE的回调函数中实现的,由于我们采用的是双缓冲机制,需要在预回调函数与后回调函数之间进行信息的传递。譬如在DPC中断级别下无法获取卷上下文,我们可以在Pre函数中获取到卷上下文然后在Post函数中释放。传递是依靠PVOID CompletionContext这个回调函数中用的不是很多的参数,具体参数的结构体定义与存储结构定义如下。
参数结构体定义与存储结构
typedef struct _PRE_2_POST_CONTEXT {
PVOLUME_CONTEXT VolCtx;//预回调函数中获取的卷上下文
PSTREAM_CONTEXT pStreamCtx ;
PVOID SwappedBuffer;// 将我们分配的缓冲区地址传给Post函数以便于释放
} PRE_2_POST_CONTEXT, *PPRE_2_POST_CONTEXT;
PRE_2_POST_CONTEXT结构体主要用来在Pre函数与Post函数之间进行参数传递,这样会导致每一个IRP请求都会向系统申请一块存储空间并且不会被及时释放。这样频繁地申请内存会导致系统中存在着大量的内存空洞,而使得即使内存中有大量的可用内存,也会导致申请内存失败。
在本驱动中,为了解决这个问题采取了Lookaside对象的方式进行数据存储。Lookaside对象类似于一个内存容器,在初始的时候,它先向Windows申请了一块比较大的内存,以后程序员每次申请内存的时候,不是直接向Windows申请内存,而是向Lookaside对象申请内存。Lookaside对象会智能地避免产生内存空洞。而如果Lookaside对象内部的内存不够用时,它会向操作系统申请更多的内存。当Lookaside对象内部有大量的未使用的内存时,它会自动让Windows回收一部分内存。总而言之,Lookaside是一个自动的内存分配容器。通过对Lookaside对象申请内存,效率要高于直接向Windows申请内存。
定义Lookaside:NPAGED_LOOKASIDE_LIST Pre2PostContextList;
申请内存:PPRE_2_POST_CONTEXT p2pCtx = ExAllocateFromNPagedLookasideList( &Pre2PostContextList );
双缓冲机制的具体实现
Ⅰ:预回调函数处理
这里以文件的读过程为例说明双缓冲机制及文件解密的具体过程。
首先在IRP_MJ_READ的Pre函数中,分别使用FltGetVolumeContext函数获取到文件对象所属的卷上下文(包含了该文件对象所拥有的加解密密钥)以及Ctx_FindOrCreateStreamContext()函数获取到流上下文。然后会判断读写是否为Fast IO,如果为Fast IO则设置返回状态为FLT_PREOP_DISALLOW_FASTIO,此返回状态在MSDN中的定义如下:
The operation is a fast I/O operation, and the minifilter driver is not allowing the fast I/O path to be used for this operation. The filter manager does not send the fast I/O operation to any minifilter drivers below the caller in the driver stack or to the file system. In this case, the filter manager only calls the post-operation callback routines of the minifilter drivers above the caller in the driver stack.
在过滤了Fast IO和非机密文件的IRP请求之后,我们会为机密文件的IRP请求申请新的内存空间用于存放解密之后的明文信息。最后申请属于该IRP的MDL空间并通过PPRE_2_POST_CONTEXT结构体传递到Post函数中。在这里要注意一点,在修改了回调数据包Data之后务必调用FltSetCallbackDataDirty()函数通知系统对于回调数据包做的修改。
Ⅱ:后回调函数处理
当文件磁盘驱动完成对于文件内容的分页读写之后,会自动将数据传入到IRP_MJ_READ的后回调函数之中。
在Post函数的处理中,我们首先要获取到系统分配的缓冲区,鉴于系统会采取三种方式(缓冲区读写、直接读写与其他方式读写)进行内存读写,要进行不同的判断。
首先使用 MmGetSystemAddressForMdlSafe( iopb->Parameters.Read.MdlAddress, NormalPagePriority )函数尝试获取到系统分配的MDL的缓冲区地址,如果不存在,则对回调数据包的Flags1进行判断,如果Flags中存在 FLTFL_CALLBACK_DATA_SYSTEM_BUFFER或者
FLTFL_CALLBACK_DATA_FAST_IO_OPERATION则说明系统使用缓冲区读写的方式,那么可以直接从iopb->Parameters.Read.ReadBuffer中获取到系统分配的缓冲区地址。
如果系统不是用的上述两种读写方式,那么说明系统使用的是其他方式读写,此时在DPC的中断级别下是无法获取到正确的缓冲区地址,需要更改我们的IRQL;
此时使用fltKernel中提供的API:
FltDoCompletionProcessingWhenSafe( Data,
FltObjects,
CompletionContext,
Flags,
PostReadWhenSafe,
&FltStatus )
其中PostReadWhenSafe是我们定义的回调函数,该回调函数执行的中断级别就是我们所需要的安全的中断级别。
此时我们已经获取到了系统分配的缓冲区地址,换言之,即是系统分配的应用程序的内存地址,而我们真实的明文数据存放在p2pCtx->SwappedBuffer中。在Post函数的最后,我们调用RtlCopyMemory函数将明文数据复制到用户的内存空间,至此完成了对于机密进程的数据读取功能。
对于用户打开一个未知文件时候,我们需要在文件被打开之前对文件进行判断,换言之,当驱动程序收到IRP_MJ_CREATE时候,驱动程序需要判断文件是否存在、判断文件实际大小、读取文件尾、判断是否有加密标识等。在传统的sfilter中提供的文件操作函数如ZwCreateFile、ZwReadFile等,他们发出的文件操作请求被系统传入了设备栈后又被文件过滤驱动自己捕获,会导致重入的问题。在本驱动中我们采取的是直接发送IRP到设备栈底层设备的方式来进行文件的读取。
获取文件句柄
在minifilter架构中,Windows提供了FltCreateFile函数方便我们获取要打开的文件的文件句柄:
status = FltCreateFile( gFilterHandle, //文件过滤微驱动实例句柄
FltObjects->Instance,
//当前文件过滤驱动器实例,保证IRP是从该实例开始向下发送,避免了打开句柄时候的重入问题
&hFile, //输出的文件句柄
FILE_READ_DATA,
&objectAttributes, //包含要打开的文件信息的ObjectAttribute数组
&ioStatus,
(PLARGE_INTEGER) NULL,
FILE_ATTRIBUTE_NORMAL,
FILE_SHARE_READ,
FILE_OPEN_IF, //该参数指示若文件不存在则创建文件
FILE_NON_DIRECTORY_FILE,
NULL,
0,
IO_IGNORE_SHARE_ACCESS_CHECK );
分配内存
首先我们使用ZwQueryInformationFile函数用来获取文件的FIlESTANDARDINFORMATION结构体
status = ZwQueryInformationFile(hFile,
&iostatus,
&fsi,
sizeof(FILE_STANDARD_INFORMATION),
FileStandardInformation);
从返回的fsi结构体的EndOfFile.QuadPart变量中我们可以获取到文件的长度信息,即实际物理大小,根据这个我们可以会缓冲区分配内存同时将文件句柄使用ObReferenceObjectByHandle函数转化为文件对象以供使用。
获取底层设备
我们可以根据当前文件过滤驱动生成的设备对象来获取设备栈底层的设备,具体调用如下:
pVolumeDevObj = IoGetDeviceAttachmentBaseRef(FltObjects->FileObject->DeviceObject) ;
这里获取到的卷设备对象就是我们实际绑定到的卷设备。
封装并发送IRP请求
首先我们申请分配一个IRP结构体
irp = IoAllocateIrp(pNextDevObj->StackSize, FALSE);
下面开始填写IRP请求的主体内容:
irp->AssociatedIrp.SystemBuffer = NULL;
irp->MdlAddress = NULL;
irp->UserBuffer = pBuffer;
驱动程序读写设备的方式主要有缓冲区读写、直接读写与其他方式读写这三大种主要的方式,但是在分页读写的状态下,我们必须使用UserBuffer方式。
irp->UserEvent = &event;
另外,需要分配一个事件结构体以方便设置设备完成回调函数,同时设置IoStatusBlock以获取底层驱动设备返回的相关信息。
irp->UserIosb = &IoStatusBlock;
最后,使用IoCallDriver函数将IRP请求发送到底层驱动中。
(void) IoCallDriver(pNextDevObj, irp);
设备完成回调函数
本模块的目标是为了获取文件信息,只有底层设备完成了文件读写并且返回了相关的信息发出IRP的驱动设备才能进行下一步的作业,在这里我们要使用Event实现内核同步:
IoSetCompletionRoutine(irp, File_ReadWriteFileComplete, 0, TRUE, TRUE, TRUE);
首先我们设置IRP请求的完成函数:File_ReadWriteFileComplete,同时将本线程陷入响应等待模式:
KeWaitForSingleObject(&Event, Executive, KernelMode, TRUE, 0);
而在完成函数File_ReadWriteFileComplete中使用KeSetEvent函数唤醒过滤设备:
KeSetEvent(irp->UserEvent, 0, FALSE);
其他查询请求的处理
上文中我们已经简述了文件透明加解密驱动的主要关键技术点及其实现,最后在驱动的设计中还有部分有关于文件查询、设置以及文件夹控制的IRP请求需要处理,这里统计介绍一下:
① IRP_MJ_QUERY_INFORMATION
在预回调函数PreQuery()中主要完成了文件对象的流上下文的获取与对于非机密文件的过滤,对于非机密文件直接返回FLT_PREOP_SUCCESS_NO_CALLBACK即不需要后回调函数。
在后回调函数PostQuery中的处理,主要是要对于用户隐去文件尾加密标识的大小,但是因为在添加加密标识的时候我们是将文件大小补全为页面大小的整数倍,这里我们返回给用户的大小设置如下(以FileStangInformation为例):
psFileStandardInfo->AllocationSize.QuadPart = pStreamCtx->FileValidLength.QuadPart + (PAGE_SIZE - (pStreamCtx->FileValidLength.QuadPart%PAGE_SIZE));
psFileStandardInfo->EndOfFile = pStreamCtx->FileValidLength ;
② IRP_MJ_SET_INFORMATION
类似于Query的处理,这里在用户发出设置文件大小的请求的时候会自动添加上文件尾加密标识的长度。不过当用户设置大小之后需要使用Ctx_UpdateNameInStreamContext函数在文件的流上下文中更新文件的相关信息以防对于别的操作产生影响。