「スタックオーバフローって
良く聞くけどあまり実感がない!」
という方の向けに、、、
その危険性をあえて実感してみましょう。
スタックオーバーフローですが、
よく似た言葉に、
バッファオーバーフロー
というのも存在します。
スタックオーバーフローは、
バッファオーバーフローの一種で、
その他にヒープオーバーフロー
といったのも存在します。
どのメモリ領域で、
オーバーフローが起こるか
というのが区別の仕方です。
スタックオーバーフローなので、
スタック領域をゴチャゴチャやって、
プログラムの抜け目を作ります。
「ゴチャゴチャ」とは、
期待してない操作によって、
メモリ(スタック)領域にデータを
格納しきれなくなって、
バグを引き起こすことを言います。
コードサンプルは以下になります。
※参考「Hacking:美しき策謀」
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
#include<stdio.h> #include<stdlib.h> #include<string.h> int check_passwd(char *passwd){ char buf[16]; int auth_flag = 0; strcpy(buf, passwd); if(strcmp(buf, "iloveyou")==0) auth_flag = 1; return auth_flag; } int main(int argc, char *argv[]){ if(argc<2){ printf("[-]How to use: %s <password>\n", argv[0]); exit(0); } if(check_passwd(argv[1])) printf("[+]Access Sucsess\n"); else printf("[-]Access Denied\n"); return 0; } |
このソースは、
パスワード判定プログラムです。
実行時にパスワードを指定して、
パスワードが正しいかを出力します。
正しいパスワードは、
"iloveyou"です。
ここで最も恐ろしいこととして、
偽のパスワードが入力されたのに、
それを正しいと判定して、
処理を進めてしまうことです。
どういうことか、やってみます。

"aaaaa"という
パスワードを入れてみます。
当たり前のように拒否されました。
次に正しいパスワードを入力します。
当たり前のように成功しました。
最後に"aを30個"入力してみます。
するとなんと、、、
「パスワードが正しい」と
判定されてしまいました。
もしオーバーフローの脆弱性を持った
コードがログイン画面に使わていたら、
恐ろしいことですよね!?
何が起こったのか、
デバッグしてみましょう。
緑枠がタイプする箇所です。

-gオプションを付与して
コンパイルします。
-gがないとデバッグが出来ません。
gdb(The GNU Debugger)を
-qオプションで起動します。
-qはなくてもOkです。
list 1をタイプすると、
ソースコードが出てきますので、
ブレイクポイントを指定します。
break 9 と break 13をタイプし、
9行目と13行目にブレイクします。
9行目:パスワードをバッファに格納
13行目:main関数にリターン
ブレイクポイントを指定したら、
run $(python -c "print 'a'*30") で
実行させます。
$(python -c "print 'a'*30")は
aを30個タイプしたのと同じです。
aをひたすら30個入力すると、
数え間違え、時間がかかる
の理由からこのようにしてます。
9行目でブレイクした時点で
スタックはどのように
なっているのでしょうか。
※リトルエンディアンである点に注意

x/x bufで、bufの
アドレスとその中身を見ます。
この時点ではパスワードが、
まだ格納されていないので、
中身は空っぽのままです。
x/x &auth_flagで、
auth_flag変数の中身を見ます。
この時点ではまだ、
初期化された状態と
変わってないので0です。
x/32xw $rspで、
ここで肝心なスタックが
どのようになっているのか
実際に見ることが出来ます。
rspはスタックの先頭アドレスで、
そこから32ワード(4×8)を出力します。
スタック領域を見てみると、
bufやauth_flagにまだ0が
格納されていることが分かります。
ここで、cをタイプして
次のブレイクポイントに移行します。
cとは、continueの意味です。
確認ですがこのブレイクポイントは
check_passwd関数がauth_flagを
main関数にリターンする直前です。
そして先ほどと同様に、
この時点での変数の中身を
チェックしてみましょう。
パスワードが入ったbuf配列には、
0x6161616161・・・が
入っています。
この0x61ASCIIコードで
「a」を意味しています。
そう。このaは実行時に
入力したパスワードです。
入力時にaを30個入力したので、
bufの先頭から30バイト分の
メモリが0x61によって
占められています。

30個のa(0x61)は、
buf配列を飛び出して、
auth_flag変数にまで、
浸食しています。
それを証明するかのように、
x/x &auth_flagで
変数の中身を表示したときに、
0x00006161が出力されました。
なのでcheck_passwd関数の
戻り値はauth_flagを返すので、
0x00006161がmain関数に
リターンされてしまいます。
これは偽、つまり0
ではないので真として判定され、
if文が実行されてしまいます。
これがスタックオーバーフローです。
ではこれを回避するためには、
どの対策したらよいでしょうか。
今回の例だと、
buf配列がauth_flag変数に
スタック領域上で意図せず、
影響を与えてしまったことです。
解決策の一つに
static修飾子として、
buf配列を宣言する
という方法があります。
これはbuf配列を
スタック領域以外の
メモリ領域に置くことを
意味しています。
具体的にいうと、
static修飾をした変数は、
bssセグメント(領域)に
格納されることになります。
※未初期化の場合
やり方は以下になります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
#include<stdio.h> #include<stdlib.h> #include<string.h> int check_passwd(char *passwd){ static char buf[16]; int auth_flag = 0; strcpy(buf, passwd); if(strcmp(buf, "iloveyou")==0) auth_flag = 1; return auth_flag; } int main(int argc, char *argv[]){ if(argc<2){ printf("[-]How to use: %s <password>\n", argv[0]); exit(0); } if(check_passwd(argv[1])) printf("[+]Access Sucsess\n"); else printf("[-]Access Denied\n"); return 0; } |
ただ、buf配列の前に、
staticを付けるだけです。
それでは実際に上手く
動作するか確認してみましょう。

このように、aを30個入力しても、
スタックオーバーフローが起こらず、
正確な答えが返ってきました。
余談ですが、逆に
auth_flag変数を
スタック領域以外の場所の
確保したらどうなるのか。
答えはダメです。
確かにauth_flag変数は
buf配列の影響を
受けなくなります。
しかしスタック領域には、
main関数に戻るための
フレームポインタや
リターンアドレスなどの
他にも大事な情報が詰まっています。
auth_flagだけがbuf配列からの
影響を逃れたとしても、
buf配列が他の大事な情報に
危害を加える可能性があります。
従って、危険物をスタック領域から
違うメモリ領域に隔離してやること
これが最も手っ取り早いです。
最後まで読んでいただきありがとうございました。