分享

SFilter框架理解

 梦醉千秋 2021-04-20
                     控制设备(接受我们自己的客服端)----过滤设备(接受别的进程的IRP) IRP栈每层对应的设备不同  
绑定后返回的:最顶层的设备 看图 为什么返回最顶层,当我们的设备处理好后要发给下面的 而下面的第一个就是最顶层的设备 看图理解






过滤
分层驱动中再加一层而不影响它的上下层,以过滤它们之间的数据,对数据或行为进行安全控制。过滤是通过设备绑定实现的。
图:



绑定
设备栈绑定的形式。驱动自己生成一个设备(过滤设备),调用系统提供的绑定API,绑定到目标设备上。并返回一个在未绑定之前目标设备所在设备栈的最顶层设备。这样发往下层的IRP或者发往上层的数据都会被过滤设备截获。
  1. PDEVICE_OBJECT
  2. IoAttachDeviceToDeviceStack(
  3. IN PDEVICE_OBJECT SourceDevice,
  4. IN PDEVICE_OBJECT TargetDevice );


AttachedDevice需要记录,以便调用IoCallDriver()继续下发IRP
为什么要返回一个DEVICE_OBJECT呢?
"并返回一个在未绑定之前目标设备所在设备栈的最顶层设备"


一个系统中可能存在多个过滤设备
  1. ------------
  2. SourceDevice
  3. ------------<-----AttachedDevice
  4. Filter2Device
  5. ------------
  6. Filter1Device
  7. ------------
  8. TargetDevice



此时最顶层的对象为Filter2Device 返回这个以便IRP下发 比如放行
图:






绑定API:
  1. IoAttachDevice()
  2. IoAttachDeviceToDeviceStackSafe(2000 SP4以及XP以上)
  3. IoAttachDeviceToDeviceStack()

windbg 查看相关信息:
  1. !devobj 查看设备对象信息
  2. !drvobj 查看驱动对象信息
  3. !devstack 查看设备栈
  4. !devnode 0 1 系统设备树



文件系统过滤框架
  1. Filemon //不常见 不能动态监控移动设备
  2. Sfilter //走在被淘汰的路上
  3. Minifilter //未来主流
  4. Filespy //不常见


Sfilter总体流程:

创建控制设备
创建控制设备符号链接
过滤分发函数
Fastio
过滤与绑定
生成一个过滤设备
IoRegisterFsRegistrationChange( 
DriverObject,
SfFsNotification ); (文件系统设备绑定)
SfFsControl (卷设备绑定)
一个驱动,看见几个文件系统设备,看见几个卷设备,对应每一个设备就生成相应设备附载上去,然后进行相应处理。 


绑定文件系统是在回调中(也就是上面的SfFsNotification),绑定卷设备在分发函数中 在绑定文件系统后 在FILR_SYSTEM_COMTEL中就会收到卷设备创建的信息 就可以绑定卷设备了 在分发函数中创建过滤设备对象


比如一个U盘插入电脑 ,它第一步需要创建一个文件对象,然后再创建一个卷设备
我们要动态Attach到这上面,我们需要Hook这个过程,而这个HOOK过程,我们用回调的方法
这个回调的作用是监视创建文件设备行为和绑定文件设备,绑定文件设备后,当这个U盘要创建卷设备的时候就会被我拦截到,这个时候我们就可以生成过滤设备绑定上去了
动态监控卷的挂载(VolumeMounting)概述: 一个新的存储介质被系统发现并在文件系统中创建一个新的卷Volume的过程被称为Mounting。其过程是: 文件系统的控制设备对象(FSCDO)将得到一个IRP,其主功能码(MajorFunctionCode)为IRP_MJ_FILE_SYSTEM_CONTRO。副功能码(MinorFunctionCode)为IRP_MN_MOUNT,呵呵,仔细想一下,不要死板,如果我们创建一个设备对象,并将其绑定到文件系统控制设备对象的设备对象栈上,那么我们的设备对象不就能接受到这个IRP了吗!这样,我们就能时刻的知道新卷的产生了。解决了上面的问题。


这里说说FileMon过滤
FileMon里的方法:
枚举26个盘符,打开文件,获得FileObjectDeviceObject.
然后通过自己驱动生成一个过滤设备,Attach过滤设备到DeviceObject上
无法监控类似U盘等动态加载上去的


而IoRegisterFsRegistrationChange是动态获取的


Fastio
文件系统除了处理正常的IRP之外,还要处理所谓的FastIo.
FastIo是Cache Manager调用所引发的一种没有irp的请求。换句话说,除了正常的Dispatch Functions之外,你还得为DriverObject撰写另一组Fast Io Functions.
这组函数的指针在 driver->FastIoDispatch 
代码:

  1. fastIoDispatch = ExAllocatePoolWithTag( NonPagedPool,
  2. sizeof( FAST_IO_DISPATCH ),
  3. SFLT_POOL_TAG_FASTIO );
  4. if (!fastIoDispatch) {
  5. IoDeleteDevice( gSFilterControlDeviceObject );
  6. return STATUS_INSUFFICIENT_RESOURCES;
  7. }
  8. RtlZeroMemory( fastIoDispatch, sizeof( FAST_IO_DISPATCH ) );
  9. fastIoDispatch->SizeOfFastIoDispatch = sizeof( FAST_IO_DISPATCH );
  10. fastIoDispatch->FastIoCheckIfPossible = SfFastIoCheckIfPossible;
  11. fastIoDispatch->FastIoRead = SfFastIoRead;
  12. fastIoDispatch->FastIoWrite = SfFastIoWrite;
  13. fastIoDispatch->FastIoQueryBasicInfo = SfFastIoQueryBasicInfo;
  14. fastIoDispatch->FastIoQueryStandardInfo = SfFastIoQueryStandardInfo;
  15. fastIoDispatch->FastIoLock = SfFastIoLock;
  16. fastIoDispatch->FastIoUnlockSingle = SfFastIoUnlockSingle;
  17. fastIoDispatch->FastIoUnlockAll = SfFastIoUnlockAll;
  18. fastIoDispatch->FastIoUnlockAllByKey = SfFastIoUnlockAllByKey;
  19. fastIoDispatch->FastIoDeviceControl = SfFastIoDeviceControl;
  20. fastIoDispatch->FastIoDetachDevice = SfFastIoDetachDevice;
  21. fastIoDispatch->FastIoQueryNetworkOpenInfo = SfFastIoQueryNetworkOpenInfo;
  22. fastIoDispatch->MdlRead = SfFastIoMdlRead;
  23. fastIoDispatch->MdlReadComplete = SfFastIoMdlReadComplete;
  24. fastIoDispatch->PrepareMdlWrite = SfFastIoPrepareMdlWrite;
  25. fastIoDispatch->MdlWriteComplete = SfFastIoMdlWriteComplete;
  26. fastIoDispatch->FastIoReadCompressed = SfFastIoReadCompressed;
  27. fastIoDispatch->FastIoWriteCompressed = SfFastIoWriteCompressed;
  28. fastIoDispatch->MdlReadCompleteCompressed = SfFastIoMdlReadCompleteCompressed;
  29. fastIoDispatch->MdlWriteCompleteCompressed = SfFastIoMdlWriteCompleteCompressed;
  30. fastIoDispatch->FastIoQueryOpen = SfFastIoQueryOpen;
  31. DriverObject->FastIoDispatch = fastIoDispatch;

