本系列文章为看雪星星人为看雪安全爱好者创作的原创免费作品。
欢迎评论、交流、转载,转载请保留看雪星星人署名并勿用于商业盈利。
本人水平有限,错漏在所难免,欢迎批评指正。
在微过滤驱动中实际运用可疑库时,关键问题是如何在微过滤驱动中获得文件的全路径。一般地说,在微过滤驱动的请求过滤回调中,文件的全路径推荐用FltGetFileNameInformation来获取。但仅调用这个函数会有些麻烦:
(1)该函数返回的结果是一个FLT_FILE_NAME_INFORMATION结构的指针。虽然说信息很全面,但用不了那么多,获取全路径还需要再取一次结构成员。
(2)FltGetFileNameInformation调用成功之后,返回的结果是要用FltReleaseFileNameInformation释放的。否则不但资源泄漏,有可能本驱动都无法正常卸载。
(3)从FLT_FILE_NAME_INFORMATION中拿到的全路径可能是大小写混杂的,直接拿去计算散列值肯定是错误百出的,所以必须转换成大写。
因此我们应该对FltGetFileNameInformation进行一个封装,而且也不能直接使用它返回的字符串,而应该将字符串复制到我们自己分配的内存空间中并完成大写化。在实际操作中我是直接将它生成一个如代码4-1中的DUBIOUS_PATH结构的。
封装了FltGetFileNameInformation的函数DubiousGetFilePathAndUpcase的实现如代码4-11所示。
代码4-11 函数DubiousGetFilePathAndUpcase的实现
注意上述代码中(1)处,该函数的参数data。这个参数来源于前回调,可参考代码3-2 请求前回调函数WriteIrpProcess中的第一个参数。这是因为(3)处的FltGetFileNameInformation的调用需要这个函数。这也意味着本函数只能在请求前后回调中使用。
同时因为该函数只能在中断级低于等于APC_LEVEL的时候调用,所以(2)处检查了中断级。如果中断级过高,这个函数会放弃执行,直接返回NULL。
data中已经含有要访问的目标文件的信息,FltGetFileNameInformation无需再额外指定文件对象,就会主动去获取这次请求的目标文件的路径。(4)处的参数FLT_FILE_NAME_NORMALIZED指示系统我们需要获得是“规范化”的路径。这个参数非常有必要,否则可能得到五花八门的路径(想想一下意外拿到一个8~3短文件名然后去计算散列值带来的巨大麻烦)。最后获得的信息被存入(5)处的name_info中。name_info->Name就是文件的全路径。
在(6)处有一系列检查。FltGetFileNameInformation如果失败了,本函数也会返回NULL。接着是name_info->Name.Length不太正常,比如太长。对路径长度之类的可以由外部用户控制的参数如果不加检查,很容易在后面的处理中一不小心就缓冲区溢出,所以这里限制了最大长度。
同时(6)处的这些限制会导致有时候无法获得文件路径,这也就不存在去检查一个路径是否可疑的可能了。这种情况下,若是要安全优先,则应该所有无法获取的路径一律假定为可疑或禁止执行。若要易用优先,则应该允许。当然,允许毫无疑问会带入安全漏洞。比如攻击者可以构造一个特别长的路径去绕过可疑路径检查。
接下来的(7)处的代码用ExAllocatePoolWithTag分配了一个我们之前定义过的数据结构DUBIOUS_PATH的空间。其实际空间长度是结构本身的长度加上路径字符串所需要的长度。
最后在(8)处,函数RtlUpcaseUnicodeString将name_info->Name转成全大写保存到了我们分配的空间中。然后别忘了,name_info是要释放的。在(9)处,FltReleaseFileNameInformation释放了name_info。
请注意在(8)处的下一行有一个DUBIOUS_PATH_ASSERT,这是一个调试时使用的检查宏。它会对DUBIOUS_PATH这个结构的节点进行一系列检查,以确保我在调试的时候及早发现问题。该宏中用到一个关键的内核函数MmIsAddressValid,能检查一个指针指向的空间是否有效。
在Windows内核中拦截可执行模块的加载有多种方法。比较常用的是使用PsSetLoadImageNotifyRoutine系列接口来设置回调函数。本例用了微过滤驱动,所以使用微过滤驱动来进行拦截。请回顾第3章中的代码3-1。和当时的情况类似,为了拦截模块加载,我们必须在在过滤操作数组为模块加载指定专门的处理函数,如代码4-12所示。
代码4-12 在过滤操作数组为模块加载指定专门的处理函数
以上IRP_MJ_ACQUIRE_FOR_SECTION_SYNCHRONIZATION是一类新的文件请求。当可执行模块作为文件加载到内核中时,该请求会被拦截到。如果让该请求返回错误,那么加载无法成功。同时,在处理该请求的过程中,微过滤驱动可以用4.2.1中提到的方法来获取模块全路径。
同时,相关处理函数AcquireSectionIrpProcess完整的实现如代码4-13所示。
代码4-13 AcquireSectionIrpProcess完整的实现
实际上,除了可执行文件加载执行之外,其他杂七杂八的文件映射等操作也可能让Windows内核调用到这里。为了筛选出真正的可执行文件加载,上述代码中有(1)和(2)两处重要的判断。
data->Iopb->Parameters. AcquireForSectionSynchronization.SyncType为SyncTypeCreateSection说明这次请求涉及的时Section的生成。在Windows内核中任何可执行文件加载执行都会先生成对应的Section。此外data->Iopb->Parameters.AcquireForSectionSynchronization.PageProtection同样重要。它为PAGE_EXECUTE则表明生成的Section中的页面是可执行的。
确认条件之后,在(3)处用4.2.1节中的DubiousGetFilePathAndUpcase函数获得了全大写的路径,然后在(4)处用4.1.2节中说明过的函数IsDubious进行了路径判断。如果路径并非可疑路径,那么直接跳出处理即可。
假定路径是可疑路径,此时应该根据文件路径获得文件,然后计算文件的散列值,并和服务器下发的白库中的散列值对比。如果确认该文件为服务器认证过的合法文件,则应该将文件路径从可疑库中删除,然后允许请求。
计算文件散列值和对比白库比较麻烦。此外,对比白库之前,可能还需要和服务器通信询问是否需要更新白库。若白库中不含有该文件散列值,那么还需要提交文件样本到服务器。这些操作不宜放在内核中,应该编写一个用户态服务进程来解决。
微过滤驱动可以使用FltSendMessage来和用户态进程通信来发出请求和获得用户态处理的结果。本章代码将略去这些,而只做一个简单的处理:如果路径在可疑库中,则阻止这个模块的加载。相关处理在上述代码中的(5)处。
注意,阻止模块的加载也有两种形式。其中之一是“明确阻止”。明确的阻止会导致用户进程弹框,一般会弹出“无法找到模块xxx.dll”或类似的提示信息。用户点击“确定”之后进程会退出。这样对用户来说比较明确,但缺点是进程将会必定退出。
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
最后于 2024-5-20 08:53
被星星人编辑
,原因: