バッファオーバーラン

出典: フリー百科事典『ウィキペディア(Wikipedia)』
ナビゲーションに移動 検索に移動

バッファオーバーラン: buffer overrun)、バッファオーバーフロー: buffer overflow)とは、コンピュータプログラムにおけるバグのひとつ、またはそれにより引き起こされた現象で、プログラムで用意されたバッファよりも大きなサイズのデータをバッファに書き込む事で、データがバッファ境界からあふれ、バッファの外側のメモリを上書きしてしまう事により、元々そのメモリにあったデータを破壊してしまうものを指す。バッファオーバーフローは上書きされるメモリ領域がスタック領域なのかヒープ領域なのかに応じてそれぞれスタックオーバーフローヒープオーバーフロー英語版と呼ばれる。

サイバーセキュリティ/情報セキュリティの分野ではバッファオーバーフローはメモリ破壊系の脆弱性の一つとして知られ[1]、攻撃者がバッファオーバーフローのあるプログラムに意図的に悪意のあるデータ(コード)を与えることにより、コンピュータの動作を乗っ取ってしまえる可能性がある。バッファオーバーフローを悪用した攻撃をバッファオーバーフロー攻撃という[2]

バッファオーバーフローの具体例[編集]

簡単な例[編集]

以下の例では、プログラム中の隣接したアドレスに2つのデータ項目が定義されている。一つは 8 バイトの文字列バッファ A、もう一つは 2 バイトの整数 B である。初期状態では、A は 0 で初期化されており可読な文字は入っていない。また、B には整数1979が格納されている。文字のバイト幅は 1 バイトとする。

変数名 A B
NUL NUL NUL NUL NUL NUL NUL NUL 1979
16進数値 00 00 00 00 00 00 00 00 07 BB

ここで、プログラムがバッファ Aヌル終端文字列「excessive」を書きこもうとした場合を考える。文字列の長さチェックが行われていないと、この処理で B の値が上書きされてしまう。

変数名 A B
「e」 「x」 「c」 「e」 「s」 「s」 「i」 「v」 25,856
16進数値 65 78 63 65 73 73 69 76 65 00

プログラマとしては B を変更する意図はなかったが、B の値は文字列の一部で置き換えられてしまった。この例ではビッグエンディアンASCII コードを仮定しているため、文字「e」とゼロというバイト列は整数 25,856 として解釈される。ここで、仮にプログラム中で AB 以外にデータ項目変数が定義されていないとものすると、さらに長い文字列を書き込んで B の終端を超えた場合にはセグメンテーション違反などのエラーが発生してプロセスが終了する。

電子メールアドレスを題材にした例[編集]

コンピュータプログラムを作るとき、固定長のバッファとよばれる領域を確保してそこにデータを保存するという手法がよく使われる。

たとえば、電子メールアドレスは200文字を超えないだろうと予想して

  1. 200文字分の領域をバッファとして用意する。
  2. ユーザが200文字より長いメールアドレスを入力する。
  3. プログラムがバッファの大きさをチェックせずに入力データを書き込む。
  4. バッファとして確保した領域をはみだしてデータが書き込まれてしまう。

これがバッファオーバーランである。仮にはみ出した部分にプログラムの動作上意味を持つデータがあれば、これを上書きして破壊することにより、プログラムはユーザの意図しない挙動を示すであろう。

このようにバッファオーバーランは、プログラムが用意したバッファの大きさを超えてデータを書き込んでしまうバグである。

C言語特有の例[編集]

C言語の標準入出力関数であるgets関数はバッファ長のチェックを行わないで標準入力をバッファに書き込むので、この関数を使う全てのプログラムには、バッファオーバーランによる不正動作の危険性がある。また使い方が分かりやすいという理由でC言語初心者向けの入門プログラミングでしばしば用いられるscanf関数も書式指定を誤った場合は同じ危険性を持っている[3]。これらの関数を実用的なプログラムで用いる場合には注意が必要である。

