友人がCTFというものをやっているらしく、

わたしもやってみました、CTF。

どうもCTFというのは"Capture the flag"の略で、コンピュータセキュリティについての知見を試す競技(?)だそうですね。

ちょっと調べてみると、ksnctf
ksnctf.sweetduet.info
というのが初心者向けだそうで、早速試してみました。

なるほど、問題によって点数が異なるのね……とりあえずぱっと見て点数の高い4問目

ksnctf - 4 Villager A
に挑戦してみました。

早速sshでアクセスして、

ssh -p 10022 q4@ctfq.sweetduet.info

ちょっと調べてみる。
f:id:lilyext:20160319205937p:plain
ムカつくなぁ……
とりあえずq4をscpで落としてきた。
さて、どうするか……
とは言っても、こちとらコンピュータセキュリティについては無知もいいところ、プログラミングについてもちょくちょくやっているくらい。まぁ思いつかない。
しばらく考えていると、scanfとかは実際のプログラムに使っちゃいけないよ!!バグの元になるからね!!とかC言語の初心者用のサイトとかに書いてあったなぁ……と思い出した。このしょぼい思いつきを元に調べていくと
書式文字列攻撃 - Wikipediaというのに行き着いた。
これかなぁと思ったので書式文字列攻撃をq4に仕掛けていくという方針で考えてみることにした。
Wikiには具体的な方法は書いてはなかったのでさらに調べていくと
inaz2.hatenablog.com
こちらのサイトが出てきたので参考にさせて頂いた。
へぇ……すっごい……
まとめると

  • 書式文字列攻撃は入力された文字列をそのままprintfやfputsといった書式文字列を解釈する関数に渡す際に起こりうるということ。
  • 入力された文字列に%d,%cなどの書式文字列が入っていた場合はスタックの中身を見られ、%nが入っていた場合はメモリの中身を任意に書き換えられる恐れがあること。
  • つまり入力する書式文字列を工夫することによって任意の命令を実行することすることすら可能になることが可能になること(!)

ことがわかった。

ちょっとq4に試してみると、
f:id:lilyext:20160319210016p:plain
おおお!!何か有効っぽい!!('A'は0x41として表される.)

なんで有効だと思うのかといえば、

  • printfなどは入力された書式文字列に対応する値が引数としてスタックに積まれているものとして値を取りに行く。(実際の引数がどのくらい積まれているかはprintfにはわからない)
  • スタックに積まれているものはリターンアドレスや引数だけでなく、局所変数も積まれている。
  • 入力された文字列は局所変数に収められている(かも?)

→つまり%xによって入力した0x41(='A')が表示されるということは入力された文字列が局所変数に収められている

ここで%xの数をいろいろ変えて試してみると、入力された文字列が収められている局所変数の配列の先頭アドレス(これがprintfの第一引数)から6番目のスタックの位置から入力された文字列が収めれれていることがわかる。

とりあえず書式文字列攻撃が有効らしいことはわかったのでこれを利用してどうやってflagを手に入れるかを考えてみた。
何をするにしても情報が不足しているのでまずはq4をディスアセンブルしてみることにした。

objdump -D ./q4 >> ./q4.s

眺めているとfopenを発見。これによってflag.txtの中身を見ることが出来るが、何らかの要因によってfopenが呼び出されない。→つまり、書式文字列攻撃によるメモリ書き換えによってこの関数にたどり着けばいいのではないか、と予想を立てる。

先ほど参考にさせていただいたサイトによって、GOTという場所を書き換えればいいということがわかった。
putsのアドレスを書き換えることにする。

この時点で

  • 入力された文字列のスタック上の位置
  • 書き換える場所→0x80499f4(putsのアドレスがあるメモリのアドレス)
  • 書き込む値→0x8048691(fopenの引数を積む命令がここから始まる)

が決定できた。

とりあえずgdbでこの方針が正しいか確かめる。
最初のfgetsが実行された直後にブレイクポイントを指定しそのタイミングで0x80499f4の値を0x8048691に書き換えてflag.txtの中身が表示されるか確かめてみる。ちなみにflag.txtにはとりあえず"ふらぐだよーん。"と入力しておいた。

f:id:lilyext:20160319210043p:plain
上手くいった!!

あとはどんな書式文字列を送り込むかだけだ。
エンディアン(多バイトデータの並べ方)を考慮して、以下のように書き換える。
[アドレス] [値]
0x80499f4←0x91
0x80499f5←0x86
0x80499f6←0x04
0x80499f7←0x08

まず文字列の先頭に来るのは書き込む対象であるアドレスたち。(これらがスタック上において文字列のアドレスがある位置から6番目から並ぶ。)

%n(それまでに書き込んだ文字数を指定するアドレスに書き込む書式文字)を使って書き換えるので書き込みたい値となるまで文字を出力する必要があるのでその計算をする。
まず0x91を書き込む時点ではすでにアドレス分だけ出力している(16バイト)ので0x80499f4に値を書き込む前に129(=0x41-16)だけ適当な文字を出力する必要がある。よって次に来るのは"%129c"。その次にようやく"%6$n"。
同様にして0x86+0x100-0x91を計算して"%245c"。その次に"%7$n"。(0x100を足しているのは負にならないようにするため。0x100を足しても0x80499f5には下位1バイトのみ書き込まれるので影響はない。)
同様にして0x04+0x100-0x86を計算して"%126c"。その次に"%8$n"。
同様にして0x08-0x04を計算して"%4c"、その次に"%9$n"。

これにて送り込む文字列が完成しました!!こんな感じです!!

[0x80499f4][0x80499f5][0x80499f6][0x80499f7]+"%129c%6$n%245c%7$n%126c%8$n%4c%9$n"(アドレスは16進数)

では、こんな感じのコードを書いて、

import struct


addr=0x080499f4
stratk=struct.pack('<I',addr)
stratk+=struct.pack('<I',addr+1)
stratk+=struct.pack('<I',addr+2)
stratk+=struct.pack('<I',addr+3)
stratk+='%129c%6$n%245c%7$n%126c%8$n%4c%9$n'
print stratk

試してみると……
f:id:lilyext:20160319210056p:plain

成功しました。やった!!

つ、疲れた……

けど、面白かった……