【技术分享】对抗 DeviceGuard:深入分析 CVE-2017-0007

http://p8.qhimg.com/t01e239c613a0d1865f.jpg

翻译:myswsun

预估稿费:190RMB

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


0x00 前言

过去几个月以来,我很高兴和Matt GraeberCasey Smith一起研究Device Guard用户模式完整性(UMCI)绕过。如果你不熟悉Device Guard,可以阅读:https://technet.microsoft.com/en-us/itpro/windows/keep-secure/device-guard-deployment-guide 。简言之,Device Guard UMCI阻止未经批准的二进制执行,限制Windows Scripting Host,并将PowerShell置于受限语言模式下,除非脚本由可信任的签名者签名过。在花了一些时间研究Device Guard启动的系统中如何处理脚本,我最终确定了一种方法能执行任何未经批准的脚本。问题报告给MSRC后,这个bug被标记为CVE-2017-0007,并补丁修复。这个特殊的bug只会影响PowerShell和Windows Scripting Host,不会影响编译的代码。


0x01 分析

当执行一个签名的脚本,wintrust.dll处理文件签名的验证。理想情况下,如果你将一个微软签名的脚本修改,文件的完整性将被破环,并且签名不再可靠。这种验证对于Device Guard是重要的,它唯一的目的是阻止未签名或者不受信的代码运行。CVE-2017-0007规避了这种保护,允许你通过简单的修改之前已获得可信签名的脚本来运行任何你想要的未签名的代码。在这种情况下,可以选择一个微软的签名的脚本,因为微软签名的代码需要运行在Device Guard下。举个例子,如果我们试图运行未签名的PowerShell脚本来执行受限制的行为(如大部分COM实例化),由于PowerShell的受限语言模式将失败。任何签名并受信的PowerShell代码能通过部署的代码完整性策略批准在FullLanguage模式下运行,执行没有任何限制。这种情况下,我们的代码未签名或受信,因此位于受限语言模式下,将执行失败。

http://p1.qhimg.com/t01fc1aa8e1be722c93.png

幸运的是,微软的脚本有他们的代码签名证书签名。你能使用sigcheck或PowerShell的cmdlet “Get-AuthenticodeSignature“验证脚本确实由微软签名。这种情况下,我抓取了来自Windows SDK中的一个签名的PowerShell脚本,将它重命名为”MicrosoftSigned.ps1“:

http://p8.qhimg.com/t0153d98161d3348ac7.png

当这些脚本被签名时,他们经常在脚本体中包含一个嵌入的认证代码签名。如果你修改了文件的任何内容,文件的完整性将被破环,并且签名不再可靠。你也能简单的从一个签名文件中复制认证代码证书,并粘帖到一个未签名的脚本中:

http://p0.qhimg.com/t0156a326a20691a9cd.png

如你所见,脚本的原始内容用我们的自己的代码替代,并验证签名,结果为“对象的数字签名不能验证“,意味着文件的完整性已经破环,代码将被阻止运行,对吗?

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

如你所见,我们的代码还是执行了,尽管数字签名不受信。微软将这个bug定为CVE-2017-0007,分类为MS17-012。这里潜在的问题是确保文件完整性的函数返回的错误代码没有得到验证,导致成功执行了未签名的代码。

因此,什么是这个bug的原因,且如何被修复的?Device Guard依赖wintrust.dll处理签名文件的签名和完整性校验。使用bindiff比较补丁前(10.0.14393.0)和补丁后(10.0.14393.953)的wintrust.dll,揭露了添加的代码块。Wintrust.dll中有一个改变,这是验证签名脚本的唯一变化。由于这个,补丁如下:

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

仔细看,你能看到sub_18002D0F8的一些代码被移除了:

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

看下新添加的sub_18002D104的代码块,你将看见它包含了一些来自sub_18002D0F8的代码。这些特别的函数没有符号,因此我们必须参考定义的名字。或者,你也能在IDA中重命名这些函数。

http://p0.qhimg.com/t0196e663db239f15a8.png

