Wrote By Fanxiushu 2015-03-30,引用或转载请注明原始作者。
接上文。
因为整个驱动结构采用把所有数据转发到应用层来处理的,
所以需要在应用层处理各种文件请求,才能最终实现目录重定向 。
文件的各种请求是非常多的,现总结一下需要发送到应用层处理的请求包括:
1,CREATE 文件打开创建请求 ,这个请求在驱动收到 IRP_MJ_CREATE触发
2,CLOSE文件关闭请求,这个请求在驱动收到 IRP_MJ_CLOSE触发
3,READ文件读请求,驱动收到 IRP_MJ_READ触发
4,WRITE文件写请求,驱动收到IRP_MJ_WRITE触发
5,QUERYINFO文件信息查询请求,驱动收到 IRP_MJ_QUERY_INFORMATION触发
6,SETINFO文件信息设置请求,驱动收到IRP_MJ_SET_INFORMATION触发
7,QUERYDIR查询文件夹下的文件请求,驱动收到IRP_MJ_DIRECTORY_CONTROL触发
至少以上7个大的请求需要发到应用层处理,而 CREATE,QUERYINFO,SETINFO等命令,又有许多子请求,所以整个结构非常复杂。
为了尽量简化处理,把能合并的都合并,比如 CREATE,QUERYINFO等, 我把各种子请求合并到一块发送到应用层。
同样为了简化,使用两个 IOCTL控制码来通讯所有这些请求,如上篇文章所述的通讯办法。
一个是 BEGIN IOCTL,驱动触发请求时候发给应用层的;一个是 END IOCTL,应用层完成这个请求之后通知驱动的。
每次通讯,都使用同一个结构,只是这个结构非常的复杂,可以跟IRP的结构做比较了。
应用层接口函数看起来的伪代码如下:
struct ioctl_fsvar_t;////这个结构是跟驱动进行通讯的数据结构, 下边会列出这个结构。
struct fsdev_request_t; ////这个结构跟ioctl_fsvar_t 差不多,只是比它更简化,主要是在应用程序内部通讯
//首先打开驱动设备,创建 一个信号量, 把句柄传递给驱动,驱动用他来通知应用层有新的请求达到。
void* fsdev_open()
{
HANDLE hFile = CreateFileA("\\\\.\\xFsRedir", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0 , NULL);
HANDLE hSemaphore = CreateSemaphore(NULL, 0, MAXLONG, NULL);
DeviceIoControl(hFile, IOCTL_SET_SEMAPHORE_HANDLE, (PVOID)&hSemaphore, sizeof(HANDLE), ... );
..........
}
// 接着告诉驱动,我们需要监控的目录是什么
int fsdev_set_monitor_directory(void* handle, const char* mon_dir)
{
........
DeviceIoControl(dev->hFile, IOCTL_SET_MONITOR_DIRECTORY, wMonDir, wDirLen, NULL, NULL, &bytes, NULL);
.........
}
//// 接着实现两个核心函数
//// 此函数获取驱动的文件请求包
fsdev_request_t* fsdev_begin_request(void* handle);
{
_fsdev_t*dev = (_fsdev_t*)handle;
........
buffer = (LPBYTE)malloc(sizeof(ioctl_fsvar_t) + sizeof(fsdev_request_t));
if (!buffer)return NULL;
ioctl = (ioctl_fsvar_t*)buffer;
req = (fsdev_request_t*)( buffer + sizeof(ioctl_fsvar_t) );
::WaitForSingleObject(dev->hSemaphore, dev->wait_time); //等待,只要驱动有文件请求,就会增加信号量,程序就会从这里醒来
.......
/////从驱动获取请求信息,填写到 ioctl_fsvar_t结构中。
DeviceIoControl(dev->hFile, IOCTL_TRANS_DATA_BEGIN, NULL, 0, ioctl, sizeof(ioctl_fsvar_t), &bytes, NULL);
///////分析处理,并转换成 fsdev_request_t结构
.........
}
int fsdev_end_request(void* handle, fsdev_request_t* req)
{
/////分析处理 req结构,并转换成 ioctl_fsvar_t结构
..........
/////通知驱动,这个请求已经完成
DeviceIoControl(dev->hFile, IOCTL_TRANS_DATA_END, ioctl, inlen, outbuf, outlen, &bytes, NULL);
......
//////善后处理
}
ioctl_fsvar_t 的结构如下:
(fsdev_request_t跟ioctl_fsvar_t差不多,只是更加简化,这里也就不列出了,详细信息请查看CSDN上发布的源代码。)
/////////
#define CREATE 0
#define CLOSE 1
#define READ 2
#define WRITE 3
#define QUERYINFO 4
#define SETINFO 5
#define QUERYDIR 6
union fs_create_t
{
struct {
ULONG CreateDisposition;
ULONG CreateOptions;
ULONG AccessMode; // GENERIC_READ GENERIC_WRITE
ULONG ShareMode; ///
ULARGE_INTEGER PathName;
///////
UCHAR bCaseSensitive; // 文件名是否要区分大小写
UCHAR bForceAccessCheck; // 访问检查???
UCHAR bPagingFile; // 打开的是 pagefile文件
UCHAR bOpenTargetDirctory; // 这个标志则表示并不是真的要打开指定的文件系统对象,而是要检查对象是否可以删除以及它所在的目录是否可以进行创建等操作。
// 通常这样的请求会发生在重命名文件系统对象之前
};
///// CREATE Success RESULT
struct {
UCHAR bDirectory;
LARGE_INTEGER FileSize;
LARGE_INTEGER AllocationSize;
////下边的没用上
LARGE_INTEGER ValidDataLength;
ULONG Attributes;
LARGE_INTEGER CreationTime;
LARGE_INTEGER LastAccessTime;
LARGE_INTEGER LastWriteTime;
}Success;
};
struct fs_readwrite_t
{
LARGE_INTEGER Offset;
ULONG Length;
ULONG IoType; //读写类型,目前就两个,0是一般读写,1是Page IO,增加这么个字段其实是无法解决为什么直接把PageIO的IRP->MdlAddress映射到用户空间后,调用recv等函数会失败甚至蓝屏.
};
struct fs_queryinfo_t
{
LARGE_INTEGER CreationTime;
LARGE_INTEGER LastAccessTime;
LARGE_INTEGER LastWriteTime;
LARGE_INTEGER ChangeTime;
ULONG FileAttributes;
ULONG bDirectory;
LARGE_INTEGER AllocationSize;
LARGE_INTEGER FileSize;
};
#define tpInfo_BasicInfo 0
#define tpInfo_DelFile 1
#define tpInfo_Rename 2
#define tpInfo_CurPos 3
#define tpInfo_NewSize 4
struct fs_setinfo_t
{
ULONG type;
////
USHORT ReplaceIfExists;
union{
struct{
LARGE_INTEGER CreationTime;
LARGE_INTEGER LastAccessTime;
LARGE_INTEGER LastWriteTime;
LARGE_INTEGER ChangeTime;
ULONG FileAttributes;
};
UCHAR DeleteFile;
LARGE_INTEGER NewFileSize;
LARGE_INTEGER CurrentOffset;
ULARGE_INTEGER NewFileName; //改成的新文件名
};
};
#define MAX_FILENAME_LENGTH 260
struct fs_fileinfo_t
{
WCHAR FileName[MAX_FILENAME_LENGTH]; /// MAX_PATH
////
LARGE_INTEGER CreationTime;
LARGE_INTEGER LastAccessTime;
LARGE_INTEGER LastWriteTime;
LARGE_INTEGER ChangeTime;
LARGE_INTEGER FileSize;
LARGE_INTEGER AllocationSize;
ULONG FileAttributes;
ULONG bDirectory;
};
struct fs_querydir_t
{
USHORT bRestart; ///驱动传上来,表示需要重新扫描目录
USHORT bOnlyOne; ///只返回一条记录
ULONG RequestLength; ///
ULARGE_INTEGER OptionName; //查找的名字,可以为空,可以带通配符
};
struct ioctl_fsvar_t
{
ULONGLONG inter_handle; // 是等待处理的文件IRP 指针
////
ULONGLONG file_object;
ULONGLONG user_context; // 用户传递的context
USHORT type; // 操作方式
USHORT bMoreRequest; // 更多请求
////
union {
fs_create_t Create;
fs_readwrite_t ReadWrite;
fs_queryinfo_t QueryInfo;
fs_setinfo_t SetInfo;
fs_querydir_t QueryDir;
};
////数据传输缓冲
ULONG buffer_size;
ULARGE_INTEGER buffer_address;///这里本来应该是 PVOID的,但是为了方便64和32位驱动直接跟32位程序通讯,才设置成这样。
///返回结果
LONG Status;
LONG Information;
};
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////
可以看到 fsdev_begin_request 和 fsdev_end_request 都是主动调用,才能从驱动获得数据。
这并不符合文件请求的习惯。
因为一个文件请求过来,都是被动的得到通知和处理数据,
就跟服务器一样,客户端有请求,服务器才会有响应。
因此,我们应该还要简化,设计成回调函数的样子。
比如CREATE到来,就有一个跟CREATE相关的回调函数被调用。
我们在这个回调函数中 处理真正的文件打开操作,然后返回。
具体办法就是开几个线程,每个线程都调用 fsdev_begin_request 函数,
fsdev_begin_request 函数返回之后,分析处理返回的数据,然后调用对应操作的回调函数。
有兴趣可自行设计,或者可以查看我发布到CSDN的源代码。
经过层层封装,于是更简化的接口如下:
struct fsdev_init_t
{
const char* mon_dir; ///监控目录
int thread_cnt;///线程数
PVOID param;////额外参数
///
//open_flag 取值 CREATE_NEW,CREATE_AWLAYS,OPEN_EXISTENG,OPEN_ALWAYS
int (*create)(const char* pathname, int is_tryopen_dir, int is_open_target_dir,
int open_flag, int access_mode, int request_pid, PVOID param, PVOID* pcontext );文件打开或者创建回调
void (*close)(PVOID context, int request_pid);文件关闭回调
int (*read)(PVOID context, OUT char* buf, __int64 offset, int len, int iotype, int request_pid);文件读
int (*write)(PVOID context, IN char* buf, __int64 offset, int len, int iotype, int request_pid);文件写
int (*query_information)(PVOID context, fileinfo_t* info, int request_pid);查询文件信息
void (*query_directory)(PVOID context, const char* findname, int is_restart, int is_onlyone, int request_pid);查询目录信息
///////Set Info
int (*set_basicinfo)(PVOID context, time_t atime, time_t mtime, time_t ctime, int attr, int request_pid);设置文件基本属性
int (*del_file)(PVOID context, const char* pathname, int request_pid);删除文件
int (*ren_name)(PVOID context, const char* oldname, const char* newname, int request_pid);重名
int (*set_curpos)(PVOID context, __int64 cur_pos, int request_pid);设置位置
int (*set_newsize)(PVOID context, __int64 new_size, int request_pid);设置新大小
};
int fsdev_init(fsdev_init_t* fs); ///初始化函数。
初始化 fsdev_init_t 结构,实现以上各种回调函数,然后调用 fsdev_init函数。
回调函数接口做好了,接着就是需要把真正的文件操作关联起来,这里使用的FTP通讯,把FTP服务器上的目录映射到本地。
当初之所以想到用FTP,是因为想到有现成wininet.dll库提供的类似本地文件操作的 WIN32 API函数,
而且服务端都是现成的FTP Server,不用自己再开发一个服务端程序,于是想偷懒少写点代码,能很快把应用层程序搞定。
然而等我真正利用 wininet的那套函数的时候,才真是噩梦的开始。
文件系统的操作是非常频繁的,比如我用 explorer浏览我的监控目录,很有可能就会同时执行好几百个 CREATE打开操作,
而在 CREATE的回调函数中,我必须连接到FTP服务端查询需要打开的文件是否存在以及相关的文件信息。
因此,同一时间就会出现非常多的FTP连接到FTP服务端,这样也无所谓,反正网络系统都能支持非常多的TCP连接。
可是连接一多, InternetConnect 函数经常性的出毛病,在里边永远不返回了。这可是非常糟糕的事情。
这会造成explorer等访问监控目录的程序假死。后来想过各种办法,比如阻塞设置超时,再后来做成异步调用,问题依然存在。
于是,只能断定这是 wininet库在处理FTP上有BUG。
再后来,干脆使用 纯粹的socket,自己实现FTP客户端,最终才解决这个问题。
然后想想花了这么长时间折腾在FTP客户端上,其实这些时间,完全可以轻轻松松的自己实现私有协议的客户端和服务端,
而且FTP本身的毛病,通讯效率也不高,实在不适合用在文件系统这样访问非常频繁的地方。
所以后来的版本,完全抛弃了FTP,使用自己开发的通讯协议,有兴趣的朋友可自行实现。
至此,一套完整的目录重定向系统总算完成了。
可是还没结束,测试中发现跟360的文件过滤驱动有点冲突,
冲突现象是,在装有360的机器上,如果在重定向目录里直接执行exe文件,系统就会蓝屏。
360也有文件过滤驱动 qutmdrv.sys,我们知道windows的过滤驱动都是一层挂载一层的,从而形成一个设备栈。
比如一个NTFS文件系统卷上挂载一个 C过滤驱动,C过滤驱动上边再挂一个B过滤驱动,B上挂一个A过滤驱动。。。
A在最上边,windows对过滤驱动的调用都是从最上层的开始,不会也不应该越级调用。
就是上层的接口,只能调用设备栈的A驱动,A驱动负责传递给B驱动,B传递给C驱动,这样一层层传递,
而不应该出现上层接口直接调用B驱动,B驱动越过C驱动,调用C驱动下边的驱动,这样做法是不符合过滤驱动开发规范的。
而不知为何 360在处理exe文件的某些IRP请求时候,是跳跃式调用。
具体就是 IRP_MJ_CREATE打开exe文件时候,360按照正常过滤驱动设备栈调用文件过滤驱动,
这个时候获得一个打开的FILE_OBJECT文件对象。
而在对这个打开的FILE_OBJECT的某些其他IRP请求,却不是走的设备栈,而是直接越过某些上层的过滤驱动,
但不是绕过所有过滤驱动直接调用ntfs.sys的派遣函数,
360还会调用 partmgr驱动, 它到是没做绝,这样能保证minifilter框架的过滤驱动能正常使用。
我们的过滤驱动使用的是传统的NT过滤驱动,如果不做特殊处理,无一例外都会挂载到 partmgr的上边,
这样在处理exe文件时候,360就会绕过我们的过滤驱动。这样会有什么问题?
因为我们的目录重定向,在IRP_MJ_CREATE中就拦截 IRP,并且不会再把这IRP下发给下层驱动,由我们自己处理这些IRP。
因此这些IRP,下层驱动是不能识别,甚至某些私有数据结构,比如fsContext和FsContext2等跟底层驱动不符合,
如果把我们自己处理的IRP,传递给下层驱动,他们因为不识别,只会失败返回,甚至因为数据不符合而系统蓝屏死机。
这就是为何在装360机器上,如果在重定向目录里直接执行exe会蓝屏的原因了,
幸好360不是做得那么绝,不是所有文件访问都是跳过某些过滤驱动。
至于其他杀毒软件是不是也有这种情况,我没再测试了,也懒得去测试。有兴趣可自行测试实验。
要解决这个问题也不是没办法,
办法一:
如果你非要坚持要在装360机器上使用目录重定向驱动,那就设法把我们的过滤驱动挂载到 partmgr之前来启动。
这可以办到,只要让驱动在 BOOT阶段启动,并且启动顺序比partmgr之前就行,
驱动的启动顺序,你可以研究其他资料,这里也就不多解释了,本源代码工程也可以办到。
办法二:
如果你的需求只是把远端目录重定向到一个新的盘符中,就是不 重定向到某个目录中。
那你不使用过滤驱动来实现这个功能,代替的方案是,开发一个新的文件系统。
因为文件系统总是在过滤驱动最下边,因此360如何玩,都不会影响到你的新文件系统。
源代码工程没有提供这部分的实现,但是除了如何注册一个新的文件系统等框架外,具体到处理各种IRP细节,
就跟我们的文件过滤驱动实现目录重定向的做法没什么区别了。您可以查询 fastfat的实现代码。
实现目录重定向,是个挺复杂的课题,这里只是粗浅的介绍,希望对您有些帮助。
本文在 CSDN上BLOG:
http://blog.csdn.net/fanxiushu/article/details/43636575 以及后续章节
本文在CSDN上提供的程序:
http://download.csdn.net/detail/fanxiushu/8448785
本文在 CSDN上提供的源代码工程:
http://download.csdn.net/detail/fanxiushu/8545567