【技术分享】Windows PsSetLoadImageNotifyRoutine的0day漏洞(续)

http://p9.qhimg.com/t01c8891c5cb0abb965.jpg

译者:anhkgg

预估稿费:200RMB

投稿方式:发送邮件至linwei#360.cn,或登陆网页版在线投稿


传送门

【技术分享】Windows PsSetLoadImageNotifyRoutine的0day漏洞


简介

安全服务商和内核开发者需要注意,Windows内核的一个代码编写错误可以能让你无法再运行中拿到是哪个模块被加载了的信息。并且修复它不是你想象的那么简单。


快速回顾

在研究windows内核过程中,我们遇到了一个有趣的问题,关于PsSetLoadImageNotifyRoutine,就是通知模块加载的。在注册了一个加载PE模块的通知之后,内核回调函数可能会收到一个不合法的镜像名字。

在我们之前发布的博客中,我们研究了这个bug相关的不同内核组件。在弄明白了原因之后,我们找到了一种解决它的方法。


疑惑:一个持久的问题

有一件事对我们来说非常奇怪,就是这个机制已经非常老了,但是在我们之前没有人遇到这个问题。这么多年来一个文档化的广泛使用的windows内核机制怎么可能这样,没有修复这么一个明显的问题,或者发布新的文档。

经过在线的一番与这个bug相关的信息的搜索,我们没有发现任何官方的文档,或者可能引起的任何信息。我们也在社区遇到一个类似问题的特殊说明,它让我们知道了一些重要的事情:

a. 这个问题,我们也可以复现,好像来源于同样的问题

b. 一个关于回调通知的同样的bug在十年前甚至更早就已经出现了(我们没有发现在2001年以前的信息了)

c. 微软应该也知道这个问题


错误的解决方案

在寻找解决这个bug的方案时,我们发现了几款工具试图通过不同的方法解决它,但其实没什么用。有些使用ObQueryNameString或者各种API其实会有相同的结果,在其他地方简单增加和FILE_OBJECT.FileName相关的DEVICE_OBJECT的名字,但其实在object的生命周期从来没有使用它。


我们还在继续做

此时,我们决定继续检查其他提供PE加载回调通知的函数,如PsSetCreateProcessNotifyRoutineEx,这个函数在Windows Vista Sp1之后才能使用。我们想看看他是否也有同样的bug在内核中。

让我们困惑的是在这个函数中CreateInfo参数有一个特定flag,FileOpenNameAvailable,当设置为TRUE时,表示这个字符串指定的完整文件名字是要打开的可执行文件的名字。如果设置为FALSE,操作系统只是提供一部分名字。

在查看nt!PspInsertThread的反汇编代码是,这个flag明显被设置为FALSE,ImageFileName是FILE_OBJECT(进程的SECTION)的FileName字段。像我们之前提到的加载镜像通知回调的问题一样,这个flag表明这个穿拿来的参数的完整性是不可信任的。

http://p2.qhimg.com/t013f31aba5b72dde00.png

图1. Nt!PspInsertThread – 在调用注册的回调之前初始化CreateInfo.ImageFileName和CreateInfo.FileOpenNameAvailable

看起来微软像是注意到了这个问题,至少负责PsSetCreateProcessNotifyRoutineEx的开发者注意到了。事实上,微软为什么至今也没有解决PsSetLoadImageNotifyRoutine的bug依然让人不解。

用微软的方式(几乎正确)

在搜索更多关于PsSetCreateProcessNotifyRoutineEx的文档的时候,我们找到了一篇2007年5月的文档,叫做“支持Windows Server 2008的内核数据和过滤”,目前已经在微软的网站上已经没有了(这里有引用)。这个文档指明在使用进程创建回调时,驱动可以通过过滤管理API获取额外的属性,比如FltGetFileNameINformationUnsafe

根据这些信息我们确认FltGetFielNameInformationUnsafe可以提供给我们最优雅和简单的解决这个bug的方案。使用这个函数可以让我们不用实现文件系统小过滤驱动就可以解决这个问题。我们从PsSetLoadImageNotifyRoutine回调中拿到FILE_OBJECT的方式和从PsSetCreateProcessNotifyRoutineEx回调中非常类似,所以在我们的问题中它看起来是一个可行的解决方案。

让我们更放心的是在某些Windows自己的组件中也使用了这个函数,比如Windows Defender和防火墙。

 http://p3.qhimg.com/t01e92cfeb6763324ff.png

图2. Windows 10Redstone中Windows Defender(WdFilter.sys)和防火墙的回调

尽管未尽明

在加载镜像通知回调中使用FltGetFileNameInformationUnsafe偶尔会失败,返回STATUS_OBJECT_NAME_NOT_FOUND。在微软文档中这个错误没记录未这个函数可能的错误。

经过一些实验,我们找到能始终重现这个错误状态的准确的事件序列。FltGetFileNameInformationUnsafe会在一定阶段调用fltmgr!FltpExpandShortNames,这个是实际验证文件路径是否存在的函数。

http://p9.qhimg.com/t012cd2a2f4c3a84841.png

图3. Fltmgr!FltGetFileNameINformationUnsafe中Fltmgr!FltpExpandShortNames的回调

问题是这个验证仅仅是一部分:代码验证路径中所有目录是否存在,但是却忽略了检查给定路径中文件本身是否存在。因此,我们现在知道它给出的错误代码只在文件当前不在他以前的目录中才是有效的,我们至少可以获取到它打开的名字(在给FltGetFileNameInformationUnsafe传递适当的flag的时候)。

因此,我们只有最后一件事需要处理:不管什么时候FltGetFileNameInformationUnsafe调用成功,我们需要确保拿到的路径是文件实际存在的。还有,我们需要验证这个文件是和我们在加载镜像回调中拿到的文件是同一个。

总结

理论上上一篇博客描述的这个Windows内核的bug,具有潜在的危险,用来欺骗依赖通知机制提供信息的安全产品。这个缺陷看起来来自于一个代码编写错误,从Windows2000最新的Windows10发布版都会收到影响。这意味着只有微软修复了这个bug,安全厂商在windows环境中开发的产品不能依赖回调通知提供的错误信息。安全厂商必须寻找替代的更可信的方法来获取通知机制提供的不可靠信息,希望可以用到本博客中提供的研究内容。

(完)