看看我们的分发函数:
  1. for (i = 0; i <= IRP_MJ_MAXIMUM_FUNCTION; i++) {
  2. DriverObject->MajorFunction[i] = SfPassThrough;//通用分发函数 下发IRP 私用IoSkip 和 IoCallDriver
  3. }
  4. //
  5. // We will use SfCreate for all the create operations
  6. //
  7. DriverObject->MajorFunction[IRP_MJ_CREATE] = SfCreate;
  8. //DriverObject->MajorFunction[IRP_MJ_CREATE_NAMED_PIPE] = SfCreate;
  9. //DriverObject->MajorFunction[IRP_MJ_CREATE_MAILSLOT] = SfCreate;
  10. DriverObject->MajorFunction[IRP_MJ_FILE_SYSTEM_CONTROL] = SfFsControl;//注意这个 当我们下面绑定了文件系统设备,我们就会收到这个IRP 在这个IRP处理函数中 我们就可以//绑定了卷设备对象了
  11. DriverObject->MajorFunction[IRP_MJ_CLEANUP] = SfCleanupClose;
  12. DriverObject->MajorFunction[IRP_MJ_CLOSE] = SfCleanupClose;


Sfilter代码通读分析



将我们驱动的对象保存起来---等会绑定文件系统设备的时候要用到
 gSFilterDriverObject = DriverObject;

首先我们初始化一个链表,这个链表用来内存管理,防止碎片,等会再SfCreate中有很多数据,可能需要频繁的申请内存,这样不便于管理
我们在开头就初始化这个一个链表,当需要用到内存的时候就到这面来拿

  1. ExInitializeFastMutex( &gSfilterAttachLock );
  2. //
  3. // Initialize the lookaside list for name buffering. This is used in
  4. // several places to avoid having a large name buffer on the stack. It is
  5. // also needed by the name lookup routines (NLxxx).
  6. //
  7. ExInitializePagedLookasideList( &gSfNameBufferLookasideList,
  8. NULL,
  9. NULL,
  10. 0,
  11. SFILTER_LOOKASIDE_SIZE,
  12. SFLT_POOL_TAG_NAME_BUFFER,
  13. 0 );

接下来就是创建设备对象
首先在
"\\FileSystem\\Filters\\SFilterDrv"

中创建,如果创建失败就在

"\\FileSystem\\SFilterDrv"

中创建

与NTMod不同的是 NTmodel中我们创建对象的时候对象是UNKNOW_DEVICE 这里我们创建的是磁盘文件系统设备

  1. RtlInitUnicodeString( &nameString, L"\\FileSystem\\Filters\\SFilterDrv" );
  2. status = IoCreateDevice( DriverObject,
  3. 0, //has no device extension
  4. &nameString,
  5. FILE_DEVICE_DISK_FILE_SYSTEM,
  6. FILE_DEVICE_SECURE_OPEN,
  7. FALSE,
  8. &gSFilterControlDeviceObject );//保存下来 后面判断是发给我们的驱动的还是发给文件系统的
  9. if (status == STATUS_OBJECT_PATH_NOT_FOUND) {
  10. //
  11. // This must be a version of the OS that doesn't have the Filters
  12. // path in its namespace. This was added in Windows XP.
  13. //
  14. // We will try just putting our control device object in the
  15. // \FileSystem portion of the object name space.
  16. //
  17. RtlInitUnicodeString( &nameString, L"\\FileSystem\\SFilterDrv" );
  18. status = IoCreateDevice( DriverObject,
  19. 0, //has no device extension
  20. &nameString,
  21. FILE_DEVICE_DISK_FILE_SYSTEM,
  22. FILE_DEVICE_SECURE_OPEN,
  23. FALSE,
  24. &gSFilterControlDeviceObject );
  25. if (!NT_SUCCESS( status )) {
  26. KdPrint( ("SFilter!DriverEntry: Error creating control device object \"%wZ\", status=%08x\n",
  27. &nameString,
  28. status ));
  29. return status;
  30. }
  31. } else if (!NT_SUCCESS( status )) {
  32. KdPrint(( "SFilter!DriverEntry: Error creating control device object \"%wZ\", status=%08x\n",
  33. &nameString, status ));
  34. return status;
  35. }


    然后就是设置我们的通信模式了
    默认是BUFFIO
   
 gSFilterControlDeviceObject->Flags |= DO_BUFFERED_IO; //gSFilterControlDeviceObject是我们保存的我们驱动的对象



    接下去就是初始化分发函数了
    
  
  1. for (i = 0; i <= IRP_MJ_MAXIMUM_FUNCTION; i++) {
  2. DriverObject->MajorFunction[i] = SfPassThrough;
  3. }
  4. //
  5. // We will use SfCreate for all the create operations
  6. //
  7. //还有其他的 比如读写 改 删
  8. /*
  9. FilterCreate(创建)
  10. FilterRead(一般不拦,加解密处理)
  11. FilterWrite(修改,加解密处理)
  12. FilterSetInfo(删,重命名)
  13. FilterClose(一般不拦)
  14. FilterClean(写关闭等)
  15. */
  16. DriverObject->MajorFunction[IRP_MJ_CREATE] = SfCreate;
  17. //DriverObject->MajorFunction[IRP_MJ_CREATE_NAMED_PIPE] = SfCreate;
  18. //DriverObject->MajorFunction[IRP_MJ_CREATE_MAILSLOT] = SfCreate;
  19. 注意这个 当我们下面绑定了文件系统设备,我们就会收到这个IRP 在这个IRP处理函数中 我们就可以//绑定了卷设备对象了
  20. DriverObject->MajorFunction[IRP_MJ_FILE_SYSTEM_CONTROL] = SfFsControl;//卷设备绑定
  21. DriverObject->MajorFunction[IRP_MJ_CLEANUP] = SfCleanupClose;
  22. DriverObject->MajorFunction[IRP_MJ_CLOSE] = SfCleanupClose;


