Corrosionというヒープ系exploitテクニックが公開されたのですが、全然文献が無いのでまとめようと思います。使えると思うし元記事の考察もよく書かれているので個人的に好きな手法です。説明が並んでいるだけでPoCとかは無さそうなので、実例を挙げつつ紹介します。Corrosionやることはglobal_max_fastを書き換えた際にfastbin[i]にデータを書き込める原理を使おう、という話です。CorrosionはGLIBCの2.27向けのヒープ系exploit手法です。Orangeと似ていますが、2.23から2.27の間に追加されたセキュリティ機構の下でも動きます。PIEとASLRが有効で、かつアドレスリークが無い場合にも使え、UAFで最低10バイト(8バイト+bkの下位2バイト)書き込めれたらOKです。原理まずはglobal_max_fastを書き換えることでどうやって任意アドレスに任意データを書き込むかの説明です。tcacheサイズのチャンクが確保できないときとか)覚えておくと良いと思います。attackで改竄すると巨大なチャンクもfastbinに入るわけですが、もちろんmain_arenaにはそんな巨大な配列は用意されていません。実際に大きなチャンクをfreeすると(今の場合はfastbinが使われるので)fastbinを超えてbinsとかもっと後ろの場所にアドレスが書き込まれるわけです。このときの計算式は次のように表せます。0x20たとえば&main_arena->fastbinから0x1000バイト先をいじりたい場合、チャンクサイズが0x820のチャンクをfreeすれば良いわけです。ここで_int_freeには次のようなチェックがあります。(free)");が、__libc_freeはhave_lockを0にして_int_freeを呼び出すのでチェックされません。(は?)原理2原理1で、適当なサイズのチャンクをfreeすることでfastbin+8*iみたいな場所にfreeしたチャンクのアドレスを書き込めました。次に任意の値をfastbin+8*iに書き込む方法を考えます。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を用意します。Aの順にfreeします。fastbin(dst)は次のようになります。Overflow)を使い、Aの最下位バイトをAを指すように書き換えます。...※AやBのサイズは巨大でAの最下位バイトを書き換えるだけではBからAに変えられません。corruptionをしておきましょう。この時fastbinのサイズチェックに引っかからないように次のチャンクのサイズヘッダ的なものを適当に用意しておくことにも注意してください。valueと呼ばれています。freeの状況が作れました。freeが無い前提でお話していました。一度Aと同じサイズのチャンクをmallocすると、Aのアドレスを手元に確保しつつ、dstにはAのアドレスが入っている状況になりました。...手札:AここでUAFを使い、値を持ってきたい元のアドレスfastbin+8*j(以降srcと表記)に繋がるようにAのチャンクサイズを変更します。この状態で手札のAをfreeすると、fastbin(src)にAが繋がります。このときもともとsrcに欲しい値があったので、Aに欲しい値(以降valueと表記)が繋がることに注意してください。valueさらにUAFを使ってAのチャンクサイズをdstに入るようなサイズに戻します。(mallocした時にabortしないように。ここでdstサイズのチャンクをmallocすると、srcにあったvalueがdstに移植されることが分かるでしょう。value手札:Aなお、2つ前のsrcとdstにそれぞれA-->valueが繋がった状態で(UAFで)Aを変更することで、valueを部分的に変更できます。srcにあるlibcのどこかのアドレスの下位2バイトを変更して好きな場所に繋げる、といったことが可能になります。in-flight"と呼んでいます。シェルが起動する仕組みここまででも十分便利そうですが、欲しいのはシェルです。Corrosionのおいしい点はアドレスリークが無くても良いという点です。ここまでで説明した原理を使ってシェルを取るまでの流れを説明します。Shuiっていうのはpwn分野では初めて聞いたので開発者のオリジナルだと思うのですが、拼音は風水で、元記事では最初に上手いことチャンクをオーバーラップするなど、最初にいい感じにしておく手順を指して使われているようです。(【追記】昔からあるpwn用語だそうです。1)でやっておくことは次の5つです。(順番はどうでもいい)ヒープ上に偽のチャンクサイズを置いておき、あとで怒られないようにする。values)(UAFの場合は)チャンクサイズを改竄できるように上手いこと操作しておく。チャンクをlargebinに入れてサイズを改竄する。attack用のチャンクをmallocする。原理1&2で使う用のチャンクをmallocする。これで風水的に良いヒープの状態になります。1と2は良いでしょう。binにつなぎ、さらにサイズ0x420以上のチャンクをmallocし、largebinに入ったところでそのチャンクのサイズのNON_MAIN_ARENAビットを立てておきます。これは最後のStageで使うので理由は後で説明します。4つ目も問題ないでしょう。5つ目は次のStageで使う用のチャンクなので、いろんなサイズのチャンクをmallocしておく必要があります。基本的に0x3b00バイト以内のmallocがあれば問題ないらしいです。細かいサイズはあとで例を挙げて説明します。attackglobal_max_fastを書き換えましょう。この際4ビットの運ゲーがあります。(正確には1/16よりやや低い確率。5で_int_malloc中のassertにわざと引っかかる必要があります。binのbkが指すアドレスにある(ヒープ上ではない)チャンク用にサイズとbkを作る必要があります。こいつのサイズはdumped_main_arena_startと被っており、bkフィールドはpedanticと被っています。1でNON_MAIN_ARENAを立てたチャンクと同じサイズに(原理2で)設定し、bkはどこか書き込み可能なアドレスに(原理1で)しておきましょう。stderrの改竄ここが一番大変なステップです。Exploitをするだけです。これ自体はいくつか方法があり、代表的なものは普通にstdoutを使う方法や、exitを呼び出す方法などですが、ここではプログラム中でstderrもstdoutも使われておらず(leakless)、かつexitも呼び出せないという最悪の状態を考えます。libc-2.27を対象にしているので、stderrを改竄した後に_IO_str_overflowを呼び出すことを目標にします。※原理3を使うと一時的にvtableが不正なアドレスになり、_IO_str_overflowに到達するまでにクラッシュしてしまいます。stream)が使われている場合は_modeを1に設定しておくことで出力系が全部失敗するので問題なしです(^o^)bということで、ここから原理2&3を使って書き換える値について説明します。_flagsまずはstderrの_flagsを0にしましょう。理由は以下の2つです。_IO_NO_WRITES)みたいな処理がいくつかあるので、それでreturnしないようにします。gadgetを今回は使います。_IO_str_overflowの中で_flagsからrcxに値がコピーされるらしいです。_mode元記事には書いていませんが、前述したようにstreamが使われている場合は1にしましょう。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にすれば良いと思います。(試してないから怪しいけど。gadgetになっている必要があります。gadgetのアドレスを入れることになります。gadgetのアドレスは存在しないので、原理3を使います。具体的には、__default_morecore()関数のアドレスが__morecoreに置かれているので、原理3でそいつを_IO_buf_endに移します。gadgetのアドレスに持っていきます。attackが成功した時点で下から2バイト目の上位4ビットは分かっているのでここは運ゲーにならない。gadgetの差にしなくてはなりません。いうことで原理3は次の_IO_buf_endに使い、ここでは原理2を使います。gadgetのアドレスになります。_IO_buf_end0でいいのでは?元記事ではなぜか頑なにlibcのアドレスを入れようとしている。EOF;というチェックがありますね。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が少なく済みます。LTS)のlibc-2.27には20個ほどあるらしいです。raxな理由はalignされていないとmovapsで死ぬからです。activityあとはstderrに何か吐き出せばシェルが取れますね。今回はstdoutもstderrも使われていない場合を想定していますが、libcの中にはstderrを使ってくれる場所があります。abortは_IO_flush_all_lockpを呼ばなくなりましたし、malloc_printerrもwriteしてるだけという悲しい変更が加わっていますが、assertは裏切りません。1でNON_MAIN_ARENAフラグを立てたのですが、それがここで役に立ちます。(bck->bk));mallocでlargebinから引っ張るときのassertですが、こいつが失敗するのでstderrが使われます。本来なら__xsputnが呼ばれますが、vtableを改竄したので_IO_str_oveflowが呼ばれます。ちなみにassertの中はこんな感じでstderrをflushしています。(stderr);えらい。あとはここまでの積み重ねでシェルが起動するという感じです。元記事に書いてあったりなかったりすることを考察します。gadgetに変更して__free_hookに直接入れれば良いんじゃね?って話です。考えたら、原理3の利用中に__free_hookがヒープのアドレスなどに書き換わるので、原理3中で使うfreeが失敗して死にます。__malloc_hookも同じです。バイナリ中でmallocと独立にreallocなどが使われていたらそれは使えると思います。gadgetが無いからです。raxじゃないかというと、movupsではなくmovapsを使っているからです。callを使うと良い具合にスタックがalignされるんですね。検証とりあえずstdoutもstderrも使わないバイナリを用意しました。Rabbitも使えません。(というか既知アドレスが無い時点で無理か。)UAFがありますが、解説したような「つらい状況」を作りました。attack用のチャンクと、NON_MAIN_ARENAを後で1にするチャンクを用意します。0x430)やるだけなので詳細は割愛。あとはfastbinで使う場所を押さえておく必要があります。解説で使ったアドレスをまとめると、次の場所に向けてチャンクを押さえる必要があります。0x7ffff7dcfc50offsetからmallocすべきサイズに変換してくれる関数を作っておきます。原理3に向けて2つのチャンクAとBを近いアドレスに配置する関数overlapも用意します。b'\xa0')原理1&2はUAFで直接書き換えられるので、overlapする必要があるのは原理3を使うものだけです。attack後にBとAを順次freeすればfdに入るアドレスはB≒Aになります。fastbinでfreeすると次のチャンクのサイズを調べられるので、次のように偽のチャンクを置いておきましょう。payload)後で__morecoreとかに合わせるのも面倒なので全部サイズっぽくしておきました。2をfreeしてlargebinにつなぎ、NON_MAIN_ARENAフラグを立てます。1gdbでlargebinとチャンクの様子を見ます。guessは4ビットって言ったんですがASLR切るとちょうど嫌な場所に当たるんですよねー。0x420)一応確認しておきます。3原理1と2を使います。attack用のチャンクサイズは0x430なので、dumped_main_arena_startには0x440を入れておきましょう。pedanticは普通にfreeすれば書き込み可能アドレスが入るのでOKです。pedantic確認します。4はやることが多すぎますが、地道に1つずつ潰していきましょう。今回は_modeを書き換える必要はありません。_IO_2_1_stderr_の初期状態を確認します。0xffffffffffffffffまぁ使ってないのでそうですね、という感じです。原理1と2を使うものは簡単なので先にやります。size_buf_base)確認します。raxは\xff\xd0なので__default_morecore周辺を探しておきましょう。_s._allocate_bufferを改竄し、また原理2でvtableを_IO_str_jumpsに向けます。size_s_alloc)確認してみましょう。0x0000000000000000ええやん。5heap問にはよくexit選択肢が存在しますが、今回は存在しないので明示的にexitを呼べません。したがって、stderrにassertで出力してやる必要があります。3で偽装した適切なサイズとbkが入っています。binを使うようなmallocが来たらassertが発生するはずです。binを使わせるにはfastbinが空のチャンクを使う必要があります。gadgetが発火します。0x30)全体像ということで全体のexploitコードはこんな感じになりました。記事は本家しか見つかりませんでした。Writeup月別アーカイブはてなブログをはじめよう!ptr-yudaiさんは、はてなブログを使っています。ブログを報告するスターをつけました引用をストックしましたストック一覧を見る閉じる引用するにはまずログインしてくださいログイン閉じる引用をストックできませんでした。再度お試しください閉じる限定公開記事のため引用できません。