无需Native Code的RCE——IE8中的写入原语利用

2019-06-28 约 130 字 预计阅读 1 分钟

声明:本文 【无需Native Code的RCE——IE8中的写入原语利用】 由作者 ret2nullptr 于 2019-06-28 09:47:00 首发 先知社区 曾经 浏览数 19 次

感谢 ret2nullptr 的辛苦付出!

原文链接:https://www.zerodayinitiative.com/blog/2019/5/21/rce-without-native-code-exploitation-of-a-write-what-where-in-internet-explorer?tdsourcetag=s_pcqq_aiomsg

在2018年的最后一天,我在Internet Explorer中发现了一个类型混淆漏洞,可以利用它产生一个write-what-where原语。它于今年四月得到修复,编号为CVE-2019-0752。作为练习,我使用原始的开发技术为此漏洞编写了一个完整的exp。即使漏洞本身仅产生受控写入并且无法触发以产生信息泄漏,但是仍然存在直接且高度可靠的代码执行路径

此外值得注意的一点是,该利用过程不需要使用shellcode

背景

IE == 8或更低的仿真级别,Internet Explorer通过该IDispatchEx机制执行DOM方法和属性。尽管这是最自然的实现选择,但在性能方面还有很多不足。为了提高性能,IEDOM属性和方法的子集实现了快速路径,这些是通过位于静态表mshtml!_FastInvokeTable中的函数指针调用的。当可用时,快速路径通过避免使用某些常规的调度机制来实现加速。以下反编译代码来自mshtml!CBase::ContextInvokeEx中的IDispatchEx::InvokeEx

如上述的代码片段显示,如果请求的是put操作,则不会使用加速调用机制

原因很显然,对于给定的方法或属性,_FastInvokeTable只能包含一个条目,并且从属性的角度来讲,它将指向更频繁调用的属性getter而不是setter

漏洞分析

上述代码中漏洞的根源在于IDispatchEx允许两种不同的属性放置于同一处的事实。典型属性put将例如整数或字符串的标量值分配给属性,此操作类型由标志DISPATCH_PROPERTYPUT指出,其值为0x4。另一种属性的put是将对象引用分配给属性,在使用时,需要在调用时提供标志值DISPATCH_PROPERTYPUTREF,其值为0x8

有点令人困惑的是,标志值被定义为这两个看似不相关的操作类型,因此无法通过DISPATCH_PROPERTYPUT位检测到putref类型的操作。因此,在上述代码中,类型的操作DISPATCH_PROPERTYPUTREF将被错误地路由到_FastInvokeTable属性的条目,而其中存放的属性是get方法的指针。get方法和put方法的函数签名必然是不同的,因此,这里传递的、用于给属性赋值的值就会出现类型混淆。

接下来发生的事情,取决于与被调用的特定属性相对应的混淆的get/put函数的签名。

我找到了三个可能的函数签名子句,如下所示:

在每种情况下,我们都能够调用get方法来代替put方法。

对于Case 1来说,并没有安全隐患,会调用get_className_direct函数,并且对于其out参数(类型为BSTR *),传递的是不兼容类型BSTR的值。当get_className_direct执行时,它会实例化一个新的BSTR来保存get操作的结果,并在BSTR* value参数指定的地址处写入一个指向这个新字符串的指针。在我们的例子中,这会覆盖所提供的BSTR的字符数据的前四个字节。除了覆盖它们外,不会发生其他的内存损坏。注意,4字节指针值实在太短了,绝不可能溢出BSTR分配的字符数据部分覆盖相邻的内存空间。此外,脚本无法访问损坏的字符串数据以进行信息泄漏,因为传递给get_className_directBSTR是临时的。之后,脚本所访问的BSTR的内存空间,和一开始的是不同的。因此,案例1是无法利用的。

对于Case 2来说,被利用的可能性更大一点,通过属性的put操作赋值的对象将作为struct tagVARIANT的值进行传递,但由于将调用get方法,因此,tagVARIANT结构的前4个字节将被解释为VARIANTARG *——一个指向将被结果值填充的VARIANTARG结构的指针。当然,我们能够对tagVARIANT的前4个字节施加部分控制,使其等于指向我们希望破坏的数据的地址。然而这种情况下,混淆的getput函数具有不同的堆栈参数总长度,因此这里很难加以利用。当getter返回时,堆栈指针将无法进行适当的调整。函数调用方会立即检测到这种差异,并且安全地关闭该进程。