前面说过了,不但有Dispatch分发函数 也有 fastIoDispatch
  1. fastIoDispatch = ExAllocatePoolWithTag( NonPagedPool,
  2. sizeof( FAST_IO_DISPATCH ),
  3. SFLT_POOL_TAG_FASTIO );
  4. if (!fastIoDispatch) {
  5. IoDeleteDevice( gSFilterControlDeviceObject );
  6. return STATUS_INSUFFICIENT_RESOURCES;
  7. }
  8. RtlZeroMemory( fastIoDispatch, sizeof( FAST_IO_DISPATCH ) );
  9. fastIoDispatch->SizeOfFastIoDispatch = sizeof( FAST_IO_DISPATCH );
  10. fastIoDispatch->FastIoCheckIfPossible = SfFastIoCheckIfPossible;
  11. fastIoDispatch->FastIoRead = SfFastIoRead;
  12. fastIoDispatch->FastIoWrite = SfFastIoWrite;
  13. fastIoDispatch->FastIoQueryBasicInfo = SfFastIoQueryBasicInfo;
  14. fastIoDispatch->FastIoQueryStandardInfo = SfFastIoQueryStandardInfo;
  15. fastIoDispatch->FastIoLock = SfFastIoLock;
  16. fastIoDispatch->FastIoUnlockSingle = SfFastIoUnlockSingle;
  17. fastIoDispatch->FastIoUnlockAll = SfFastIoUnlockAll;
  18. fastIoDispatch->FastIoUnlockAllByKey = SfFastIoUnlockAllByKey;
  19. fastIoDispatch->FastIoDeviceControl = SfFastIoDeviceControl;
  20. fastIoDispatch->FastIoDetachDevice = SfFastIoDetachDevice;
  21. fastIoDispatch->FastIoQueryNetworkOpenInfo = SfFastIoQueryNetworkOpenInfo;
  22. fastIoDispatch->MdlRead = SfFastIoMdlRead;
  23. fastIoDispatch->MdlReadComplete = SfFastIoMdlReadComplete;
  24. fastIoDispatch->PrepareMdlWrite = SfFastIoPrepareMdlWrite;
  25. fastIoDispatch->MdlWriteComplete = SfFastIoMdlWriteComplete;
  26. fastIoDispatch->FastIoReadCompressed = SfFastIoReadCompressed;
  27. fastIoDispatch->FastIoWriteCompressed = SfFastIoWriteCompressed;
  28. fastIoDispatch->MdlReadCompleteCompressed = SfFastIoMdlReadCompleteCompressed;
  29. fastIoDispatch->MdlWriteCompleteCompressed = SfFastIoMdlWriteCompleteCompressed;
  30. fastIoDispatch->FastIoQueryOpen = SfFastIoQueryOpen;
  31. DriverObject->FastIoDispatch = fastIoDispatch;


然后就是初始化一些回调函数被设置在我们的驱动对象上 也就是注册回调函数
  1. FS_FILTER_CALLBACKS fsFilterCallbacks;
  2. if (NULL != gSfDynamicFunctions.RegisterFileSystemFilterCallbacks) {
  3. //
  4. // Setup the callbacks for the operations we receive through
  5. // the FsFilter interface.
  6. //
  7. // NOTE: You only need to register for those routines you really
  8. // need to handle. SFilter is registering for all routines
  9. // simply to give an example of how it is done.
  10. //
  11. fsFilterCallbacks.SizeOfFsFilterCallbacks = sizeof( FS_FILTER_CALLBACKS );
  12. fsFilterCallbacks.PreAcquireForSectionSynchronization = SfPreFsFilterPassThrough;
  13. fsFilterCallbacks.PostAcquireForSectionSynchronization = SfPostFsFilterPassThrough;
  14. fsFilterCallbacks.PreReleaseForSectionSynchronization = SfPreFsFilterPassThrough;
  15. fsFilterCallbacks.PostReleaseForSectionSynchronization = SfPostFsFilterPassThrough;
  16. fsFilterCallbacks.PreAcquireForCcFlush = SfPreFsFilterPassThrough;
  17. fsFilterCallbacks.PostAcquireForCcFlush = SfPostFsFilterPassThrough;
  18. fsFilterCallbacks.PreReleaseForCcFlush = SfPreFsFilterPassThrough;
  19. fsFilterCallbacks.PostReleaseForCcFlush = SfPostFsFilterPassThrough;
  20. fsFilterCallbacks.PreAcquireForModifiedPageWriter = SfPreFsFilterPassThrough;
  21. fsFilterCallbacks.PostAcquireForModifiedPageWriter = SfPostFsFilterPassThrough;
  22. fsFilterCallbacks.PreReleaseForModifiedPageWriter = SfPreFsFilterPassThrough;
  23. fsFilterCallbacks.PostReleaseForModifiedPageWriter = SfPostFsFilterPassThrough;
  24. status = (gSfDynamicFunctions.RegisterFileSystemFilterCallbacks)( DriverObject,
  25. &fsFilterCallbacks );
  26. if (!NT_SUCCESS( status )) {
  27. DriverObject->FastIoDispatch = NULL;
  28. ExFreePoolWithTag( fastIoDispatch, SFLT_POOL_TAG_FASTIO );
  29. IoDeleteDevice( gSFilterControlDeviceObject );
  30. return status;
  31. }
  32. }


接下去就是创建一个回调,监视文件系统设备创建, 在这个回调中,我们可以监控文件系统创建和绑定文件系统设备
  1. status = IoRegisterFsRegistrationChange( DriverObject, SfFsNotification );//在里面绑定文件系统设备,也是卷设备绑定的前提条件
  2. if (!NT_SUCCESS( status )) {
  3. KdPrint(( "SFilter!DriverEntry: Error registering FS change notification, status=%08x\n",
  4. status ));
  5. DriverObject->FastIoDispatch = NULL;
  6. ExFreePoolWithTag( fastIoDispatch, SFLT_POOL_TAG_FASTIO );
  7. IoDeleteDevice( gSFilterControlDeviceObject );
  8. return status;
  9. }


