セキュリティセンターTOP > セキュアプログラミング講座 > C/C++言語編 > 著名な脆弱性対策 > バッファオーバーフロー: #5 運用環境における防御

第10章 著名な脆弱性対策
バッファオーバーフロー: #5 運用環境における防御

近年、オペレーティングシステムには、バッファオーバーフロー等の攻撃を失敗に終わらせるためのいくつかの防御機能が備わりつつある。それは、主に次の3つである

運用環境における防御
図10-5: 運用環境における防御

スタック保護

近年Cコンパイラも進歩し、関数をコンパイルして生成される機械語コードの中に、領域あふれによってスタック上の関数リターンアドレスが改変されていることを検知する機械語コードを挿入できるものが登場している。

その主なものが、GCC の -fstack-protector オプションと、Visual C++ の /GS オプションである。

(1) GCC の -fstack-protector

このオプションを指定してC言語のソースコードをコンパイルすると、プログラムはスタック上の変数のあふれによって生じた関数リターンアドレスの書き変えを検出するようになる。

$ gcc -fstack-protector 他のオプション foo.c ...

このオプションは厳密には、スタックの中の関数リターンアドレスの付近で不適当な値の書き変えが起こっていることを検知するものである。

このオプションが指定されていないとき、関数リターンアドレスの付近のスタックの内容は次のようになっている(Intel 32 ビットアーキテクチャの場合)。

無防備な関数リターンアドレス
図10-6: -fstack-protectorオプションなし

このコンパイルオプションをつけてコンパイルされた関数では、ebp退避領域と関数リターンアドレスの前に4バイトの「カナリア」と呼ばれるワードが配置される。

カナリア
図10-7: -fstack-protectorオプションによるカナリアの挿入

カナリアは、ローカル変数領域のあふれによるスタック破壊を検知するためのものである。このオプションが付けられてコンパイルされた関数では、関数からのリターン直前にカナリアの値が書き変わっていないかがチェックされ、スタック破壊が検出される。

スタック破壊検出コードの生成はすべての関数に対して行われるのではない。ローカル変数に文字配列が無いか、あっても8バイト未満である関数には、スタック破壊検出コードは生成されない。なお、この8バイトというしきい値は、次のオプションを用いて変更できる。

--param ssp-buffer-size=N

また、次のオプションを使うことでコンパイルするどの関数についてもスタック破壊検出コードを生成させることができる。

-fstack-protector-all

このほか、-fstack-protector オプションでは、スタック上に配列とポインタ変数の両方が存在するときに、ポインタ変数のほうが配列よりも若い(小さい)アドレスの側に配置されるよう変数の位置関係を調整するはたらきをもつ。これは、配列があふれたときの影響をなるべく少なくする工夫である。

(2) Visual C++ の /GS

Winodows の Visual C++ にも、GCC の -fstack-protector と類似のはたらきをもつコンパイルオプション /GS がある。加えて、Visual C++ 2005 からは、/GS の指定がデフォルトになった。

/GS でもスタックの ebp 退避領域と関数リターンアドレスの前に4バイトの照合用の値が置かれるが、Visual C++ ではこれを「クッキー」と呼んでいる。GCC -fstack-protectorの「カナリア」とほぼ同じ用途であり、関数からリターンする直前にこのクッキーの値が書き変わっていないか検査することによってスタック破壊を検出するものである。

クッキー
図10-8:Visual C++の/GSオプションによるクッキー挿入

/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 を取り上げた。

(1) Exec-Shield のランダマイズ機能

Linux カーネルパッチの Exec-Shield を使うと、スタックおよびヒープの位置をランダムに変更することができるようになる。ただし、スタックおよびヒープの位置はプロセスの起動時にいちど決められたら、その後プロセスが終了するまで変わらない(変わるとプログラムの実行に支障が出る)。

