一、概述
任何使用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具有替换函数,该函数用特定值替换特定模式的一个或多个匹配内容。以下面的代码为例: