点击此处查看最新的网赚项目教程

第3章 微过滤驱动与模块执行防御(下)

3.3 利用微过滤捕获文件改名

除了写操作之外,文件的改名(对文件系统来说,改名和移动是等价的,3.2.5节中详细介绍)同样需要捕获。原因是可疑库中保存的是文件路径。如果一个可疑库中的文件进行了改名,那么显然必须同步更新可疑库中保存的路径,否则就会发生“可疑逃逸”而成为不可疑的文件。

对文件改名操作的捕获和对写操作的捕获类似,都是在请求前回调和后回调中做相应处理即可。但值得注意的是,微软的文件系统中并没有专门的“改名”请求。改名操作是通过设置请求(3.2.5节中详细介绍)来实现的。

3.3.1 设置请求的前回调中发现文件改名

本书中的设置请求全称应该是“信息设置(Set Information)请求”,其主功能号为IRP_MJ_SET_INFORMATION。与设置请求相对的还有一个查询请求(Query Information)。设置和查询操作的对象是文件的属性,这些属性由信息类(Information Class)进行区分,有着非常复杂庞大的体系,是文件系统中极为重要的操作。

文件的名字(也包括路径),被认为是文件的属性之一。因此文件的改名也是通过设置请求来实现的。后文将这种特别的设置请求称为改名请求。这里要注意的是文件系统并不认为文件名的修改和文件路径的修改有什么不同。很显然,文件路径的修改会导致文件的移动。因此,文件的移动和文件的改名对文件系统来说是同一件事。

这里常令人疑惑的是实际操作和文件系统请求的对应关系。比如在Windows的资源管理器中将一个文件从一个文件夹拖动到另一个文件夹中的行为,这明显对应着文件的改名请求。但你会发现当你将一个文件从C盘拖动到D盘,文件系统种并不会发生任何改名请求。

原因是文件的改名和移动只能局限在一个卷(Volume)内。C盘、D盘在文件系统看来都是卷。因此类似C盘拖动文件到D盘这种操作必须分解:首先将文件从C盘复制到D盘,然后将C盘上的文件删除。

这种情况虽然没有法被我们的改名请求过滤捕获,但D盘上新建文件的行为显然会被写请求过滤捕获并加入可疑路径,因此不会造成漏洞。

与之相关的另一种操作是文件的删除。在本书的第4章中我还会更详尽地介绍删除操作。这里我们要知道的是,有时[1]删除也是一种设置请求,但设置时指定的信息类不同。但一种常见的操作,即将文件删除到回收站里,这并不是删除,而是一种改名(移动到回收站),我们能捕获到改名请求。

下面首先要将设置请求加入到操作数组中。参考代码3-1,在操作数组的初始化中加入IRP_MJ_SET_INFORMATION的前后操作回调如代码3-12所示。

代码3-12操作数组的初始化中加入IRP_MJ_SET_INFORMATION的前后操作回调     //文件过滤驱动需要过滤的回调     CONST FLT_OPERATION_REGISTRATION callbacks[] = {         {              IRP_MJ_SET_INFORMATION,          FLTFL_OPERATION_REGISTRATION_SKIP_PAGING_IO,              SetInformationIrpProcess,              SetInformationIrpPost         },               {              IRP_MJ_OPERATION_END         }     };

以上代码和写请求过滤时操作数组的初始化如出一辙。接下来是函数SetInformationIrpProcess的代码的实现。这些代码和WriteIrpProcess的实现也是极为类似的。SetInformationIrpProcess具体的实现如代码3-13所示。