相比之下,Case 3提供了出色的可利用性。设置属性时传入的值将传递给CElement::get_scrollLeft,后者会将这些值解释为int*指针写入结果的位置。因此,scrollLeft的当前值将按照我们选择的地址写入内存。之后,控制权将返回给这个脚本。这为攻击者提供了一个write-what-where原语。唯一的限制似乎是scrollLeft的值不能设置为大于0x001767dd的值,所以,这个值就是我们可以写入的最大的DWORD值,然而这也不会造成很大的障碍。

以下PoC演示了如上所述的write-what-where原语。注意,这里使用的是VBScript。据我所知,这是生成所需DISPATCH_PROPERTYPUTREF的唯一方法。

为了触发该漏洞,我们可以将一个MyClass实例赋给scrollLeft

这样,系统会生成一个带有标志DISPATCH_PROPERTYPUTREF的调用。由于mshtml!CBase::ContextInvokeEx中存在安全漏洞,故被调用的将是CElement::get_scrollLeft,而不是CElement::put_scrollLeft

我们知道,CElement::put_scrollLeft具有一个整型参数,因此,调度机制会将MyClass实例强制转换为整型,当CElement::get_scrollLeft接收这个整数后,后面的函数会将该它解释为指向内存位置的指针

总而言之,值0x1234将写入0xdeadbeef。由于实现细节的原因,这里首先会对0xdeadbeef进行一些无关的读写操作。为了查看整体效果,最简单的方法是使用已知的有效地址替换PoC中的0xdeadbeef

漏洞利用

第一部分:从任意写到任意读

利用该漏洞的主要障碍在于,它虽然提供了写入原语,却没有读取原语信息泄漏功能。因此,攻击者首先面临的问题是,不知道任何安全或有用的地址。

但是,只需分配一个非常大的数组使得所选的常量地址几乎总是位于该数组的内存空间中,就能轻松搞定这个问题:

创建ar1时,会在内存中为VARIANT结构分配一段地址连续的缓冲区,总长度为0x30000000字节。如果是从一个干净的进程开始的话,这段内存空间肯定会包括我们选择的地址0x28281000

最初,ar1中的所有VARIANT结构的内容都为0,因此,每个元素的类型都为VT_EMPTY。如果我们在0x28281000处写入一个新值,比如说0x4003 (VT_BYREF | VT_I4),那么,它将改变ar1的一个元素的类型,使其不再是空值。

通过遍历数组,我们可以找出损坏的元素。这里,我们将这个元素称为gremlin,因为gremlin叫起来很气派。在我们的漏洞利用代码中,变量gremlin用于索引,因此,gremlin本身被引用为ar1(gremlin)。

注意,数组的起始地址的可变性是受约束的,因为该地址总是位于内存页的边界处,也就是说,是0x1000的倍数。因此,查找gremlin时,我们不必检查每个数组元素,我们可以检查每个第0x100(0x1000除以VARIANT的大小)处的元素即可。通过这种方法,可以快速完成对gremlin的搜索,通常不到一秒钟。顺便提一下,这种对地址可变性的约束也是我们可以确定0x28281000必定位于一个VARIANT元素的开头而不是VARIANT中间某处的原因。

现在,为什么我选择给gremlin类型为VT_BYREF | VT_I4

因为通过这种类型的VARIANT能够间接获取一个针对整数值的读取原语

换句话说,假设我们按如下方式对gremlin的内存空间执行写操作:

然后,当读取gremlin的值时ar1(gremlin),它将对地址0x12345678进行dereference(*addr)`操作,并返回从那里找到的4字节整数。也就是说,我们终于获取了读原语

实际上,我掩饰了一个优点

要构造以上所示的gremlin,我们要将目标地址写入位置0x28281008。但是,如前所述,我们的write原语有一个限制,即它不能写大于的值0x001767dd

我使用的解决方案是一次写一个小值,每个值在范围内0x00-0xff,每个值从后续地址开始。通过重复这个过程4次,我们可以在内存中建立一个任意的4字节值,但需要注意的是,后面的3个字节最终会被零覆盖。在VARIANT如上图所示的结构中,在字段后面有一个未使用的4字节字段0x28281008,所以不需要的零不会造成伤害(更重要的是,它们都是零开始)。下图显示了如何把0x12345678通过四个单独的受限DWORD写入来构建任意DWORD

应对下一个挑战:我们仍然不知道任何有趣的地址

同样,这很快就得到了补救。由于我们知道数组元素ar1(gremlin)位于0x28281000,因此数组元素ar1(gremlin+1)位于0x28281010。我们可以放置一个任意对象ar1(gremlin+1),然后使用gremlin作为read原语泄露目标对象的地址

上图展示了我如何将gremlin与后续数组元素一起使用。布置好后,ar1(gremlin)`上会放置目标对象的地址

