スタックバッファオーバーフロー(英:Stack Buffer Overflow)またはスタックバッファオーバーラン(英:Stack Buffer Overrun)は、ソフトウェアにおいてプログラムが意図されたデータ構造(通常は固定長のバッファ)の外部にある、プログラムのコールスタック上のメモリアドレスに書き込みを行った際に発生する。 スタックバッファオーバーフローのバグは、プログラムがスタック上のバッファに対し、実際に割り当てられた以上のデータを書き込むことによって引き起こされる。これはほぼ常にスタック上の隣接するデータの破損を招き、誤ってオーバーフローが引き起こされた場合には、多くの場合プログラムがクラッシュしたり誤動作したりする原因となる。スタックバッファオーバーフローは、バッファオーバーフロー(またはバッファオーバーラン)として知られる、より一般的なプログラミングの不具合の一種である。スタック上のバッファをあふれさせることは、ヒープ上のバッファをあふれさせるよりも、プログラムの実行を狂わせる可能性が高い。なぜなら、スタックにはすべてのアクティブな関数呼び出しの戻り先アドレスが含まれているからである。

スタックバッファオーバーフローは、スタックスマッシングと呼ばれる攻撃の一環として意図的に引き起こされることがある()。影響を受けるプログラムが特別な権限で実行されている場合、または信頼できないネットワークホスト(Webサーバなど)からデータを受け取る場合、このバグは潜在的なセキュリティ脆弱性となる。スタックバッファが信頼できないユーザから提供されたデータで満たされた場合、そのユーザはスタックを破損させて実行可能コードを実行中のプログラムに注入し、プロセスの制御を奪うことができる。これは、攻撃者がコンピュータへの不正アクセスを得るための、最も古く、かつ信頼性の高い方法の一つである。

悪用 スタックベースのバッファオーバーフローを悪用する典型的な方法は、関数の戻り先アドレスを、攻撃者が制御するデータ(通常はスタック自体にある)へのポインタで上書きすることである。これは、以下の例におけるstrcpy()で示されている。#include <string.h>