上面的文本有点小,但是我将深入分析具体做了什么。我不会详细介绍使用bindiff,但是如果你想学习更多,可以参考手册。有了bug修复的位置,我看是确定了当我们的未签名的代码执行时发生了什么。了解到在sub_18002D0F8中删除了一些代码,且添加了一个新块sub_18002D104,这两个地方是个好的分析的起点。首先,我在IDA中打开补丁前版本的wintrust.dll(10.0.14393.0),导航到修改的sub_18002D0F8。这个函数由几个变量开始,然后调用“SoftpubAuthenticode”。

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

看下“SoftpubAuthenticode”揭露了它调用了另一个函数”CheckValidSignature“:

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

很明显,“CheckValidSignature”处理验证文件执行的签名/完整性验证。看下这个函数,我们能得到返回前最后一个执行的位置。

http://p4.qhimg.com/t019d855fff5898a653.png

通过设置windbg断点,我们能看见CheckValidSignature中eax寄存器的错误值,黄色高亮显示如下。

http://p4.qhimg.com/t0160f035fbf595c34f.png

这个情况下,错误值是0x80096010,意为TRUST_E_BAD_DIGEST。这就是为什么我们看见“对象的数字签名不能验证”。对一个修改的签名的文件执行sigcheck。在CheckValidSignature返回后(通过retn),我们来到了SoftpubAuthenticode。

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

SoftpubAuthenticode继续调用SoftpubCallUI,然后回到sub_18002D0F8,并在eax寄存器存储错误值0x80096010。现在我们知道了错误值存储在哪里了,我们能进一步看下为什么我们的脚本被允许运行,即使CheckValidSignature返回了TRUST_E_BAD_DIGEST。到了这里,在SoftpubAuthenticode调用后,我们恢复执行sub_18002D0F8。

http://p4.qhimg.com/t017e3266791f3294cc.png

因为我们的错误码存储在eax中,在SoftpubAuthenticode返回后,立即通过mov rax,[r12]覆盖。

因为错误码表明我们的脚本的数字签名不是可靠的,它没有得到验证,因此脚本允许执行:

http://p6.qhimg.com/t0142819ebec0fef220.png

对于这个bug有了透彻的理解,我们能看下微软如何补丁修复它的。为了做这个,我们需要安装KB4013429。看下新版本的wintrust.dll(10.0.14393。953),我们能浏览sub_18002D104,其中添加了验证代码块。我们知道这个bug源于存储我们的错误码的寄存器被覆盖了,且没有得到验证。我们能看见补丁添加了新的调用sub_18002D4BC,跟在SoftpubAuthenticode的后面。

http://p7.qhimg.com/t01bb49d92966eacea1.png

你在图片中也可能注意到我们的错误码放在了ecx寄存器中,并且覆盖rcx寄存器的指令依赖一个测试指令,接着是“jump if zero”指令。这意味着现在我们的错误码存储在ecx中,只有在不跳转时才会覆盖。看下新加入的sub_18002D4BC,你将看到:

http://p8.qhimg.com/t018a5fa3227691d252.png

这个函数返回一个BOOL值,取决于错误码执行结果。这个额外的校验可以校验调用SoftpubAuthenticode是否成功(< 0x7FFFFFFF),否则返回值为0x800B0109,为CERT_E_UNTRUSTEDROOT。这种情况下,SoftpubAuthenticode返回0x80096010(TRUST_E_BAD_DIGEST),不匹配任何一个条件,将返回1。

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

在设置al为1后,返回到前一个函数,我们能看到这个bug如何打补丁的:

http://p0.qhimg.com/t01fc11ec08b88b9689.png

“al”设为1,函数匹配另一个逻辑,以查看al是否为0。如果不是,设置r14b为0(因为之前的test指令不会设置ZF标志)。然后逻辑校验r14b是否为0。如果是,将跳转并跳过覆盖rcx寄存器的代码(保持ecx为我们的错误码)。错误码得到验证,且脚本在受限语言模式下运行,将执行失败。

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

(完)