公開日:2007年9月26日
独立行政法人情報処理推進機構
セキュリティセンター
本ページの情報は2007年9月時点のものです。
記載の資料は資料公開当時のもので、現在は公開されていないものも含みます。
近年、オペレーティングシステムには、バッファオーバーフロー等の攻撃を失敗に終わらせるためのいくつかの防御機能が備わりつつある。それは、主に次の3つである
関数リターンアドレス書き変えを検出し、それ以上の実行を止める
メモリ上の特定の位置を狙ってのアドレスワードの不正書き変えや、侵害コードへのジャンプといった操作を失敗させる
万一侵害コードへのジャンプが行われたとしてもデータ領域に置かれた機械語コードを実行させない
近年Cコンパイラも進歩し、関数をコンパイルして生成される機械語コードの中に、領域あふれによってスタック上の関数リターンアドレスが改変されていることを検知する機械語コードを挿入できるものが登場している。
その主なものが、GCC の -fstack-protector オプションと、Visual C++ の /GS オプションである。
このオプションを指定してC言語のソースコードをコンパイルすると、プログラムはスタック上の変数のあふれによって生じた関数リターンアドレスの書き変えを検出するようになる。
$ gcc -fstack-protector 他のオプション foo.c ...
このオプションは厳密には、スタックの中の関数リターンアドレスの付近で不適当な値の書き変えが起こっていることを検知するものである。
このオプションが指定されていないとき、関数リターンアドレスの付近のスタックの内容は次のようになっている(Intel 32 ビットアーキテクチャの場合)。
このコンパイルオプションをつけてコンパイルされた関数では、ebp退避領域と関数リターンアドレスの前に4バイトの「カナリア」と呼ばれるワードが配置される。
カナリアは、ローカル変数領域のあふれによるスタック破壊を検知するためのものである。このオプションが付けられてコンパイルされた関数では、関数からのリターン直前にカナリアの値が書き変わっていないかがチェックされ、スタック破壊が検出される。
スタック破壊検出コードの生成はすべての関数に対して行われるのではない。ローカル変数に文字配列が無いか、あっても8バイト未満である関数には、スタック破壊検出コードは生成されない。なお、この8バイトというしきい値は、次のオプションを用いて変更できる。
--param ssp-buffer-size=N
また、次のオプションを使うことでコンパイルするどの関数についてもスタック破壊検出コードを生成させることができる。
-fstack-protector-all
このほか、-fstack-protector オプションでは、スタック上に配列とポインタ変数の両方が存在するときに、ポインタ変数のほうが配列よりも若い(小さい)アドレスの側に配置されるよう変数の位置関係を調整するはたらきをもつ。これは、配列があふれたときの影響をなるべく少なくする工夫である。
Winodows の Visual C++ にも、GCC の -fstack-protector と類似のはたらきをもつコンパイルオプション /GS がある。加えて、Visual C++ 2005 からは、/GS の指定がデフォルトになった。
/GS でもスタックの ebp 退避領域と関数リターンアドレスの前に4バイトの照合用の値が置かれるが、Visual C++ ではこれを「クッキー」と呼んでいる。GCC -fstack-protectorの「カナリア」とほぼ同じ用途であり、関数からリターンする直前にこのクッキーの値が書き変わっていないか検査することによってスタック破壊を検出するものである。
/GS オプションによるスタック破壊検出コードの生成も -fstack-protector と同様、一部の関数については行われないことがある。/GS オプションでは、ローカル変数に文字配列が無いか、あっても5バイト未満である関数については行われない。
/GS のクッキーが -fstack-protector のカナリアと異なるのは、関数呼び出しごとに異なる値が用いられる点である。-fstack-protector では、プロセスごとにカナリアの値は異なるものの、ひとつのプロセスの中ではどの関数においても同じ値が使われる。
それに対し /GS では、関数呼び出しごとにクッキーが異なる値をとるようになっている。具体的には、プロセスごとに決められたひとつの値と、関数が呼び出された時点で ebp レジスタがもっていた値との排他的論理和(XOR)がとられる。
もし攻撃者がカナリアあるいはクッキーと呼ばれる照合用の値をあらかじめ想定することができれば、その値で領域をあふれさせるように仕向け、スタック破壊を検知するロジックの裏をかくことができる。/GS では、照合用の値を事前予測することをより困難にしている。
[例 /GSオプションによるスタック破壊検出プログラム]
1 #include <string.h>
2
3 void bar() {
4 char buf[16];
5
6 memset(buf, 0xAA, sizeof(buf) + 1);
7 }
[bar() 関数からリターンする時点のスタックの内容]
-------- ----------
アドレス 32ビット値
-------- ----------
0012FE70: 0012fe84
0012FE74: 10204e89
0012FE78: 00000000
0012FE7c: 003a1ec0
0012FE80: 00000000
0012FE84: 0012fe90
0012FE88: aaaaaaaa ← char buf[16]
0012FE8C: aaaaaaaa
0012FE90: aaaaaaaa
0012FE94: aaaaaaaa
0012FE98: a38f81aa ← クッキー(壊れている)
0012FE9c: 0012ff14 ← ebp退避領域
0012FEa0: 004112c8 ← 関数リターンアドレス
0012FEa4: 00000000
0012FEa8: 098af99c
0012FEac: 7ffdd000
--------- ----------
[bar() 関数からリターンする箇所の機械語コード]
00411273 pop edi
00411274 pop esi
00411275 pop ebx
※00411276 mov ecx,dword ptr [ebp-4]
※00411279 xor ecx,ebp
※0041127B call @ILT+15(@__security_check_cookie@4) (411014h)
00411280 mov esp,ebp
00411282 pop ebp
00411283 ret
注 /GS によって追加されたコード。関数からリターンする前に「クッキー」が壊されていないかをチェックし、壊れていると例外を発生させるようになっている。
メモリ内に悪意の機械語コードを送り込む攻撃では、メモリ上におかれた何らかのジャンプアドレスの内容を書き変えてプログラムの実行の流れを変え、侵害コードに制御を渡すことが必要である。そのため、攻撃者による攻撃データの組み立ては、書き変えたいジャンプアドレスワードや、制御を移したい侵害コードが置かれるであろうアドレスの想定の下に行われる。
いくつかのシステムでは、プログラムが実行される際のスタックやヒープの位置をプロセスが起動されるたびにランダムに変更し、特定のアドレスを狙った攻撃を逸らすことのできる機能が用意されている。
ここでは Exec-Shield のランダマイズ機能と、Windows Vista の ASLR を取り上げた。
Linux カーネルパッチの Exec-Shield を使うと、スタックおよびヒープの位置をランダムに変更することができるようになる。ただし、スタックおよびヒープの位置はプロセスの起動時にいちど決められたら、その後プロセスが終了するまで変わらない(変わるとプログラムの実行に支障が出る)。
[システム全体を制御する exec-shield-randomize カーネルパラメータ]
$ echo {0|1} >/proc/sys/kernel/exec-shield-randomize
設定する値の意味
0 ランダマイズ機能を使用しない
1 ランダマイズ機能をはたらかせる
Windows Vista には、スタック、ヒープ、プログラム領域等の位置をランダムに変更することができる ASLR (Address Space Layout Randomization、メモリ空間レイアウトのランダマイズ機能) が備わった。ASLR では、スタック、ヒープに加えて、dataおよびbssの静的変数領域や、EXE や DLL の命令コードがロードされるプログラム領域のアドレスもランダマイズされる。
原則として、/dynamicbase というリンクオプションでリンクされたプログラムが ASLR を利用できるが、ヒープの位置のみ /dynamicbase リンクオプションが使われていないプログラムにおいてもランダマイズされる。
ランダマイズされてアドレスが決められるタイミングには次の二通りがある。
従来のプロセッサ(CPU)は、ある箇所のメモリ内容の本来の用途がどのようなものであれ、いちど命令カウンタレジスタがそこを指し示せば、それを機械語命令として実行するよう造られていた。近年、バッファオーバーフロー等の攻撃を未然に防ぐ観点から、あらゆるメモリ内容が命令として解釈されては都合が悪いという認識が広まっている。そして、特定のメモリ内容については機械語コードとして実行されないよう、対策を講じられたプロセッサが登場した。
いわゆる NX ビット(AMD)や XD ビット(Intel)と呼ばれる、新しいメモリ属性を備えたプロセッサ群である。
プロセッサのそのような機能を活用し、いくつかのオペレーティングシステムにおいても、スタックやヒープ等のデータ領域の内容を機械語コードとして実行することを禁止できるようになってきた。次にその例を挙げる。
Linux カーネルパッチの Exec-Shield は、ランダマイズ機能に加え、データ実行防止機能をもつ。実行禁止ビット(NXビット、XDビット)がサポートされたプロセッサの上では、data、bss、ヒープ、スタックの各セグメントにおける機械語コードの実行を禁止できる。これらのセグメントでメモリ内容を機械語として実行させようとすると、SIGSEGV シグナルが発生する。
実行禁止ビットがサポートされないプロセッサにおいても Exec-Shield は、スタックにおけるデータ実行を禁止できる(data, bss, ヒープにおける実行禁止はできない)。
Exec-Shied のデータ実行防止機能の適用・不適用は、次のようなラインモードコマンドで制御できる。
[システム全体を制御する exec-shield カーネルパラメータ]
$ echo {0|1|2|3} >/proc/sys/kernel/exec-shield
設定する値の意味
0 データ実行をすべて許可
1 データ実行を原則許可。execstack を用いて個別に禁止できる
2 データ実行を原則禁止。execstack を用いて個別に許可できる
3 データ実行を一切禁止
注 上記0~3の値は、Linuxのディストリビューションやバージョンによって多少異なる
[個々のプログラムに対する設定を変更する execstack コマンド]
$ execstack {-s|-c|-q} {プログラム}
オプションの意味
-s データ実行を許可
-c データ実行を禁止
-q データ実行許可設定の問合せ
execstack コマンドには「stack」という名前がついているが、data、bss、ヒープ、スタックの各メモリセグメントにおけるデータ実行禁止・許可を制御するはたらきをもつ。
Windows XP service pack 2 およびそれ以降の Windows には DEP(Data Execution Prevention、データ実行防止機能)が備わっている。
DEP を使うと、スタックのみならず、data、bss、ヒープの各種データ領域における機械語コード実行を禁止できる。DEP の対象となっているプログラムの中でデータ領域のバイト列を機械語コードとして実行させようとすると例外が発生し、デフォルトではプロセスが終了する。
DEP のデフォルトは「重要な Windows のプログラムおよびサービスについてのみ有効」になっている。必要に応じて設定を変更する。設定変更は、Windows XPの場合次の箇所で行う。
上記以外にも次のようなデータ実行防止機能が知られている。
バッファオーバーフロー等の攻撃に対し、プログラムが稼働するプラットフォームにおいても防御機能がいくつか備わるようになってきた。
スタック保護、ランダマイズ、データ実行防止がその主なものである。本稿では取り上げていないが、この3つ以外にもプラットフォーム特有の弱点をカバーする等の取り組みが進められている。
バッファオーバーフロー脆弱性を生まないよう、プログラムを設計・実装するとともに、プラットフォームが備える防御機能を併用すると対策効果が大きい。