void foo(char* bar) {     char c;

    strcpy(c, bar); // 境界チェックなし }

int main(int argc, char* argv[]) {     foo(argv);     return 0; }このコードは、コマンドラインから引数を取り、それをローカルのスタック変数cにコピーする。これは、コマンドライン引数が12文字未満の場合は問題なく動作する(以下の図Bを参照)。11文字を超える引数を指定すると、スタックが破損する(C言語では文字列がヌルバイト文字で終端されるため、ここで安全な最大文字数はバッファのサイズより1つ少なくなる。したがって、12文字の入力には保存に13バイトが必要であり、入力の後に番兵となるゼロバイトが続く。その結果、ゼロバイトがバッファの終端を1バイト超えたメモリ領域を上書きしてしまう)。

さまざまな入力におけるfoo()のプログラムスタックは以下のようになる。 なし|サムネイル|244x244ピクセル|A. - データがコピーされる前。なし|サムネイル|244x244ピクセル|B. - "hello" が最初のコマンドライン引数の場合。なし|サムネイル|245x245ピクセル|C. - "AAAAAAAAAAAAAAAAAAAA\x08\x35\xC0\x80" が最初のコマンドライン引数の場合。 上記の図Cにおいて、コマンドラインで11バイトを超える引数が指定されると、foo()はローカルのスタックデータ、保存されたフレームポインタ、そして最も重要な戻り先アドレスを上書きする。foo()が処理を戻す際、スタックから戻り先アドレスをポップし、そのアドレスへジャンプする(すなわち、そのアドレスから命令の実行を開始する)。このようにして、攻撃者は戻り先アドレスをスタックバッファchar cへのポインタで上書きし、そこには攻撃者が提供したデータが含まれることになる。実際のスタックバッファオーバーフローの悪用では、"A"の羅列ではなく、プラットフォームや目的の機能に適したシェルコードが用いられる。このプログラムが特別な権限(たとえば、スーパーユーザーとして実行するためのSUIDビットのセット)を持っていた場合、攻撃者はこの脆弱性を利用して、影響を受けるマシンのスーパーユーザー権限を取得することができる。

攻撃者は、内部変数の値を変更して一部のバグを悪用することもできる。 以下の例を示す。#include <stdio.h> #include <string.h>

void foo(char* bar) {     float myFloat = 10.5; // アドレス = 0x0023FF4C     char c; // アドレス = 0x0023FF30

    // 10.500000 と出力される     printf("myFloat value = %f\n", myFloat);

    /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~        メモリマップ:        @ : c に割り当てられたメモリ        # : myFloat に割り当てられたメモリ

           *c                      *myFloat        0x0023FF30                  0x0023FF4C

           @@@@@@@@@@@@@@@@@@@@@@@@@@@@#####       foo("my string is too long !!!!! XXXXX");

    memcpy は myFloat の値に 0x1010C042 (リトルエンディアン) を配置する。    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/

    memcpy(c, bar, strlen(bar));  // 境界チェックなし...

    // 96.031372 と出力される     printf("myFloat value = %f\n", myFloat); }

int main(int argc, char* argv[]) {     foo("my string is too long !!!!! \x10\x10\xc0\x42");     return 0; }スタックに保存されたアドレスを変更するためには、一般に直接的および間接的の2つの手法が使用される。直接的な攻撃を軽減するために講じられた保護対策を回避するため、攻撃者は依存関係の少ない間接的な攻撃を開発し始めた。

プラットフォーム間の差異 多数のプラットフォームにおいて、コールスタックの実装には微妙な違いがあり、それがスタックバッファオーバーフローの悪用の働きに影響を与える可能性がある。一部のマシンアーキテクチャでは、コールスタックの最上位の戻り先アドレスをレジスタに保存する。これは、上書きされた戻り先アドレスが、後でコールスタックが巻き戻されるまで使用されないことを意味する。悪用手法の選択に影響を与えるマシン固有の仕様のもう一つの例として、ほとんどのRISCスタイルのマシンアーキテクチャでは、メモリへのアラインメントされていないアクセスが許可されないという事実がある。マシンオペコードの固定長という仕様と組み合わせることで、このマシンの制限は、スタックへジャンプする手法を実装することをほぼ不可能にする可能性がある(唯一の例外は、プログラムが実際にスタックレジスタへ明示的にジャンプするという、ありそうもないコードを含んでいる場合である)。

上方に伸びるスタック スタックバッファオーバーフローの話題において、しばしば議論されるものの滅多に見られないアーキテクチャとして、スタックが逆方向に成長するものがある。同一のスタックフレーム内で発生するスタックバッファのオーバーフローは戻り先ポインタを上書きできないため、このアーキテクチャの変更はスタックバッファオーバーフロー問題の解決策として頻繁に提案される。しかし、以前のスタックフレームのバッファでオーバーフローが発生した場合は、依然として戻り先ポインタを上書きし、バグの悪用を許してしまう。たとえば、上記の例において、オーバーフローは実際にmemcpyのスタックフレーム内で発生するため、fooの戻り先ポインタは上書きされない。しかし、memcpyの呼び出し中にオーバーフローするバッファは以前のスタックフレームに存在するため、memcpyの戻り先ポインタはバッファよりも数値的に高いメモリアドレスを持つことになる。つまり、fooの戻り先ポインタが上書きされる代わりに、memcpyの戻り先ポインタが上書きされるのである。せいぜい、スタックを逆方向に成長させても、スタックバッファオーバーフローが悪用される仕組みのいくつかの詳細が変わるだけであり、悪用可能なバグの数を大幅に減らすことにはならないのである。

保護スキーム

長年にわたり、悪意のあるスタックバッファオーバーフローの悪用を阻止するために、多数の制御フロー完全性のスキームが開発されてきた。これらは通常、以下の3つのカテゴリに分類される。

スタックバッファオーバーフローが発生したことを検出し、悪意のあるコードへの命令ポインタのリダイレクトを防止する。 スタックバッファオーバーフローを直接検出することなく、スタックからの悪意のあるコードの実行を防止する。 実行可能コードを見つけることを困難にするため、メモリ空間をランダム化する。

スタックカナリア

スタックカナリアは、炭鉱のカナリアのたとえにちなんで名付けられ、悪意のあるコードの実行が行われる前にスタックバッファオーバーフローを検出するために使用される。この方法は、プログラム開始時にランダムに選ばれた小さな整数値を、スタックの戻り先ポインタの直前のメモリに配置することで機能する。ほとんどのバッファオーバーフローは、低いメモリアドレスから高いメモリアドレスへとメモリを上書きしていくため、戻り先ポインタを上書きしてプロセスの制御を奪うためには、カナリア値も上書きしなければならない。この値は、ルーチンがスタック上の戻り先ポインタを使用する前に、変更されていないことを確認するためにチェックされる。この手法は、攻撃者に対し、スタック上の他の重要な変数を破損させるといった非伝統的な手段で命令ポインタの制御を奪うことを強いるため、スタックバッファオーバーフローの悪用難易度を大幅に高めることができる。

実行不可スタック

スタックバッファオーバーフローの悪用を防ぐためのもう一つのアプローチは、スタック領域からの実行を許可しないメモリポリシー(W^X、「Write XOR Execute」)をスタックメモリ領域に適用することである。これは、スタックからシェルコードを実行するためには、攻撃者がメモリから実行保護を無効化する方法を見つけるか、シェルコードのペイロードをメモリの保護されていない領域に配置する方法を見つける必要があることを意味する。現在では、ほとんどのデスクトッププロセッサで実行不可フラグのハードウェアサポートが利用可能になっており、この方法はより一般的になりつつある。

この方法は典型的なスタックスマッシング攻撃を防ぐものの、スタックオーバーフローは他の方法で悪用される可能性がある。第一に、ヒープのような保護されていないメモリ領域にシェルコードを保存する方法を見つけるのは一般的であり、そのため悪用方法を大きく変える必要はない。

もう一つの攻撃は、シェルコード作成のためのいわゆるreturn-to-libc攻撃手法である。この攻撃において、悪意のあるペイロードはスタックにシェルコードではなく、適切なコールスタックをロードする。これにより、実行ベクタは標準ライブラリの呼び出しチェーンに向けられ、通常はメモリ実行保護を無効化し、シェルコードを通常通り実行できるようにする効果を持つ。実行が実際にスタック自体に向けられることはないため、これが機能するのである。

return-to-libcの亜種としてリターンオリエンテッドプログラミング(ROP)がある。これは、既存のプログラムコードやシステムライブラリ内にある、厳選された機械語命令の小さなシーケンスをそれぞれ実行する一連の戻り先アドレスを設定し、シーケンスはリターン命令で終了する。これらのいわゆる「ガジェット」は、リターンする前に単純なレジスタ操作や同様の実行をそれぞれ完了させ、それらを繋ぎ合わせることで攻撃者の目的を達成する。リターン命令と非常によく似た振る舞いをする命令や命令群を悪用することで、「リターンを使わない」リターンオリエンテッドプログラミングを使用することさえ可能である。

ランダム化 コードとデータを分離する代わりに、実行中のプログラムのメモリ空間にランダム化を導入するという緩和手法もある。攻撃者は利用可能な実行可能コードがどこにあるかを特定する必要があるため、実行可能なペイロードが提供されるか(実行可能スタックとともに)、ret2libcやリターンオリエンテッドプログラミング(ROP)のようなコード再利用によって構築される。メモリレイアウトをランダム化することは、概念として、攻撃者が任意のコードの場所を知ることを防ぐ。しかしながら、実装において通常すべてがランダム化されるわけではなく、たいてい実行ファイル自体は固定アドレスにロードされるため、ASLRと実行不可スタックを組み合わせたとしても、攻撃者はこの固定されたメモリ領域を利用することができる。したがって、このメモリ領域でさえもランダム化されるように、すべてのプログラムはPIE(位置独立実行可能ファイル)としてコンパイルされるべきである。ランダム化のエントロピーは実装によって異なり、エントロピーが十分に低いことは、それ自体がランダム化されたメモリ空間のブルートフォース攻撃の観点から問題となり得る。

対策の回避 これまでの緩和策は、悪用の手順をより困難にするものである。しかし、一部の脆弱性が存在したり、特定の条件が満たされたりする場合には、依然としてスタックバッファオーバーフローを悪用することが可能である。

スタックカナリアの回避

情報漏えい 攻撃者は、脆弱なプログラム内のメモリの場所を明らかにするために、フォーマット文字列の脆弱性を悪用することができる。

実行不可スタックの回避 スタックへのいかなる実行アクセスも禁止するためにデータ実行防止が有効になっている場合でも、攻撃者は上書きされた戻り先アドレス(命令ポインタ)を使用して、コードセグメント(Linuxにおける)やプログラムの他のすべての実行可能セクション内のデータを指し示すことができる。目的は既存のコードを再利用することである。

ROPチェーン プログラムのリターン命令(x86におけるret)の少し前の戻り先ポインタを上書きすることからなる。新しい戻り先ポインタとリターン命令の間にある命令が実行され、リターン命令は悪用者が制御するペイロードへと処理を戻す。

JOPチェーン ジャンプオリエンテッドプログラミング(Jump Oriented Programming)は、ret命令の代わりにジャンプ命令を使用してコードを再利用する手法である。

ランダム化の回避 64ビットシステムにおけるASLR実装の限界は、メモリの開示や情報漏えい攻撃に弱いことである。攻撃者は、情報漏えい攻撃を利用して単一の関数アドレスを明らかにし、ROPを起動させることができる。次のセクションでは、ASLR保護を突破するための既存の類似の戦略について説明している。

主な事例

1988年のモリスワームは、Unixのfingerサーバにおけるスタックバッファオーバーフローを悪用することで、一部感染を広げた。 2003年のSlammerワームは、MicrosoftのSQL Serverにおけるスタックバッファオーバーフローを悪用して感染を広げた。 2003年のBlasterワームは、MicrosoftのDCOMサービスにおけるスタックバッファオーバーフローを悪用して感染を広げた。 2004年のWittyワームは、インターネットセキュリティシステムズのBlackICE Desktop Agentにおけるスタックバッファオーバーフローを悪用して感染を広げた。 改造されていないシステム上で任意のコードの実行を許可してしまうWiiの例はいくつか存在する。『ゼルダの伝説 トワイライトプリンセス』において主人公の馬に長い名前をつけるという「トワイライトハック」や、『大乱闘スマッシュブラザーズX』においてSDカードを使用してゲーム内のステージ作りエディタに特別に用意されたファイルを読み込ませる「Smash Stack」などである。どちらも任意のコードを実行するために使用できるが、後者は単に変更を適用した状態で『スマブラX』自体を再読み込みするために使用されることが多い。

関連項目

サイバー犯罪 ExecShield ヒープオーバーフロー 整数オーバーフロー NXビット - メモリ領域の実行不可ビット Security-Enhanced Linux スタックオーバーフロー - スタック自体がオーバーフローする場合 記憶域違反

脚注

出典