然后就是对文件计数引用
  1. /*
  2. IoGetDeviceObjectPointer函数的功能是:
  3. 它从下层的设备对象名称来获得下层设备指针。该函数造成了对下层设备对象以及下层设备对象所对应的文件对象的引用。
  4. 如果本层驱动在卸载之前对下层的设备对象的引用还没有消除,则下层驱动的卸载会被停止。因此必须要消除对下层设备对象的引用。
  5. 但是程序一般不会直接对下层设备对象的引用减少。因此只要减少对文件对象的引用就可以减少文件对象和设备对象两个对象的引用。
  6. 事实上,IoGetDeviceObjectPointer返回的并不是下层设备对象的指针,而是该设备堆栈中顶层的设备对象的指针。
  7. IoGetDeviceObjectPointer函数的调用必须在 IRQL=PASSIVE_LEVEL的级别上运行。
  8. */
  9. {
  10. PDEVICE_OBJECT rawDeviceObject;
  11. PFILE_OBJECT fileObject;
  12. //
  13. // Attach to RawDisk device
  14. //
  15. RtlInitUnicodeString( &nameString, L"\\Device\\RawDisk" );
  16. status = IoGetDeviceObjectPointer(
  17. &nameString,
  18. FILE_READ_ATTRIBUTES,
  19. &fileObject,
  20. &rawDeviceObject );
  21. if (NT_SUCCESS( status )) {
  22. SfFsNotification( rawDeviceObject, TRUE );//绑定RawDisk 激活这个设备 然后被我们捕捉到,绑定过滤设备
  23. ObDereferenceObject( fileObject );
  24. }
  25. //
  26. // Attach to the RawCdRom device
  27. //
  28. RtlInitUnicodeString( &nameString, L"\\Device\\RawCdRom" );
  29. status = IoGetDeviceObjectPointer(
  30. &nameString,
  31. FILE_READ_ATTRIBUTES,
  32. &fileObject,
  33. &rawDeviceObject );
  34. if (NT_SUCCESS( status )) {
  35. SfFsNotification( rawDeviceObject, TRUE );//同上
  36. ObDereferenceObject( fileObject );
  37. }
  38. }
  39. //
  40. // Clear the initializing flag on the control device object since we
  41. // have now successfully initialized everything.
  42. //
  43. ClearFlag( gSFilterControlDeviceObject->Flags, DO_DEVICE_INITIALIZING );
  44. DbgPrint("Sfilter installed\n");
  45. return STATUS_SUCCESS;
  46. }

去看看SfFsNotification
//回调例程,当文件系统被激活或者撤销时调用//在该例程中,完成对文件系统控制设备对象的绑定.
绑定文件系统: 首先,我们需要知道当前系统中都有那些文件系统,例如:NTFS,FAT32,CDFS。因为,卷设备对象是由文件系统创建的。 其次,我们要知道什么时候去绑定文件系统。当一个卷设备对象动态产生的时候,其对应的文件系统就被激活。例如,如果一个FAT32的U盘被插入到电脑上,则对应的FAT32文件系统就会被激活,并创建一个“J:”的卷设备对象。 IoRegisterFsRegistrationChange是一个非常有用的系统调用。这个调用注册一个回调函数,当系统中有文件系统被激活或者撤销时,该回调函数就被调用。OhGood,那么我们就可以在这个回调函数中去绑定文件系统的控制设备对象。 这里要注意:文件系统的加载和卷的挂载是两码事,卷的挂载是建立在文件系统被激活的基础上。当一个文件系统被激活后,才能创建卷设备对象
SfFsNotification是我们要注册的回调函数,它调用SfAttachToFileSystemDevice完成真正的设备绑定。当然,它还有其他功能,代码说明问题。 SfAttachToFileSystemDevice创建过滤设备对象,并调用我们设备绑定外包函数:SfAttachDeviceToDeviceStack来将过滤设备对象绑定到文件系统控制设备对象的设备栈上。 这样,我们的过滤设备对象就能接受到发送到FSCDO的IRP_MJ_FILE_SYSTEM_CONTRO的请求,动态监控卷的挂载。那么以后的工作就是完成对卷的监控绑定了。
回调例程,当文件系统被激活或者撤销时调用
 //在该例程中,完成对文件系统控制设备对象的绑定.// 
 //例程描述:
 这个例程在文件系统激活或者销毁的时被调用
 这个例程创建一个设备对象将它附加到指定的文件系统控制设备对象
 //的对象栈上,这就允许这个设备对象过滤所有发送给文件系统的请求.
 //这样,我们就能获得一个挂载卷的请求,就可以附加到这个新的卷设备对象
 //的设备对象栈上//
  //参数:
  DeviceObject:指向被激活或者撤销的文件系统的控制设备对象
  FsActive:激活或者撤销标志
  1. {
  2. PNAME_CONTROL devName;
  3. PAGED_CODE();
  4. //
  5. // Display the names of all the file system we are notified of
  6. //
  7. devName = NLGetAndAllocateObjectName( DeviceObject,
  8. &gSfNameBufferLookasideList );
  9. if (devName == NULL) {
  10. SF_LOG_PRINT( SFDEBUG_DISPLAY_ATTACHMENT_NAMES,
  11. ("SFilter!SfFsNotification: Not attaching to %p, insufficient resources.\n",
  12. DeviceObject) );
  13. return;
  14. }
  15. SF_LOG_PRINT( SFDEBUG_DISPLAY_ATTACHMENT_NAMES,
  16. ("SFilter!SfFsNotification: %s %p \"%wZ\" (%s)\n",
  17. (FsActive) ? "Activating file system " : "Deactivating file system",
  18. DeviceObject,
  19. &devName->Name,
  20. GET_DEVICE_TYPE_NAME(DeviceObject->DeviceType)) );
  21. //
  22. // Handle attaching/detaching from the given file system.
  23. //
  24. if (FsActive) {
  25. SfAttachToFileSystemDevice( DeviceObject, devName );
  26. } else {
  27. SfDetachFromFileSystemDevice( DeviceObject );
  28. }
  29. //
  30. // We're done with name (SfAttachToFileSystemDevice copies the name to
  31. // the device extension) so free it.
  32. //
  33. NLFreeNameControl( devName, &gSfNameBufferLookasideList );
  34. }


