FSOP
Challenge
|
|
既存の構造体の利用・関数ポインタの書き換え
exploitの基本的なプリミティブであるREAD/WRITEプリミティブには、いくつかのレベルがあります。
たとえば、どれだけ好きな値をWRITEできるかが考えられます。
アプリケーション上の制約によってはASCII文字列しか入力できないとか、あるいは30文字しか入力できないなどの文字数制限がある場合があります。
また、FSBの章にも書いたとおりOverflowなのかOoBなのかによってターゲット以外の余計なところも書き換えてしまうか否かが決まります。
それからREADできる値にも種類があり、libcbaseをleakできるもの・.text
baseを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 <= 0
fp->_IO_write_ptr > fp->_IO_write_base
または
fp->_mode > 0
fp->_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
|
|