1.バッファオーバフローの概要
(1)バッファとは
メインメモリ内の、一時的な記憶領域である。
バッファという英語は、本来は緩衝材という意味である。なので、ハードディスクとメモリなどの、速度の違いを吸収するバッファ(緩衝材)という意味がもともとだ。だが、ここでのバッファは、一時的な記憶領域という意味で使われる。バッファ領域には、メモリ上のヒープ領域とスタック領域がある。
◆参考:データの構造
午前問題でやったかと思うが、キューとスタックがある。
キューはFIFO(First In First Out)、スタックはLIFO(Last in First Out)である。
(2)バッファオーバフローの動作
ここで、情報処理安全確保支援士試験の過去問(H19春SV午後1問1)を見てみよう。この問題では、C言語にてstrcpy関数を使ったプログラムが掲載されている。
C言語strcpy関数を使ったプログラム(H19春SV午後1問1より) |
---|
T君 :はい。このプログラムでは,バッファオーバフローの結果,[ a:スタック ]領域に確保された変数の値が,意図に反して書き換えられる可能性があります。私がこのプログラムを実行して確認したところ,変数val1がメモリ上に展開されたときの先頭アドレスは0xbffffa60で,同様に変数val2では0xbffff9e0,変数val3では0xbffff5e0でした。このとき,コマンドライン引数として一定バイト数以上の長さの文字列が与えられると,変数[ b:val2 ]がバッファオーバフローを起こして,変数[ c:val1 ]の値が書き換えられてしまいます。 |
決められた領域に記憶されるのであるが、文字数が大きいと、他の領域も書き換えてしまう。
■まつもとゆきひろさんの「コードの世界」
まつもとゆきひろ コードの世界‾スーパー・プログラマになる14の思考法
まつもとゆきひろ
日経BP出版センター
2009-05-21
この本に、バッファオーバフローの解説があるので、引用させてもらう。
・「バッファ」という言葉の解説として、「データを保持するために確保する記憶領域」と述べられている。
・「バッファオーバーフロー」の解説として「固定長のバッファに対して入力していた場合、想定よりもはるかに大きなデータを入力することで、プログラムを異常終了させるものです」
・「最悪の場合、プログラムの制御を奪われます」という理由として、「Cではローカル変数はシステム・スタック上に取られますから、システム・スタックをある特定のパターンで書きつぶすことで、関数からの戻り先アドレスを書き換えることが可能だからです」
・解決策として「これらの関数には配列の長さを指定する「より良い代替関数」が存在しますから、そちらをお勧めします」として、strcpyではなくstrncpy、strcatではなくstrncatなどが紹介されている。
「Cでは」という表現がありますが、
C言語(CやC++)以外では、起こらないのでようか?
そう考えてよい(と思います)。
まつもとさんの上記の本でも「そもそも、Cのように、配列の長さをチェックさえしない言語を使っていることが、トラブルの原因であるといえます」と述べられている。C#やRubyなどを使いましょうということだろう。
2.本当に実現できるのか
よく分からないなりにも、なんとなく仕組みが分かってきました。
でも、手の込んだ攻撃ですね。本当に実現できるのでしょうか?
単純な例を考えよう。Wordに必要以上に大きなファイルや、中身がWordでは無いデータを開かせると不正終了するだろう。このようなイメージだ。
Webサーバのhttpdというプロセスに、不正なものを送り込めば、同じようにhttpdが不正終了してしまうから、サービスをダウンさせることができる。
なるほど。不正終了させる手口はなんとなくイメージできました。
でも、情報流出させるなどの込み入った処理をさせるのは大変ですよね。
確かにそう。
「戻り先アドレス」の場所に、ピタリとあふれさせることは不可能ではないかと思ってしまう。
しかし、ほんとんどの場合は、攻撃ツールによるものだ。
ApacheやJavaやOfficeなどの脆弱性が見つかり、それを攻撃するバッファオーバフローのツールが出回ると、一気に被害が増える。
でも、ApacheやJavaやOfficeの脆弱性があっても、ApacheやJava上やOffice上での不正実行ですよね。
そうであれば、大した問題にならないのでは?
それは違う。
その脆弱性を突いて、任意のプログラムを実行させるのである。
具体的には、プログラムの戻りアドレスまで上書きさせて、違う場所に戻らせる。そこに、任意のプログラムを実行させる命令を書いておけば、何でもできてしまう。つまり、コンピュータが乗っ取られたのと同然の状態になる。
3.バッファオーバフローの過去問
バッファオーバフローに関する情報処理安全確保支援士試験の過去問(H28秋SC午後1問2)を見てみましょう。
過去問(H28秋SC午後1問2) |
---|
脆弱性の中でも,バッフアオーバフロー脆弱性(以下,BOF脆弱性という)は,最近開発されたソフトウェアにおいても数多く報告されている。BOF脆弱性は,主に,スタックベースBOF脆弱性とヒープベースBOF脆弱性に分類される。ソフトウェアにBOF脆弱性がある場合,当該ソフトウェアへの入力によって,①開発者が想定しないメモリ領域に書きこまれ、開発者の意図しない命令が実行されることがある。 設問2(1)本文中の下線①について,スタックベースBOF脆弱性を悪用する攻撃の場合,関数呼出し時にスタックに必ず積まれるはずの,何の値を書き換えることによって攻撃が開始されるか。 20字以内で答えよ。 |
↓
↓
↓
↓
↓
正解は、
「呼び出し元関数への戻りアドレス」です。
4.メモリ空間を見てみよう
C言語でのメモリ空間とアドレスに関して、実際に試しながら解説します。
以下のプログラムを書いてみましょう。
過去問 |
---|
#include <stdio.h> int main(){ int a,b; //整数a,bを定義 a = 10,b = 20; //aに10,bに20を代入 printf("aのアドレス:%p\n",&a); // &を付けることで、aのアドレスを表示する。 printf("bのアドレス:%p\n",&b); //pはアドレス(ポインタ)を表示する場合に指定する。 return 0; } |
非常に簡単なプログラムで、整数a、bを定義し、それぞれ10と20という値を入れます。
この値は、パソコンのメモリ空間上に保存されます。
このとき、これらのaやbの値が、どこに保存したかを管理しておく必要があります。そこで、メモリにはアドレス(住所)があります。
では、これらaやbの値が、メモリ空間上のどこに保存されているかを表示するのが上記のプログラムです。
では、このプログラムを実行してみましょう。
プログラム |
---|
C:\C>gcc -o pointer main.c C:\C>pointer.exe aのアドレス:0028FF2C bのアドレス:0028FF28 |
1行目でコンパイルし、作成されたpointer.exeを実行すると、aのアドレスが「0028FF2C」でbのアドレスが「0028FF28」であることが分かります。
メモリ空間上では、以下のようになっています。
このように、int型ではメモリ上の4つ分の領域を確保します。
5.バッファオーバフローを実際にやってみよう
バッファオーバーフローのサンプル
以下のURLをもとにこちらで加筆しながら実行。
(旧リンク)https://www.uquest.co.jp/embedded/learning/lecture08.html
■メモリ上の配置の例
int型で宣言すると、メモリを4バイト(=32ビット)確保する。
参考までに、char型(1文字)は1バイト(=8ビット)
①以下のプログラムを作成する
# vim pg1.c
----pg1.c
#include <stdio.h>
int main(){
char a,b; //文字a,bを定義
a = 1; //aに10を代入
b = 2; //bに20を代入
printf("aの値は%c\n", a );
printf("aのアドレス:%p\n",&a); // &を付けることで、aのアドレスを表示する。
printf("bの値は%c\n", b );
printf("bのアドレス:%p\n",&b);
}
※Linuxでやったときは、%cではなく%dで正しく動作した。
②プログラムの実行
# ./pg1
aの値は
aのアドレス:0x7ffdef7c2c1f
bの値は
bのアドレス:0x7ffdef7c2c1e
※それぞれ、メモリ上のどのアドレスに配置されているかがわかる
③メモリ空間の様子
上記の結果をもとに、メモリの状態を説明すると、以下のようになる。
1)a=1やb=2をセットする前
メモリのアドレス メモリ上の値
7ffdef7c2c1g 00000000
7ffdef7c2c1f 00000000
7ffdef7c2c1e 00000000
7ffdef7c2c1d 00000000
2)a=1やb=2をセットした後
文字列「1」はASCIIコードで「00110001」、「2」は「00110010」であるので、以下のようになります。
メモリのアドレス メモリ上の値
7ffdef7c2c1g 00000000
7ffdef7c2c1f 00110001 ←文字列「1」がセット
7ffdef7c2c1e 00110010 ←文字列「2」がセット
7ffdef7c2c1d 00000000
■次は配列に入れてみる
①以下のプログラムを作成する
# vim pg2.c
----pg2.c
#include <stdio.h>
int main(int argc, char* argv)
{
// ・4文字格納できるchar配列の変数aを初期文字列"aaa"と終端文字\0の4文字で初期化 ※文字列を扱うには終端文字\0が必要。これにより、文字数+1の配列が必要なため、a[3]ではなくa[4]になっている。
// 具体的には配列のa[0]に"a"、a[1]にも"a"、a[2]にも"a"がセットされる
// ・4文字格納できるchar配列の変数bを宣言(初期値設定なし)
char a[4] = "aaa", b[4];
printf("フォーマット"\n 引数)でフォーマットに従った文字列を出力する
// %sを記載すると、%sと書かれた部分は後続の引数で渡された文字列
printf( "before a=<%s>\n", a );
// a[0]~a[3]の値を表示
int i;
for(i=0;i<=3;i++){
printf("a[%d]の値は%c\n",i,a[i]);
}
printf("a[%d]の値は%c\n",i,a[i]);
//char配列aのアドレスをa[0]~a[3]まで表示
int k;
printf("char a[4] address:\n");
for (k=0;k<=3;k++){
printf("a[%d]=%p\n",k,&a[k]);
}
int m;
printf("char b[4] address:\n");
for (m=0;m<=3;m++){
printf("b[%d]=%p\n",m,&b[m]);
}
}
②コンパイルして、プログラムの実行
# gcc pg2.c -o pg2
# ./pg2
before a=
a[0]の値はa
a[1]の値はa
a[2]の値はa
a[3]の値は
char a[4] address:
a[0]=0x7ffe053d6060
a[1]=0x7ffe053d6061
a[2]=0x7ffe053d6062
a[3]=0x7ffe053d6063
char b[4] address:
b[0]=0x7ffe053d6050
b[1]=0x7ffe053d6051
b[2]=0x7ffe053d6052
b[3]=0x7ffe053d6053
→aの値やaのメモリ上のアドレス(住所)が表示される。
※必要に応じて、コンパイルに必要なパッケージのインストール
#sudo yum install gcc
#sudo yum install glibc-devel.i686 libgcc.i686 libstdc++-devel.i686 ncurses-devel.i686
→i686はインテルの32BitCPUアーキテクチャの名称。
i686→32bit で、x86_64→64bit
参考:https://kazmax.zpp.jp/linux/linux.html
■バッファオーバーフロー
さて、今からバッファオーバフローを実行したいのであるが、32bit環境のプログラムと64bit環境のプログラムでは変数がおかれるアドレスの境界(パディング)が異なるる。32bit版でコンパイルしないとバッファオーバーフローにならない。
よって、32bit版でコンパイルする
①32bit版でコンパイル
#gcc -m32 pg2.c -o pg2
②実行結果
# ./pg2
before a=
a[0]の値はa
a[1]の値はa
a[2]の値はa
a[3]の値は
char a[4] address:
a[0]=0xffe3b840
a[1]=0xffe3b841
a[2]=0xffe3b842
a[3]=0xffe3b843
char b[4] address:
b[0]=0xffe3b83c
b[1]=0xffe3b83d
b[2]=0xffe3b83e
b[3]=0xffe3b83f
このように、配列aと配列bのメモリが続いている。(3fの次は40である)
③バッファオーバフローとなるstrcpyを実行するプログラムにする。
概要としては
・aの配列に値 aaa を入れる
・bの配列にstrcpyで値 bbbbbbb を入れる →もちろん、aは何もしていない
・ところが、aの値が書き換わってしまうのだ。
―――pg3.c
#include
// mainの中で使用しているstrcpyを使うために、C言語標準ライブラリのstring.hを読み込む
#include <string.h>
int main(int argc, char* argv)
{
char a[4] = "aaa", b[4];
printf( "before a=<%s>\n", a );
// 配列bのサイズ(4文字)を超えた文字列と終端文字\0をコピーする
strcpy( b, "bbbbbbb");
// aに格納されている文字列を表示すると、値が書き換わっている
printf( "after a=<%s>\n", a );
int i;
for(i=0;i<=3;i++){
printf("a[%d]の値は%c\n",i,a[i]);
}
int k;
printf("char a[4] address:\n");
for (k=0;k<=3;k++){
printf("a[%d]=%p\n",k,&a[k]);
}
int m;
printf("char b[4] address:\n");
for (m=0;m<=3;m++){
printf("b[%d]=%p\n",m,&b[m]);
}
}
④実行結果
# ./pg3
before a=
after a[]=
a[0]の値はb
a[1]の値はb
a[2]の値はb
a[3]の値は
char a[4] address:
a[0]=0xff96e800
a[1]=0xff96e801
a[2]=0xff96e802
a[3]=0xff96e803
char b[4] address:
b[0]=0xff96e7fc
b[1]=0xff96e7fd
b[2]=0xff96e7fe
b[3]=0xff96e7ff
⑤メモリの状態
メモリの状態を説明します。
1)配列aにaaaをセットした状態
文字列「a」はASCIIコードで「01100001」、「b」は「01100010」であるので、以下のようになります。
メモリのアドレス メモリ上の値
ff96e803 00000000
ff96e802 01100001 ←a
ff96e801 01100001 ←a
ff96e800 01100001 ←a
ff96e7ff 00000000
ff96e7fe 00000000
ff96e7fd 00000000
ff96e7fc 00000000
2)配列bにbbbbbbbをセットした状態
配列bのメモリ空間4バイトでは足らないので、配列aのメモリ空間に上書きしてしまう。
メモリのアドレス メモリ上の値
ff96e803 00000000
ff96e802 01100010 ←b ※本来は配列aのメモリアドレス
ff96e801 01100010 ←b ※本来は配列aのメモリアドレス
ff96e800 01100010 ←b ※本来は配列aのメモリアドレス
ff96e7ff 01100010 ←b
ff96e7fe 01100010 ←b
ff96e7fd 01100010 ←b
ff96e7fc 01100010 ←b
■バッファオーバフローの対策
基本的には、strcpyなどの関数を使わない。
安全な関数とは言えませんが、とりあえず、strcpy → strncpyに変えてみましょう。
strncpyは「strncpy ( char * destination, const char * source, size_t num );」の定義ですので、strcpyと引数の数を変える必要がある。
char a[4]= "aaa", b[4];の場合は次のようにすると使えます。
// 配列bにsizeof(b)を上限として文字列コピー(終端文字\0のコピーは保証されない)
strncpy( b, "bbbbbbb",sizeof(b));
// 終端文字が保証されないため、配列の最後に\0をつけることでprintfなどでオーバーランしないようにする
b[sizeof(b)-1]= '\0';
参考:http://www.cplusplus.com/reference/cstring/strncpy/
※strncpyは\0コピーを保証しません。
6.実際にやってみよう2
こちらも、以下のURLの2つ目をもとに実行
(旧リンク)https://www.uquest.co.jp/embedded/learning/lecture08.html
こちらは、プログラムに引数として値を渡します。バッファオーバーフローの脆弱性をついて、任意のコードが実行できるという
様子を表したものです。
①ソースファイル pg5.c
// mainの中で使用しているprintfとsystemを使うために、C言語標準ライブラリのstdio.hを読み込む
#include <stdio.h>
// mainの中で使用しているstrcpyを使うために、C言語標準ライブラリのstring.hを読み込む
#include <string.h>
// プログラム実行時に呼び出されるmain関数の定義を行う。
// argcはプログラム引数の数+1が格納される
// argvはプログラム引数の配列(argv[0]は実行ファイルの名前が格納される)
int main(int argc, char* argv)
{
// 32文字格納できるchar配列の変数cmdと8文字格納できるchar配列のtmpを定義。
char cmd[32], tmp[8];
// tmpの最後のアドレスとcmdの最初のアドレスが連続していることを確認する
// cmdに文字列"ls"と終端文字の\0をコピー
strcpy( cmd , "ls");
// cmdに格納されている文字列を表示する
printf( "before cmd=<%s>\n", cmd );
// argcが2以上(プログラム引数ありの場合)はtmpに文字列をコピーする
if ((argc > 1) && (argv[1] != NULL)) {
// argv[1]の文字列長を確認せずにtmpにコピーしているため、
// 8文字以上コピーするとバッファオーバーランする(tmpは7文字と終端文字\0が最大サイズ)
strcpy( tmp , argv[1] );
}
// printfでcmdの文字列を確認するためにcmdの32番目に終端文字\0を強制的に設定する
// 終端文字がないとprintfでSEGVする可能性があるため
cmd[31] = '\0';
// cmdに格納されている文字列を表示すると、値が書き換わっている場合がある
printf( "after cmd=<%s>\n", cmd);
printf("\n---exec cmd ---\n");
// system関数は引数で指定したコマンドを実行する関数
// char配列cmdが壊れていない場合はlsコマンドが実行される
system(cmd);
printf("\n------\n");
// 次のようにtmp[7]とcmd[0]のアドレスが連続しているため、プログラム引数が8文字以上の場合はcmdの値が壊れることがわかります
printf("tmp[7] address:\n tmp[7]=<%p>\ncmd[0] address:\n cmd[0]=<%p>\n "
, &tmp[7], &cmd[0]);
// tmpがバッファオーバーランしている場合、cmdの文字列も出力されます
printf("tmp=<%s>\n", tmp);
return 0;
}
②32bit版でコンパイル
# gcc -m32 pg5.c -o pg5
③実行
引数を大きくしていくと、途中からエラーになります。
そして、以下のように引数を渡すと、実行するコマンドがlsからuname -srに変わります。
すると、引数を渡すだけなのに、そのコマンドが実行できてしまうのです。
# ./pg4 "00000000uname -sr"
before cmd=
after cmd
---exec cmd ---
Linux 3.10.0-1062.9.1.el7.x86_64