元ネタ
結論
入力値チェックは大事だね。
古事記にもそう書いてある。
バッファオーバーフロー脆弱性
バッファーオーバーフロー脆弱性は、コンピューターの黎明期から今に至るまで見かけるものです。
InternetExploreのVML脆弱性についても、バッファオーバーフローが用いられていました。
C言語のバッファオーバーフロー
C言語は高水準プログラミング(?)と呼ばれますが、データの整合性確保についてはプログラマが責任を持ちます。
この責任をコンパイラに任せてしまうと、全ての変数に対して整合性チェックが行われ、実行プログラムの処理速度が低下してしまうのです。
C言語は簡潔さと速度を信条としますが、データの整合性は捨てています。
プログラマがコントロールできる部分を増やし、実行効率を高めてはいますがその代償として、プログラマのうっかりミスによってバッファオーバーフローやメモリりーくが発生してしまうのです。
8バイトしか割り当てられない領域に、10バイトのデータを割り当てようとすると、プログラムがクラッシュしてしまうこともあるのです。
具体的にコードを見てみます。
バッファオーバーフロー サンプルコード
環境構築
Note
この章ではバッファオーバーフローを実演します。
Dockerおよびgitをインストールしている方は、次のコードでC言語の実行環境を整えることができ、ソースコードも同封されています。
git clone https://github.com/minegishirei/hacking_lab
cd hacking_lab
cd c_language
./run.sh
bashが立ち上がった後は、次のコマンドでビルドと実行をしてみてください。
cd buffer_overflow
gcc -g -O0 main.c
./a.out aaa
サンプルコード
例えば、次のようなコードを実行してみる。
# include <stdio.h>
# include <string.h>
int main(int argc, char *argv[]){
int value = 5;
char buffer_one[8], buffer_two[8];
strcpy(buffer_one, "one");
strcpy(buffer_two, "one");
printf("buffer_two ポインター : %p \n", buffer_two);
printf("buffer_two 値 : %s \n", buffer_two);
printf("buffer_one ポインター : %p \n", buffer_one);
printf("buffer_one 値 : %s \n", buffer_one);
printf("value ポインター : %p \n", value);
printf("value 値 : %d \n", value);
/*最初の引数をbuffer_twoにコピーする*/
strcpy(buffer_two, argv[1]);
printf("buffer_two ポインター : %p \n", buffer_two);
printf("buffer_two 値 : %s \n", buffer_two);
printf("buffer_one ポインター : %p \n", buffer_one);
printf("buffer_one 値 : %s \n", buffer_one);
printf("value ポインター : %p \n", value);
printf("value 値 : %d \n", value);
}
後はこのコードを保存し、 gcc -g -O0 main.c
を実行してみてください。
(Gitリポジトリの中にはすでに同封済み。)
a.out
ファイルが作成されたら完了です。
実験
メモリ内部に収める
まずは通常通り、 aaaa
と入力してみる
./a.out aaaa
結果、特に何も起きなかった。
# ./a.out aaaa
buffer_two ポインター : 0xffffca961a38
buffer_two 値 : one
buffer_one ポインター : 0xffffca961a40
buffer_one 値 : one
value ポインター : 0x5
value 値 : 5
buffer_two ポインター : 0xffffca961a38
buffer_two 値 : aaaa
buffer_one ポインター : 0xffffca961a40
buffer_one 値 : one
value ポインター : 0x5
value 値 : 5
オーバーフローさせる
次に、配列の確保分を超えた aaaaaaaaa
と入力してみる
./a.out aaaaaaaaa
結果、buffer_two
から溢れた一文字のa
が、buffer_one
に格納されてしまった。オーバーフロー発生!
root@275f7b1d6b9e:/code/buffer_overflow# ./a.out aaaaaaaaa
buffer_two ポインター : 0xffffdc6761e8
buffer_two 値 : one
buffer_one ポインター : 0xffffdc6761f0
buffer_one 値 : one
value ポインター : 0x5
value 値 : 5
buffer_two ポインター : 0xffffdc6761e8
buffer_two 値 : aaaaaaaaa
buffer_one ポインター : 0xffffdc6761f0
buffer_one 値 : a
value ポインター : 0x5
value 値 : 5
さらにオーバーフローさせる
buffer_two
をこえ、buffer_one
をこえ、16文字以上の引数を入れた場合... aaaaaaaaaaaaaaaaaaaaa
オーバーフローする文字に制限はないことがわかった
root@275f7b1d6b9e:/code/buffer_overflow# ./a.out aaaaaaaaaaaaaaaaaaaaa
buffer_two ポインター : 0xffffd238f2e8
buffer_two 値 : one
buffer_one ポインター : 0xffffd238f2f0
buffer_one 値 : one
value ポインター : 0x5
value 値 : 5
buffer_two ポインター : 0xffffd238f2e8
buffer_two 値 : aaaaaaaaaaaaaaaaaaaaa
buffer_one ポインター : 0xffffd238f2f0
buffer_one 値 : aaaaaaaaaaaaa
value ポインター : 0x61
value 値 : 97
予防方法
buffer_two のサイズは8バイトですが、argv[1] の長さがこれを超える場合、メモリが破壊される可能性があります。
コピーする際には、strncpy などの安全な関数を使ってバッファオーバーフローを防ぐべきです。
#include <stdio.h>
#include <string.h>
int main(int argc, char *argv[]){
int value = 5;
char buffer_one[8], buffer_two[8];
strcpy(buffer_one, "one");
strcpy(buffer_two, "one");
printf("buffer_two ポインター : %p , 値 %s\n", buffer_two, buffer_two);
printf("buffer_one ポインター : %p , 値 %s\n", buffer_one, buffer_one);
printf("value ポインター : %p , 値 %d\n", &value, value);
if (argc > 1) {
strncpy(buffer_two, argv[1], sizeof(buffer_two) - 1);
buffer_two[sizeof(buffer_two) - 1] = '\0'; // ヌル終端
}
printf("buffer_two ポインター : %p , 値 %s\n", buffer_two, buffer_two);
printf("buffer_one ポインター : %p , 値 %s\n", buffer_one, buffer_one);
printf("value ポインター : %p , 値 %d\n", &value, value);
return 0;
}
まとめ
入力値チェックは大事だね。
古事記にもそう書いてある。
参考

Hacking:美しき策謀 第2版
―脆弱性攻撃の理論と実際
Jon Erickson 著、村上 雅章 訳