搜索
当前位置: 秒秒彩官网 > 读入原语 >

没有本机代码的RCE:利用INTERNET EXPLORER中的写入内容

gecimao 发表于 2019-06-30 07:08 | 查看: | 回复:

  2018年年末,我在Internet Explorer浏览器中发现了一个类型混淆漏洞,利用该漏洞可以获得一个write-what-where原语。直到今年4月份,该漏洞才得到了修复,相应的编号为CVE-2019-0752。虽然通过该漏洞本身只能获得受控的写入原语,并且不会导致信息泄漏,但是仍然存在直接且高度可靠的代码执行路径。此外,该漏洞利用代码无需使用shellcode。在本文中,我们将为读者详细介绍漏洞本身机理,以及具体的利用方法。

  在IE=8或更低的仿真级别,Internet Explorer浏览器是通过IDispatchEx机制来执行DOM方法和属性的。从实现的难度来说,这是最自然的选择,但对于性能而言,这种方式还存在许多不足之处。为了提高性能,IE专门为DOM属性和方法的子集实现了相应的“快速路径”。当然,这些都是通过位于静态表mshtml!_FastInvokeTable中的函数指针进行调用的。如果可能的话,快速路径会通过避免使用某些常规的调度机制来实现加速。以下是mshtml!CBase::ContextInvokeEx中IDispatchEx::InvokeEx的实现代码的反编译结果:

  如上所示,如果请求的操作是属性的put操作,则不会使用快速调用机制。原因很明显,对于给定方法或属性来说,_FastInvokeTable只能包含一个条目,并且对于属性来说,它将指向调用更频繁的属性的getter方法,而不是setter方法。

  对于上面显示的代码中的漏洞来说,根源在于IDispatchEx允许使用属性两种不同的put操作。属性典型的put操作是将标量值分配给属性,例如,整数或字符串。该操作类型由标志DISPATCH_PROPERTYPUT指出,其值为0x4。属性第二种类型的put操作是将对象引用分配给属性。如果要使用第二种类型的操作,需要在调用时提供标志值DISPATCH_PROPERTYPUTREF,其值为0x8。有点令人困惑的是,这个标志值被定义为两个毫不相关的操作类型,因此,无法通过DISPATCH_PROPERTYPUT来确定属性的put操作类型。因此,在上面显示的代码中,通过属性的_FastInvokeTable条目来确定操作类型DISPATCH_PROPERTYPUTREF的做法是错误的,实际上,该条目包含的是指向属性的get方法的指针。我们知道,get方法和put方法的函数签名肯定是不同的,因此,这里传递的、用于给属性赋值的值就会出现类型混淆。

  接下来发生的事情,取决于与被调用的特定属性相对应的混淆的get/put函数的签名。我找到了三种可能的函数签名处理情形,具体如下所示:

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

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

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

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

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

  创建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地址的引用,并返回从那里找到的长度为4字节的一个整数。好了,也就是说,我们终于获得了一个读取原语。

  实际上,这里忽略了一个细节。要想构造如图1所示的gremlin,我们需要将所需的目标地址写入0x28281008位置。但是,如前所述,我们的写入原语有一个限制,即它不能写入大于0x001767dd的值。我使用的解决方案是一次写一个小值,每个值在0x00-0xff范围内,每个值从后续地址开始。通过重复这个过程4次,我们就可以在内存中创建一个任意的长度为4字节的值,但需要注意的是,后面的3个字节最终会被零覆盖。在如图1所示的VARIANT结构的情况下,在0x28281008处的字段后面有一个未使用的4字节字段,因此,多余的零不会造成任何损害(更重要的是,它们本来就是零)。下图显示了如何通过四个单独的受限DWORD写入操作在0x12345678处构建任意的DWORD值。

  现在我们面临的下一个挑战是,如何确定要读取的地址。同样,这个问题也没有什么大不了的。由于我们知道数组元素ar1(gremlin) 位于0x28281000处,因此,下一个数组元素ar1(gremlin+1) 位于0x28281010处。为此,我们可以先将任意对象放入ar1(gremlin+1)元素中,然后,使用gremlin作为读取原语来“泄露”该对象的地址:

  图3展示了我是如何将gremlin与后续数组元素结合使用的。之后,读取ar1(gremlin)的值就能得到目标对象的地址。

  我们已经展示了攻击者是如何随意读写内存,包括泄露任意对象的地址。实际上,如果能够对当前进程的内存空间进行任意读写访问的话,基本上就大功告成了。

  传统上,接下来要做的事情,就是利用我们的内存读写功能来启动ROP风格的代码执行,从而进入本地payload阶段。实际上,这些都是老套路了,下面,我们来玩点新鲜的。

  我的灵感来自tombkeeper在2014年公布的“Vital Point Strike”技术。该攻击方法的基本思想是使用内存读/写功能来定位和篡改内存中的数据结构,从而关闭系统的“SafeMode”保护机制。一旦得手,脚本就可以实例化一个任意的ActiveX对象,比如“WScript.Shell”,并利用该对象提供的丰富功能。

  tombkeeper在2014年的黑帽大会上公布该技术后,微软已经为tombkeeper所谓的“Vital Point”增加了强大的防篡改保护,所以,针对这些原始关键点的攻击技术已经很难奏效了。但是,我们关心的是,是否能够找到其他的“Vital Point”呢?

  我推测,攻击者一旦对进程的地址空间拥有了任意的读/写访问权限,总能设法在内存中构建危险的对象,从而简化代码执行攻击。考虑到这一点,我开始探索新型的漏洞利用方法——不仅适用于当前的Internet Explorer浏览器,而且无需使用任何ROP或shellcode就可以轻松实现代码执行攻击。

  这就意味着需要颠覆调度机制。在调用对象的方法或属性时,调度机制会封装脚本提供的参数,将它们转换为基于本机堆栈的参数,最后调用实现所需方法或属性的本机函数。因此,调度机制完成了从脚本到本机函数进行调用所需的所有繁重的工作。我们可以通过颠覆它来调用我们选择的本机代码吗?

  事实上,篡改调度的本机目标地址是比较容易的。通常,在调度期间,可以通过在vtable中查找目标函数来定位目标函数。借助于读写内存的能力,我们可以创建一个虚假的vtable,其中一些条目已被改为指向我们选择的本机API。在我看来,在实现代码执行攻击时,WinExec是一种最容易使用的API。通过将vtable条目改为指向WinExec,我们就能借助调度机制从脚本中调用这个API。

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

  对此,我们的解决方案是:令在内存中准备的COM对象,不仅是一个可用的对象,同时还是一个有效的ANSI命令字符串。虽然听起来很复杂,但是实际情况并非如此。考虑一下:当我们准备通过伪造的vtable调用WinExec时,我们不再需要COM对象处于运行状态。我们不会调用COM对象的任何方法,因为WinExec将代替对象的原始方法执行。因此,我们可以随意覆盖内存中COM对象的所有字段。为了让COM对象保持可用状态,只要不破坏调度机制本身正常运行所需的那些字段即可。

  在这里,我们使用的是ActiveX对象Scripting.Dictionary。我认为这个对象是一个很不错的选择,因为它非常简单,尤其是在IDispatch的实现方面。

  整个对象的长度为0x40字节,其中,只有三个字段对于调度机制至关重要,其中每个字段的长度与DWORD类型的数据长度一致。第一个字段是主vtable指针,这里以红色显示。我们将用指向伪造的vtable的指针来替换它,其中一个函数指针已被指向WinExec的指针所替换。第二个字段,以蓝色显示,是一个引用计数器。在调度调用期间,这个值将递增。实际上,这个值到底是多少并不重要。最后一个字段以绿色显示,是一个指向小型结构(大小为0xc)的指针,该结构似乎被称为“Pld”。我们推测,这可能表示“Per-LCID Dispatch”。

  总的来说,这表明这一切对我们都很有利。我们可以用任何东西覆盖整个对象,当然,第一个和最后一个字段除外,因为它们必须分别指向可用(伪造)vtable和完整的pld结构。回想一下,为了发动攻击,这个COM对象所在的内存中的内容还必须是一个有效的ANSI命令字符串,只有这样才能传递给WinExec。

  我们的第一个挑战是:在第一个字段中,我们怎样才能编写一个4字节的值,使其既是vtable指针,同时还是ANSI命令字符串的前4个字符?我的解决方案是将下列内容写入对象的前8个字节:

  这下读者应该能看明白了吧?前4个字节可以作为指针值0x28282828读取,我们可以将伪造的vtable放在该位置。此外,当将其作为ANSI字符进行读取时,它们代表字符串((((。这是一个有效的Win32路径组件。之后,我们放入字符串..\,使用路径遍历来“废掉”伪路径组件((((。请注意,这里并不要求磁盘上面必须存在名为((((的文件夹。对于Windows系统中路径处理的详细介绍,请参阅James Forshaw撰写的这篇文章。

  我们面临的下一个障碍是引用计数,即如图4中的蓝色所示的部分。实际上,这个问题也不难解决。对于这段内存,我们基本上可以在其中写入任意值,但是需要牢记的是,在调用WinExec之前DWORD会递增。因此,我们将预处理的数据放到那里,以便将其递增到我们想要的值。这里,我决定运行一些PowerShell代码,因此,当前内存布局如下所示:

  可以看到这里是.ewe,所以,我们要通过递增操作使其读取.exe(字节0x77是字符w,它是上面在199e3fd4处显示的DWORD的低位字节)。

  在此之后,我们开始放置PowerShell脚本。不幸的是,到现在为止,我们的内存空间已经不多了。在我们处理第三个障碍(即pld指针)之前,只有0x1c字节空间可用了。我们如何防止pld指针的出现会破坏PowerShell脚本的内容呢?我是借助PowerShell注释来解决这个问题的:

  之后,我们可以关闭PowerShell注释,并编写所需的PowerShell脚本,此外没有任何其他的限制。这样,我们编写的代码就能越过Scripting.Dictionary分配的内存空间的边界,但只要我们正确地准备好相应的堆,就不会引发任何问题。

  我们需要面对的一个问题是pld指针有时会包含一个字节,如0x00或0x22(双引号),这会过早地终止PowerShell注释。为了防止这种情况,我编写了一些脚本来复制pld结构,并将其覆盖固定位置,即0x28281020处的内存中。然后,我将0x28281020作为pld指针放入Scripting.Dictionary。处理好这些细节之后,当从一个干净的进程开始时,该漏洞利用代码会非常的稳定可靠。

  我是在Windows 7系统上开发文中所用的漏洞利用代码的,因为在Windows 10上不允许使用VBScript脚本。不久之后,James Forshaw公开了一种绕过技术,使得VBScript可以在Windows 10上顺利运行。这样一来,我就可以为Windows 10上的IE编写相应的漏洞利用代码了。微软已经通过CVE-2019-0768修复了这个漏洞,但我们仍然可以用它进行演示。

  在Windows 10上,代码执行前有一条最后的防线:CFG。CFG会阻止尝试从vtable调用WinExec吗?它并没有这么做。微软似乎认为使用CFG来限制对WinExec的调用是不恰当的。一旦攻击者对进程的内存空间具有完全读/写访问权限,尝试锁定代码执行的所有可能途径就不值得冒险了。

  下面展示的适用于Windows 10 1809上Internet Explorer浏览器的漏洞利用完整代码。这个PoC也可以从我们的GitHub存储库中下载。如果使用干净的进程的话,它运行起来还是非常可靠得。增强保护模式可以处于关闭或打开状态(只要不处于64位渲染器进程的增强保护模式下)。启用增强保护模式后,代码执行将受到IE EPM AppContainer的约束。

  在我看来,这里仅仅触及了通过使用对地址空间的读/写访问所能实现的目标的皮毛。这种访问级别使得破坏任意数据结构成为可能,甚至可以手工创建先前并不存在于内存中的新对象实例。攻击者无需执行任何机器级指令,就可以通过该命令达到其目的。

  如文章侵犯了您的权利,请通过邮箱联系我们删除。详情查看:版权纠纷E-Mail:

本文链接:http://k-mood.com/duruyuanyu/491.html
随机为您推荐歌词

联系我们 | 关于我们 | 网友投稿 | 版权声明 | 广告服务 | 站点统计 | 网站地图

版权声明:本站资源均来自互联网,如果侵犯了您的权益请与我们联系,我们将在24小时内删除。

Copyright @ 2012-2013 织梦猫 版权所有  Powered by Dedecms 5.7
渝ICP备10013703号  

回顶部