微软在北京时间2015年1月22日,公布了其新一代操作系统Windows 10的新技术预览版,并在随后的北京时间1月24日向公众开放了该预览版本的下载(Build:9926)。在这个新的Windows10预览版中,内核版本直接从6.4提升到了10.0,同时也带来了一些新的产品形态和用户体验上的诸多改进。
作为国际顶尖的安全厂商,360除了在第一时间为使用Windows 10新预览版的用户提供安全防护,也在同时关注新操作系统中引入的新安全特性。
微软从Windows 8开始,就为其新的操作系统不断引入了很多新的安全特性,包括零页禁用、高熵随机化、执行流保护(Control Flow Guard , CFG)、管理模式执行保护(Superior Mode Execution Prevention, SMEP)等等,对Windows平台上的应用和内核漏洞利用明显地提高了门槛。这次Windows 10的新技术预览版又为操作系统加入了什么样新的保护呢?
在拿到内核并进行分析之后,首先进入笔者视线的就是本文将要介绍的,Windows 10预览版中对于字体的安全防护的两项增强。
这里要提到的是,本文在完成前(北京时间1月24日早上9点~11点左右),国外的一些安全研究人员,包括CrowdStrike公司的Alex Ionescu(@aionescu)和Google公司的Kostya Kortchinsky(@crypt0ad),也都同时发现并在Twitter上提到了这两项增强相关的关键字。
这里本文将带读者第一时间深入地分析Windows 10字体防护增强的实现和策略,了解其中的来龙去脉。
由于诸多历史原因,Windows的内核模式字体引擎长期以来是Windows操作系统内核中典型的复杂程度高而代码安全质量并不高的组件。出于字体引擎性能的考虑,微软也不得不将其一直放在内核模式。因此,一旦Windows内核中的字体引擎出现安全漏洞,漏洞攻击代码就可以在用户浏览网页或文档中的远程字体文件时,直接在Windows内核中执行,可以完全绕过几乎所有的 安全防护机制,杀伤力相当大。
360的安全研究人员王宇对于内核字体引擎的安全历史和安全漏洞进行了深入的研究。他将一些技术成果公开在国际安全会议Syscan360 2012( http://www.syscan360.org/achive_2012_speech.html )和Blackhat2014( https://www.blackhat.com/us-14/briefings.html#understanding-tocttou-in-the-windows-kernel-font-scaler-engine )上。对相关技术有兴趣的读者可以深入地学习一下。
历史上微软修复的内核字体漏洞并不少,但最出名的,也是打破了字体漏洞神秘感的,可能就要数2011年被安全厂商披露的感染伊朗等国家的震网二代”Duqu”病毒。
该病毒是利用Windows内核字体引擎中的一处漏洞(CVE-2011-3402),将利用该漏洞的、命名为嗜血法医Dexter的字体文件嵌入到word文档中,当用户打开word文档、触发字体加载,恶意代码就直接进入内核模式运行。
这应该是首次被公开披露的字体漏洞用于真实的APT攻击的案例。其后,该漏洞还被一些Exploit Kit改造为通过网页浏览器进行攻击的版本,开始大范围的传播和攻击。
在今年的10月,国外安全公司Fireeye也披露了他们新发现的被用于真实的APT攻击的字体漏洞:(CVE-2014-4148)( https://www.fireeye.com/blog/threat-research/2014/10/two-targeted-attacks-two-new-zero-days.html )。这个漏洞攻击巧妙地利用字体引擎中一处整数符号问题,实现在浏览嵌入了字体的文档或网页时,直接从内核模式执行恶意代码,绕过系统中的安全防护机制,安装恶意程序。
无论是白帽子安全研究人员还是可能具备国家级背景的高级黑客,针对Windows字体引擎安全问题的深入挖掘,都将这一攻击面的危险状况更多地暴露出来。微软在修复漏洞的同时,也在计划着一些更通用的解决办法。
在2014年的8月补丁日,微软在针对字体引擎内核漏洞的补丁中,就引入了一项针对字体缓存机制的安全优化,来缓和通过字体缓存进行的某些字体漏洞攻击手法。这次Windows 10新预览版中的这两项改进,则是微软针对字体漏洞攻击缓和的另一次尝试。
下面笔者就来详细介绍下这次引入的改进。
从Windows 8开始,微软引入了一个新的API: SetProcessMitigationPolicy。该API通过最终调用NtSetInformationProcess(ProcessMitigationPolicy),设置进程EPROCESS对象中Flags(Flags2,Flags3)域中的某些位,来打开/关闭进程粒度的一些漏洞缓和机制的开关。
这些缓和机制包括强制DEP/ALSR、零页内存禁用、句柄强制检查等。除了通过这个API,相关的开关也可以通过父进程继承、StartupInfo(Process Thread Attributes)、IEFO和全局缓和选项等方式来控制。
本次引入的一个新的缓和选项被称为ProcessFontDisablePolicy。在对进程设置这个缓和策略后,表现在EPROCESS->Flags3.DisableNonSystemFonts这个标记上,同时还有AuditNonSystemFontLoading作为辅助选项。
这个选项的功能顾名思义,就是针对进程禁止加载非系统的字体,AuditNonSystemFontLoading则是审计记录非系统字体的加载。
那么系统是如何实现该机制的呢?这里我们就要看看内核字体引擎了。在Windows 10中,内核字体引擎主要位于Win32k内核驱动的完整版本: win32full.sys中。
桌面内核引擎win32k提供了多个系统调用接口允许用户模式加载字体到内核字体引擎中,包括NtGdiAddFontResourceEx,NtGdiAddFontMemResourceEx等等。在内核字体引擎内部根据不同的场景,主要是使用以下四个函数来实现字体文件的加载处理的:
PUBLIC_PFTOBJ::bLoadFonts PUBLIC_PFTOBJ::hLoadMemFonts PUBLIC_PFTOBJ::bLoadRemoteFonts DEVICE_PFTOBJ::bLoadFonts
这四个函数是内核字体引擎在不同场景下加载字体文件的关键函数。在这些函数中,内核将分配(或直接使用)关键的字体文件视图对象(FontFileView),并调用vLoadFontFileView函数来加载字体文件视图对象,将其描述的字体文件数据映射到内核内存中,并进行解析和处理,以供后面渲染使用。
本次Windows 10新预览版就在这些函数中调用vLoadFontFileView加载字体文件视图对象之前,进行检查。
增加的检查代码的伪代码如下(以PUBLIC_PFTOBJ::bLoadFonts为例):
... FontLoadingOptions = GetCurrentProcessFontLoadingOption(); if ( FontLoadingOptions == 2 ) { NonSystemFontPath = GetFirstNonSystemFontPath(FontFilePathNames, FontFileCount); if ( NonSystemFontPath ) { LogFontLoadAttempt(FONT_LOAD_NORMAL, NonSystemFontPath, TRUE); return FALSE; } } else if ( FontLoadingOptions == 1 ) { NonSystemFontPath = GetFirstNonSystemFontPath(FontFilePathNames, FontFileCount); if ( NonSystemFontPath ) LogFontLoadAttempt(FONT_LOAD_NORMAL, NonSystemFontPath, FALSE); // log only } ...
这里我们可以看到代码首先通过GetCurrentProcessFontLoadingOption调用获取字体加载缓和的选项。
该函数实际是通过NtQueryInformationProcess(ProcessMitigationPolicy)获得_PROCESS_MITIGATION_FONT_DISABLE_POLICY结构。
在结构中,bit0为DisableNonSystemFonts,bit1为AuditNonSystemFontLoading,接着此结构被转换成下面三个选项值:
0: 不审计、不禁用
1:审计、不禁用
2:审计并禁用
然后,系统会调用GetFirstNonSystemFontPath函数解析要加载的字体文件列表(可能是多个文件)。该函数分别通过IoCreateFile得到这些文件的文件句柄,再通过句柄得到文件对象的完整路径,最后同系统字体目录(%Systemroot%/Fonts)进行对比。
如果待加载的字体列表中存在非系统字体目录下的字体文件,那么系统会首先调用LogFontAttempt函数,通过ETW机制记录到日志中。
该函数的第一个参数为触发字体加载的类型:
0: LoadPublicFonts 1: LoadMemFonts 2: LoadRemoteFonts 3: LoadDeviceFonts
第二个参数是触发的字体路径。
第三个参数是是否要对该字体拒绝加载。
接着,系统会根据进程缓和选项的设置,决定对于这种情况,是仅审计记录,还是要拒绝这个字体的加载。
对于调用时不含有直接的字体文件路径的LoadMemFonts/LoadRemoteFonts/LoadDeviceFonts的情况,内核将不会判断字体路径,如果在缓和选项设置了禁止非系统字体加载,则会禁用这些接口在所有情况下的字体加载并进行审计。
从微软公开的Windows 10技术预览版来看,这项防护开关并没有对一些关键应用如Internet Explorer、内置的PDF浏览器等默认开启。
从目前的功能设计看来,该功能更像是为特定的企业用户提供的防止高级攻击的安全措施。尤其是针对字体加载的审计功能,可以帮助IT管理人员快速定位可能的高级威胁攻击。
值得一提的是,在360的XP盾甲的2.0中,我们就引入了同Windows10本次更新加入的字体禁止策略类似的机制。通过XP盾甲的隔离引擎机制,我们将系统中的字体同沙箱隔离环境中的字体隔离开来,使得沙箱隔离环境中的字体无法加载到内核中,而隔离环境之外的字体在隔离环境内则不受影响,达到了和Windows10这项防护类似的漏洞防御效果。
在XP盾甲4.0的产品中,我们则引入了更强的内核字体引擎锁定机制,针对内核字体引擎的攻击防护强度又上了一个台阶。360一年前设计的XP盾甲防护机制同微软公布的新一代操作系统中的新防护措施的不谋而合,也从侧面印证了360在漏洞防护研究方面的探索。
在Windows 10的新技术预览版中,针对内核字体引擎的另一项引人注意的重大改进是“用户模式字体驱动”(User Mode Font Driver,UMFD)机制。
就像前面我们说到的,位于内核模式的字体引擎的一旦被突破,攻击者通过精巧的数据构造可以直接从远程获得内核代码执行的能力,危险极大。
同时,由于字体引擎在内核中运行,因此针对字体引擎的修改也必须非常谨慎,稍有不慎就可能引发大面积的蓝屏或应用程序异常。
关于修改内核字体引擎带来的副作用,眼前就有典型的案例:在今年8月的补丁日中,微软的KB2982791补丁为了修复字体引擎中的安全漏洞,修改了内核字体缓存引擎。结果,这一改动中存在的兼容问题不幸引发了大面积的用户Windows系统启动即蓝屏的问题。该问题遭到了大量用户和媒体的指责,微软不得不宣布将该补丁撤回重发。
在这个Windows 10技术预览版中,微软首次建立了用户模式和内核模式的字体引擎交互机制,将内核字体引擎中的多个字体驱动引擎移植到用户模式,使得字体引擎能够部分在隔离的用户模式进程中运行,同时也保留了内核模式字体引擎驱动的机制,尽量将可能出现安全漏洞的渲染引擎放到用户模式,同时使用一些内核预先加载、内核/用户模式交互的方式,减少这种模式对于性能的损耗。
在这套机制应用后,如果相关的字体引擎代码出现安全漏洞,攻击者只能控制被隔离的用户模式字体驱动宿主,无法通过字体漏洞直接控制操作系统内核。而修复相关漏洞的成本和可能的风险,也将大大降低。
这里笔者将主要介绍这套新的UMFD机制的运作原理和一些关键细节。限于时间和篇幅的原因,关于UMFD很多具体实现、实际性能状况、实际漏洞隔离效果等,这里无法详尽地覆盖到,感兴趣的读者可以去深入逆向、分析和测试相关的代码和功能来发现更多的内容,也可以在本Blog同笔者讨论。
在前面一节中,我们介绍了,*::Load*Fonts系列函数将使用vLoadFontFileView加载字体文件视图对象,而这节要介绍的增强里的一个关键点就在vLoadFontFileView函数中,在Windows10 build 9926中,该函数的伪代码如下:
void vLoadFontFileView(...) { *(pchecksum + 4) = 0; *pchecksum = 0; if ( gbNetworkFontsLoaded && gbAttemptedEnableEUDC && gbFntCacheClosed ) { UmfdLoadFontFileView( FontPathName, pfileview, filecount, pview, countview, pdesignvector, hff, devlist); } else { KmfdLoadFontFileView( FontPathName, cwc, pfileview, filecount, pview, countview, pdesignvector, countdv, hff, devlist, pchecksum); } }
对比上个公开发布的Windows 10技术预览版(Build 9879)我们可以看到,vLoadFontFileView根据多个开关进行判断,分别执行到UmfdLoadFontFileView和KmfdLoadFontFileView这两种情况。
而我们仔细观察下KmfdLoadFontFileView就可以发现,它的代码和9879中原有的vLoadFontFileView的代码是非常相似的。因此我们可以推测:在Build9926中,微软针对gbNetworkFontsLoaded (有远程字体加载) + gbAttemptedEnableEUDC(曾调用过NtGdiEnableEUDC启用EUDC) + gbFntCacheClosed(系统启动字体已经加载到缓存中)的情况下, 就会进入名为UmfdLoadFontFileView的分支来进行用户模式字体加载,否则就使用KmfdLoadFontFileView,即原有的vLoadFontFileView逻辑来加载字体。
根据我们的推测,UmfdLoadFontFileView应该就是用户模式字体驱动(UMFD)的字体加载函数。如何证实这点呢?该函数又是如何同用户模式的字体驱动引擎交互的呢?
我们继续进入UmfdLoadFontFileView函数,看看他的实现。在该函数中,我们可以看到它会调用UmfdHostLifeTimeManager::EnsureUmfdHost,该函数是用户模式的字体引擎宿主的生命周期管理函数,用于确保用户模式的字体引擎宿主客户端存在。
我们查看这个函数,可以发现该函数的关键点是:如果Winlogon进程存在,那么使用PostWinlogonMessage发送一条1033的消息,并等待UmfdHostLifeTimeManager::s_WinlogonCallbackEvent事件触发。
PostWinlogonMessage函数实现在win32kbase.sys中,实际是调用msrpc.sys导出的相关LPC/RPC函数给winlogon.exe的LPC接口发送内核模式LPC消息。
winlogon.exe则会在WMsgKMessageHandler函数中接收这个消息。这个1033号消息实际的作用,就是去加载用户模式的字体驱动客户端fontdrvhost.exe。
在收到1033号消息后,Winlogon会使用LaunchUmfdHostWithRestrictedToken函数,通过WTSQueryUserToken获取当前登录的用户token句柄。
接着,它使用SetTokenInformation(TokenIntegrityLevel)将token的完整性级别设置为Low。
最后,它通过CreateProcessAsUser,使用这个受限的token来创建fontdrvhost.exe(默认位于%systemroot%/system32下)进程。
接下来的一个关键步骤是通知UMFD的驱动部分,在UMFD的内核部分引擎中注册fontdrvhost.exe这个用户模式客户程序。
这里使用的是gdi32!NamedEscape函数,该函数实际上是通过调用NtGdiExtEscape来实现同win32k内核驱动通讯的。
NtGdiExtEscape过去被win32k内核部分用于同gdi32进行一些内核/用户模式交互,在新的Windows10预览版中,该函数被umfd使用来进行umfd的相关内核交互,是UMFD的内核/用户模式交互基础。
在创建了fontdrvhost进程后,Winlogon通过NamedEscape->NtGdiExtEscape将创建的进程PID发送给内核。
在新的NtGdiExtEscape中,内核检测到发起该调用的进程是winlogon进程,就识别传入的 参数,通过UmfdHostLifeTimeManager::InitializeUmfdAndRegisterHost获得该进程的进程对象,将其注册为UMFD的宿主客户端,并设置我们前面提到的 UmfdHostLifeTimeManager::s_WinlogonCallbackEvent事件,来通知UMFD客户端已经准备完毕。
在注册为UMFD的宿主客户端后,fontdrvhost.exe就可以通过NamedEscape->NtGdiExtEscape同内核模式进行通讯。在这个系统调用的实现里,会识别如果是UMFD的宿主进程进行的调用,则进入一个UMFD的专用通讯函数:UmfdDispatchEscape。
在UmfdDispatchEscape中,内核提供了一些用户模式字体驱动客户端所需要的内核-用户模式数据交互、文件映射与访问接口等,这个后面我们会提到。
上面我们说到的是UMFD用户模式客户端的创建以及它同内核模式部分通讯的过程。
接下来,我们回到刚才说的vLoadFontFileView函数过程。上面我们说到在注册用户模式字体宿主进程后,内核会置UmfdHostLifeTimeManager::s_WinlogonCallbackEvent事件来通知UMFD用户模式部分已经开始工作并使得字体加载过程继续下去。
那么接下来,内核就开始进行实际的用户模式字体加载,通过调用PDEVOBJ::LoadFontFile,实际上内核会Attach到CSRSS进程上,并调用UMFD的字体加载接口函数:UmfdLoadFontFile来完成字体加载。
UmfdLoadFontFile是UMFD的接口列表UmfdDDIs中用于处理UMFD字体实际加载工作的接口函数。它的核心功能是通UmfdClientSendAndWaitForCompletion函数将UMFD请求插入UMFD工作队列中,并通知UMFD的用户模式部分处理队列中的数据。
除了UmfdLoadFontFile外,UmfdDDIs这个接口列表中还包含了同内核模式字体接口一一对应的诸如UmfdQueryFontData、UmfdQueryFontTree、UmfdGetTrueTypeFile等字体相关的重要接口。
当对应的内核模式接口被调用时,实际也是由这些接口函数使用UmfdClientSendAndWaitForCompletion发送请求到UMFD用户模式处理队列中,通知用户模式客户端来完成实际的字体工作的。
用户模式字体驱动客户端宿主fontdrvhost.exe启动后,则会建立一个ServerRequestLoop线程,该线程通过NamedEscape(NtGdiExtEscape)(Dispatch id = 0 )来同内核交互.
当NameEscape传递的Dispatch id = 0 时,实际内核执行的对应是UmfdEscSendCompleteWaitReceive函数。该函数同UmfdClientSendAndWaitForCompletion是成对的,用于“接收”通过UmfdClientSendAndWaitForCompletion“发送”到队列的请求数据。
在宿主从内核获得字体操作请求数据返回用户模式后,用户模式字体驱动宿主再使用DispatchRequest函数分发到内核的字体处理请求到各个用户模式字体驱动接口中。
这些用户模式字体驱动目前也都是编译在fontdrvhost.exe这个进程的代码中的,主要有ttf字体驱动、bmf字体驱动和vtf字体驱动等相关的处理代码。
在处理内核字体请求时,DispatchRequest会根据请求数据中指定的设备和指定的请求类型(QueryFontData,LoadFontFile…)来选择不同的字体驱动处理例程进行最终的字体处理过程。值得一提的是,在用户模式字体引擎中,包括针对字体虚拟机的指令实现部分(itrp_*函数),也是在用户模式中实现的。
最近几年被曝光用于真实攻击的内核模式字体漏洞,都是在特定的itrp_*函数处理数据中发生的安全问题,也都需要依赖itrp_*函数来操纵内核数据来实现最终的漏洞攻击。这项新的增强将这些函数隔离在低完整性级别的受限用户模式进程中,显然极大地提高了这类漏洞的攻击难度和利用门槛。
简单总结来说,UMFD这套机制实际上就是通过在内核并行纯内核模式字体引擎和需交互的用户模式字体引擎两套系统。在系统启动过程中使用性能更好的内核模式加载不会有安全问题的、已注册的系统字体文件。
在系统启动完成并开始加载非本地的字体文件时,就开启用户模式字体引擎。通过Winlogon控制的用户模式字体引擎客户端fontdrvhost.exe,利用NtGdiExtEscape同内核进行交互,为内核的UMFD部分实现字体文件的解析、渲染工作,并给内核模式返回需要的接口。
两套接口并行、对外保持一致的数据交互,在不影响功能、消耗非常见情况下的较少性能的前提下,实现了字体数据解析和渲染的用户模式+受限权限隔离。
即使字体解析和渲染过程中存在安全漏洞,也被限制在受限的用户模式进程中,需要进一步利用其他安全漏洞才能最终完成攻击,无法再直接获得内核的最高权限,使得这类型漏洞的风险大大降低,也使限制、发现这类漏洞攻击更容易和更具备可能性。
本文所介绍的非系统字体禁用和用户模式字体驱动机制,都是微软在内核模式字体引擎漏洞攻击方面的缓和措施,分别通过安全策略、安全隔离的思想,提高内核模式字体引擎漏洞的利用难度,降低了相关漏洞的安全风险。在特定的环境下,甚至可能完全杜绝远程字体漏洞的攻击。这个安全措施称得上是继默认开启CFG后,Windows10又一可圈可点的新安全特性。
如同CFG在Windows10中默认开启后,逐步开放到Windows8.1 Update3的更新上,希望这个机制也能尽早磨练成熟,升级到后续的操作系统中,为更多用户提供更好的安全保障。