##序文(読み飛ばされるべきもの)
私は、よく居る「CTFを遅ればせもいい加減ながらに始めた社会人エンジニア」の一人です。
そんな私は、職業バックグランドがネットワーク主体のため、CTFジャンルでは「リバースエンジニアリング」や、その応用となる「Pwn」の問題は苦手としていました。
と言っても、それらの分野の面白みや重要性は重々知るところなので、重い尻を上げて勉強する昨今です。
##本投稿の趣旨
Pwn入門として色々と学ん内容のうちで、少々飲み込み/使いこなしに手間取った部分、あるいはアイディアとして面白みを感じたトピックを、整理記録しようと思った次第です。(随時に追記もしくは、シリーズ投稿化を予定)
なお、本記事は特定のPwn問題のWriteupでとはせず、CTF攻略に必要とされる技術要素を、私なりにまとめて記録とした内容としたいと思います。
##Contents
###Pwn攻略方針の立案(予備調査)
Pwn問題では、攻略する(解く)上で複数の制約条件が課されます。
制約上けんとは、RELROやNXといった防御のメカニズムの有効化などが挙げられます。こうした制約の存否は下記の方法で判別・推定されます。
- 問題文やヒント部分に明示・暗示されるケース
- checksec.shのようなツールを使って対象バイナリを調査するケース
- リモート・エクスプロイトで、攻撃対象のレスポンスから調査・推察するケース
主な防御メカニズムのそれぞれについては、以下のページが大変参考になります。
https://book.mynavi.jp/manatee/detail/id=64271
さて、CTF以外のどんな問題でも同じことですが、まずすべき事は、達成するべきゴールとそれに当たっての制約条件の把握です。
Pwnでは、「ゴール」はフラグの表示やシェルの奪取が相当し、「制約条件」には防御メカニズムやセキュア・プログラミングの実践が相当します。
一つ例を挙げて説明したいと思います。先にも言及したksnctf Q23「村人B」について、私が行った考察の一例を示します。
この問題では、問題文からの引用が充実しており、以下のセキュリティ機構が課されていることが判別できます。
以上の条件を受けて、私はFSB + ROP + ret2libcというアプローチを選択しました。
・入力時の文字数制限やカナリア・コードの存在(SSP)から、BOFは困難
・操作可能なバッファが狭く、ASCIIアーマーがあるので、シェルコード挿入は困難(Outbound発呼Callbackも遮断する旨記載あり)
・Full RELROの有効化とPIEの有効化のため、GOT Overwriteは困難
上のような考慮の結果、printf関数に入力文字列を渡す箇所でFSBを利用し、ASCIIアーマーに抵触するような\x00を含むアドレスを
避けながら、アドレス指定でスタック上のデータを書き換える事にしました。スタック書き換えの目的は、ROPによるlibc関数の呼び出しを
実現する事です。
###個別の技術アプローチのメモ
(1)FSB (Format String Bug)攻撃
printf, sprintfなどの関数の第一引数に、フォーマット指定子を含んだ文字列をエスケープせず渡しているプログラムの脆弱性を突いて起こせる攻撃。
printfなどの関数の第一引数に後続する(老番となる)番地のメモリ情報のリークや、任意のアドレスを指定しての読み出し/書き込みが可能となります。
CTFで頻用する用法としては、下記の2パターン。
- 読み出し
%<n>$x
printfの第一引数から数えて<n>バイト目のスタック上のアドレスを16進数で表示する
- 書込み
\xd0\xc0\xb0\xa0\xd2\xc0\xb0\xa0\xd4\xc0\xb0\xa0%16693x%<n>$hn%65536x%<n+1>$hn%48831x%<n+2>$hn
0xa0b0c0d0番地の4バイトに"AAAA" + "\x00"を書き込む。書き込み先番地とその2バイト後毎(書込み単位が2バイトのため)の番地を、printf第一引数から後ろ<n>バイト目から3つ指定している。(冒頭のバイナリ12バイト部分)
このトピックの入門ハンズオンには以下の書籍のPwnの章が平易で良いと思います。
https://book.mynavi.jp/ec/products/detail/id=75673
あるいは"FSB", "Format String Bug"でググると、情報が沢山見つかります。
簡単なFSB作成を行うPython関数を例として掲載します。
offsetはprintf第一引数からのバイト数("AAAA"で判別するオフセット)で、hexdest番地以降にlist_hexint([0x00000001, 0xfffffff0]とか)を書き込むための
FSB攻撃ストリングを生成します。
def p(x):
return struct.pack('<I', x)
def str_forge_fsb_margs (offset, hexdest, list_hexint):
sum = len(list_hexint) * 8
addrs = []
fmts = []
for i, word in enumerate(list_hexint):
addrs.append(p(hexdest + (i * 4)))
rt = 0x0000ffff & word
nrt = (rt - sum)
while (nrt) <= 0:
nrt += 0x10000
sum += nrt
idx = offset + (i * 2)
fmts.append("%{0:d}x%{1:d}$hn".format(nrt, idx))
addrs.append(p(hexdest + (i * 4) + 2))
lt = word >> 16
nlt = lt - sum
while (nlt) <= 0:
nlt += 0x10000
sum += nlt
idx += 1
fmts.append("%{0:d}x%{1:d}$hn".format(nlt, idx))
return "".join(addrs + fmts)
(2)ROP
Return Oriented Programmingの略号。NX有効化によりスタック上にシェルコードを書き込んで実行できない場合(実行可能フラグの剥奪)や、スタックの書換え可能なスペースが限られている場合に、ライブラリ関数など既知の関数の呼び出しを連鎖することで、目的とする攻撃処理を遂行する手法。
呼び出し元が何らかの関数を呼び出す前後と、呼び出し先から呼び出し元へ制御を戻す時に、下図のようにスタックが変化する事を利用し、関数の戻りを連続的に偽造します。(リターン指向プログラミングの名称の由来はそこにある)
ただし、この成立の前提として、BOF, FSBなどの脆弱性を利用してスタックやBSSセグメントの書き換えが出来る必要があリます。
よく出る例は、strcpyやwrite関数をret2pltやret2libcで呼び出して、GOTエントリを書込み先として、シェルコードやリークさせたsystem関数の番地を書込んだ上で、続くROPの連鎖呼び出しで、書き込んだ目的の処理をコールするというもの。(ROP + GOT Overwrite)
例としては以下の通り。
"A" * <n>(JUNK) + wwite@plt + (pop pop pop ret) + 1(標準出力) + GOT_of_ロード済み関数(__libc_start_mainなど) + 4(バイト数) + # __libc_start_main in libcのアドレスをリーク
read@plt + (pop pop pop ret) + 0(標準入力) + GOT_of_ロード済み関数 + (4 + len("/bin/sh\x00")) + # この行のペイロードの実行時に、サーバがreadで入力を待っている。この際に、上でリークした関数のアドレスからlibcのベースアドレスを計算する
__libc_start_main@plt + JUNK (0xdeadbeef) + (__libc_start_main@plt + 4) # libcのベースアドレスを元に、オフセットを用いてsystem関数のアドレスを計算しておく。最後の追加アドレスはshの文字列ポインタ。
現在のスタックフレームのRetrunアドレスに連鎖してROPの関数アドレスや引数を記載していくが、呼び出しごとのEBPやESPのフレーム位置調整のために、しばしばpop ebp, retやpop XXX< pop XXX, leaveなどのスタックフレームの操作を行う短いコードの断片を挿入します。
(この断片コードは、ROPガジェットと呼ばれる)
このROPガジェットを利用する際に、popするレジスタの変化が後続のROPでの処理に影響をもたらさないように気をつける必要があります。
例えば、PIE有効な実行ファイルに対してpop ebxを含むようなガジェットを利用してしまうと、PIE呼び出しのベースオフセットが変化し、意図したライブラリ関数にジャンプしないばかりか、プログラムが異常終了します。
よって、pop ebxやpop ebpを無闇に含まないガジェットを利用するか、任意のpop後の値を仕込んで、pop処理による影響をコントロールする必要があると考えます。
ROP理解の大変参考になったのが、katagaitaiチームのPwn勉強会の資料#2。
https://speakerdeck.com/bata_24/katagaitai-ctf-number-2
(3)ASCIIアーマー回避
上述したFSBもそうですが、strcpy関数などを筆頭に入力に文字列(ポインタ)を取るものは、その文字列に終端文字となる"\x00"が含まれていた時点で後続の文字列を処理せず動作を終了してしまいます。
strcpyやprintfなど、Exploitに悪用される際、これらの関数は攻撃者の入力文字列を引数として処理します。これらの関数の特性として、引数の文字列に終端文字となる"\x00"が含まれていた時点で後続の文字列を処理せず動作を終了してしまいます。
セキュリティ性向上の目的でこの特性を利用し、不正な改ざんEIPやGOTキャッシュを書き込めないようにしようとするメカニズムがASCIIアーマーです。
具体的にはlibcなどのライブラリ関数の仮想メモリ上へのマッピング・アドレスが0x00faXXXXなどの\x00開始の若番アドレスに配置されていきます。
このメカニズムが有効な場合は、FSBでも%$x書式などを使い、番地の直接指定を避ける手があります。
また、どうしても直接にアドレスを決め打ちしたデータの書込みが必要な場合には、strcpyなどをROPで連鎖させて、漸進的に目的を果たす必要があります。
(後者の例を以下に記載します。)
"A" * <n>(JUNK) + strcpy@plt + (pop pop ret) + GOT_of_puts[0] + 書込みたいアドレス[0] +
strcpy@plt + (pop pop ret) + GOT_of_puts[1] + 書込みたいアドレス[1] +
strcpy@plt + (pop pop ret) + GOT_of_puts[2] + 書込みたいアドレス[2] +
strcpy@plt + (pop pop ret) + GOT_of_puts[3] + 書込みたいアドレス[3] +
PLT_of_puts + JUNK (0xdeadbeef) + ポインタ to /bin/bash
(Ex.書込みたいアドレス:libcのsystem関数とか)
あと、特定のアドレスが\x00を含んでしまうようなケース(書き込み先が、0xaabbccfc, 0xaabbcd00となるような場合)は、\x00を含むアドレスをpopガジェットでやり過ごして、その次のアドレスや、pop ebpした先のアドレスを改ざんするという手もあります。