バッファー‐オーバーラン【buffer overrun】
読み方:ばっふぁーおーばーらん
バッファオーバーラン
【英】buffer overrun
バッファオーバーランとは、プログラムで用意してあるバッファの大きさを越えるデータが入力されたり送り込まれることで、システムが誤動作を起こしたり、悪意のあるプログラムが実行できてしまう状態のことである。
バッファオーバーランは、想定以上のデータ長を受け入れないようにガードされていないプログラムで発生し、その脆弱性につけ込む。バッファオーバーランを悪用して、ウィルスを混入したり、システムに侵入してファイルを削除したり、情報を盗んだりすることも可能とされる。
なお、スタック領域のように、本来はプログラムが配置されないメモリ領域でプログラムを実行不可能にすることでバッファオーバーランが発生した場合の被害を抑制する方法もある。
バッファオーバーフロー
バッファオーバーフロー(英: buffer overflow)またはバッファオーバーラン(英: buffer overrun)は、コンピュータのプログラムにおけるバグのひとつ、またはそれにより引き起こされる現象で、プログラムがバッファに割り当てられた空間よりも大きなデータを書き込むことで、データがバッファ境界からあふれ、バッファの範囲外のメモリを上書きし、元々そのメモリにあったデータを破壊してしまうことを指す。
バッファオーバーフローは、上書きされるメモリ領域がスタック領域なのかヒープ領域なのかに応じてそれぞれスタックベースのバッファオーバーフロー、ヒープベースのバッファオーバーフローと呼ばれる。なお、名称が似ているスタックオーバーフローとは別の現象である。
サイバーセキュリティ・情報セキュリティの分野では、バッファオーバーフローはメモリ破壊系の脆弱性の一つとして知られ[1]、攻撃者がバッファオーバーフロー脆弱性のあるプログラムに意図的に悪意のあるデータ(コード)を与えることにより、コンピュータの制御を乗っ取ってしまうことを可能にする。バッファオーバーフロー脆弱性を悪用した攻撃をバッファオーバーフロー攻撃という[2]。
バッファオーバーフローの具体例
簡単な例
以下の例では、プログラム中の隣接したアドレスに2つのデータ項目が定義されている。一つは 8 バイトの文字列バッファ A
、もう一つは 2 バイトの整数 B
である。初期状態では、A
は 0 で初期化されており可読な文字は入っていない。また、B
には整数 1,979 が格納されている。文字のバイト幅は 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」 | 25856 | |
16進数値 | 65 | 78 | 63 | 65 | 73 | 73 | 69 | 76 | 65 | 00 |
プログラマとしては B
を変更する意図はなかったが、B
の値は文字列の一部で置き換えられてしまった。この例ではビッグエンディアンとASCIIコードを仮定しているため、文字「e」とゼロというバイト列は整数 25,856 として解釈される。ここで、仮にプログラム中で A
と B
以外にデータ項目変数が定義されていないとものすると、さらに長い文字列を書き込んで B
の終端を超えた場合にはセグメンテーション違反などのエラーが発生してプロセスが終了する。
電子メールアドレスを題材にした例
コンピュータプログラムを作るとき、固定長のバッファとよばれる領域を確保してそこにデータを保存するという手法がよく使われる。
たとえば、電子メールアドレスは200文字を超えないだろうと予想して
- 200文字分の領域をバッファとして用意する。
- ユーザが200文字より長いメールアドレスを入力する。
- プログラムがバッファの大きさをチェックせずに入力データを書き込む。
- バッファとして確保した領域をはみだしてデータが書き込まれてしまう。
これがバッファオーバーフローである。仮にはみ出した部分にプログラムの動作上意味を持つデータがあれば、これを上書きして破壊することにより、プログラムはユーザの意図しない挙動を示すであろう。
このようにバッファオーバーフローは、プログラムが用意したバッファの大きさを超えてデータを書き込んでしまうバグである。
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++において、配列サイズをチェックする事なく、配列にstrcpyやscanfで文字列等をコピーするといったものがある[4]。攻撃者は意図的に大きな入力をstrcpy、scanf、getsに与えることで、古典的バッファオーバーフローを不正に生じさせる事ができる[4]。
例えば攻撃者がバッファAをバッファオーバーフローさせる事により、Aの隣りにある変数xを不正に変更できた場合、xがセキュリティ上重要なデータ(例えば管理者権限を持っているか否かを判定するビットを保持している)であれば、セキュリティ上重要な問題が生じる。
スタックベースのバッファオーバーフロー攻撃
スタックベースのバッファオーバーフローは「上書きされるバッファがスタック(すなわち、局所変数や、まれに関数のパラメータ)にアロケートされる」[5]事が可能な場合に生じる脆弱性である。このような脆弱性はファジングを使用して発見されることが多い[12]。
C言語・C++におけるメモリ領域
スタックベースのバッファオーバーフローについて説明するためにプロセスのメモリ利用方法を復習する。オペレーティングシステム (OS) は各ユーザプロセスに仮想アドレス空間を割り振り、WindowsやLinuxなどのOSでは上位のアドレスをカーネルが使うカーネル空間とし、残りをユーザプロセス自身が用いるユーザ空間とする[注釈 1]。カーネル空間はユーザプロセスがアクセスする事はできず、通常のプログラミングでは意識する事はない。
C言語やC++で書かれたプログラムの場合、ユーザ空間をさらに分割する。これらの言語で書かれたプログラムでは、仮想アドレスの最低位の箇所から順にプログラムの実行コードを置くコード領域(テキスト領域とも)、初期化された静的変数・大域変数を置くデータ領域、初期化されていない静的変数・大域変数を置くbss領域[注釈 2][注釈 3]、malloc等で動的に確保されたメモリを置く(可変サイズの)ヒープ領域を確保する[13]。
一方、ユーザ空間における仮想アドレスの最高位の箇所は関数のコールスタックを保存する(可変サイズの)スタック領域として用いられる[13]。
最低位 | … | 最高位 | |||
コード領域 | 静的領域 | ヒープ領域 (高位に向かって成長→) | … | スタック領域 (←低位に向かって成長) | |
データ領域 | bss領域 |
スタック領域はプロセス中で呼ばれる関数のコールスタックを格納する領域で、コールスタック中の各関数のデータ(スタックフレームという)を並べて格納している[14]。プロセス中で関数fが関数gを呼び出した場合、コールスタックは以下のようになる[14][15]:
低位アドレス | … | 高位アドレス | ||||||
… | gのスタックフレーム | fのスタックフレーム | … | |||||
… | gの処理に必要な一時的な情報 | gの局所変数 | gのSFP | gのリターンアドレス | gの引数の値 | fの処理に必要な一時的な情報 | … | … |
プロセスで現在実行中の関数のスタックフレームの位置を覚えるためにプロセッサによって用いられるのがフレームポインタ[注釈 4]で、具体的には(現在実行中の関数がgであれば)gのSFPのアドレスを指している。SFPは関数呼び出し時に呼び出し元の関数のフレームポインタのアドレスを覚えるための領域で、fがgを呼び出した際、スタックフレームの値(=fのSFPのアドレス)をgのSFPに格納する。なお、SFPは「Saved Frame Pointer」の略で、日本語訳は「退避されたフレームポインタ」である[14]。
一方gのリターンアドレスは呼び出し元関数fのプログラムカウンタ[注釈 5]のアドレスを格納する[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 … | NOP …NOP (シェルコード)…(戻りアドレス)…(戻りアドレス) | (戻りアドレス) | (戻りアドレス)… |
これで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スレッドを指定する。ヒープ領域上のNOPスレッドにはスレッド領域のNOPスレッドと違い前述した長さの上限が存在しないため、「長いNOPスレッド+シェルコード」の組み合わせを大量にヒープ中に書き込むことで攻撃成功率を上げる[21]。
ウェブブラウザではJavaScriptなどのクライアントサイドスクリプトにより任意の長さのヒープを作成できるので、ブラウザを対象にしたドライブバイダウンロード攻撃ではヒープスプレーが使われる事が多い[21]。
トランポリング
攻撃者の入力したデータ(エクスプロイトなど)のアドレスは未知であるが、そのデータのアドレスがレジスタに格納されていることは分かっているという場合には、トランポリン(trampolining)と呼ばれる手法が利用される。この手法では、攻撃者の入力したデータにジャンプするオペコードのアドレスをリターンアドレスへ上書きする。例えばアドレスがレジスタRに格納されている場合、jump R(あるいはcall Rなど)というオペコードが格納されているアドレスにジャンプさせることでユーザの入力したデータを実行させる。この手法で使用するオペコードはDLLや実行ファイルの中のものを利用する。ただし、一般的にオペコードのアドレスにヌル文字が含まれていてはならず、また処理に使用するオペコードのアドレスはアプリケーションやオペレーティングシステムのバージョンによって異なる。Metasploitプロジェクトはこのような目的に適したオペコードのデータベースの一つであり、Windowsで使用できるオペコードが記載されている[22]。
ヒープベースのバッファオーバーフロー
mallocなどでヒープ領域に動的にメモリを確保する関数に対するオーバーフロー攻撃である[6]。基本的な攻撃手法としては、関数がヒープに確保したメモリ領域が2つあるとき、そのうち一方の領域に対して確保済みのメモリサイズより大きなデータを入力する事でオーバーフローを起こし、もう一方のメモリ領域を書き換えるというものである[23]。mallocは複雑な方法でメモリ確保の場所を決定しているものの、連続して2度mallocを実行した場合、その結果として確保される2つのメモリ領域は(仮想アドレス空間上で)近くに配置されている傾向があるため、上述のようなバッファオーバーフロー攻撃が可能になる。
mallocのメモリ管理
より高度なヒープベースバッファオーバーフロー攻撃手法を説明する為の準備として、mallocのメモリ管理方法を説明する。なおメモリ管理方法の詳細はmallocの実装に依存するため、実行環境によって細かなところは下記の説明と異なる部分があるので注意されたい。
mallocは未使用なヒープ領域(の一部)をメモリプールとして管理しており、メモリプールは複数のchunkと呼ばれる単位からなっている[24]。プログラム中でmallocが実行されるたびに、管理しているchunkの中から適切なサイズのものをプログラムに返す[24]。適切なサイズのchunkがない場合は、システムコールにより新たなchunkを確保してプログラムに返す[24]。mallocはchunkを(サイズ毎に異なる複数の[25])連結リストとして管理しており[24]、chunkをプログラムに渡す際にこの連結リストからchunkを削除し、プログラムがメモリ領域をfreeすると、freeされたchunkが連結リストに加わる[24]。
chunkは連結リストとして管理されているので、各chunkには「次のchunk」を指定するポインタ(Windowsでは「flink」[26]、linuxでは「fd」[24])や「前のchunk」を指定するポインタ(Windowsでは「blink」[26]、linuxでは「bk」[24])がある。
未使用chunkと隣接するメモリ領域が解放された場合は、解放されたメモリ領域と未使用chunkとを連結 (coalesce) する事で1つの大きなchunkを作って管理する[26]。
mallocのメタデータの書き換え
より高度なヒープベースバッファオーバーフロー攻撃として、mallocがメモリ管理に使うメタデータを書き換える手法がある。例えばWindows XP SP1またはそれ以前のWindowsでは、mallocしたメモリ領域をオーバーフローさせる事で、そのメモリ領域の(仮想アドレス空間で)隣りにある未使用chunkのflinkやblinkを任意のアドレスに書き換えるという攻撃手法が可能であった[27]。flinkやblinkはcoalesceのタイミングでmallocにより参照されるので攻撃が成功する[27]。
バッファオーバーフローの結果
write-what-where状態
バッファオーバーフロー攻撃の結果として、write-what-where状態("write-what-where condition"[28]、CWE-123[28]。任意の場所に任意の値を書き込むことができる状態[29])になる危険がある[28]。
write-what-where状態になると、セキュリティポリシーのスコープ外にメモリのデータを書き込むことができる[28]。セキュリティポリシーのスコープ外にコードを置くことで、攻撃者はほぼ確実に任意のコードを実行できるようになる[28]。管理者権限を制御しているフラグ等が書き換えられる場合も、攻撃者は任意のコードが実行可能になる[28]。
プログラムのフリーズ・クラッシュ
攻撃者は意図的にバッファオーバーフローを起こすによりプログラムをクラッシュさせたり[4][5][6]、処理を書き換えて無限ループに追い込むことでプログラムをフリーズさせてたり[4][5][6]する事でプログラムの可用性を侵害できる。
関数ポインタの書き換え
古典的バッファオーバーフロー[30]やヒープオーバーフロー[6]などの結果として、関数ポインタの書き換えが可能になるケースがある。攻撃者はnmコマンドを用いる事でプログラム中で用いられている様々な関数のアドレスを知る事ができるので[30]、nmの結果を参照して攻撃に利用可能な関数に関数ポインタの値を書き換えられる。
技術的対策
コンパイラやライブラリによる対策
カナリア
バッファオーバーフローを検出するコードをコンパイル時に実行コードに挿入する手法がある。典型的手法としては、ローカル変数とSFPの間に、カナリア(canary)[31][32]もしくはクッキー[33]と呼ばれる領域を挿入する方法である。プログラムは実行中カナリアを監視し続け、バッファオーバーフローによりカナリアが書き換わったらプログラムを停止する。
- StackGuard[33][34]
- Microsoft Visual C++のGSオプション[33][35]
安全なライブラリへの置き換え
標準Cライブラリ等にはバッファオーバーフロー検知機能が施されていない関数が収録されているので、これを検知機能を持った関数に置き換えたライブラリを標準Cライブラリの代わりに使う事で攻撃を検知できる。例えばLibsafe[36]は標準Cライブラリのstrcpy(*dest,*src)をより安全な関数に置き換えており、入力srcのサイズがコピー先のdestのサイズが大きいか否かを検知できる[37]。
実行環境での対策
Write XOR eXecute
典型的なスタックベースのバッファオーバーフロー攻撃では、本来データを格納すべき領域にシェルコードやNOP命令のような実行コードを置き、これをプログラムに実行させる事で攻撃が成立する。そこでこのような攻撃を防ぐため、データを格納すべき領域では実行不可にする、Write XOR eXecute[37](W
バッファオーバーラン
出典: フリー百科事典『ウィキペディア(Wikipedia)』 (2021/11/23 00:15 UTC 版)
他の C言語の文字列処理関数と同様に、scanf を使用する際にもバッファオーバーランが発生する可能性がある。例えば char a[20];scanf("%s", a); というコードがあった場合、a に入力できる文字列は終了文字を除く、19バイトまでしか入力することが出来ず、それ以上の文字列が入力されるとバッファオーバーランが発生する。a の領域が十分であれば問題はないが、しかし入力されてくる文字列の長さを予測することは不可能であり、結局 a の長さをいくら大きくしても、さらにそれを上回る長さの入力が行われてしまえばバッファオーバーランが発生してしまう危険性がある。この問題を防ぐ手段として一般的なのは最大フィールド幅を指定することである。上記の例の場合は char a[20];scanf("%19s", a); とすることで、aには最初の19バイトが読み込まれ入力を終了する。ただしこの場合は残りの文字列はストリーム上に残ってしまうので実際には以下のようにすることで対処される。 char a[20];scanf("%19s%*[^\n]", a); これは19バイト読み込みさらに改行文字までの文字列を空読みすることを意味する。 さらに前述の改行コードがストリームに残る問題を考慮すると char a[20];scanf("%19s%*[^\n]%*c", a); となるが、この場合はaに入る文字列が 19バイト以下の場合には、入力ストリームにやはり改行文字が残る。よって実際には、 char a[20];scanf("%19s%*[^\n]", a);getchar(); のように入力終了後、改行文字を空読みするなどの方法がとられる。空白文字の取り扱いも考慮する場合 char a[20];scanf("%19[^\n]%*[^\n]", a);getchar(); となる。 バージョン8.0 (2005) 以降のMicrosoft Visual C++コンパイラには、scanf_s() という関数が用意されていて、バッファのサイズを指定することができる。scanf_s はC11規格で標準化されたが、実装は任意である。
※この「バッファオーバーラン」の解説は、「scanf」の解説の一部です。
「バッファオーバーラン」を含む「scanf」の記事については、「scanf」の概要を参照ください。
- バッファオーバーランのページへのリンク