第二部分:从内存控制到代码执行

传统上,此时的下一步是利用我们的内存读写功能来进行ROP,最终导致本机的代码执行。但是,这些方法已经得到了很好的探索。我很想尝试一些更具创新性的东西。

我受到了tombkeeper在2014年描述的Vital Point Strike [PDF幻灯片]技术的启发。该攻击的基本思想是使用内存读/写功能定位和更改内存中的数据结构,从而关闭SafeMode。完成后,脚本可以简单地实例化任意ActiveX对象,如WScript.Shell,并利用它提供的丰富功能。

自2014年blackhat演讲以来,微软已经为tombkeeper的原始演示文稿的“生命点”添加了强大的篡改保护,所以我不相信这是一种可行的技术。但问题仍然是关于可以找到其他“关键点”的问题。

我猜想,一旦攻击者对进程的地址空间进行任意读/写访问,总会有一些方法在内存中构造危险对象,从而简化代码执行。考虑到这一点,我开始寻找一种简洁的新技术,可以在今天用于Internet Explorer,轻松实现代码执行,而无需使用任何ROPshellcode

我决定采用的想法是颠覆调度机制。在对象上调用方法或属性时,调度机制打包脚本提供的参数,将它们转换为基于本机堆栈的参数,最后调用 实现所需方法或属性的 本机函数。因此,调度机制完成了从脚本到本机函数过程中所需的所有繁重工作。我们可以颠覆它以调用我们选择的本机代码吗?

事实上,更改调度的本机目标地址是很容易的部分。通常,在调度期间,可以通过在vtable中查找来找到目标函数。通过读写内存的能力,我们可以创建一个虚假的vtable,其中一些条目已被更改为指向我们选择的本机API。我认为这WinExec是一个可以最容易用于代码执行的API。通过将vtable条目更改为指向WinExec,我们实际上可以通过脚本调度来调用此API。

但是,该计划存在一个主要问题:功能签名并不完全正确。每当通过dispatch调用一个函数时,第一个参数将是一个指向调用该方法的COM对象的指针(this参数)。这对我们来说是个坏消息,因为我们通常需要完全控制传递给目标API第一个堆栈参数。当然,情况就是如此WinExec,其中第一个堆栈参数是指向要执行的命令字符串的指针。

我对这个问题的解决方案是正面的:我在内存中准备的COM对象需要同时是可用的,也是一个有效的ANSI命令字符串——一种内存中的多语言。这比听起来简单得多。考虑一下:当我们准备WinExec通过伪造的vtable调用时,我们不再需要COM对象处于运行状态。不会调用COM对象的任何方法,正是因为WinExec将执行代替对象的原始方法。因此,我们可以随意覆盖内存中COM对象的所有字段。我们必须小心保持COM对象的唯一部分是调度机制本身正常运行所需的那些字段。

我选择了ActiveX对象Scripting.Dictionary。我认为它是一个很好的选择,因为它简单,特别是由于它有一个相对简单的实现IDispatch

尝试Scripting.Dictionary实例的内存布局会显示以下内容:

上图为Scripting.Dictionary的Dispatch-critical字段

整个对象的大小为0x40字节,只有三个DWORD大小的字段对于调度机制至关重要。

第一个,以红色显示,是主要的vtable指针。我们将用指向伪造vtable的指针替换它,其中一个函数指针已被替换为WinExec

第二个,以蓝色显示,是参考计数器。在调度调用的持续时间内,这将增加1。它的精确值并不重要。

最后一个字段以绿色显示,是一个指向小结构(大小为0xc)的指针,它似乎被称为Pld。采取有根据的猜测,我认为这代表Per-LCID Dispatch

