0x00 前言
作者:mj0011@360Vulcan Team
北京时间2015年5月12日, 微软推送了5月的补丁日补丁,包含IE、Windows内核、Windows内核驱动、Office等多个组件的安全更新。 本月修复的两个0day漏洞
MS15-052中修复的Windows内核安全特性绕过漏洞:CVE-2015-1674 (https://technet.microsoft.com/en-us/library/security/MS15-052)
MS15-051中修复的Windows内核模式驱动权限提升漏洞:CVE-2015-1701(https://technet.microsoft.com/en-us/library/security/MS15-051)也引起了我们的注意。
经过确认, CVE-2015-1674是笔者在2014年中发现的一个内核KASLR绕过漏洞,CVE-2015-1701则是Fireeye在今年4月18日发布的关于Operation RussianDoll(俄罗斯套娃行动)报告中发现的,俄罗斯APT28黑客组织发起的针对极其特定目标的攻击中使用的用于权限提升的内核0day漏洞,在微软发布补丁上线同时,来自俄罗斯安全社区kernelmode.info的黑客hfiref0x也在他的Github上公布了针对CVE-2015-1701漏洞的完整攻击代码(https://github.com/hfiref0x/CVE-2015-1701)。 本篇Blog笔者就这三个本月修复的0day漏洞,讲讲他们的原理、细节和修补方法和一些周边信息。
0x01 CVE-2015-1674/MS15-052
漏洞信息
MS15-052是微软专为修复CVE-2015-1674漏洞发布的针对CNG.sys的安全更新。该漏洞实际和笔者在去年10月微软发布Windows 10 第一个预览版(9860)时发布的一篇微博(http://weibo.com/1648808737/BpGpHhEyD)上介绍的漏洞CVE-2015-0010/MS05-010(https://technet.microsoft.com/library/security/MS15-010)是同一个问题,属于微软在修复CVE-2015-0010时没有修复完全,遗留的安全漏洞。
在Windows 10发布后,笔者在其上测试了两个KASLR绕过的漏洞,一个是j00ru在NoSuchCon 2013上公布的一个利用内核KiTrap01处理调试异常的问题探测内核地址绕过KASLR的问题(http://j00ru.vexillium.org/blog/21_05_13/nsc2013_slides.pdf) ,另一个则是笔者在2014年逆向Windows 8.1内核发现的一个CNG.sys中存在的尚未公开的KASLR绕过漏洞,在当时发布的Windows 9860预览版上,这两个漏洞都没有被修复。
由于CNG.sys的设备(\Device\CNG)是系统中为数不多的设置了ALL APPLICATION PACKAGES DACL,从而允许高度隔离的AppContainer
也可以随意访问的设备, 而且此问题同时影响x86和x64的系统(j00ru的KASLR绕过仅能用于x86系统),因此后者的实用性更强。此漏洞是360Vulcan Team为Pwn2Own类型的比赛储备的内核漏洞/缺陷之一,而且微软对KASLR绕过类型漏洞一直是比较暧昧的态度(j00ru的KASLR绕过直至目前Windows 10最新版本10074上仍未得到修复),因此笔者未将此漏洞报告给微软,在此次补丁日中, CVE-2015-1674即是笔者这里提到的该漏洞。
可能是由于该漏洞会影响IE和Spartan中的EPM(增强保护模式,主要使用AppContainer
进行保护),微软还是决定在Windows 10的新版本中修复这个漏洞,我们看到,在2015年1月发布的Windows 10 9926中,微软就已经悄悄地完全修复了这个漏洞,同时在2月的补丁日中, 微软也为同样受影响的Windows 8/8.1/Server 2012/Server 2012 R2推送了MS05-010的来试图修复这一问题。
但是很有意思的是,在MS05-010中,尽管微软给予了该漏洞CVE-2015-0010的编号,但实际并没有完全修复这个问题,导致这个漏洞最终在Pwn2Own 2015上被来自韩国的选手lokihardt使用,攻破Windows内核。也正是因为Pwn2Own 2015上使用,微软才再次发布MS15-052安全更新,为该漏洞换了一个新的编号:CVE-2015-1674。实际这个“新”漏洞和CVE-2015-0010是几乎完全相同的问题,属于CVE-2015-0010没有完全修复而遗留的问题,让人难以理解的是,在Windows 10 9926里,CVE-2015-0010和CVE-2015-1674的问题都是被一次性修复的,不得不说微软内部似乎在开发Windows 10的过程中,对补丁修复和管理发生了疏忽和混乱,才引发了现在的问题。
在ZDI的官方网站上已经公开了这个漏洞的一些细节:http://www.zerodayinitiative.com/advisories/ZDI-15-189/ ,由于cng.sys中的攻击面并不多,具备经验的安全研究人员根据这个信息已经可以比较轻易地发现这个漏洞的细节,因此这里笔者就直接介绍这个漏洞的具体信息了。
漏洞细节
这个漏洞存在于cng.sys的设备控制处理代码中。CNG.SYS是微软的下一代内核密码学驱动,它通过设备控制(DeviceControl)和函数输出提供很多密码学相关接口,和很多Windows内核驱动一样,他的设备控制处理中混合了同时开放给其他内核驱动和用户模式程序的控制功能。这往往是很多内核安全漏洞的来源,我在ISC 2014上关于360 XP盾甲3.0内核防护中曾提到过一个曾影响Windows系统,至今仍影响许多重要第三方驱动的控制接口内的KASLR绕过,也存在类似的问题。
CNG.sys的特殊性在于,他在创建设备(\Device\CNG)后,会使用ObSetSecurityObjectByPointer
为设备设置一个特殊的Security Descriptor
,该安全描述符是允许ALL APPLICATION PACKAGES
权限的用户完全控制该设备的。对微软的AppContainer/EPM
机制稍有了解的同学可以知道,设置了这个权限的设备,即使在IE或Spartan的隔离保护模式下的渲染进程,也是可以直接访问的,CNG这么设置的目的也是希望所有进程都能够访问它的相关接口,所以在驱动的IRP_MJ_CREATE
处理中,也是直接允许任意访问,没有做任何检查,也就是CNG.SYS内的相关接口,即使是被IE/Spartan
的保护模式或者增强保护模式保护的进程,也都可以随意访问的。
在CNG.SYS的设备控制码中,有多个控制码是专门提供给外部驱动使用的,如0x39024,0x39040,0x39044,0x39048,0x39064
等,这些设备控制码将为调用者返回包括FIPSSHA, FIPS3Des,HMAC MD5,FIPS GenRandom ,SSL加解密和 Key管理,BCrypto系列接口等一系列在CNG内部实现的函数接口地址, 通过这样,外部驱动可以直接调用这些函数的接口,进行相关的密码学操作,而无需自己实现这些接口。
这里的问题就是,针对这些专门外内核模式驱动设置的接口,并没有检查IRP的来源是否是内核模式,因此用户模式的程序直接通过DeviceIoControl
函数,一样可以调用这些设备控制码,获取这些函数的接口。当然,用户模式的程序无法直接使用这些接口,但配合CNG.SYS的镜像布局,用户模式的程序就可以获得CNG.sys的基址和相关关键数据的位置,这样就完全绕过了微软内核的KASLR内核模式地址随机化技术。
在微软的MS05-010中,针对0x39024
这个接口进行了处理和修复(CVE-2015-0010),而就在修复代码的正下方一行,就躺着存在问题的0x39040/0x39044/0x39048/0x39064
等接口,直接在2月的补丁中被忽略了。使用该漏洞参加3月Pwn2Own的lokihardt选手看到补丁时,肯定是笑得的开了花:)
直到这个月微软的MS15-052中,才正式修复了0x39040~0x39064
系列接口的问题,并将这个未成功修复的漏洞重新命名为CVE-2015-1674。
这里再介绍一些背景知识:在微软Windows 8.1操作系统之前,微软并不关注本地的KASLR问题,尽管从Windows NT开始, 微软的内核模块就是随机地址的(尤其在x64系统 + Windows 8以上使用了高熵随机数后),但还仅限于为远程内核漏洞增加难度。这主要是因为在Windows 8.1操作系统之前, 任意权限的进程都可以通过NtQuerySystemInformation
,使用SystemModuleInformation
来获得内核和内核驱动程序、模块的基址,直接可以无视KASLR。
在Windows 8.1操作系统中,情况改变了,微软为了进一步对抗在IE的保护模式、增强保护模式下使用内核漏洞进行穿透沙箱的攻击,引入了针对本地程序的KASLR缓和机制,Alex Ionescu在其BLOG上分析了这一机制:(http://www.alex-ionescu.com/?p=82),在Windows 8.1操作系统中,如果进程运行在低完整性级别以下(保护模式或增强保护模式),那么SystemModuleInformation
等相关获得内核模块基址的方法都会被阻止,这样,即使攻击者在保护模式或增强保护模式下触发了内核漏洞,由于无法获得内核基址,也很难进行进一步利用。
CNG的这个安全漏洞就为攻击者打开了在这种缓和下绕过KASLR的大门,因为他正好是一个在保护模式和增强保护模式下也可以使用的漏洞,同时可以在x64和x86系统上都稳定调用,获得了cng相关的关键数据地址后,如果攻击者具备一个内核任意地址写入的漏洞,就可以直接覆盖存储在CNG.SYS数据段的函数列表,当其他内核模式驱动进行调用时,直接获得内核代码执行的权利。
漏洞修复方法
该漏洞的修复相对就简单了,在IRP_MJ_DEVICE_CONTROL
的处理代码中,针对0x39024,0x39040
等控制码,检查Irp->RequestMode
,就可以区分内核模式驱动和用户模式的调用,避免漏洞的发生。 如此简单的漏洞,几乎并列的代码,却被微软补丁修复部门忽略,要分两次,历时半年才能完全修复,不得不说是个悲剧。
0x02 CVE-2015-1701 / MS15-051
漏洞信息
CVE-2015-1701是Fireeye在今年4月18日的报告“Operation RussianDoll: Adobe & Windows Zero-Day Exploits Likely Leveraged by Russia’s APT28 in Highly-Targeted Attack”中提到的一个被用于配合Adobe Flash 漏洞使用进行攻击的Windows内核权限提升漏洞,由于漏洞在4月还没有被修复,因此Fireeye官网上关于此漏洞的信息也是语焉不详。
在这个月的补丁日中,微软通过MS15-051补丁修复了这一权限提升漏洞,而就在补丁发布的几乎同时,俄罗斯kernelmode.info论坛上的黑客hfiref0x在其github上公布利用该漏洞的完整攻击代码(https://github.com/hfiref0x/CVE-2015-1701),似乎暗示着该漏洞同俄罗斯的密切关系。
该漏洞同去年10月曝光的CVE-2014-4113漏洞有相似之处,是Windows内核驱动win32k.sys在创建窗口过程的一处时序问题引发的内核窗口对象标志错乱,导致内核任意代码执行。从Windows 8 操作系统开始,微软就修复了这一问题,但不知为何此漏洞一直遗留在Windows XP/2003/Vista/Windows 7/ Server 2008/ Server 2008 R2操作系统中。
微软的另外一个错误似乎是,在MS15-051的补丁公告中(https://technet.microsoft.com/en-us/library/security/MS15-051)提到, 该漏洞在Windows 7/Windows 2008 R2上的影响是:None,但实际这个漏洞是影响Windows 7和Server 2008 R2的, Github上的攻击代码就是针对的Windows7系统。
漏洞细节
通过分析攻击代码,我们可以知道,这个漏洞是在内核模式驱动win32k.sys中的窗口创建函数:xxxCreateWindowEx
中发生的,该函数是GDI内核中创建内核窗口对象的复杂函数,通过用户模式的user32!CreateWindow(Ex)->NtUserCreateWindowEx
可以最终调用到该内核函数。
xxxCreateWindows
在分析一系列参数后,会使用HMAllocateObject分配一个窗口类型的win32对象, 并为其填充一些参数,如窗口的Class(类)对象等。使用Hex-rays Decompiler
,我们看到,在Windows 8之前+没修复漏洞的操作系统上,有这样的一个逻辑过程:
if ( pcls->spicn && !pcls->spicnSm )
{
xxxCreateClassSmIcon(pcls);
}
pwnd->hModule = hMoudle;
pwnd->lpfnWndProc = MapClientNeuterToClientPfn(pcls, 0, bansi);
这里xxxCreateClassSmIcon
的目的是为该窗口类的图标创建小图标缓存,接下来,系统会通过MapClientNeuterToClientPfn
根据窗口类为窗口设置WindowProc。
看上去这里似乎没有问题,但我们深入来看xxxCreateClassSmIcon
的实现就会发现,这个xxxCreateClassSmIcon
是通过xxxClientCopyImage->KeUserModeCallback
来实现的,也就是说这个调用最终是配合用户模式回调来实现的。
对Window窗口管理相关比较熟悉的读者可能知道,KeUserModeCallback
实际最后会调用放置在PEB->KernelCallbackTable
中的对应函数来实现功能的,而这些函数都是最终实现在用户模式的,例如这里就将最终调用user32!__xxxClientCopyImage
函数来实现。
Kernel Mode Callback是一套win32k专用的user mode-kernel mode
交互回调机制,也是在窗口内核系统中为了提升性能而设计的功能,而由于这个机制的存在,用户模式程序可能通过挂钩系统的kernel callback函数来实现控制、中断内核执行的逻辑流程,从而引发了很多安全漏洞, 包括历史上的CVE-2013-3167漏洞等都是该机制引发的。
曾是Norman Threat Research团队(现在安全公司Azimuth Security)的内核牛人Tarjei Mandt,也曾在Blackhat 2011上发表过专门深入分析该类call back机制引发的安全漏洞的议题《Kernel Attacks through User-Mode Callbacks》,讲述了多个由该机制引发的安全漏洞,有兴趣的读者可以深入了解下。
我们继续来讲解这个漏洞,从刚才的分析我们知道, 通过挂钩user32!__xxxClientCopyImage
函数,我们就可以实现在上面代码的xxxCreateClassSmIcon
位置,也就是在系统填充pwn->lpfnWndProc
前中断xxxCreateWindowEx
的过程,并执行我们想要的操作。 那么接下来该如何利用这点呢? 在hfiref0x贴的代码中我们看到,在hook了该函数后,代码只做了一行操作:
SetWindowLongPtr(GetFirstThreadHWND(), GWLP_WNDPROC, (LONG_PTR)&DefWindowProc);
SetWindowLongPtr
是设置窗口相关数据、属性的函数,这里GWLP_WNDPROC
这个功能索引(index)的作用是对窗口进行子类化(subclass)/去子类化(unsubclass),可以通过子类化,替换窗口的调用过程为自己的函数,来接管窗口的一些处理,也可以通过设置为DefWindowProc
来去子类化,取消接管过程。
这里面GetFirestThreadHWND
是一个获得当前正在被创建的窗口句柄的一个技巧,因为现在xxxCreateWindowEx
正在被中断在内核过程中,仅仅通过用户模式的代码和xxxCreateClassSmIcon
的信息,我们是无法得知当前正在被创建的窗口对象/句柄的。
但是我们知道,在Win32k内核中,所有的内核窗口信息是全部被映射到用户模式的一块内存地址上的,通过user32!gSharedInfo
我们可以得到它的地址(是内核模式窗口信息列表的一个只读映射),而我们刚才说过内核窗口对象在我们中断时已经经由HMAllocateObject
被创建了,那么它实际就已经可以在gSharedInfo中检索到。这并不是什么新鲜的技巧,在过去的一些漏洞攻击代码,例如MWR Labs在Pwn2Own2013中使用的CVE-2013-1300 Exploit中就使用了类似的技巧。
我们看到这里代码使用SetWindowLongPtr
将当前线程正在创建的窗口的WindowProc
替换为了DefWindowProc
,接下来似乎就得到了内核代码执行的能力,这是为什么呢?我们深入看看SetWindowLongPtr->NtUserSetWindowLongPtr
的实现就可以得到解答。 通过分析内核的实现我们可以得知,NtUserSetWindowLongPtr->xxxSetWindowLongPtr
,当index (GWLP_WNDPROC(-4) ) <0
,会调用xxxSetWindowData
来完成最终的设置,我们继续看xxxSetWindowData
的实现,发现有这样的逻辑:xxxSetWindowData
在判断到index是GWLP_WNDPROC
时,会执行如下逻辑:
ptr = MapClientToServerPfn(dwData);
if ( ptr )
{
ClrWF(pwn, WFANSIPROC);
SetWF(pwn, WFSERVERSIDEPROC);
pwn->lpfnWndProc =ptr;
这里的逻辑,是检查此处GWLP_WNDPROC
是不是一个去子类化操作(unsubclass),如果是的话,就认为这里需要设置为内核来接管窗口过程,给窗口设置Server Side Proc
的标志,这个标志的含义是窗口的窗口过程函数将在内核模式下调用。
这里MapClientToServerPfn
的检查方法是核对给当前窗口设置的WindowProc
(也就是SetWindowLongPtr
的参数dwData
)是否是gpsi(PCSERVERINFO)
内apfnClientA/W
中预先设置的函数,即user32
中预先准备的针对各类窗口的处理函数,例如ScrollBarWndProcW
,MenuWndProcW
,ButtonWndProcW
,等等,当然也包括这里的DefWindowProcW
。
如果验证是这些预设的窗口过程,那么就将窗口标志设置为在内核模式运行窗口过程函数,并将窗口过程函数修改为gpsi中->aStoCidPfn中
对应包含的内核处理函数,例如xxxSBWndProc
,xxxDefWindowProc
,xxxMenuWindowProc
,等等。
这里看似没什么问题,因为在内核模式中执行的总是被xxxWindowData
设置的WindowProc(win32k!xxxDefWindowProc)
。
但是,我们放到最前面我们说到的xxxCreateWindowEx
这个逻辑时序来看,就会发现这里会引发严重的问题: xxxCreateWindowEx
:
1. xxxCreateWindowEx调用HMAllocateObject创建窗口对象
2. 调用xxxCreateClassSmIcon -> xxxClientCopyImage
3. xxxClientCopyImage被中断,用户模式函数调用SetWindowLongPtr给当前正在创建的窗口设置WindowProc
4. WindowProc是user32!DefWindowProc,被xxxSetWindowData认为是去子类化,于是将窗口过程设置为内核模式执行,并将WindowProc设置为内核函数win32k!xxxDefWindowProc
5. 从xxxCreateClassSmIcon返回,继续调用MapClientNeuterToClientPfn转化当前窗口类函数的默认WindowProc(也就是用户模式可控的函数),再将窗口对象的WindowProc设置为用户自己的窗口对象
我们看到,因为这个中断过程恰好在xxxCreateWindowProc
为窗口设置WindowProc
前面,所以xxxSetWindowData
修改窗口的WindowProc
为xxxDefWindowProc
是无效的,窗口的WindowProc
还是被修改为用户模式应用程序设置的WindowProc
,而此时,这个窗口的标志已经被设置为是需要在内核模式执行WindowProc
,那么接下来再遇到xxxSendMessage
等函数对这个窗口发送消息时,就会在内核模式下直接跳转、调用实际在用户模式的函数来进行处理,从而直接导致内核模式代码执行。
上面就是这个漏洞的大致原理,也就是说通过在即将给窗口设置WindowProc前,中断xxxCreateWindowEx
的过程,并通过SetWindowLongPtr
让系统认为是在去子类化,并设置窗口为内核模式执行,返回xxxCreateWindowEx
时,设置窗口过程函数时覆盖了去子类化的函数,但没有去掉窗口函数的内核模式执行标记,导致了内核模式任意代码执行。
Exploit代码最终获取了系统进程的token后覆盖自身的token,来进行权限提升,这就没什么好说的了。
漏洞修复方法
在Windows 8及以后的操作系统上,微软已经修正了这块的时序逻辑,先修改窗口的WindowProc,再调用xxxCreateClassSmIcon
,这样xxxCreateClassSmIcon
即使被中断并去去子类化窗口,最终窗口的处理函数也将是win32k!xxxDefWindowProc
,而不会被修改为用户模式的函数,因此不存在这个漏洞。
本月的补丁中,针对Windows 2003/Vista/7/Server 2008/Server 2008 R2也是如此修复这个漏洞的。
在Windows 8及以上的操作系统上,如果CPU支持SMEP功能(IvyBridge以上),这种内核模式直接调用用户模式函数会被拦截,不过攻击者也可能将payload隐藏在内核地址空间来绕过这一拦截。 对于微软没有官方补丁的XP操作系统,360 XP盾甲从v2.0开始就提供了win32k内核函数接管处理,针对CVE-2015-1701这种类型的漏洞无需升级,就可以直接免疫。