次のプログラムはgets関数を用いた例である(セキュリティ上、gets関数はそれ自体をテストする以外の目的で使用されるべきではない。Linux Programmer's Manualには「gets()は絶対に使用してはならない。」と書かれている)。バッファ長として200バイト確保されている。gets関数はバッファの長さについては関知しないため、200バイトを超えても改行文字かEOFが現れなければバッファオーバーランが発生する。

#include <stdio.h>
int main(int argc, char *argv[])
{
  char buf[200];
  gets(buf);
  ....
}

gets関数の代わりにfgets関数を用いることで、この問題を回避できる(fgets#getsを置き換える例等を参照)。fgets関数にはバッファのサイズを渡すことができ、このバイト数を超えてバッファに書き込みを行わない。したがってバッファサイズが正しく設定されていれば、fgets関数においてバッファオーバーランは起こり得ない。

これ以外のC言語の標準文字列処理関数の多くにも同様の問題(脆弱性)がある。


バッファオーバーフロー攻撃[編集]

情報セキュリティ/サイバーセキュリティにおいてバッファオーバーフロー攻撃は、バッファオーバーフローの脆弱性を利用した情報セキュリティ/サイバーセキュリティに対する攻撃である。バッファオーバーフローの脆弱性は整数オーバーフロー書式文字列バグUse-After-Freeなどと同様、メモリ破壊系の脆弱性に相当する[1]

共通脆弱性タイプCWEには、「CWE-120:入力サイズをチェックしないバッファのコピー(古典的バッファオーバーフロー)」[4]、「CWE-121: スタックベースのバッファオーバーフロー」[5]、「CWE-122:ヒープベースのバッファオーバーフロー」[6]などが登録されており、これら3つはいずれも「CWE-119: メモリバッファの境界内における操作の不適切な制限」[7]に属している[7][8]

これら3つのバッファオーバーフロー脆弱性が頻繁に生じるのはC言語C++であり[4][5][6]、古典的バッファオーバーフローはアセンブリ言語でも生じる[4]

これら3種類のバッファオーバーフローはセキュリティポリシーの外側で任意のコードを攻撃者に実行可能にする事が頻繁にある[4][5][6]。さらに任意のコードの実行により他のセキュリティサービス機構を破壊する事も可能になる[4][5][6]。またこれら3種類のバッファオーバーフローはクラッシュの原因にもなるので[4][5][6]、意図的にクラッシュさせる攻撃が可能になる[4][5][6]

CWEでは「CWE-193: Off-by-oneエラー[9]がバッファオーバーフローの原因になると述べられており[10]、整数オーバーフローもバッファオーバーフローの原因になる事が述べられている[11]

古典的バッファオーバーフロー攻撃[編集]

バッファAの値を他のバッファBにコピーするとき、BのバッファサイズがAのバッファサイズよりも大きいことをチェックしない場合に生じる脆弱性である[4]。この脆弱性は、プログラマーがこのようなチェックの実装を怠った事により生じる[4]。これはプログラマーが最低限のセキュリティチェックすらしていないことを強く示唆する[4]

脆弱性の具体例としては、例えばC言語C++において、配列サイズをチェックする事なく、配列にstrcpyscanfで文字列等をコピーするといったものがある[4]。攻撃者は意図的に大きな入力をstrcpyscanfgetsに与えることで、古典的バッファオーバーフローを不正に生じさせる事ができる[4]

例えば攻撃者がバッファAをバッファオーバーフローさせる事により、Aの隣りにある変数xを不正に変更できた場合、xがセキュリティ上重要なデータ(例えば管理者権限を持っているか否かを判定するビットを保持している)であれば、セキュリティ上重要な問題が生じる。


スタックベースのバッファオーバーフロー攻撃[編集]

スタックベースのバッファオーバーフローは「上書きされるバッファがスタック(すなわち、局所変数や、まれに関数のパラメータ)にアロケートされる」[5]事が可能な場合に生じる脆弱性である。このような脆弱性はファジングを使用して発見されることが多い[12]

C言語・C++におけるメモリ領域[編集]

スタックベースのバッファオーバーフローについて説明するために、C言語C++におけるメモリの利用方法を復習する。OSは各プロセスに仮想アドレス空間を割り振るが、C言語C++で書かれたプログラムの場合、仮想アドレスの最低位の箇所から順にプログラムの実行コードを置くコード領域(テキスト領域とも)、初期化された静的変数・大域変数を置くデータ領域、初期化されていない静的変数・大域変数を置くbss領域[注 1]malloc等で動的に確保されたメモリを置く(可変サイズの)ヒープ領域を確保する[13]。(なおデータ領域とbss領域を合わせて静的領域という)。

一方、仮想アドレスの最高位の箇所は関数のコールスタックを保存する(可変サイズの)スタック領域として用いられる[13]

最低位         …          最高位
コード領域 静的領域 ヒープ領域

(高位に向かって成長→)

スタック領域

(←低位に向かって成長)

データ領域 bss領域

スタック領域はプロセス中で呼ばれる関数のコールスタックを格納する領域で、コールスタック中の各関数のデータ(スタックフレームという)を並べて格納している[14]。プロセス中で関数fが関数gを呼び出した場合、コールスタックは以下のようになる[14][15]

低位アドレス                             … 高位アドレス
gのスタックフレーム fのスタックフレーム
gの処理に必要な一時的な情報 gの局所変数 gのSFP gのリターンアドレス gの引数の値 fの処理に必要な一時的な情報


プロセスで現在実行中の関数のスタックフレームの位置を覚えるためにプロセッサによって用いられるのがフレームポインタ(=x86ではebp)で具体的には(現在実行中の関数がgであれば)gのSFPのアドレスを指している。SFPは関数呼び出し時に呼び出し元の関数のフレームポインタのアドレスを覚えるための領域で、fがgを呼び出した際、スタックフレームの値(=fのSFPのアドレス)をgのSFPに格納する。なお、SPFは「Saved Frame Pointer」の略で日本語訳は「退避されたフレームポインタ」である[14]

一方gのリターンアドレスは呼び出し元関数fのプログラムカウンタ(命令ポインタとも。x86ではeip)のアドレスを格納する[14]

攻撃の基本的アイデア[編集]

今例えば、関数gはユーザから(ASCIIコードの)文字列を入力を受け取り、入力された文字列を配列char A[10]に格納するとする。Aのサイズは10であるので、関数gのプログラマはユーザから受け取った文字列をAに格納する前に、その文字列が本当に10文字以下なのかをチェックする機構をgに実装しておかねばならない。このようなチェック機構を実装するのを忘れていた場合、悪意のあるユーザ(以下、「攻撃者」と呼ぶ)によりスタックベースのバッファオーバーフロー攻撃を受けてしまう危険がある。


具体的には、攻撃者は以下のような文字列をgに入力する:

シェルコード)…(シェルコードの仮想アドレス

ここでシェルコードとは、何らかの悪意のある短いプログラムで、例えば攻撃者のためにバックドアを開けたり、マルウェアのダウンロードを行ったりする[15]

この文字列が配列Aの先頭から順に書き込まれていくと、A[i]のアドレスはiが大きいほど大きくなるので、攻撃者が入力文字列の長さを適切に選べば、アドレス空間には以下のようにデータが書き込まれ、gのリターンアドレスがシェルコードの仮想アドレスに上書きされる事になる[15]

低位アドレス                             … 高位アドレス
gのスタックフレーム fのスタックフレーム
gの処理に必要な一時的な情報 gの局所変数 gのSFP gのリターンアドレス gの引数の値 fの処理に必要な一時的な情報
(シェルコード)… (シェルコードの仮想アドレス)

よって関数gが終了したとき、gのリターンアドレス(の箇所に上書きされたシェルコードの仮想アドレス)が読み込まれるので、プログラムカウンタはシェルコードの位置にジャンプし、攻撃者の狙い通り、シェルコードが実行される事になる[15]

NOPスレッド[編集]

上で述べた攻撃のアイデアが実行可能であるためには、攻撃者がリターンアドレスに上書きすべき想アドレスを正確に知り、それを関数gに入力する必要があるが、スタックは動的に変化するため、これは容易ではない[16]。そこで本節ではリターンアドレスに上書きすべき仮想アドレスの「おおよその値」さえ分かれば攻撃が可能になるテクニック(NOPスレッド)を述べる。

NOPとは「何も行わない」事を意味するアセンブリ命令で[16]、本来はタイミングを合わせるなどの動機により何もせずにCPU時間を消費するために用いられる[16]。NOPスレッド(sled、そり)[16]とはこのNOP命令を複数個並べたもので、これを利用する事により攻撃対象の関数をシェルコードの位置まで滑走させる。

具体的には攻撃者は下記のような文字列を攻撃対象の関数gに入力する:

NOP … NOP (シェルコード)(戻りアドレス)…(戻りアドレス)

最初の「NOP … NOP」の部分がNOPスレッドである。攻撃者がNOPスレッドの長さや戻りアドレスの繰り返し回数を適切に選んでgに入力すると、スタック領域は例えば以下のようになる(攻撃に関係する部分だけ抜書き):

                            gのスタックフレーム fのスタックフレーム
gの局所変数 gのSFP gのリターンアドレス
NOP… NOP NOPNOP (シェルコード)…(戻りアドレス)…(戻りアドレス) (戻りアドレス) (戻りアドレス)…

これでgのリターンアドレスは「戻りアドレス」にセットされるので、攻撃者が「戻りアドレス」としてNOPスレッド部分のアドレスを指定する事に事前に成功していれば、gの終了時にNOPスレッドへとプログラムカウンタが移動する。するとプログラムはNOPを順に実行して右へ右へと移動し、シェルコードの位置にたどり着いてシェルコードが実行されるので、攻撃成功となる[16][17]。戻りアドレスがNOPスレッドのどこかに落ちさえすればよいので、前節で述べた攻撃違い、リターンアドレスにセットする値を完璧に制御する必要はなく、NOPスレッドの長さ分の誤差が発生しても攻撃が成功する事になる。

NOPスレッドは頻繁に使用されるため、多くの侵入防止システムベンダーでシェルコードの判定に使用されている。このため、エクスプロイトの作成者側では、シェルコードの実行に影響を及ぼさない(NOP以外の)任意の命令をランダムに選んでスレッドを構成することが常套手段となっている[18]

戻りアドレスの値の予想[編集]

攻撃を実行するには、あとは「戻りアドレス」として具体的にどの程度の値を代入すればよいかを知ればよい。しかし攻撃の標的となる組織の環境で戻りアドレスの絶対アドレスがいくつ程度の値になるのかを事前に知る事は難しい。そこで相対アドレスを利用して戻りアドレスを適切に選ぶ攻撃テクニックを紹介する。この攻撃のシナリオでは、攻撃者はシェルコードを含んだ攻撃用のプログラムh(をコンパイルして作った実行コード)を攻撃の標的となる組織に送りつけ、hのサブルーチンとしてgを呼び出す事で攻撃を行う。

この攻撃用プログラムhでは、変数xを宣言が宣言されているものとする。hが標的の環境でgを呼び出したとき、gのスタックフレームはスタック領域上でhのスタックフレームのすぐ隣に配置される事から、攻撃用文字列を入れ込むgの変数の絶対アドレスvar_addは

var_add = &x - (小さな値)

になるはずである[16]。ここで「&x」はxのアドレスを表す。既に述べたようにNOPスレッドを使った攻撃では戻りアドレスとしてvar_add近辺の値を選べば成功するので、攻撃者はこの「小さな値」を決定しさえすればよい。

よって攻撃者は関数gの実行コードを事前に入手し、(シェルスクリプト等を使って)NOPスレッドの長さや戻りアドレスを変えながら攻撃対象のプログラムを何度も実行することで適切な「小さな値」を選び、その「小さな値」を攻撃用プログラムhに書き込んでおけばよい[16]

埋め込めるコード量が小さい場合の対処[編集]

関数gに埋め込む攻撃用の文字列は「NOPスレッド+シェルコード+戻りアドレスの繰り返し」という形をしており、gのリターンアドレスが「戻りアドレスの繰り返し」の部分に落ちない限り攻撃は成功しないので、関数gに攻撃用文字列を埋め込む箇所とgのリターンアドレスとが(仮想アドレス空間上で)あまりに近い場合は、攻撃に必要な長さのシェルコードを埋め込めないという問題が攻撃者に生じる。

しかし攻撃者が攻撃の標的となるマシンの環境変数を設定できる状況下では、攻撃者はこの問題を回避した攻撃が可能である。標的マシンでプロセスが実行される際には、そのプロセスの仮想アドレス空間に環境変数が読み込まれるので、攻撃者が事前に標的マシンの環境変数に「NOPスレッド+シェルコード」を書き込んでおけば、プロセスの仮想アドレスに「NOPスレッド+シェルコード」ができあがる事になる。プロセス中で関数gが実行された際、攻撃者は攻撃用文字列をgに入力して、リターンアドレスをそのNOPスレッドに書き換えれば攻撃が成功する事になる[19]

より確実な攻撃方法として、攻撃プログラムhの中に環境変数を読み込む関数(getenv()等)を用いるものもある[20]

ヒープスプレー[編集]

NOPスレッドを長くしすぎると、gのリターンアドレスの位置にすらNOPが書き込まれてしまって攻撃に失敗する為、NOPスレッドを長くして攻撃成功率を上げる手法には限界がある。しかし攻撃者がプロセスのヒープ領域の値をも自由に操れるという条件下では、攻撃者はヒープスプレーというテクニックを用いる事により、この限界を突破した攻撃を行う事が可能になる[21]

ヒープスプレーでは、NOPスレッドとシェルコードを、スレッド領域ではなくヒープ領域に埋め込み、戻りアドレスとしてヒープ領域中のNOPスレッドを指定する[21]。ヒープ領域上のNOPスレッドにはスレッド領域のNOPスレッドと違い前述した長さの上限が存在しないため、非常に長いNOPスレッドを作成し、攻撃成功率を上げる事ができる[21]

なお、ヒープ領域には他のプログラムモジュール等で利用されているメモリ領域が存在しているので、NOPスレッド内にそのような他のプログラムモジュールXのメモリ領域があった場合、戻りアドレスをNOPスレッドに飛ばしても、プログラムはシェルコードではなくXに飛んでしまうので、攻撃は失敗する事になる[21]。この問題を回避する為、ヒープスプレーでは、「NOPスレッド+シェルコード」の組み合わせを何度も繰り返してヒープ中に書き込むことで攻撃成功率を上げる[21]

ウェブブラウザではJavaScriptなどのクライアントサイドスクリプトにより任意の長さのヒープを作成できるので、ブラウザを対象にしたドライブバイダウンロード攻撃ではヒープスプレーが使われる事が多い[21]

トランポリング[編集]

ここで、ユーザの入力したデータのアドレスは未知であるが、アドレスがレジスタに格納されていることは分かっているという場合には、トランポリン(trampolining)と呼ばれる手法が利用される。この手法では、ユーザの入力したデータにジャンプするオペコードのアドレスをリターンアドレスへ上書きする。例えばアドレスがレジスタRに格納されている場合、jump R(あるいはcall Rなど)というオペコードが格納されているアドレスにジャンプさせることでユーザの入力したデータを実行させる。この手法で使用するオペコードはDLLや実行ファイルの中のものを利用する。ただし、一般的にオペコードのアドレスにヌル文字が含まれていてはならず、また処理に使用するオペコードのアドレスはアプリケーションやオペレーティングシステムのバージョンによって異なる。Metasploitプロジェクトはこのような目的に適したオペコードのデータベースの一つであり、Windowsで使用できるオペコードが記載されている[22]

名前が似ているスタックオーバーフローと混同しないよう注意すること。


ヒープベースのエクスプロイト[編集]

ヒープ領域で発生するバッファオーバーフローはヒープオーバーフローと呼ばれ、エクスプロイトもスタックベースのオーバーフローとは異なる。ヒープ領域上のメモリはアプリケーションの実行時に動的に確保され、一般的にはプログラムのデータが格納される。エクスプロイトは、何らかの方法でこのデータを破壊し、アプリケーションが内部構造(例えば連結リストのポインタ)を上書きするように仕向けることで行われる。基本的なヒープオーバーフローのテクニックでは、動的メモリ確保で使われる連結リストの連結部分(mallocのメタデータなど)を上書きし、その結果のポインタを使ってプログラムの関数ポインタを上書きする。

マイクロソフトGDI+におけるJPEG処理の脆弱性は、ヒープオーバーフローの危険性を示す例といえる[23]

エクスプロイトに対する障壁[編集]

バッファの読み込みや実行の前に行われるバッファの操作が原因で、エクスプロイトが失敗する場合もある。このような操作を利用してエクスプロイトの脅威を軽減することはできるが、それでもエクスプロイトが不可能にはならない。バッファに対する操作としては大文字小文字変換、メタ文字の除去、非英数字のフィルタリングなどがあるが、これらの処理をくぐり抜けるテクニックも存在する(英数字コード(w:alphanumeric code)、ポリモルフィックコード自己書き換えコードReturn-to-libc攻撃など)。また、侵入検知システムをすり抜けるのにも同様の方法が使用できる。また場合によっては、コードがUnicodeでエンコードされている場合など、実際にはリモートから任意のコードの実行が可能であるにも関わらず、発見者によってただのDoSであると不正確に伝えられているような脆弱性もある[24]


レジスタに格納されているアドレスにジャンプする方法[編集]

jump to registerとは、NOPスレッドの格納領域もスタックのオフセットの推測も必要とせずに、スタックバッファオーバーフローを用いた確実なエクスプロイトを可能にするテクニックである。戦略としては、リターンポインタを上書きして、レジスタへ格納されている既知のポインタへのジャンプを起こさせる(このポインタはコントロールされたバッファ、ひいてはシェルコードを指している)。例えばレジスタAがバッファの先頭へのポインタを格納しているとすると、そのときレジスタAをオペランドにとる任意のjumpまたはcall命令が実行フローの支配権を得るのに使用できる[25]

ntdll.dll中のDbgPrint()ルーチンを呼び出す命令にはi386のオペコードjmp espが含まれている

実際には、特定のレジスタへのジャンプ命令を意図的に含めないようにしたプログラムもある。よくある解決法としては、プログラムのメモリ中の固定の位置にあるオペコードの中に、使える命令のインスタンスが意図せず作られているのを探してくる方法がある。左図はそのような意図しないインスタンスの例で、i386jmp esp命令が作られている。この命令のオペコードはFF E4である[26]

この2バイトの並びは、call DbgPrint命令の先頭から1バイトオフセットした位置(アドレス0x7C941EED)にある。攻撃者がプログラムのリターンアドレスをこのアドレスで上書きしたら、プログラムはまず最初に0x7C941EEDへジャンプし、オペコードFF E4jmp espと解釈する。そしてスタックの先頭へジャンプし、攻撃者の指定したコードを実行する[27]

このテクニックが利用可能な場合、脆弱性の重大性は相当に高くなる。これは、処理を自動化してもほぼ確実に攻撃が成功するほどエクスプロイトの成功率が高くなるためである。そのため、これはスタックバッファオーバーフローの脆弱性を利用するインターネットワームにおいて最もよく使われるテクニックとなっている[28]

また、この方法を使えば、Windowsプラットフォームにおいては上書きしたリターンアドレスの後ろにシェルコードを配置することもできる。実行ファイルは多くの場合アドレス0x00400000から配置され、またx86はリトルエンディアンアーキテクチャのため、リターンアドレスの末尾のバイトは必ずNULLになる。そのため、バッファへのコピーはそこで終了されてしまい、それ以降には何も書き込まれない。これにより、シェルコードのサイズはバッファのサイズに制限されることになるが、これは場合によっては非常に厳しい制限となる。一方DLLはハイメモリ(アドレス0x01000000より上)に配置されるため、アドレスにNULLバイトが含まれないようにできる。そのため、この方法であれば上書きするリターンアドレスがNULLバイト(あるいはその他の禁止された文字)を含まないようにできる。このようにDLLを使った方法はDLLトランポリン(DLL Trampolining)などとも呼ばれる。

防御的対策[編集]

バッファオーバーフローの検出や防止には様々なテクニックが用いられ、それぞれにトレードオフがあるが、いまだに決め手となる技術は存在していない。バッファオーバーフローを防ぐ最も信頼性の高い方法は言語レベルの自動的な保護であるが、この種の保護はレガシーコードには適用できない。また、技術的・業務的・文化的制約によって、自動的な保護機能を備えていない言語を使わなければならない場合もある。以降では、利用可能な対策方法やその実装について述べる。

プログラミング言語の選択[編集]

プログラミング言語の選択はバッファオーバーフローの発生の有無に大きく影響する。2008年時点ではC言語またはC++言語で実装されているソフトウェアが数多く存在するが、これらの言語ではメモリ中のいかなる場所に対する読み書きに関しても組み込みの保護機構が提供されていない。より具体的に言えば、バッファに書きこまれたデータが、そのバッファの境界に収まっているかのチェックは行われない。ただし、標準C++ライブラリは安全にバッファリングを行うための各種の機構を提供しているし、バッファオーバーフローを避けるためのテクニックはC言語にも存在する。

具体的には、

  1. 不当な引数があれば、エラー終了する。
  2. memset()関数を用いてバッファをゼロクリアする
  3. 転送バイト数に制限をかけられるstrncpy()などの関数を使用する

等のテクニック[29]があげられる。

その他多くのプログラミング言語では、実行時に、また場合によってはコンパイル時にチェックを行い、CやC++ではプログラムがクラッシュするような場合でも、警告を送ったり例外を上げたりできるようになっている。このような言語の例としては、AdaEiffelLISPModula-2SmalltalkOCaml、およびC言語から派生したCycloneD言語が挙げられる。また、Javaプラットフォーム.NET Frameworkでは全ての配列に対して境界チェックが必須とされる。ほぼすべてのインタプリタ言語ではバッファオーバーフローへの対策が行われており、エラー発生時にはその状態が明確に伝えられる。境界チェックを行うのに十分な型情報を保持しているようなプログラミング言語では、境界チェックの有効・無効を切り替えるためのオプションが提供されていることもある[30]。 ただし、インタプリタそのものは多くがコンパイラ言語によって書かれているので、絶対に信頼できるわけではない。また言語に付属するライブラリやモジュールには母体となった言語のライブラリやシステムコールに由来した脆弱性が組み込まれてしまうこともある。

また、静的コード解析を行えば多くの動的な境界チェック・型チェックを取り除くことができる。これについてもすべてのエラーを発見できるわけではなく、解析処理の実装が貧弱だったり、解析対象のコードに厄介なケースが含まれている場合には、静的解析の効果は低下する。ソフトウェア技術者は、使用する言語やコンパイラの設定の決定に際しては安全性と性能のトレードオフについて慎重に検討すべきであると言える。

安全なライブラリの利用[編集]

バッファオーバーフローがC言語およびC++言語に多い原因として、これらの言語では、データ型のコンテナとしてのバッファの低レベルな実装の詳細が公開されてしまっていることが挙げられる。従って、バッファ管理を行うコード中で高いレベルの正当性を維持することができれば、バッファオーバーフローは回避できるはずだと言える。例えば、標準ライブラリ関数のうち、getsstrcpyといった境界チェックを行わない関数は使用を避けることが昔から推奨されている。 例えば、Morris wormfingerdgetsを使用しているのを突いて攻撃を行った[31]

よく書けたテスト済みの抽象データ型ライブラリにより、境界チェックを含むバッファ管理を集中化・自動化することで、 バッファオーバーフローの発生率や影響度合いを減らすこともできる。プログラミング言語の構成要素となるデータ型のうち、バッファオーバーフローが多発するのは文字列と配列である。したがって、これらのデータ型においてバッファオーバーフローを防止することで、保護が必要となる部分の大半をカバーすることができる。だが、このような安全性を提供するライブラリの誤った使用法によりバッファオーバーフローや脆弱性を引き起こす可能性は残っている。またもちろん、これらライブラリ自身のバグも潜在的な脆弱性となる。「安全な」ライブラリの実装としては、"The Better String Library"[32]、 Vstr[33]、 Erwin[34]などが挙げられる。 OpenBSD標準Cライブラリでは、strlcpystrlcatといった関数が提供されているが、これらの関数の提供する安全性は上記のような実装と比べると限定されている。

2007年にはC標準委員会によるTechnical Report 24731が公開された。ここでは、C標準ライブラリの文字列関数及び入出力関数に、バッファのサイズを引数として追加したものを定義しているISO/IEC TR 24731-1:2007 (PDF)” (2007年3月28日). 2012年2月8日閲覧。Rationale for TR 24731 Extensions to the C Library Part I: Bounds-checking interfaces (PDF)”. 2012年2月8日閲覧。。 しかしながら、これらの関数がバッファオーバーフローの低減に寄与するかは疑わしい。これらの関数では関数呼び出しの度にプログラマによる判断が必要となるが、これでは標準ライブラリ中に昔からある類似の関数を呼び出す際にその都度プログラマが判断を行うのと大して変わらない[35]

また、配列領域をチェックする機能自体は16bit時代からx86 CPUのBOUND命令などすでに実装されていたが、チェックの結果問題があった場合には例外を発生させる等扱いが難しく、現在のCPUにおいてもチェックにまつわるオーバーヘッドが無視できないという理由から、リリースバイナリにこれらの機能を含められないということもある。

バッファオーバーフロー保護[編集]

バッファオーバーフロー保護は最も多いタイプのバッファオーバーフローを検出するのに使用される。方法としては、関数から戻る際にコールスタックが変更されていないかチェックし、変更されていた場合はプログラムがセグメンテーション違反で終了する。このような処理系としては、Libsafe[36]や、 StackGuard[37] およびProPolice[38]などのGNUコンパイラコレクションへのパッチが挙げられる。

マイクロソフトのデータ実行防止モードでは、SEH例外ハンドラへのポインタが上書きされないように明示的に保護を行う[39]

より強力なスタック保護の方法としては、スタックを2つに分けて、片方をデータ用、もう片方を関数の戻りアドレス用とする方法がある。セキュリティを意図した設計によるものではないが、Forthではこのようなスタックの分割が行われている。ともかく、この方法もバッファオーバーフローに対する完璧な解決策とは成り得ない。これは、戻りアドレスは上書きされないが、プログラムの動作に影響を与えるデータの上書きは依然として発生しうるためである。

ポインタ保護[編集]

バッファオーバーフローは、最終的にはポインタやアドレスの操作によって効果を発揮する。PointGuardは攻撃者がポインタ及びアドレスを確実に操作するのを妨害するためのコンパイラの拡張として提案された [40]。 このアプローチでは、ポインタに対してXOR演算によるエンコードを行うコードをコンパイラがポインタの使用前後に自動的に追加する。これにより、攻撃者には(理論的には)ポインタのエンコード・デコードにどのような値を使用しているか分からないため、ポインタを別の値で上書きしてもそれが実際にはどこを指すのか前もって知ることができない。PointGuardが実際にリリースされることはなかったが、Microsoftは似たような機能をWindows XP SP2およびWindows Server 2003 SP1で実装している[41]。 マイクロソフトはポインタの保護機能が自動的に行われるよう実装するのではなく、プログラマが自らの判断で呼び出せるようにAPIルーチンを追加する形で実装を行った。これはパフォーマンスの面では有利(エンコード処理が常に入るのではないため)だが、使う必要のある場所がどこであるかプログラマが知っているため、プログラマには負担となる。

ここで、XORは線形演算であるため、攻撃者がエンコードされたポインタを操作する際にアドレス中の下位バイトのみを書き換える方法が考えられる(上位バイトは上書きされないため正しくデコードされる)。これにより、攻撃者が攻撃を複数回試行できるか、またはポインタがある範囲のうちの一箇所(例えばNOPスレッド中のある一箇所)を指せば攻撃が成功するという条件に当てはまる場合には、攻撃が可能となってしまう[42]。 マイクロソフトでは、このアドレスの部分的な上書きに対する脆弱性への対処として、エンコード処理にランダムなローテーション処理を追加した [43]

実行保護[編集]

実行保護とはバッファオーバーフロー保護のアプローチの一つで、スタックやヒープ上のコードの実行を防止するものである。攻撃者がバッファオーバーフローを利用するのは任意のコードをプログラムのメモリ上に挿入するためであるが、実行保護が行われている場合は、そのようなコードを実行しようとしても例外が発生する。

一部のCPUではNXビット("No eXecute"の略)と呼ばれる機能が提供されており、ソフトウェアと連携し、スタックやヒープの内容を含むページをマーク付けすることで、読み書きは可能だが実行は不可能であるようにし、データ領域の任意のデータをコードとして実行させないようにできる。 インテル製品ではXDビット("eXecute Disabled"の略)という名前でNXビットと同じ機能を実装しており、NetBurstアーキテクチャPentium4ファミリにのIntel 64対応プロセッサ、および現在リリースされているCoreマイクロアーキテクチャの全てのプロセッサが対応している。AMD製品ではAMD64 Enhanced Virus Protectionという名前で、Opteron、Athlon 64、Turion 64、Sempronの一部製品がこれに対応している。

一部のUNIX(OpenBSDmacOSなど)は実行保護(W^Xなど)が有効になった状態で出荷されている。それ以外のオプショナルなパッケージとしては以下のものが挙げられる。

また、Windowsでも最近のものはデータ実行防止(DEP)と呼ばれる実行保護機能を備えている[47]。 また、プロプライエタリなアドオンとしては以下のものがある。

実行保護は一般に、Return-to-libc攻撃や、その他攻撃者が作成したコードの実行に依存しないタイプの攻撃に対しては効果がない。 また、実行保護を行なっている場合でも、バッファオーバーフローによりプログラムを異常動作させることは可能である。

ハーバードアーキテクチャのハードウエアとオペレーティングシステムの組み合わせを使用することでもこの保護を実現できる。

アドレス空間配置のランダム化[編集]

アドレス空間配置のランダム化(address space layout randomization, ASLR)はコンピュータのセキュリティ機能の一種で、攻撃の鍵となるデータの格納エリアの位置(通常は実行コードの開始位置や、ライブラリ、ヒープ、スタックなどの位置を含む)をプロセスのアドレス空間中でランダムに設定するものである。 バッファオーバーフローは本来メモリ中の特定の位置にプログラムが配置されていることを前提としている点に着目した対策である。

関数や変数に対する仮想記憶アドレスのランダム化によりバッファオーバーフローによるエクスプロイトをより困難にすることが可能と見られるが、ただし不可能とはならない。また、これにより攻撃者はエクスプロイトを個別のシステムごとに誂えなければならなくなるため、ワームの増殖を防ぐのにも役立つ[50]。 似た方法でより効果が弱いものとしては仮想記憶上で行うプロセスやライブラリの再配置が挙げられる。

ディープパケットインスペクション[編集]

ディープパケットインスペクション(deep packet inspection, DPI)を使用すれば、ネットワークの周辺から行われるバッファオーバーフローを狙った攻撃を、攻撃シグネチャとの比較やヒューリスティクスを利用して検知することができる。これらの方法は既知の攻撃のシグネチャを持つパケットをブロックすることができる他に、長いNOP命令の列(NOPスレッド)が見つかった場合、エクスプロイトのペイロードが微妙に変化しても使用することができる。

パケットスキャンは決して効果的な方法ではない。これは、既知の攻撃だけしか防ぐことができず、またNOPスレッドは様々な方法でエンコードできるためである。攻撃者の使うシェルコードは、ヒューリスティックなパケットスキャナや侵入検知システムを回避するため、英数字コードメタモーフィックコード自己書き換えコードなどを使って構成される。

歴史[編集]

バッファオーバーフローがある程度公に文書化されたのは1972年の初めで、Computer Security Technology Planning Studyで以下のように説明されている[51]

"The code performing this function does not check the source and destination addresses properly, permitting portions of the monitor to be overlaid by the user. This can be used to inject code into the monitor that will permit the user to seize control of the machine."

「この処理を実行するコードは読み込み元と書き込み先のアドレスに対するチェックを適切に行なっておらず、モニターの一部に対しユーザによる上書きを許すことになっている。これはモニタにコードを挿入するのに利用される可能性があり、結果としてユーザがマシンの制御を掌握する可能性がある」

モニターとは、現在カーネルと呼ばれているのと同じものである。

バッファオーバーフローを利用した悪意のあるエクスプロイトで最初に文書化されたのは、1988年に書かれたMorris wormがインターネット上で増殖するのに利用していたエクスプロイトのうちの一つである。攻撃対象のプログラムはUNIXサービスであるfingerであった[52]。 1995年、Thomas Lopaticはそれとは独立にバッファオーバーフローを発見し、セキュリティに関するメーリングリストBugtraqへ投稿した[53]。 1996年、エリアス・レヴィ(ハンドルネームAleph one)はオンラインマガジンPhrackで記事"Smashing the Stack for Fun and Profit"を発表した[54]。 これはスタックベースのバッファオーバーフローを使用したエクスプロイトを手順を追って説明していく内容である。

これ以降、少なくとも2つの有名なインターネットワームがバッファオーバーフローを利用したエクスプロイトで多くのシステムに被害を与えている。2001年にはCode RedがマイクロソフトのInternet Information Services(IIS) 5.0のバッファオーバーフローを利用している[55]。 また2003年にはSQL SlammerMicrosoft SQL Server 2000の動作するマシンに被害を与えている[56]

2003年には、市販のXboxのゲームに含まれるバッファオーバーフローが利用され、無認可のソフトウェア(例えばHomebrewのゲームなど)をModチップなどのハードウェアの改造なしに動作させるのに利用された[57]PlayStation 2では同じ目的のためにPS2 Independence Exploitが使用される。またWiiではHomebrewが利用されるが、これはゼルダの伝説 トワイライトプリンセスに存在するバッファオーバーフローを利用している。

参考文献[編集]


関連項目[編集]

外部リンク[編集]

脚注[編集]

  1. ^ a b 八木、村山、秋山 2015 p.59
  2. ^ 第10章 著名な脆弱性対策バッファオーバーフロー: #1 概要”. セキュアプログラミング講座 C/C++言語編 旧2007年公開版. 情報処理推進機構. 2018年12月14日閲覧。
  3. ^ [迷信] scanf ではバッファオーバーランを防げない” (日本語). C/C++迷信集. 株式会社きじねこ. 2010年2月28日閲覧。 “書式指定が不適切なために発生する脆弱性であって、scanf の問題ではありません。”
  4. ^ a b c d e f g h i j k l CWE-120: Buffer Copy without Checking Size of Input ('Classic Buffer Overflow')”. Mitre. 2018年12月18日閲覧。
  5. ^ a b c d e f g CWE-121: Stack-based Buffer Overflow”. Mitre. 2018年12月18日閲覧。
  6. ^ a b c d e f CWE-122: Heap-based Buffer Overflow”. Mitre. 2018年12月18日閲覧。
  7. ^ a b CWE-119: Improper Restriction of Operations within the Bounds of a Memory Buffer”. Mitre. 2018年12月18日閲覧。
  8. ^ CWE-787: Out-of-bounds Write”. Mitre. 2018年12月18日閲覧。
  9. ^ CWE-193: Off-by-one Error”. Mitre. 2018年12月18日閲覧。
  10. ^ CWE-131: Incorrect Calculation of Buffer Size”. 2018/12/18. 2018年12月18日閲覧。
  11. ^ CWE-680: Integer Overflow to Buffer Overflow”. Mitre. 2018年12月18日閲覧。
  12. ^ The Exploitant - Security info and tutorials”. 2009年11月29日閲覧。
  13. ^ a b Erickson(2011) p.87-88
  14. ^ a b c d Erickson(2011) p.84
  15. ^ a b c d 八木、村山、秋山 2015 pp.60-61
  16. ^ a b c d e f g Erickson(2011) p.161-164
  17. ^ 八木、村山、秋山 2015 p.64
  18. ^ Akritidis, P.; Evangelos P. Markatos, M. Polychronakis, and Kostas D. Anagnostakis (2005). “STRIDE: Polymorphic Sled Detection through Instruction Sequence Analysis.” (PDF). Proceedings of the 20th IFIP International Information Security Conference (IFIP/SEC 2005). IFIP International Information Security Conference. http://dcs.ics.forth.gr/Activities/papers/stride-IFIP-SEC05.pdf 
  19. ^ Erickson(2011) p.164-168
  20. ^ Erickson(2011) p.168-173
  21. ^ a b c d e f 八木、村山、秋山 2015 pp.65-67
  22. ^ The Metasploit Opcode Database”. 2007年5月12日時点のオリジナルよりアーカイブ。2007年5月15日閲覧。
  23. ^ Microsoft Technet Security Bulletin MS04-028”. 2007年5月15日閲覧。
  24. ^ Creating Arbitrary Shellcode In Unicode Expanded Strings (PDF)”. 2007年5月15日閲覧。
  25. ^ Shah, Saumil (2006). 1 - Saumil Shah - Writing Metasploit Plugins.pdf “Writing Metasploit Plugins: from vulnerability to exploit” (PDF). 1 - Saumil Shah - Writing Metasploit Plugins.pdf Hack In The Box. Kuala Lumpur. http://conference.hitb.org/hitbsecconf2006kl/materials/DAY 1 - Saumil Shah - Writing Metasploit Plugins.pdf 
  26. ^ (PDF) Intel 64 and IA-32 Architectures Software Developer's Manual Volume 2A: Instruction Set Reference, A-M. Intel Corporation. (2007-05). pp. 3-508. http://developer.intel.com/design/processor/manuals/253666.pdf. 
  27. ^ Alvarez, Sergio (2004年9月5日) (PDF). Win32 Stack BufferOverFlow Real Life Vuln-Dev Process. IT Security Consulting. http://packetstormsecurity.org/papers/win/windowsdev.pdf. 
  28. ^ Ukai, Yuji; Soeder, Derek; Permeh, Ryan (2004). “Environment Dependencies in Windows Exploitation”. BlackHat Japan. Japan: eEye Digital Security. http://www.blackhat.com/presentations/bh-asia-04/bh-jp-04-ukai-eng.ppt 
  29. ^ [1]IPAセキュアプログラミング講座
  30. ^ Neil Moffatt. “Delphi Basics : $RangeChecks command”. 2012年2月3日閲覧。 例えばDelphiでは$RangeChecksディレクティブで境界チェックの有効・無効を切り替えられる。
  31. ^ http://wiretap.area.com/Gopher/Library/Techdoc/Virus/inetvir.823
  32. ^ The Better String Library”. 2012年2月8日閲覧。
  33. ^ The Vstr Homepage”. 2012年2月8日閲覧。
  34. ^ The Erwin Homepage”. 2012年2月8日閲覧。
  35. ^ CERT Secure Coding Initiative”. 2007年7月30日閲覧。
  36. ^ Libsafe - Free Software Directory”. 2012年2月9日閲覧。
  37. ^ StackGuard: Automatic Adaptive Detection and Prevention of Buffer-Overflow Attacks by Cowan et al. (PDF)”. 2012年2月9日閲覧。
  38. ^ X.ORG Wiki - ProPolice”. 2007年2月12日時点のオリジナルよりアーカイブ。2007年5月20日閲覧。
  39. ^ Bypassing Windows Hardware-enforced Data Execution Prevention”. 2007年5月20日閲覧。
  40. ^ Crispin Cowan, Steve Beattie, John Johansen and Perry Wagle (2003年8月27日). “PointGuardTM: Protecting Pointers From Buffer Overflow Vulnerabilities”. 2012年2月14日閲覧。
  41. ^ Michael Howard (2006年1月31日). “Protecting against Pointer Subterfuge (Kinda!)”. 2012年2月14日閲覧。
  42. ^ Steven Alexander (2005年6月). “defeating compiler-level buffer overflow protection (PDF)”. 2012年2月14日閲覧。
  43. ^ Michael Howard (2006年8月16日). “Protecting against Pointer Subterfuge (Redux)”. 2012年2月14日閲覧。
  44. ^ PaX: Homepage of the PaX team”. 2012年2月17日閲覧。
  45. ^ KernelTrap.Org”. 2012年5月29日時点のオリジナルよりアーカイブ。2012年2月17日閲覧。
  46. ^ Openwall Linux kernel patch 2.4.34-ow1”. 2012年2月17日閲覧。
  47. ^ Windows XP Service Pack 2、Windows XP Tablet PC Edition 2005、および Windows Server 2003 のデータ実行防止 (DEP) 機能の詳細”. 2012年2月17日閲覧。
  48. ^ BufferShield: Prevention of Buffer Overflow Exploitation for Windows”. 2012年2月17日閲覧。
  49. ^ NGSec Stack Defender”. 2007年5月13日時点のオリジナルよりアーカイブ。2012年2月17日閲覧。
  50. ^ PaX at GRSecurity.net”. 2007年6月3日閲覧。
  51. ^ :p.61Computer Security Technology Planning Study (PDF)”. 2007年11月2日閲覧。
  52. ^ "A Tour of The Worm" by Donn Seeley, University of Utah”. 2007年5月20日時点のオリジナルよりアーカイブ。2007年6月3日閲覧。
  53. ^ Bugtraq security mailing list archive”. 2007年9月1日時点のオリジナルよりアーカイブ。2007年6月3日閲覧。
  54. ^ Smashing the Stack for Fun and Profit”. 2007年6月3日閲覧。
  55. ^ eEye Digital Security”. 2007年6月3日閲覧。
  56. ^ Microsoft Technet Security Bulletin MS02-039”. 2007年6月3日閲覧。
  57. ^ Hacker breaks Xbox protection without mod-chip”. 2007年9月27日時点のオリジナルよりアーカイブ。2007年6月3日閲覧。


引用エラー: 「注」という名前のグループの <ref> タグがありますが、対応する <references group="注"/> タグが見つからない、または閉じる </ref> タグがありません