简而言之,设备用户模块代码完整性防护措施是指阻止没有授权的程序运行,除非他在windows当中经过了信任授权。当然这一防护措施只有windows当中存在,并且它存在于Powershell中防护语言模式(Contrainted Language mode)。当我研究开启防护措施的机器上是如何执行脚本一段时间后,我最后找到了一种方法可以在开启防护措施的系统中执行任意脚本。在向MSRC报告问题后,最终将该漏洞命名为CVE-2017-0007(MS17-012),并且已经修补。这个特定的错误只会影响PowerShell和Windows脚本主机,而不会影响编译代码。
这个漏洞是我第一个CVE,同时也是我第一次分析补丁。所以这篇文章不仅介绍了漏洞的生成,还将会逆向分析补丁如何工作。由于这是我第一次做这种工作,所以可能会有一些错误。如果在文章中出现了错误,请在下方评论告诉我,我加以改正。
当执行一个拥有签名的程序时,wintrust.dll会验证这个文件的签名。这是文件执行后查看得知的。事实上,如果你使用有微软签名的脚本对这个程序进行修改,那么这个程序的完整性就会受到破坏,并且签名不会有效。此类验证对于设备防护至关重要,它的唯一目的就是防止没有签名或者不受信任的代码运行。
CVE-2017-0007就绕过了这种防护措施,允许你通过简单地修改先前已经获得批准的签名的脚本来运行任何未签名的代码。这种情况,我们选择有一个微软签名的脚本是为了能够在开启设备防护措施的机器上运行。举个例子,如果我们尝试在PowerShell中运行一个没有签名的脚本(可以是实例化网站的脚本),会因为PowerShell处于防护语言模式(Constrained
Language
mode)导致执行失败。通过部署代码的完整性策略可以批准任何有签名并且受信任的Powershell脚本在任何语言模式(Fulllanguage)下没有任何限制的运行。在这种情况,我们的代码并没有签名而且也不是受信任代码,所以他在防护语言模式不会被执行成功。
很幸运的是,微软有能够进行签名的脚本,你可以使用sigcheck或者Powershell命令Get-AuthenticodeSignature
。在这种情况,我从Windows SDK中抓取到了一个具有签名的Powershell脚本,并且将它重命名为:"MicrosoftSigned.ps1"
像这样具有签名的脚本,它们往往在正文中被嵌入一个认证签名。如果你修改了这个文件的任何内容,则这个文件的完整性就会破坏,那么这个签名就会无效。你可以简单的从已经存在签名文件中复制签名到一个没有签名的文件中。
就像你看到的这样,这个脚本的内容已经被我们自己的代码替换了,并且sigcheck脚本给出“这个文件的签名无效”的提示。这意味着文件的完整性已经遭到了破坏,并且代码会被阻止运行,不过确实会这样吗?
如上图所示,尽管我们的签名是无效的,但是我们已经执行了我们的程序。微软将这个漏洞命名为CVE-2017-0007,归类为:MS17-012。这个漏洞根本原因是确保文件的完整性的代码并没有验证成功,也就是验证程序没有对存在错误代码的未签名程序进行验证,最后成功执行未签名的脚本。
那么,这个漏洞的是什么造成的,并且应该如何修补这个漏洞呢?设备防护依赖于wintrust.dll去解决已签名文件的验证签名以及完整性的问题。由于这个漏洞的根本原因,所以我找到了第一个漏洞发生的位置。使用bindiff比较打补丁前的wintrust.dll(10.0.14393.0)以及打补丁后的wintrust.dll(10.0.14393.953),我发现新增了一个代码块。同时,还有一个变化,这个变化是在验证签名这一方面的唯一变化。因此,我猜出这一部分是漏洞的补丁:
进一步查看,你可以看到从"sub_18002D0F8"中删去了一些代码:
新添加的代码块叫做"sub_18002D104",你可以从中发现它包含一些来自"sub_18002D0F8"的代码同时一些补充。这些函数没有特定的符号,所以我们必须用他们定义的名字。或许,你可以在IDA中对这些函数进行更有意义的命名。
上图中的文字有点小,不过我会更深一层次的解释上面代码做了什么。我不会详细的介绍如何使用bindiff,但是如果你想了解更多,你可以查看使用手册。有了漏洞发生的确切位置,我开始探索在执行未签名程序时,到底发生了什么。我们知道修复过程中,从"sub_18002D0F8"删除了一些代码,并且添加了新的代码块叫做"sub_18002D104"。所以这两处变化是个不错的起点。首先,我在IDA中打开了没有打补丁之前的wintrust.dll(10.0.14393.0),跳转到补丁中修补的地方,即:sub_18002D0F8。这个函数通过设置一些变量开始,然后他们调用:"SoftpubAuthenticode"
查看"SoftpubAuthenticode"发现接下来它调用了"CheckValidSignature"方法:
似乎"CheckValidSignature"这个方法会在程序执行之前验证签名或者完整性。查看这个方法的内容,可找到在返回值之前调用的最后一个方法:
通过在最后一个函数设置一个断点,我们可以在eax寄存器中可以看到来自”CheckValidSignature”的错误代码。如下图黄色标记:
错误代码为:"0x80096010",根据WIN SDK中的wintrust.h源码解码之后解释为:"TRUST_E_BAD_DIGEST"也就是签名错误。这就是为什么我们运行sigcheck检查未签名文件时,我们看到了"这个文件的签名无效"的原因。在"CheckValidSignature"返回之后,我们又到达了"SoftpubAuthenticode"方法。
调用"SoftPubAuthenticode"之后,会继续调用"SoftpubCallUI"方法,接下来会返回到"sub_18002D0F8",并且在这个过程中,eax寄存器中一直都存放着"0x80096010"这个错误代码。现在我们知道了错误代码是什么,并且在哪个位置存放错误代码。现在我们可以更进一步了解为什么"CheckValidSignature"返回错误,但是我们还是可以将我们的脚本执行成功。此时,在"SoftpubAuthenticode"调用后立即在"sub_18002D0F8"中恢复执行。
由于错误代码存放在eax寄存器中,它通过mov rax,[r12]
从SoftpubAuthenticode方法返回后,直接被覆盖。
由于判断我们脚本的签名不正确的错误代码已经不存在了,所以它现在处于认证状态,并且已经被允许执行:
我们现在已经知道了这个漏洞是如何形成的,我们现在可以看看微软是如何对漏洞进行修复的。为了去了解他的做法,我们需要下载安装补丁KB4013429。通过查看新版本的wintrust.dll(10.0.14393.953),我们可以搜索"sub_18002D104"也就是之前提到的新添加的代码块。我们现在已经知道这个漏洞的原因是寄存器中的错误代码被覆盖而导致没有被认证。我们可以看到这个补丁在”SoftPubAuthenticode”返回之后添加了一条新的调用:
在上图中,你还可能注意到我们的错误代码存放在了ecx寄存器中,并且覆盖rcx寄存器的指令现在取决于一个跟随"jump if
zero"指令的测试指令。这就意味着我们存储在"ecx"寄存器的错误代码只有不遵循跳转的情况进行重写。在新引入的代码中,你可以发现以下内容:
这一方法根据我们对错误代码进行的操作进而返回Bool类型。添加的这一方法是用来检查调用"SoftpubAuthenticode"方法是否成功,或者检查返回的代码是不是和"0x800B0109"匹配,也就是和"CERT_E_UNTRUSTEDROOT"匹配。在这一方面,SoftpubAuthenticode返回0x800996010(TRUST_E_BAD_DIGEST),显然返回的代码与条件不一样,所以这一方法返回1,即True。
将“al”设置为“1”并返回到先前的方法后,我们可以看到这个错误是如何实际修补的:
将"al"设置为"1"时,这一方法又对"al"的值是否是0做了判断。如果不是0,它会设置"r14b"寄存器为0(由于前一个测试指令中没有设置ZF标志)。最后判断"r14b"寄存器中的值是不是0。如果它是0,那么会跟随跳转,将重写"rcx" 寄存器这一步骤跳过(留下ecx寄存器保留我们的错误代码)。错误代码最后被验证,以及我们的脚本会在防护语言模块被拦截,导致执行失败。