システム全体を制御する exec-shield-randomize カーネルパラメータ
$ echo {0|1} >/proc/sys/kernel/exec-shield-randomize

  設定する値の意味
   0 ランダマイズ機能を使用しない
   1 ランダマイズ機能をはたらかせる

(2) Windows Vista の ASLR

Windows Vista には、スタック、ヒープ、プログラム領域等の位置をランダムに変更することができる ASLR (Address Space Layout Randomization、メモリ空間レイアウトのランダマイズ機能) が備わった。ASLR では、スタック、ヒープに加えて、dataおよびbssの静的変数領域や、EXE や DLL の命令コードがロードされるプログラム領域のアドレスもランダマイズされる。

原則として、/dynamicbase というリンクオプションでリンクされたプログラムが ASLR を利用できるが、ヒープの位置のみ /dynamicbase リンクオプションが使われていないプログラムにおいてもランダマイズされる。

ランダマイズされてアドレスが決められるタイミングには次の二通りがある。

  1. プロセスの起動時にアドレスが決められるもの
    • ・ヒープ
    • ・スタック
  2. Windows Vista のブート時にアドレスが決められるもの
    • ・text領域(プログラムおよび定数)
    • ・data領域(初期値をもつ静的変数)
    • ・bss領域(初期値をもたない静的変数)

データ実行防止

従来のプロセッサ(CPU)は、ある箇所のメモリ内容の本来の用途がどのようなものであれ、いちど命令カウンタレジスタがそこを指し示せば、それを機械語命令として実行するよう造られていた。近年、バッファオーバーフロー等の攻撃を未然に防ぐ観点から、あらゆるメモリ内容が命令として解釈されては都合が悪いという認識が広まっている。そして、特定のメモリ内容については機械語コードとして実行されないよう、対策を講じられたプロセッサが登場した。

いわゆる NX ビット(AMD)や XD ビット(Intel)と呼ばれる、新しいメモリ属性を備えたプロセッサ群である。

プロセッサのそのような機能を活用し、いくつかのオペレーティングシステムにおいても、スタックやヒープ等のデータ領域の内容を機械語コードとして実行することを禁止できるようになってきた。次にその例を挙げる。

(1) Exec-Shield のデータ実行防止機能

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、ヒープ、スタックの各メモリセグメントにおけるデータ実行禁止・許可を制御するはたらきをもつ。

(2) Windows の DEP

Windows XP service pack 2 およびそれ以降の Windows には DEP(Data Execution Prevention、データ実行防止機能)が備わっている。

DEP を使うと、スタックのみならず、data、bss、ヒープの各種データ領域における機械語コード実行を禁止できる。DEP の対象となっているプログラムの中でデータ領域のバイト列を機械語コードとして実行させようとすると例外が発生し、デフォルトではプロセスが終了する。

DEP のデフォルトは「重要な Windows のプログラムおよびサービスについてのみ有効」になっている。必要に応じて設定を変更する。設定変更は、Windows XPの場合次の箇所で行う。

[マイコンピュータ]アイコン
	→ [システムのプロパティ]メニューコマンド
	→ [詳細設定]タブ
	→ [パフォーマンス]グループの[設定]ボタン
	→ [データ実行防止]タブ

(3) そのほかのデータ実行防止機能

上記以外にも次のようなデータ実行防止機能が知られている。

  1. PaX (Linuxカーネルパッチ)
  2. Openwall Project (Linuxカーネルパッチ)
  3. W^X (OpenBSDの機能)

まとめ

バッファオーバーフロー等の攻撃に対し、プログラムが稼働するプラットフォームにおいても防御機能がいくつか備わるようになってきた。

スタック保護、ランダマイズ、データ実行防止がその主なものである。本稿では取り上げていないが、この3つ以外にもプラットフォーム特有の弱点をカバーする等の取り組みが進められている。

バッファオーバーフロー脆弱性を生まないよう、プログラムを設計・実装するとともに、プラットフォームが備える防御機能を併用すると対策効果が大きい。