总的来说,这表明我们处于相当不错的状态。我们可以用我们选择的几乎任何东西覆盖整个对象,除了第一个和最后一个字段,它们必须分别指向可用(伪造)vtable和完整的pld结构。回想一下,为了进行攻击,此COM对象的内存也必须是要传递给WinExec的有效ANSI命令字符串

我们的第一个挑战是:在第一个字段中,我们如何编写一个同时是vtable指针的4字节值以及ANSI命令字符串的前四个字符?

我的解决方案是编写对象的前8个字节,如下所示:

看看我在那里做了什么?前四个字节可以作为指针值0x28282828读取,我们可以将伪造的vtable放在该位置。但是,当读作ANSI字符时,它们代表字符串((((。这是一个有效的Win32路径组件。之后,我们\..\使用路径遍历放置字符串以取消虚假路径组件((((

请注意,磁盘上不需要存在名为((((的文件夹。我推荐读者阅读James Forshaw 撰写的这篇文章,以便对Windows中路径处理的细微处理进行出色的处理。

要清除的下一个障碍是引用计数,如上图的蓝色所示,但由于它确实是一个低位。我们放在那里的任何值都是可以接受的,只要我们记住DWORD将在调用之前递增WinExec。因此,我们将预先缩小的数据放在那里,以便将其增加到我们想要的值。我决定要运行一些PowerShell,因此我们到目前为止所做的是:

其中.ewe将递增,以便读取.exe(字节0x77是字符w,这是在上面所示的DWORD的低位字节199e3fd4)。

在此之后,我们开始放置PowerShell脚本。不幸的是,到现在为止我们的空间已经不多了。在我们达到第三个障碍(即pld指针)之前,只有0x1c可用字节。我们如何防止pld指针的出现破坏PowerShell脚本的文本?我通过打开PowerShell Comment解决了这个问题:

之后,我们可以关闭PowerShell命令并编写所需的PowerShell脚本,而不受任何进一步的限制。那时我们将会在Scripting.Dictionary内存的末尾之后再写入一些值,但只要我们正确地准备堆,这就不会造成任何问题。

确实出现的一个问题是pld指针有时会包含一个字节,如0x000x22(双引号),这会过早地终止PowerShell命令。为了防止这种情况,我写了一些脚本来复制pld结构并在0x28281020的固定位置重写它。然后我将0x28281020作为pld指针放入Scripting.Dictionary

在完成这个细节之后,当从一个最原始的进程开始时,该漏洞利用完全可靠。

惊喜奖金

我在Windows 7开发了这个漏洞利用,因为在Windows 10上不允许使用VBScript。不久之后,James Forshaw 发布了他发现同样允许VBScriptWindows 10上运行的研究成果。这让我可以在Windows 10上为IE编写一个漏洞利用版本。微软已经修补了由CVE-2019-0768引发的漏洞,但我们仍然可以用它进行此演示。

Windows 10上,代码执行前有一条最后的防线:CFGCFG会阻止试图WinExecvtable 进行函数调用吗?很可惜它并没有,似乎微软认为不适合使用CFG限制WinExec调用GetProcAddress以及一些用于开发的API。我不会因为这个决定而对他们提出错误。一旦攻击者对进程的内存空间具有完全读/写访问权限,尝试锁定代码执行的所有可能途径就不值得冒险。

此处显示的是2019年2月补丁级别的Windows 10 1809Internet Explorer的完整漏洞。此PoC也可以在我们的GitHub存储库中找到。从最初始的进程开始,它非常可靠。增强保护模式可以关闭或打开(但不是在具有64位渲染器进程的增强保护模式下)。启用增强保护模式后,生成的代码执行将受到IE EPM AppContainer的约束。

结论

我感觉我们只是通过使用对地址空间的读/写访问来解决可能实现的问题。这种访问级别使得可以任意破坏数据结构,甚至可以预先手动创建内存中不存在的新对象实例。攻击者可以使用它来实现他们的目标,而无需执行任何单一的机器级指令

关键词:[‘安全技术’, ‘二进制安全’]


author

旭达网络

旭达网络技术博客,曾记录各种技术问题,一贴搞定.
本文采用知识共享署名 4.0 国际许可协议进行许可。

We notice you're using an adblocker. If you like our webite please keep us running by whitelisting this site in your ad blocker. We’re serving quality, related ads only. Thank you!

I've whitelisted your website.

Not now