本系列文章为看雪星星人为看雪安全爱好者创作的原创免费作品。
欢迎评论、交流、转载,转载请保留看雪星星人署名并勿用于商业盈利。
本人水平有限,错漏在所难免,欢迎批评指正。
除了写操作之外,文件的改名(对文件系统来说,改名和移动是等价的, 3.2.5节中详细介绍)同样需要捕获。原因是可疑库中保存的是文件路径。如果一个可疑库中的文件进行了改名,那么显然必须同步更新可疑库中保存的路径,否则就会发生“可疑逃逸”而成为不可疑的文件。
对文件改名操作的捕获和对写操作的捕获类似,都是在请求前回调和后回调中做相应处理即可。但值得注意的是,微软的文件系统中并无专门的“改名”请求。改名操作是通过设置请求( 3.2.5节中详细介绍)来实现的。
本书中的设置请求全称应该是“信息设置( 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.2.5节中的内容,上下文指针已经在前回调中设置为可疑文件的全路径(已全部转换为大写)。这里有一个疑问:既然一定要在后回调中处理,那何不在后回调中再获取文件路径,而要通过上下文指针来传递这么麻烦呢?
需要通过上下文指针传递文件的原始路径的原因是这是一个改名操作。到了后回调中,文件大概率已经被改名成功。此时要获得改名之前的路径可就麻烦了。所以在前回调中获得一次之后,不要释放,而要通过上下文指针传递到后回调中。
和写请求的后回调函数一样,因为很难确认后回调的中断级,真正的处理依然在安全函数中进行。所以后回调主要的工作是设置安全函数并把参数传递给安全回调。后回调函数 SetInformationIrpPost的实现 如代码 3-14所示。
代码3-14 后回调函数SetInformationIrpPost的实现
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)