这个绑定了文件设备对象,这时我们就可以收到IRP_MJ_FILE_SYSTEM_CONTROL IRP 在这个例程里我们就可以实现对卷的绑定

(注意 这里必须绑定了文件设备后才能收到哦)

看下IRP_MJ_FILE_SYSTEM_CONTROL的例程

  1. switch (irpSp->MinorFunction) {
  2. case IRP_MN_MOUNT_VOLUME://设备在Mount的时候,我们就要进行绑定了
  3. return SfFsControlMountVolume( DeviceObject, Irp );
  4. case IRP_MN_LOAD_FILE_SYSTEM:
  5. return SfFsControlLoadFileSystem( DeviceObject, Irp );
  6. case IRP_MN_USER_FS_REQUEST:
  7. {
  8. switch (irpSp->Parameters.FileSystemControl.FsControlCode) {
  9. case FSCTL_DISMOUNT_VOLUME:
  10. {
  11. PSFILTER_DEVICE_EXTENSION devExt = DeviceObject->DeviceExtension;
  12. SF_LOG_PRINT( SFDEBUG_DISPLAY_ATTACHMENT_NAMES,
  13. ("SFilter!SfFsControl: Dismounting volume %p \"%wZ\"\n",
  14. devExt->NLExtHeader.AttachedToDeviceObject,
  15. &devExt->NLExtHeader.DeviceName) );
  16. break;
  17. }
  18. }
  19. break;
  20. }
  21. }
  22. //
  23. // Pass all other file system control requests through.
  24. //
  25. IoSkipCurrentIrpStackLocation( Irp );
  26. return IoCallDriver( ((PSFILTER_DEVICE_EXTENSION)DeviceObject->DeviceExtension)->NLExtHeader.AttachedToDeviceObject,
  27. Irp );
  28. }