代码3-13 SetInformationIrpProcess具体的实现FLT_PREOP_CALLBACK_STATUS     SetInformationIrpProcess(         PFLT_CALLBACK_DATA data,         PCFLT_RELATED_OBJECTS flt_obj,         PVOID* compl_context){     //在这里,我只关心文件的重命名操作。对其他操作不需要后回调。     FLT_PREOP_CALLBACK_STATUS flt_status =         FLT_PREOP_SUCCESS_NO_CALLBACK;     PFILE_OBJECT file_obj = data->Iopb->TargetFileObject;     NTSTATUS status = STATUS_SUCCESS;     DUBIOUS_PATH *path = NULL;     FILE_INFORMATION_CLASS file_infor_class =      data->Iopb->Parameters.SetFileInformation.FileInformationClass;    (1     do {         //判断是否重命名请求。          BreakIf(data->Iopb->MajorFunction != IRP_MJ_SET_INFORMATION ||              file_infor_class != FileRenameInformation);         //当请求irql完全不符合标准的时候,这个操作没有法拦截。因为我不知道有任何方法         //可以去获取当前文件路径。但这似乎不应该发生。所以我加了ASSERT。如果一旦发         //生,那么就造成一个漏洞(当然漏洞不一定可以利用)。         ASSERT(KeGetCurrentIrql() <= APC_LEVEL);         BreakIf(KeGetCurrentIrql() > APC_LEVEL);          if (file_infor_class == FileRenameInformation)                  (2         {              path = DubiousGetFilePathAndUpcase(data);                  (3              //如果获取失败了,这没有办法,只能放弃。这也有形成漏洞的可能性。为了封              //闭这一漏洞,我们直接让请求失败              BreakDoIf(path == NULL, status = STATUS_NO_MEMORY);              //判断这个列表是否在我们的可疑列表中。如果不在的,没有需处理。              BreakIf(!IsDubious(path));                                  (4              //到了这里,确认是要处理的,把已经获得的Path记录下来。              *compl_context = (PVOID)path;                               (5              flt_status = FLT_PREOP_SUCCESS_WITH_CALLBACK;               (6         }     } while (0);     //如果中途有解析失败等情况,直接让请求失败。     if (status != STATUS_SUCCESS)     {         data->IoStatus.Status = status;         flt_status = FLT_PREOP_COMPLETE;     }     //清理path。这种情况下path没有需继续传递到后回调。     DoIf(flt_status != FLT_PREOP_SUCCESS_WITH_CALLBACK && path != NULL,         ExFreePool(path));     return flt_status;}

该函数的参数、以及函数进入之后的各种前置条件检查都和3.2节中介绍过的函数WriteIrpProcess类似,这里不再赘述。值得关注的是(1)处。在设置请求中,最重要的是要设置的信息类。该参数为一个枚举类型,可以从data->Iopb->Parameters.SetFileInformation.FileInformationClass中获取。

获得此值之后,如果为FileRenameInformation则说明这是一个文件改名请求。(2)处有相关的判断。如果确认为改名请求,那么我首先要获得改名之前的文件的全路径并全部改为大写,这在3处完成。

接下来判断这个路径是否在可疑库中(也就是说是不是可疑路径)。如果一个路径本身不是可疑路径,那么改名并不会产生新的可疑模块,对系统并没有什么威胁,所以不用处理。但如果一个可疑模块改名,就等于一个可疑模块消失,并新出现了另一个不同路径的可疑模块。所以在(4)处做了判断。

你可以看到我们的判断始终以“BreakIf”为主,将不处理的情况不断跳出,而留下必须处理的情况。这样可以确保逻辑简单,并且做漏洞分析更容易,因为漏洞大概率会在“Break”的节点产生。

如果已经确认这是一个可以路径,那么获得的全部大写化的路径的指针会保存在上下文指针中,以便传递到后回调中处理。和WriteIrpProcess类似,这也是需要后回调处理才能确认请求是否成功,才能进行实质性的操作。

如果在前回调中直接将可疑库中的路径修改掉,就会留下一个经典的漏洞:攻击者只需要发一个一定会失败的重命名请求,比如名字中含有非法字符(一些符号不允许在路径中出现)到内核,这里就会把可疑库中的路径修改掉。接着请求失败,但可疑库中原有的可疑路径已经被修改了。这等于将一个可疑模块设定为白模块,以后可以随意执行了。

因此,为了向内核表明后回调必须被调用,(6)处将本函数返回值设定为FLT_PREOP_SUCCESS_WITH_CALLBACK。至此,本函数的逻辑基本介绍完毕,接下来是后回调中的处理。

3.3.2 文件改名的后回调中调用安全函数

本节讲解文件改名的后回调。这里需要注意的是后回调参数中的上下文指针。根据3.2.5节中的内容,上下文指针已经在前回调中设置为可疑文件的全路径(已全部转换为大写)。这里有一个疑问:既然一定要在后回调中处理,那何不在后回调中再获取文件路径,而要通过上下文指针来传递这么麻烦呢?

需要通过上下文指针传递文件的原始路径的原因是这是一个改名操作。到了后回调中,文件大概率已经被改名成功。此时要获得改名之前的路径可就麻烦了。所以在前回调中获得一次之后,不要释放,而要通过上下文指针传递到后回调中。

和写请求的后回调函数一样,因为很难确认后回调的中断级,真正的处理依然在安全函数中进行。所以后回调主要的工作是设置安全函数并把参数传递给安全回调。后回调函数SetInformationIrpPost的实现如代码3-14所示。

代码3-14 后回调函数SetInformationIrpPost的实现FLT_POSTOP_CALLBACK_STATUS SetInformationIrpPost(     _Inout_ PFLT_CALLBACK_DATA data,     _In_ PCFLT_RELATED_OBJECTS flt_obj,     _In_opt_ PVOID compl_ctx,     _In_ FLT_POST_OPERATION_FLAGS Flags){     PFLT_FILE_NAME_INFORMATION name_info = { 0 };     PFILE_OBJECT file_obj = data->Iopb->TargetFileObject;     FLT_POSTOP_CALLBACK_STATUS ret = FLT_POSTOP_FINISHED_PROCESSING;     FLT_POSTOP_CALLBACK_STATUS ret_status;     BOOLEAN ret_bool = FALSE;     do {         //正常情况下rename都是irp操作          ASSERT(FLT_IS_IRP_OPERATION(data));         BreakIf(!FLT_IS_IRP_OPERATION(data));         ret_bool = FltDoCompletionProcessingWhenSafe(             data, flt_obj, compl_ctx, 0,              SetInformationSafePostCallback, &ret_status);        (1)         //请求没有法列队,也就没有法完成,放弃         BreakIf(!ret_bool);         ret = ret_status;     } while (0);     if (!ret_bool && compl_ctx != NULL)     {         //如果RenameSafePostCallback不能得到执行,那么本函数就要负责         //释放上下文。但遗憾的是这里也有中断级别要求。如果不符合,只能造         //成泄漏。但理论上不会这么悲剧,所以加入ASSERT         ASSERT(KeGetCurrentIrql() <= DISPATCH_LEVEL);         if (KeGetCurrentIrql() <= DISPATCH_LEVEL)                  (2)         {              ExFreePool(compl_ctx);                                (3)         }     }     return ret;}

这些代码和3.2.5节中写请求后回调处理中调用安全函数的代码类似。唯一需要注意的是调用FltDoCompletionProcessingWhenSafe失败的情况。从微软的文档来看,这个函数是可能失败的。我们通过它的返回值可以判断其是否失败。

相关处理见上述代码的(1)处。该函数返回值不是通常的NTSTATUS,而是TRUE和FALSE。FALSE表示失败,既未能正确执行安全函数,也未能将安全函数列入工作线程队列中。

这时候要考虑到安全函数不会再被调用,可疑模块路径自然也不会再被处理,我们只能放弃。但参数compl_ctx中保存的实际上是前回调中获取的文件全路径,其内存是在前回调中分配的。如果后回调直接放弃处理,会造成内核内存泄漏。因此,在(3)处调用的ExFreePool释放了这些内存。

但要注意的是,即便是调用ExFreePool,也是需要确认中断级的!好在微软的文档明确表面所有的后回调中断级都在

———END———
限 时 特 惠: 本站每日持续更新海量各大内部创业教程,一年会员只需98元,全站资源免费下载 点击查看详情
站 长 微 信: cai842612