FSOP
Challenge
| |
既存の構造体の利用・関数ポインタの書き換え
exploitの基本的なプリミティブであるREAD/WRITEプリミティブには、いくつかのレベルがあります。
たとえば、どれだけ好きな値をWRITEできるかが考えられます。
アプリケーション上の制約によってはASCII文字列しか入力できないとか、あるいは30文字しか入力できないなどの文字数制限がある場合があります。
また、FSBの章にも書いたとおりOverflowなのかOoBなのかによってターゲット以外の余計なところも書き換えてしまうか否かが決まります。
それからREADできる値にも種類があり、libcbaseをleakできるもの・.textbaseをleakできるもの・heap/stackをleakできるもの等があります。
exploitをする際には、得られるプリミティブとそれらの種類を考慮しながら最終的な目標(userlandならシェルを取る・kernelならrootを取る・もしくは他の方法でflagだけを読み出す)を達成する道筋を立てていきます。 これがしばしばpwnがパソコンを使ったパズルみたいだと言われる理由です。
pwnとパズル
exploit/pwnはパズルみたいで面白いですが、これはたまたまプログラムというものがパズル的性質を持っているだけであり、 pwn以外でもpwn的面白さを持っているゲームは存在します。 パソコンは好きじゃないけどpwn的面白さが好きという人には、Baba Is You というゲームがおすすめだったりします。本章では、RIPを取るための手法として FSOP: File Structure Oriented Programming を扱います。
これはglibcが標準的に利用するstdinやstdoutなどのFILE構造体を書き換える、とりわけその内部にある関数ポインタを書き換えることで、任意の関数を呼び出す手法です。
このような、既存の構造体を書き換えたり関数ポインタに細工をするという手法はuserlandだけではなくkernelのexploitにおいても頻繁に利用する手法です。
ぜひその感覚と考え方を手を動かしつつ体験してみてください。
struct FILE / struct _IO_FILE_plus
struct FILEは、stdinやstdout・その他ユーザが開いたファイルの入出力処理やバッファリング・ストリーミング等を行うための構造体です(/libio/bits/types/struct_FILE.h):
| |
stdin/stdoutなどは実際はstruct FILEをラップするstruct _IO_FILE_plusという構造体です(/libio/libioP.h):
| |
_IO_FILEの直後にstruct _IO_jump_t型のポインタを持っていることが分かります。
これは、ファイルの入出力に利用する関数ポインタのリストです(/libio/libioP.h):
| |
例として、puts()がglibcでどのように実装されているか見てみましょう(/libio/ioputs.c):
| |
最初にロックを撮ったり諸々して、ifの中ほどで_IO_sputn()を呼んでいます:
| |
マクロはこのあとも続いていきますが、要はstruct _IO_FILE_plusのvtableの中から該当するメンバ__xsputnを呼び出していることが分かります。この関数ポインタは、実際には以下の値が入っています:
gef> p _IO_2_1_stdout_->vtable->__xsputn
$18 = (_IO_xsputn_t) 0x7f6ab248b680 <_IO_new_file_xsputn>
最終的には_IO_new_file_xsputn()(/libio/fileops.c)が呼び出されています。このようにして、FILEは対応する関数を関数ポインタから呼び出すことでファイルの種類に応じて適切に振る舞いを変えながら入出力を行います。
FSOP
関数ポインタと言われると、関数ポインタを書き換えてRIPを取りたくなると思います。
しかし、関数テーブル _IO_file_jumps がどこにマップされているかを見てみると以下のようになります:
| |
_IO_file_jumpsのアドレスは0x00007f6ab2615000 - 0x00007f6ab2615000にマップされており、
この領域はr--でマップされていることが分かります。
それもそのはずで、このテーブルはconstで定義されています(/libio/fileops.c)。
つまり、 _IO_file_jumps内の関数ポインタを書き換えることはできません 。
となると次に考えるのは、自前のフェイクvtableを用意したあと、_IO_FILE_plus.vtableを偽のvtableを指すように書き換えてしまうことです。
これだと確かにRWXプロットの制限には引っかかりません。
しかし、先程の_IO_sputn()の呼び出しマクロを辿ってみると以下の箇所に突き当たることが分かります:
| |
IO_validate_vtableは以下のように定義されています(/libio/libioP.h):
| |
この関数は、指定されたvtableが有効な領域内を指しているかどうかをチェックし、そうでない場合にはプロセスを終了させます。
この有効な領域内というのは以下に示すような範囲であり、先程のvmmapのこともわかるとおり全てRead Onlyでマップされています:
gef> p __start___libc_IO_vtables
$21 = 0x7f6ab2615a00 <_IO_helper_jumps> ""
gef> p __stop___libc_IO_vtables
$22 = 0x7f6ab2616768 ""
すなわち、この場合はvtableの指し示す先を書き換えて偽のvtableを用意することもできません。
__start___libc_IO_vtables の定義
IO_validate_vtable()内で利用されている__start___libc_IO_vtables/__stop___libc_IO_vtablesの定義ですが、
おそらくvtableの定義につけられたattribute __libc_IO_vtables によって生成されていると思われます。
| |
ただし、glibcのどこでこの属性をもとにして上記の定数を生成しているかについては調べていないため、 興味のある人は調べて教えてください。
詰んだ感じがしますが、2022年10月にkylebot(kCTFやpwn2ownで荒稼ぎしてる人)がブログにおいてangrを使ってジャンプテーブルのvalidationが存在しないパスを発見しています。
このパスでは_IO_file_jumpsテーブルではなく、_IO_wfile_jumpsテーブルを利用します(/libio/wfileops.c):
| |
注目すべきはこの中のoverflowフィールドに入ってる _IO_wfile_overflow() です。
これは内部で_IO_wdoallocbuf()を呼びます:
| |
_IO_wdoallocbuf()は_IO_WDOALLOCATEマクロを実行してジャンプテーブルにアクセスします。
このマクロを辿っていくと、 _IO_file_jumpsの場合と違ってvtableの存在する範囲チェックが存在しないことが分かります 。
つまり、FILE._wide_data._wide_vtableテーブルが指し示す先は自由に書き換えてしまえます。
以上を踏まえて、FSOPの手順をまとめると以下のようになります:
FILE._wide_data._wide_vtableを任意に書き込めるアドレスを指すように書き換える- 書き換えた先に偽の
vtableを用意し、doallocateに当たる部分を実行したい命令のアドレスに書き換える FILE._vtableを_IO_file_jumpsから_IO_wfile_jumpsに書き換える (この書き換え自体は、_IO_wfile_jumpsが有効なアドレスにあるためOK)FILE._vtable.__overflow(==_IO_wfile_overflow)を呼び出す- その内部で、上に見たように
doallocate(任意の命令アドレス)にRIPが移る
4の__overflowの呼び出しについてですが、glibcではexit時に呼ばれる関数 __libc_atexit として _IO_cleanup が登録されています。
この関数は、内部で _IO_flush_all_lockp() を呼び出します:
| |
よって、__overflowの呼び出し時には単純にexitするパスを通ればよいということが分かります。
FSOPの制約
ここまでで大まかなFSOPの手順が分かったと思います。 しかし、 このような既存の構造体を利用するexploitには構造体が満たすべき制約がついてまわります 。
例えば先程の_IO_flush_all_lockp()関数が_IO_OVERFLOWに到達するための制限だけ見ても以下のものが挙げられます:
fp->_mode <= 0fp->_IO_write_ptr > fp->_IO_write_base
または
fp->_mode > 0fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base
overflowが呼び出されたあとも、いくつか満たすべき制約が存在します(Exercise 1)。
とりわけOverflowのような線形WRITEでは、書き換えたいところだけを部分的に書き換えることは難しく、
必然的にFILE構造体を最初から最後まで書き換えてやる必要があります。
よってその際にはこれらの制約を全て満たし、目的のパスまで到達させてあげる必要があります。
本Challengeの概要
脆弱性
FSOP自体の説明は以上で終わりです。最後に少しだけ本challengeの概要を見てみます。
もう解けそうな人はExerciseに進んで良いと思います。
本challengeのプログラムはbase64のデコード・エンコードをしてくれます。実際のデコード・エンコードは適当に見つけたMITライセンスで配布されてるライブラリを使っています:
| |
サイズを指定させて入力を受け付けた後、b64_encode()でエンコードしてバッファに格納しています。
脆弱性は、memcpy(data->buf + 1, tmp, strlen(tmp))でbase64エンコードした文字列をbufに格納する部分です。
base64では、エンコードした後のバイト数が30%程度増加します。
それにも関わらずエンコード前のサイズ分しか確保していないバッファにmemcpyするのでオーバーフローが発生します。
ここで、2つほど適当にencodeしたあとのheapを見てみましょう:
gef> x/60gx 0x564cfc064290
#### Data A ######################################################
0x564cfc064290: 0x0000000000000000 0x0000000000000021
0x564cfc0642a0: 0x0000564cfc0642c0 0x0000000000000040
#### Buf A ######################################################
0x564cfc0642b0: 0x0000000000000000 0x0000000000000051
0x564cfc0642c0: 0x0000003d3d515141 0x0000000000000000
0x564cfc0642d0: 0x0000000000000000 0x0000000000000000
0x564cfc0642e0: 0x0000000000000000 0x0000000000000000
0x564cfc0642f0: 0x0000000000000000 0x0000000000000000
#### Data B ######################################################
0x564cfc064300: 0x0000000000000000 0x0000000000000021
0x564cfc064310: 0x0000564cfc064330 0x0000000000000040
#### Buf B ######################################################
0x564cfc064320: 0x0000000000000000 0x0000000000000051
0x564cfc064330: 0x0000003d3d515141 0x0000000000000000
0x564cfc064340: 0x0000000000000000 0x0000000000000000
0x564cfc064350: 0x0000000000000000 0x0000000000000000
0x564cfc064360: 0x0000000000000000 0x0000000000000000
#### TOP #########################################################
0x564cfc064370: 0x0000000000000000 0x0000000000020c91
struct Dataとエンコードした後の文字列を格納するバッファがそれぞれ2つずつ生成されていますね。
とりわけDataには対応するバッファのアドレスとサイズが格納されていることが分かります。
ここで、Buf Aのエンコードされた文字列をオーバーフローさせるとData Bを書き換えられそうだということが分かります。
Data Bのbufアドレスを書き換えると、次にData Bのバッファに対してエンコード・デコードを行う際に書き換えたアドレスに対して書き込みを行えそうですね 。
例えば入力された文字列をエンコードした後の文字列が長さ0x50であり、かつ最後がP(0x70)で終わっていたとすると、
heapの状態は以下になります(なお、エンコードされた文字列はbuf + 1からコピーされるようになっています):
#### Data A ######################################################
0x564cfc064290: 0x0000000000000000 0x0000000000000021
0x564cfc0642a0: 0x0000564cfc0642c0 0x0000000000000040
#### Buf A ######################################################
0x564cfc0642b0: 0x0000000000000000 0x0000000000000051
0x564cfc0642c0: 0x4141414141414145 0x4141414141414141
0x564cfc0642d0: 0x4141414141414141 0x4141414141414141
0x564cfc0642e0: 0x4141414141414141 0x4141414141414141
0x564cfc0642f0: 0x4141414141414141 0x4141414141414141
#### Data B ######################################################
0x564cfc064300: 0x4141414141414141 0x4141414141414141
0x564cfc064310: 0x0000564cfc064370 0x0000000000000040 <== overflowで書き換えられる
#### Buf B ######################################################
0x564cfc064320: 0x0000000000000000 0x0000000000000051
0x564cfc064330: 0x0000003d3d515141 0x0000000000000000
0x564cfc064340: 0x0000000000000000 0x0000000000000000
0x564cfc064350: 0x0000000000000000 0x0000000000000000
0x564cfc064360: 0x0000000000000000 0x0000000000000000
#### TOP #########################################################
0x564cfc064370: 0x0000000000000000 0x0000000000020c91 <== `Data B`のバッファが新たに指す場所
Data Bのバッファがheap内の関係ないところを指すようになりました。
これでData Bから次にデータを読み書きする際に、heap内の関係ないところにアクセスできるようになりました。
このプリミティブを使うことで、AAR/AAWを実現することができます。
AAR/AAWを得るのはExercise 2とします。
libcbase leak
FSOPをするためにはlibcbaseをleakする必要があります。
今回は確保するバッファサイズをユーザが任意に指定し、かつfree()も任意のタイミングで行えるため 自由にunsorted chunkを生成することができます 。
heapの章でもやったとおり、unsortedbinはdouble-linked listでchunkを管理しており、
fd/bkにはそれぞれ前後のchunkのアドレスが格納されています。
また、リストの最初と最後のchunkではfd/bkに対してmain_arena内のアドレスが格納されています。
よって、生成したunsorted chunkのfd/bkを読み出すことでmain_arenaのアドレス及びlibcbaseをleakすることができます。
unsorted chunkの生成にはサイズが重要となります。
heapの章のテーブルを参照すると、tcache/fastbinに入り切らないのは0x420以上のサイズであることが分かります。
よって、0x420以上のサイズのchunkを作った後Deleteを選択すると、unsortedに繋がります。
AAW/AARの実現とlibcbaseのleakができたらあとはFSOPをするだけです。 ぜひ実際に自分でexploitを書いてみてください。
Exercise
1. 今回のFSOPでFILEが満たすべき条件
_IO_flush_all_lockp()の呼び出しから、_IO_WDOALLOCATEが呼び出されるまでに必要なFILEが満たすべき条件を、
ソースコードを追うことで列挙してみてください。
2. challengeでのAAW/AAR
challengeにおいてAAW/AARを実現してください。
challengeはExercise 3と同じです。
3. b64fsop
| |