差不多就是这样,我们来看看其它分发函数
比如SfCreate
我们要做的工作就是在分发函数里写代码
  1. NTSTATUS
  2. SfCreate (
  3. IN PDEVICE_OBJECT DeviceObject,
  4. IN PIRP Irp
  5. )
  6. /*++
  7. Routine Description:
  8. This function filters create/open operations. It simply establishes an
  9. I/O completion routine to be invoked if the operation was successful.
  10. Arguments:
  11. DeviceObject - Pointer to the target device object of the create/open.
  12. Irp - Pointer to the I/O Request Packet that represents the operation.
  13. Return Value:
  14. The function value is the status of the call to the file system's entry
  15. point.
  16. --*/
  17. {
  18. NTSTATUS status;
  19. PNAME_CONTROL fileName = NULL;
  20. PSFILTER_DEVICE_EXTENSION devExt = (PSFILTER_DEVICE_EXTENSION)(DeviceObject->DeviceExtension);
  21. PIO_STACK_LOCATION irpSp = IoGetCurrentIrpStackLocation( Irp );
  22. BOOLEAN cacheName;
  23. PAGED_CODE();
  24. //
  25. // If this is for our control device object, don't allow it to be opened.
  26. //
  27. //如果是我们的设备 返回成功 这个宏判断设备对象是不是我们保存那个和是不是我们的驱动对象
  28. if (IS_MY_CONTROL_DEVICE_OBJECT(DeviceObject)) {
  29. //
  30. // Sfilter doesn't allow for any communication through its control
  31. // device object, therefore it fails all requests to open a handle
  32. // to its control device object.
  33. //
  34. // See the FileSpy sample for an example of how to allow creates to
  35. // the filter's control device object and manage communication via
  36. // that handle.
  37. //
  38. //Irp->IoStatus.Status = STATUS_INVALID_DEVICE_REQUEST;
  39. Irp->IoStatus.Status = STATUS_SUCCESS;
  40. Irp->IoStatus.Information = 0;
  41. IoCompleteRequest( Irp, IO_NO_INCREMENT );
  42. return STATUS_INVALID_DEVICE_REQUEST;
  43. //return STATUS_SUCCESS;
  44. }
  45. ASSERT(IS_MY_DEVICE_OBJECT( DeviceObject ));
  46. //
  47. // If debugging is enabled, do the processing required to see the packet
  48. // upon its completion. Otherwise, let the request go with no further
  49. // processing.
  50. //
  51. if (!FlagOn( SfDebug, SFDEBUG_DO_CREATE_COMPLETION |
  52. SFDEBUG_GET_CREATE_NAMES|
  53. SFDEBUG_DISPLAY_CREATE_NAMES )) {
  54. //
  55. // We don't want to get filenames, display filenames, or
  56. // call our completion routine. Don't put us on the stack
  57. // and call the next driver.
  58. //
  59. //如果是Deug 就不显示 直接下发
  60. IoSkipCurrentIrpStackLocation( Irp );
  61. return IoCallDriver( ((PSFILTER_DEVICE_EXTENSION) DeviceObject->DeviceExtension)->NLExtHeader.AttachedToDeviceObject,
  62. Irp );
  63. }
  64. //如果要显示一下信息
  65. if (FlagOn( SfDebug, SFDEBUG_GET_CREATE_NAMES |
  66. SFDEBUG_DISPLAY_CREATE_NAMES ) &&
  67. !FlagOn(devExt->Flags,SFDEVFL_DISABLE_VOLUME)) {
  68. //
  69. // Debugging specifies that we need to get the filename
  70. //
  71. NAME_LOOKUP_FLAGS LookupFlags = 0x00000000;
  72. //
  73. // If DosName has been set, indicate via flags that we
  74. // want to use it when getting the full file name.
  75. //
  76. if (devExt->NLExtHeader.DosName.Length != 0) {
  77. SetFlag( LookupFlags, NLFL_USE_DOS_DEVICE_NAME );
  78. }
  79. //
  80. // Indicate we are in pre-create
  81. //
  82. SetFlag( LookupFlags, NLFL_IN_CREATE );
  83. if (FlagOn( irpSp->Parameters.Create.Options, FILE_OPEN_BY_FILE_ID )) {
  84. //
  85. // The file is being opened by ID, not file name.
  86. //
  87. SetFlag( LookupFlags, NLFL_OPEN_BY_ID );
  88. }
  89. if (FlagOn( irpSp->Flags, SL_OPEN_TARGET_DIRECTORY )) {
  90. //
  91. // The file's parent directory should be opened
  92. //
  93. SetFlag( LookupFlags, NLFL_OPEN_TARGET_DIR );
  94. }
  95. //
  96. // Retrieve the file name. Note that in SFilter we don't do any name
  97. // caching.
  98. //
  99. //申请存储数据的空间
  100. status = NLAllocateNameControl( &fileName, &gSfNameBufferLookasideList );
  101. if (NT_SUCCESS( status )) {
  102. //
  103. // We are okay not checking the return value here because
  104. // the GetFullPathName function will set the Unicode String
  105. // length to 0. So either way, in an error it will print an empty string
  106. //
  107. //拿到文件的全路径
  108. status = NLGetFullPathName( irpSp->FileObject,
  109. fileName,
  110. &devExt->NLExtHeader,
  111. LookupFlags,
  112. &gSfNameBufferLookasideList,
  113. &cacheName );
  114. }
  115. }
  116. //往下发
  117. if (FlagOn( SfDebug, SFDEBUG_DISPLAY_CREATE_NAMES |
  118. SFDEBUG_DO_CREATE_COMPLETION ) &&
  119. !FlagOn(devExt->Flags,SFDEVFL_DISABLE_VOLUME)) {
  120. //
  121. // Debugging flags indicate we must do completion.
  122. // Note that to display file names we must do completion
  123. // because we don't know IoStatus.Status and IoStatus.Information
  124. // until post-create.
  125. //
  126. KEVENT waitEvent;
  127. //
  128. // Initialize an event to wait for the completion routine to occur
  129. //
  130. KeInitializeEvent( &waitEvent, NotificationEvent, FALSE );
  131. //
  132. // Copy the stack and set our Completion routine
  133. //
  134. 设置完成例程
  135. IoCopyCurrentIrpStackLocationToNext( Irp );
  136. IoSetCompletionRoutine(
  137. Irp,
  138. SfCreateCompletion,
  139. &waitEvent,
  140. TRUE,
  141. TRUE,
  142. TRUE );
  143. //
  144. // Call the next driver in the stack.
  145. //
  146. status = IoCallDriver( devExt->NLExtHeader.AttachedToDeviceObject, Irp );
  147. //
  148. // Wait for the completion routine to be called
  149. //
  150. if (STATUS_PENDING == status) {
  151. NTSTATUS localStatus = KeWaitForSingleObject( &waitEvent,
  152. Executive,
  153. KernelMode,
  154. FALSE,
  155. NULL );
  156. ASSERT(STATUS_SUCCESS == localStatus);
  157. }
  158. //
  159. // Verify the IoCompleteRequest was called
  160. //
  161. ASSERT(KeReadStateEvent(&waitEvent) ||
  162. !NT_SUCCESS(Irp->IoStatus.Status));
  163. //
  164. // If debugging indicates we should display file names, do it.
  165. //
  166. //打印信息
  167. if (irpSp->Parameters.Create.Options & FILE_OPEN_BY_FILE_ID) {
  168. SF_LOG_PRINT( SFDEBUG_DISPLAY_CREATE_NAMES,
  169. ("SFilter!SfCreate: OPENED fo=%p %08x:%08x %wZ (FID)\n",
  170. irpSp->FileObject,
  171. Irp->IoStatus.Status,
  172. Irp->IoStatus.Information,
  173. &fileName->Name) );
  174. } else {
  175. SF_LOG_PRINT( SFDEBUG_DISPLAY_CREATE_NAMES,
  176. ("SFilter!SfCreate: OPENED fo=%p st=%08x:%08x %wZ\n",
  177. irpSp->FileObject,
  178. Irp->IoStatus.Status,
  179. Irp->IoStatus.Information,
  180. &fileName->Name) );
  181. }
  182. //
  183. // Release the name control structure if we have
  184. //
  185. if (fileName != NULL) {
  186. NLFreeNameControl( fileName, &gSfNameBufferLookasideList );
  187. }
  188. //
  189. // Save the status and continue processing the IRP
  190. //
  191. status = Irp->IoStatus.Status;
  192. IoCompleteRequest( Irp, IO_NO_INCREMENT );
  193. return status;
  194. } else {
  195. //
  196. // Free the name control if we have one
  197. //
  198. if (fileName != NULL) {
  199. NLFreeNameControl( fileName, &gSfNameBufferLookasideList );
  200. }
  201. //
  202. // Debugging flags indicate we did not want to display the file name
  203. // or call completion routine.
  204. // (ie SFDEBUG_GET_CREATE_NAMES && !SFDEBUG_DO_CREATE_COMPLETION)
  205. //
  206. IoSkipCurrentIrpStackLocation( Irp );
  207. return IoCallDriver( ((PSFILTER_DEVICE_EXTENSION) DeviceObject->DeviceExtension)->NLExtHeader.AttachedToDeviceObject,
  208. Irp );
  209. }
  210. }


再来看看分发函数
非过滤驱动中的默CommonDispatch

  1. NTSTATUS CommonDispatch(
  2. PDEVICE_OBJECT DeviceObject,
  3. PIRP Irp)
  4. {
  5. Irp->IoStatus.Status = STATUS_SUCCESS;
  6. IoCompleteRequest(Irp,IO_NO_INCREMENT);
  7. return STATUS_SUCCESS;
  8. }



过滤驱动中CommonDispatch写法


  1. NTSTATUS SfPassThrough (
  2. IN PDEVICE_OBJECT DeviceObject,
  3. IN PIRP Irp
  4. )
  5. {
  6. PIO_STACK_LOCATION pIrp = IoGetCurrentIrpStackLocation( Irp );
  7. ASSERT(!IS_MY_CONTROL_DEVICE_OBJECT( DeviceObject ));
  8. ASSERT(IS_MY_DEVICE_OBJECT( DeviceObject ));
  9. if (!IS_MY_DEVICE_OBJECT(DeviceObject) ||
  10. IS_MY_CONTROL_DEVICE_OBJECT(DeviceObject))
  11. {
  12. NTSTATUS status = Irp->IoStatus.Status = STATUS_INVALID_PARAMETER;
  13. IoCompleteRequest( Irp, IO_NO_INCREMENT );
  14. return status;
  15. }
  16. IoSkipCurrentIrpStackLocation( Irp );
  17. return IoCallDriver( ((PSFILTER_DEVICE_EXTENSION) DeviceObject->DeviceExtension)->NLExtHeader.AttachedToDeviceObject,
  18. Irp );
  19. }

