概要

微软在北京时间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 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等等。在内核字体引擎内部根据不同的场景,主要是使用以下四个函数来实现字体文件的加载处理的:

1 PUBLIC_PFTOBJ::bLoadFonts
2 PUBLIC_PFTOBJ::hLoadMemFonts
3 PUBLIC_PFTOBJ::bLoadRemoteFonts
4 DEVICE_PFTOBJ::bLoadFonts

这四个函数是内核字体引擎在不同场景下加载字体文件的关键函数。在这些函数中,内核将分配(或直接使用)关键的字体文件视图对象(FontFileView),并调用vLoadFontFileView函数来加载字体文件视图对象,将其描述的字体文件数据映射到内核内存中,并进行解析和处理,以供后面渲染使用。

本次Windows 10新预览版就在这些函数中调用vLoadFontFileView加载字体文件视图对象之前,进行检查。

01 ...
02 FontLoadingOptions = GetCurrentProcessFontLoadingOption();
03
04 if ( FontLoadingOptions == 2 )
05 {
06   NonSystemFontPath = GetFirstNonSystemFontPath(FontFilePathNames, FontFileCount);
07   if ( NonSystemFontPath )
08   {
09     LogFontLoadAttempt(FONT_LOAD_NORMAL, NonSystemFontPath, TRUE);
10     return FALSE;
11   }
12 }
13 else if ( FontLoadingOptions == 1 )
14 {
15   NonSystemFontPath = GetFirstNonSystemFontPath(FontFilePathNames, FontFileCount);
16   if ( NonSystemFontPath )
17     LogFontLoadAttempt(FONT_LOAD_NORMAL, NonSystemFontPath, FALSE); // log only
18 }
19
20 ...

这里我们可以看到代码首先通过GetCurrentProcessFontLoadingOption调用获取字体加载缓和的选项。

该函数实际是通过NtQueryInformationProcess(ProcessMitigationPolicy)获得_PROCESS_MITIGATION_FONT_DISABLE_POLICY结构。

在结构中,bit0为DisableNonSystemFonts,bit1为AuditNonSystemFontLoading,接着此结构被转换成下面三个选项值:

0: 不审计、不禁用
1:审计、不禁用
2:审计并禁用

然后,系统会调用GetFirstNonSystemFontPath函数解析要加载的字体文件列表(可能是多个文件)。该函数分别通过IoCreateFile得到这些文件的文件句柄,再通过句柄得到文件对象的完整路径,最后同系统字体目录(%Systemroot%\Fonts)进行对比。

如果待加载的字体列表中存在非系统字体目录下的字体文件,那么系统会首先调用LogFontAttempt函数,通过ETW机制记录到日志中。

该函数的第一个参数为触发字体加载的类型:

1 0: LoadPublicFonts
2 1: LoadMemFonts
3 2: LoadRemoteFonts
4 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中,该函数的伪代码如下:

01 void vLoadFontFileView(...)
02 {
03    *(pchecksum + 4) = 0;
04    *pchecksum = 0;
05
06    if ( gbNetworkFontsLoaded && gbAttemptedEnableEUDC && gbFntCacheClosed )
07    {
08       UmfdLoadFontFileView(
09                            FontPathName,
10                            pfileview,
11                            filecount,
12                            pview,
13                            countview,
14                            pdesignvector,
15                            hff,
16                            devlist);
17                            }
18     else
19     {
20       KmfdLoadFontFileView(
21                            FontPathName,
22                            cwc,
23                            pfileview,
24                            filecount,
25                            pview,
26                            countview,
27                            pdesignvector,
28                            countdv,
29                            hff,
30                            devlist,
31                            pchecksum);
32                            }
33     }

对比上个公开发布的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号

源链接

Hacking more

...