0x01 概述
2017年6月份微软补丁发布了一个针对Windows系统处理LNK文件过程中发生的远程代码执行漏洞,通用漏洞编号CVE-2017-8464。 当存在该漏洞的电脑被插上存在漏洞文件的U盘时,不需要任何额外操作,漏洞攻击程序就可以借此完全控制用户的电脑系统。同时,该漏洞也可借由用户访问网络共享、从互联网下载、拷贝文件等操作被触发和利用攻击。
与2015年的CVE-2015-0096上一代相比,CVE-2017-8464利用触发更早,更隐蔽。
早,指的是U盘插入后即触发,而前代需要在U盘插入后浏览到.lnk文件。
隐蔽,指的是本代.lnk文件可以藏在层层(非隐藏的)文件夹中,不需要暴露给受害人见到。
程序层面讲,CVE-2015-0096利用点是在explorer需要渲染.lnk文件图标时,而CVE-2017-8464利用点在于.lnk文件本身被预加载时显示名的解析过程中。
本文中,笔者将对这两个漏洞从漏洞的复现和反漏洞技术检测的防御角度进行剖析。本文是笔者在2017年6月份,没有任何PoC的情况下作的一个探索。
0x02 CVE-2017-8464原理
CVE-2017-8464利用能够成功实现基于以下3点:
对控制面板对象的显示名解析未严格认证此对象是否为已注册的控制面板应用。恶意构造的.lnk文件能够实现使explorer注册一个临时控制面板应用对象。如上.lnk文件能够将步骤2中注册的临时对象的随机GUID值传输至步骤1所述之处进行解析。本次利用原理就是由于在解码特殊文件夹时,能够有机会按上述3点完成触发。
细节见0x02节。
(显示名解析,参见IShellFolder:: ParseDisplayName, 以及shell对外的接口SHParseDisplayName。)
0x03 还原
首先,猜下问题点出现在 shell32.dll 中。
通过diff比对分析,可以得知问题点有极大概率是存在于函数 CControlPanelFolder::_GetPidlFromAppletId 中的如下代码:
易知 CControlPanelFolder::_GetPidlFromAppletId 的上层函数是 CControlPanelFolder::ParseDisplayName。
看名字大约理解为解析显示名,这很容易关联到shell提供的接口 SHParseDisplayName,查MSDN可知此函数的功能是把shell名字空间对象的显示名(字符串)转换成PIDL(项目标识符列表的指针,我更喜欢称其为对象串烧)。
(那么PIDL大约长这样子:2-bytes length, (length-2) bytes contents, 2-bytes length, (length-2) bytes contents, …, 2-bytes length(=0)。实例:04 00 41 41 03 00 41 00 00 )
shell32.dll 中调用 SHParseDisplayName 的地方有很多,先验证下从 SHParseDisplayName 能否连通到目标 CControlPanelFolder::ParseDisplayName。(另外 shell32里还有个 ParseDisplayNameChild 效用也是差不多)
建立一个例子小程序工程,代码大概如下:
至于填充names的素材,网上可以搜索到很多,注册表里也容易找到不少:
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\ControlPanel\NameSpaceHKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Desktop\NameSpaceHKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\FolderDescriptions这个地方似乎有不错的货源:https://wikileaks.org/ciav7p1/cms/page_13762807.html
调试发现类似这样的名字可以满足要求:
L"::{20D04FE0-3AEA-1069-A2D8-08002B30309D}\\::{21EC2020-3AEA-1069-A2DD-08002B30309D}\\C:\\stupid.dll"
如第一张图片中,把想要加载的动态库路径传入到 CPL_LoadCPLModule 就成功了。但这里,虽然从 SHParseDisplayName 出发,就能把文件路径送到 CControlPanelFolder::ParseDisplayName -> CControlPanelFolder::_GetPidlFromAppletId。但 CControlPanelFolder::_GetPidlFromAppletId 之前还有 CControlPanelFolder::_GetAppletPathForTemporaryAppId 这一头拦路虎:
这段代码的大概意思是要检查一下传过来的名字是否在它的临时应用识别列表里面,若是则返个对应的路径名回来(显示名<->实际路径)。
跟一下,发现它要对比的检查项,是一个GUID。
通过 CControlPanelFolder::s_dsaTemporaryAppId 这个标识符,容易得知,这个GUID是仅在 CControlPanelFolder::_GetTemporaryAppIdForApplet 中随机生成的:
这就尴尬了,也就是说,我们用 SHParseDisplayName 把动态库路径直接传到这里是不行的。我们需要先去触发CControlPanelFolder::_GetTemporaryAppIdForApplet函数,然后再把GUID替换掉动态库路径,再传过来。
就是说,如果我们先调用某个函数以参数L"::{20D04FE0-3AEA-1069-A2D8-08002B30309D}\\::{21EC2020-3AEA-1069-A2DD-08002B30309D}\\C:\\stupid.dll" 触发 CControlPanelFolder::_GetTemporaryAppIdForApplet,并从explorer内存中”偷”到那个随机GUID。再以 L"::{20D04FE0-3AEA-1069-A2D8-08002B30309D}\\::{21EC2020-3AEA-1069-A2DD-08002B30309D}\\{{GUID}}" 为参数调用 SHParseDisplayName,就可以成功加载stupid.dll(如果C盘根目录真的有)了。
好吧,那么就来看看哪个函数可以先行触发CControlPanelFolder::_GetTemporaryAppIdForApplet 来添加随机GUID。
容易得到它的上层函数是 CControlPanelFolder::GetDetailsEx。
在之前的分析过程中,有个猜测: CRegFolder 似乎是一系列 CxxxFolder 类的分发类,可以在 CControlPanelFolder::GetDetailsEx 和 CRegFolder 同名类函数上下断,搞几下就能得到一票撞过来的断点。
栈回溯中最惹眼的显然就是DisplayNameOfW了。
深入一下,发现它确实就是我们要找的火鸡!(或者SHGetNameAndFlagsW?先不关注)
那么,现在如果能结合 DisplayNameOfW 和 SHParseDisplayName,应该就能实现我们的目标,把.lnk中指定的.dll跑起来了。
不妨写个小程序验证一下是否属实:
其中ucIDList就是L"::{20D04FE0-3AEA-1069-A2D8-08002B30309D}\\::{21EC2020-3AEA-1069-A2DD-08002B30309D}\\C:\\stupid.dll" 转换成PIDL的样子。
DisplayNameOfW 参数 0x8001 表示返回目标路径,0x8000 表示返回全路径。
跑起来有点小意外,stupid并没有被加载。
原因是加载之前有一段代码检测 PSGetNameFromPropertyKey(&PKEY_Software_AppId, &ppszCanonicalName); 是否成功。在explorer里这句是成功的,自己的小程序load shell32.dll跑则失败。
好吧,这不是重点。那么把这段程序load到explorer里去跑下,果然成功了,stupid.dll被加载。或者在 PSGetNameFromPropertyKey 下断,把返回值改为0,也可以成功跑出stupid。
至此,我们知道,只要能来一发 DisplayNameOfW + SHParseDisplayName 连续技,就可以成功利用。
接下来就是寻找哪里可以触发连续技。
DisplayNameOfW的调用点也是蛮多,排除掉一眼看上去就不靠谱的,再把二眼看上去犹疑的踢到次级优先梯队,还剩下这么些需要深入排查的:
然而逐一鉴定后,发现一个都不好使,再把第二梯队拉出来溜一圈,依然不好使。
那么,再看看有关联但之前暂不关注的SHGetNameAndFlagsW吧,另外又一个功能也差不多的DisplayNameOfAsString 也一并进入视野(在分析CShellLink::_ResolveIDList时,这里面就能看到DisplayNameOfAsString,也有 ParseDisplayNameChild。这里面花了很大功夫,然而这里的GetAttributesWithFallback 函数要求满足属性值存在0x40000000位这个条件无法通过。最后不得不转移阵地。另外其实即使这里能跑通,这个函数也不是插入U盘就能立刻触发的,还是需要一定操作。)。
SHGetNameAndFlagsW,这个函数调用点很多,又花了很多时间,然而并没有惊喜。
好在DisplayNameOfAsString的调用点不多,才十多个,并且终于在这里见到了彩头。
可以回溯了:
DisplayNameOfAsString <- ReparseRelativeIDList <- TranslateAliasWithEvent <- TranslateAlias<- CShellLink::_DecodeSpecialFolder <- CShellLink::_LoadFromStream <- CShellLink::Load
就是说,加载 .lnk 文件即触发!
一如既往,再写个小程序测试一下。如料触发:
电脑接下来,按 CShellLink::_LoadFromStream 和 CShellLink::_DecodeSpecialFolder中的判断,制作出 .lnk 文件,就比较轻松愉快了。
0x04 CVE-2017-8464变形
研究发现,目前多数安全软件对利用的检测还不够完善,几种变形手段都可以逃过包括微软 Win10 Defender 在内的安全软件的检测。
1、 LinkFlag域变形
可以添加和改变各bit位包括unused位来逃避固定值检测。
事实上,所有高依赖此域的检测,都是可以被绕过的。
2、 LinkTargetIDList域变形
::{20D04FE0-3AEA-1069-A2D8-08002B30309D}(我的电脑)由SHParseDisplayName解析对应的 PIDL 内容是 0x14, 0x00, 0x1f,0x50, 0xe0, 0x4f, 0xd0, 0x20, 0xea, 0x3a, 0x69, 0x10, 0xa2, 0xd8, 0x08,0x00, 0x2b, 0x30, 0x30, 0x9d
因此 .lnk 利用文件LinkInfo域通常第一项IDList项就是这个值,但其实第[3]号字节值是可以改的,并且不影响结果。
小程序一试便知:
电脑同理,第二段 ::{21EC2020-3AEA-1069-A2DD-08002B30309D}(控制面板项)对应的PIDL内容也可以这样变形。
这样,所有精确检测LinkInfo域的安全软件也被绕过了。
3、 SpecialFolderDataBlock域变形
研究发现有的安全软件会检查SpecialFolderID值,然而这个值也是可以变的。
4、 去掉LinkTargetIDList
研究发现,LinkFlag bit0 位清0,这让所有以此为必要条件的安全软件都失效了。但这个方法在Vista及更高版本的Windows系统才有效。
0x05 CVE-2017-8464检测
那么,安全软件应该如何检测?
1、 对PIDL的检测要mask掉特殊项的[3]号字节。
更为稳妥的方法是调用 DisplayNameOf 检测其结果(相当于检查DisplayName,也就是那个”::{…..}” 字符串)。
2、 LinkFlag域只看bit0和bit7位。bit0位为1检查LinkTargetIDList,为0检查 VistaAndAboveIDListDataBlock。
0x06 关于CVE-2015-0096
简单回顾下前代CVE-2015-0096利用:
与CVE-2015-0096比较,CVE-2017-8464 的分析过程没有特别难点,就作业量而言,CVE-2015-0096 要小很多,但需要灵光一现,巧用一长名一短名双文件和恰好的切分过3处检测。
问题在这里:
电脑CControlPanelFolder::GetUIObjectOf函数中这段处理不当,Start长度限定在0x104,但v15为0x220,在ControlExtractIcon_CreateInstance中进行CCtrlExtIconBase::CCtrlExtIconBase初始化时又会截断为0x104,并且里面没有判断返回值。
意味着v14以%d输入的 “-1″值,我们可以通过增加 Start的长度到0x101,使得CCtrlExtIconBase初始化对象名最终尾部变成”xxxxx,-“的样子。
但这里的CControlPanelFolder::GetModuleMapped函数判断了大长名文件的存在性,所以这个文件一定要真的存在才行。
这样就能通过 CCtrlExtIconBase::_GetIconLocationW 中的检测,因为StrToIntW(L”-“) = 0,从而调用到CPL_FindCPLInfo:
接着,在CPL_FindCPLInfo -> CPL_ParseCommandLine -> CPL_ParseToSeparator 中我们又可以将上面使用过的大长文件名截断为短文件名,因为 CPL_ParseToSeparator 中除了使用”,”作为分割符,也是包含了空格符。
切成短名字,是为了过 CPL_LoadCPLModule 函数中的:
这里有返回值检查,超长的话就返回了。
我们的0x101长度名字,是不能在尾部附加一串”.manifest”的。
过了它,我们的短名dll(如果存在的话)就真的被加载起来了。
所以,这个利用需要用到一长名一短名双文件技巧。
长名文件任意内容,0字节都可以,只是被检测一下存在性。
比如:
3.dll3333333300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000.000
(注意dll后面有个空格)
短名文件(真正加载的就是它了):3.dll
.lnk里指定那个长名字就好了。
Hf,全文完!
原文链接:https://www.anquanke.com/post/id/202705
电脑