一个是结束IRP 一个是判断是不是发给我们驱动的 如果是就返回无效的参数,(这里是返回失败,但在拦截的分发函数中是需要放行的这个放行是返回success)

对一个主防来说:
  1. FilterCreate(创建)
  2. FilterRead(一般不拦,加解密处理)
  3. FilterWrite(修改,加解密处理)
  4. FilterSetInfo(删,重命名)
  5. FilterClose(一般不拦)
  6. FilterClean(写关闭等)

对于一个主防来说 一般是拦截创建 和 关闭  为什么要拦截关闭呢 一个人现在是好人,但他不一定一辈子都是好人
创建文件 我们放了,然后它写入了一个shellcode 如果我们拦截写的话 一个一个的拦截 效果并不好 在关闭的时候我们就可以一次性知道


深入理解下设备对象
设备对象类别


Sfilter自己的设备
控制设备
过滤设备
其它设备
文件系统设备
卷设备
设备类别
FILE_DEVICE_DISK_FILE_SYSTEM


  1. #define IS_MY_DEVICE_OBJECT(_devObj) \
  2. (((_devObj) != NULL) && \
  3. ((_devObj)->DriverObject == gSFilterDriverObject) && \
  4. ((_devObj)->DeviceExtension != NULL))
  5. #define IS_MY_CONTROL_DEVICE_OBJECT(_devObj) \
  6. (((_devObj) == gSFilterControlDeviceObject) ? \
  7. (ASSERT(((_devObj)->DriverObject == gSFilterDriverObject) && \
  8. ((_devObj)->DeviceExtension == NULL)), TRUE) : \
  9. FALSE)
  10. #define IS_DESIRED_DEVICE_TYPE(_type) \
  11. (((_type) == FILE_DEVICE_DISK_FILE_SYSTEM) || \
  12. ((_type) == FILE_DEVICE_CD_ROM_FILE_SYSTEM) || \
  13. ((_type) == FILE_DEVICE_NETWORK_FILE_SYSTEM))


三种类型的设备处理
  1. NTSTATUS FilterXXX(PDEVICE_OBJECT DeviceObject, PIRP pIrp)//过滤设备
  2. {
  3. NTSTATUS Status = STATUS_SUCCESS;
  4. ULONG ulInfomation = 0;
  5. IO_STACK_LOCATION* lpIrpStack = IoGetCurrentIrpStackLocation(pIrp);
  6. if (IS_MY_CONTROL_DEVICE_OBJECT(DeviceObject))//控制设备
  7. {
  8. //如果需要与R3交互,这里必须返回成功
  9. pIrp->IoStatus.Status = Status;
  10. pIrp->IoStatus.Information = ulInfomation;
  11. IoCompleteRequest(lpIrp, IO_NO_INCREMENT);
  12. }
  13. else if (!IS_MY_DEVICE_OBJECT(DeviceObject))
  14. {
  15. //非法参数
  16. pIrp->IoStatus.Status = Status = STATUS_INVALID_PARAMETER;
  17. pIrp->IoStatus.Information = 0;
  18. IoCompleteRequest(pIrp, IO_NO_INCREMENT);
  19. }
  20. else
  21. {
  22. //这里才是我们要过滤的操作
  23. IoSkipCurrentIrpStackLocation( pIrp );
  24. Status = IoCallDriver(((PSFILTER_DEVICE_EXTENSION)->DeviceExtension)DeviceObject->NLExtHeader.AttachedToDeviceObject, pIrp);
  25. }
  26. return Status;
  27. }



重要问题:文件路径的解析与保存
Namelookup.c(构造IRP,不能用 ObQueryNameString)
NLGetFullPathName()
NLPQueryFileSystemForFileName()IRP查询
在SfCreate里查询名字,通过FILE_OBJECT与Name保存起来。供其它过滤函数中查询使用。
保存在哪里?
List_Entry
HASH
TREE


Sfilter安装测试
原始的Sfilter框架,不支持通信
sfCreate中对自己的控制设备,返回成功
符号链接的创建(框架中没有)
  1. GroupOrder:"FSFilter Activity Monitor“
  2. L\\FileSystem\\SFilterDrv
  3. DeviceObjectFlags |= DO_BUFFERED_IO

不要在release版本使用DriverUnload。BSOD




既然是过滤 那么就有放和禁
FilterCreate:
放:
内核过来的
Irp->RequestorMode == KernelMode 
本进程的
FilterDeviceIoctrl中 PsGetCurrentProcessId()
系统进程的
DriverEntry里:PsGetCurrentProcessId()
文件夹
  1. ulOptions = IrpStack->Parameters.Create.Options ;
  2. FlagOn(IrpStack->FileObject->Flags, FO_VOLUME_OPEN) ||
  3. FlagOn(ulOptions, FILE_DIRECTORY_FILE) ||
  4. FlagOn(IrpStack->Flags, SL_OPEN_PAGING_FILE)
分页IO
  1. (Irp->Flags & IRP_PAGING_IO) ||
  2. (Irp->Flags & IRP_SYNCHRONOUS_PAGING_IO)
  3. KeGetCurrentIrql() > APC_LEVEL
如何放?
  1. IoSkipCurrentIrpStackLocation( lpIrp );
  2. IoCallDriver();

禁:自定义啦 比如禁止在driver目录写入sys文件




拿文件名/长短名转化 短文转长名
匹配规则
FsRtlIsNameInExpression 
弹框

STATUS_ACCESS_DENIED

Pending完成例程
加入表中,供其它Filter函数使用




FilterWrite:
放:
本进程
系统进程
分页IO
从表中根据file_object没有找到的
匹配规则
弹框
拦截

效率
多次写?




FilterSetInfo:拦截删除,重命名等

IrpStack->Parameters.SetFile.FileInformationClass
FileRenameInformation//重命名
如何拿重命名之后的文件名?
IrpSp->Parameters.SetFile.FileObject//测试无效
IrpSp->FileObject//测试无效

Irp->AssociatedIrp.SystemBuffer;//有效

  1. //R3UserResult = hipsGetResultFromUser(szOper, lpNameControl->Name.Buffer, ((PFILE_RENAME_INFORMATION)(lpIrp->AssociatedIrp.SystemBuffer))->FileName, User_DefaultNon);
  2. if(R3UserResult == User_Block)
  3. <span style="white-space:pre"></span>goto Ex;
  4. DbgPrint("%ws\n",((PFILE_RENAME_INFORMATION)(lpIrp->AssociatedIrp.SystemBuffer))->FileName);
