Sis puella magica!

いろいろ書く予定

ksnctf 23 Villager B

Villager Bを解いたのでその記録
大まかな方針だけ書くようにしますが自力で解きたい人は見ないでください

問題の確認

To be more secure I enabled several security options as follows:

  • Randomized addresses of the program (-pie)
  • Protected the stack (-fstack-protector-all)
  • Made GOT read-only (-Wl,-z,relro,-z,now)
  • Executed via xinetd
  • Inserted a wait in order to prevent brute force attacks
  • Ascii armor is enabled
  • pieオプションとアドレスのランダム化
  • -fstack-protector-allオプション(これでなにが変わるのかは知らない。カナリアとか?)
  • GOTがread-only(プログラムのロード時にアドレスを解決してread-onlyにするってこと?)
  • xinetd(inetd の仕組みを見てみる - naoyaのはてなダイアリーによるとソケットを標準入出力にdup2して、サーバーとなるプログラムをexecしてくれるものらしい)を通して実行
  • 待ち時間(sleep)がはさんである
  • Ascii armor(調べたところ共有ライブラリのアドレスに0x00(ヌル文字)が含まれるようにする)が有効

とのことらしい

ディスアセンブル

プログラムvillagerをダウンロードしてobjdump -dでディスアセンブルする...がcallのオペランドがなんだかおかしい
しかしgdbでdisasコマンドをやってみたら呼び出し先の関数名がちゃんと表示された
(-pieオプションのせいか)libcの関数がPLTを経由せずに呼び出されていて、プログラムのロード時にローダーが.textセクションを直接書き換えてると考えられる
ロードする前のプログラムではアドレスが解決されていないのでcallする先が正しく表示されない

自分が知らないことが多く推測ばかりになってしまったけど、gdbでとりあえずディスアセンブルはできた

アセンブリを読む

gdbでプログラムを動かしながらmain関数とそこから呼び出されている_Z4convvという二つの関数を見てみる
ちなみに

echo '_Z4convv' | c++filt

とすると、_Z4convvはマングルされる前はconv()という名前だったと分かる
main関数は3秒sleepして_Z4convvを呼び出すことを繰り返しているがこれが "Inserted a wait in order to prevent brute force attacks" の意味だろう
mainと_Z4convvから呼び出されているlibcの関数を見てみるとputs、fflush、sleep、fgets、printfなんかが見つかる
このうちprintfは書式文字列攻撃に使える可能性がある
printfの呼び出しの周辺をみると_Z4convvで二回呼び出されているうちの二回目はfgetsの第一引数、つまり標準入力から入力された文字列が入っている配列のアドレスを直接第一引数に渡している
これで書式文字列攻撃ができることが分かった

ではどうやって攻撃するか

ここまでの流れはVillager Aと大して変わらない
問題は書式文字列攻撃をどう使うかである
"Randomized addresses of the program"によって攻撃は難しくなっている
スタックのアドレスもプログラムがロードされたアドレスもlibcのアドレスも分からないのでは攻撃できない(少なくとも自分は方法が思いつかない)
なのでアドレスを調べた
printf関数を使えばスタックの中身を表示させてスタックのアドレスと、プログラムがロードされたアドレスが分かる
libcのアドレスはputsを使ってプログラム中のlibcの関数のアドレスを含む部分(内容はAscii文字の範囲とは限らないが0x00が現れるまでは問題なくputsできるので)を送らせた
アドレスが分かれば後はlibcの適当な関数を呼び出して目的のファイルの中身を標準出力に流すだけでいい
この作業を人力でやるのは大変なのでスクリプトを書いてアドレスの計算や通信を行わせた