fileObject->DeviceObject
RtlVolumeDeviceToDosName
ObQueryNameString
IoQueryFileDosDeviceName
FileDispositionInformation//删除
删除
例子: 这个例子中还没拿到重命名后的文件名 和将重命名后的文件名传给R3 (hipsGetResultFromUser的第三个参数)
  1. NTSTATUS
  2. sfSetInformation(PDEVICE_OBJECT lpDevice, PIRP lpIrp)
  3. {
  4. NTSTATUSStatus= STATUS_SUCCESS;
  5. ULONGulInfomation= 0;
  6. UNICODE_STRINGustrRule= {0};
  7. PLIST_ENTRYCurrentList= NULL;
  8. USER_RESULTR3UserResult= User_Pass;
  9. PNAME_CONTROLlpNameControl= NULL;
  10. BOOLEANbSkipped= FALSE;
  11. BOOLEANbNeedPostOp= FALSE;
  12. BOOLEANbRename= FALSE;
  13. IO_STACK_LOCATION*lpIrpStack= IoGetCurrentIrpStackLocation(lpIrp);
  14. PFILE_OBJECTlpFileObject= lpIrpStack->FileObject;
  15. PTWOWAYpTwoWay= NULL;
  16. WCHAR*szOper= NULL;
  17. if (IS_MY_CONTROL_DEVICE_OBJECT(lpDevice))
  18. {
  19. lpIrp->IoStatus.Status = Status;
  20. lpIrp->IoStatus.Information = ulInfomation;
  21. IoCompleteRequest(lpIrp, IO_NO_INCREMENT);
  22. return Status;
  23. }
  24. else if (!IS_MY_DEVICE_OBJECT(lpDevice))
  25. {
  26. lpIrp->IoStatus.Status = Status = STATUS_INVALID_PARAMETER;
  27. lpIrp->IoStatus.Information = 0;
  28. IoCompleteRequest(lpIrp, IO_NO_INCREMENT);
  29. return Status;
  30. }
  31. else
  32. {
  33. PSFILTER_DEVICE_EXTENSION lpDevExt = (PSFILTER_DEVICE_EXTENSION)(lpDevice->DeviceExtension);
  34. if (PsGetCurrentProcessId() == g_hSystemProcID)
  35. {
  36. bSkipped = TRUE;
  37. goto _EXIT;
  38. }
  39. //从HASH表中获得文件名
  40. pTwoWay = Find((DWORD)lpFileObject, g_pHashTable);
  41. if (pTwoWay == NULL)
  42. {
  43. bSkipped = TRUE;
  44. goto _EXIT;
  45. }
  46. lpNameControl = pTwoWay->data.lpNameControl;
  47. if (lpIrpStack->Parameters.SetFile.FileInformationClass == FileRenameInformation ||
  48. lpIrpStack->Parameters.SetFile.FileInformationClass == FileBasicInformation ||
  49. lpIrpStack->Parameters.SetFile.FileInformationClass == FileAllocationInformation ||
  50. lpIrpStack->Parameters.SetFile.FileInformationClass == FileEndOfFileInformation ||
  51. lpIrpStack->Parameters.SetFile.FileInformationClass == FileDispositionInformation)
  52. {
  53. switch (lpIrpStack->Parameters.SetFile.FileInformationClass)
  54. {
  55. case FileAllocationInformation:
  56. case FileEndOfFileInformation:
  57. szOper = L"设置大小";
  58. bSkipped = TRUE;
  59. goto _EXIT;
  60. // break;
  61. case FileRenameInformation:
  62. szOper = L"重命名";
  63. break;
  64. case FileBasicInformation:
  65. szOper = L"设置基础信息";
  66. bSkipped = TRUE;
  67. goto _EXIT;
  68. //break;
  69. case FileDispositionInformation:
  70. bNeedPostOp = TRUE;
  71. szOper = L"删除";
  72. break;
  73. }
  74. }
  75. else
  76. {
  77. // 允许
  78. bSkipped = TRUE;
  79. goto _EXIT;
  80. }
  81. RtlInitUnicodeString(&ustrRule, L"C:\\WINDOWS\\SYSTEM32\\*\\*.SYS");
  82. if (!IsPatternMatch(&ustrRule, &lpNameControl->Name, TRUE))
  83. {
  84. bSkipped = TRUE;
  85. goto _EXIT;
  86. }
  87. if (lpIrpStack->Parameters.SetFile.FileInformationClass == FileRenameInformation)
  88. {
  89. //重命名的目标路径?
  90. }
  91. R3UserResult = hipsGetResultFromUser(szOper, lpNameControl->Name.Buffer, NULL, User_DefaultNon);
  92. if (R3UserResult == User_Block)
  93. {
  94. // 禁止
  95. lpIrp->IoStatus.Information = 0;
  96. lpIrp->IoStatus.Status = STATUS_ACCESS_DENIED;
  97. IoCompleteRequest(lpIrp, IO_NO_INCREMENT);
  98. Status = STATUS_ACCESS_DENIED;
  99. bSkipped = FALSE;
  100. goto _EXIT;
  101. }
  102. bSkipped = TRUE;
  103. }
  104. _EXIT:
  105. if (bSkipped)
  106. {
  107. KEVENT waitEvent;
  108. IoCopyCurrentIrpStackLocationToNext(lpIrp);
  109. KeInitializeEvent(&waitEvent,
  110. NotificationEvent,
  111. FALSE);
  112. IoSetCompletionRoutine(lpIrp,
  113. SetFilterCompletion,
  114. &waitEvent,
  115. TRUE,
  116. TRUE,
  117. TRUE);
  118. Status = IoCallDriver(
  119. ((PSFILTER_DEVICE_EXTENSION)lpDevice->DeviceExtension)->NLExtHeader.AttachedToDeviceObject,
  120. lpIrp);
  121. if (Status == STATUS_PENDING)
  122. {
  123. Status = KeWaitForSingleObject(&waitEvent,
  124. Executive,
  125. KernelMode,
  126. FALSE,
  127. NULL);
  128. }
  129. Status = lpIrp->IoStatus.Status;
  130. IoCompleteRequest(lpIrp, IO_NO_INCREMENT);
  131. }
  132. return Status;
  133. }



参考:
http://blog.csdn.net/cosmoslife/article/details/7727688
http://bbs./archive/index.php?t-152338.html                 

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多