ソフトウェア・インテグリティ

 

バッファ・オーバーフローを検出、防止、緩和する方法

バッファ・オーバーフローの検出、防止、緩和

情報セキュリティ産業の誕生以来、バッファ・オーバーフローは常に注目を集め続けてきました。1980年代後半、ロバート・T・モリス氏がUNIXのfingerdプログラムを利用して、たった2日でインターネットに接続している機器の10%に感染するワームを作成しました。この一件でサイバーセキュリティがコンピューター・サイエンス史上初めて見出しのトップを飾り、以後たびたびメディアで大きく取り上げられるようになりました。

それから30年近く経った2014年、OpenSSL暗号ライブラリに存在するバッファ・オーバーフロー脆弱性が一般に公開されました。この欠陥は「Heartbleed」の名で知られるようになりました。Heartbleedによって、広く利用されているオンライン・サービスやソフトウェア・プラットフォームの数億人にのぼるユーザーの情報がOpenSSLの脆弱なバージョンに露出されました。

今後のHeartbleedやモリス・ワームを阻止するには、まずバッファ・オーバーフローとその検出方法を理解する必要があります。次に、オーバーランの悪用を成功に導くプロセスとその結果を理解することが重要です。これらが適切に行われて初めて、バッファ・オーバーフローの防止・緩和のための計画を策定することができます。

バッファ・オーバーフローの定義

コード内のバッファ・オーバーフローを探し出す前に、まずそのしくみを見てみましょう。バッファ・オーバーフロー脆弱性は、その名のとおり、読み取り/書き込みメモリーに直接低レベルのアクセスを行う言語でバッファ(メモリー割り当て)に対する処理を行います。

Cやアセンブリなどの言語では、メモリー割り当てに対する読み取り/書き込みでの境界チェックは自動的には行われません。つまり、書き込みまたは読み取りデータのバイト数が対象バッファに実際に収まるかどうかはチェックされません。そのため、プログラムでバッファの容量を「オーバーフロー」させることが可能になります。これにより、書き込みデータが終端を越え、スタックまたはヒープ上にある後続のアドレスの内容が上書きされるか、余分なデータが読み取られます。Heartbleedバグはまさしく後者のケースです。

バッファ・オーバーフローの検出

以上の定義を踏まえておけば、このような欠陥の検出方法を検討することができます。ソースコードを操作する際の簡単なバッファ・オーバーフロー対策は、バッファを使用、変更、アクセスする箇所に特別な注意を払うことです。ユーザーなどの外部ソースによって指定された入力を処理する関数は、オーバーフローを悪用しやすいベクトルとなるため特に注意が必要です。たとえば、次の例に示すように、ユーザーに「はい」または「いいえ」を問う処理の場合、”yes”という文字列が収まるだけの小さいバッファにユーザーの文字列入力を格納するとします。

バッファ・オーバーフローの検出

このコードを見ると、境界チェックが行われていないことが明らかです。ユーザーが”maybe”と入力すれば、プログラムは同じ質問を再表示してユーザーに有効な答えを要求する代わりに、おそらくクラッシュするでしょう。ユーザーの答えは、そのデータ長を問わず、そのままバッファに書き込まれます。

この例では、宣言されている変数はuser_answerだけなので、スタック上の次の値はリターン・アドレス値、すなわちaskQuestion関数の実行後のプログラムの戻り先となるメモリー上の場所です。そのため、ユーザーが4バイト(バッファ用に確保したメモリー領域を満たすデータ長)のデータを入力し、その後にメモリー上の有効なアドレスが指定されていれば、プログラムのリターン・アドレスは変更されます。これにより、ユーザーはコード内の本来の意図とは異なる箇所で関数を強制的に終了させ、プログラムに意図しない危険な動作をさせることが可能になります。

ソースコード内のバッファ・オーバーフローを検出する第1のステップはバッファ・オーバーフローのしくみを理解すること、第2のステップは外部入力とバッファ操作の探し方を把握すること、そして第3のステップはこの脆弱性の影響を受けやすく、脆弱性が存在することを示すレッドフラグ(危険信号)となる可能性がある関数を把握することです。前述の例が示すように、gets関数は指定されたバッファの境界を越えて書き込みを行います。この性質は関連する関数ファミリー(strcopystrcatprintf/sprintなど)全体に及びます。これらの関数を使用している箇所はすべてバッファ・オーバーフロー脆弱性が存在する可能性があります。

バッファ・オーバーフローの防止

ソースコード内のバッファ・オーバーフロー脆弱性を検出する機能は確かに有益ですが、コードベースから脆弱性を排除するには、着実な検出、およびセキュアなバッファ処理方法への習熟が必要です。バッファ・オーバーフロー脆弱性を防止する最も簡単な方法は、脆弱性が入り込む余地のない言語を使用することです。C言語は、メモリーに直接アクセスし、厳密なオブジェクト型が存在しないという性質により脆弱性の余地があります。これらの要素を持たない言語は一般に脆弱性の影響を受けません。Java、Python、.NETなどの言語やプラットフォームでは、オーバーフロー脆弱性を緩和するための特別なチェックや変更は不要です。

とはいえ、開発言語を全面的に変更することが常に可能とは限りません。その場合には、セキュアな方法でバッファを処理します。文字列処理関数については、使用可能なメソッド、安全なメソッド、回避すべきメソッドについて多くの議論がなされてきました。strcopyはバッファに文字列をコピーする関数、strcatはバッファの内容を別のバッファの内容に追加する関数です。この2つの関数はターゲット・バッファの境界をチェックせず、指定されたデータのバイト数がバッファより大きい場合にはバッファの制限を超えて書き込みを行うため、その動作は安全ではありません。

代わりに、それぞれの関数のstrn-バージョンが一般に推奨されます。strn-バージョンは、ターゲット・バッファの最大のサイズまでのデータのみを書き込みます。これは理想的な解決策のように思われるかもしれませんが、これらの関数にも問題を引き起こす可能性を伴う微妙な差異があります。データがバッファの制限に到達した場合、バッファの最後のバイトに終端文字が存在しないと、次のようなデータを読み取ったときに大きな問題が生じる可能性があります。

バッファ・オーバーフローの防止

この簡略化した例からは、ヌル終端文字列が存在しないことの危険性がわかります。”foo”をnormal_bufferに格納する場合には、バッファに残りスペースがあるのでヌル終端文字が付加されます。ところが、full_bufferの場合はそうはなりません。この関数を実行すると、結果は次のようになります。

バッファ・オーバーフローの防止

normal_bufferの値は正常に出力されますが、full_bufferの場合は余分な文字が出力されます。このケースはまだ良い方です。スタック内の次のバイトが別の文字バッファまたはその他の出力可能な文字列であった場合、print関数はその文字列の終端文字に到達するまで読み取りを続けます。

C言語にはこの関数の代わりになる標準のセキュアな関数はありませんが、プラットフォーム固有の実装がいくつかあります。OpenBSDにはstrn-関数と同様の機能を果たすstrlcpystrlcatがありますが、この2つの関数は文字列を1文字前で切り捨て、ヌル終端文字が入る余地を確保している点がstrn-と異なります。また、Microsoftも間違った使い方をされることが多い文字列関数strcpy_s、strcat_s、sprintf_sの独自のセキュアな実装を用意しています。なるべく避けることが望ましい関数とその代替となる安全な関数を次の表に示します。

バッファ・オーバーフローの防止

*(アスタリスク)はCの標準ライブラリに存在しない関数であることを示します。

前述の表に示されたセキュアな代替関数を使用することをお勧めします。文字列バッファを処理する場合は手動で境界チェックとヌル終端を行う必要があります。

バッファ・オーバーフローの緩和

安全でない関数がオーバーフローに見舞われる余地を残している場合でも、他に手立てがないわけではありません。コンパイルまたは実行時にオーバーフロー脆弱性を検出するための改善が進んでいます。プログラムを実行すると、多くの場合、コンパイラはカナリアと呼ばれるランダムな値を作成してスタック上の各バッファの後に配置します。カナリアが炭坑の危険を知らせる鳥であったように、その名を冠したカナリアの値は危険を警告します。カナリアの値を元の値と比較してチェックすることにより、バッファ・オーバーフローが発生しているかどうかを確認できます。値が変更されていた場合、変更された可能性があるリターン・アドレスに戻って処理を続行するのではなく、プログラムを停止またはエラー状態にすることができます。

最近のオペレーティング・システムの中には、非実行可能スタックやアドレス空間配置のランダム化(ASLR : Address Space Layout Randomization)の形で防御が強化されているものがあります。非実行可能スタック(データ実行防止:DEP)では、スタックおよび場合によってはその他の構造体をコード実行不可能な領域としてマークします。これにより、攻撃者はエクスプロイト・コードをスタックに挿入して実行を成功させることを期待できなくなります。

ASLRは、リターン指向プログラミング(ROP)(既存の断片的なコードがメモリー上のアドレスのオフセットに基づいて連結される非実行可能スタックを回避する手法)に対する防御を目的に開発されました。この機能は構造体のメモリー上の配置をランダム化することによってオフセットを特定し難くします。このような防衛策が1980年代後半に存在していれば、モリス・ワームは防止できたかもしれません。その理由は、モリス・ワームにはエクスプロイト・コードでUNIX fingerdプロトコルを用いてバッファにデータを設定し、そのバッファをオーバーフローさせて、エクスプロイト・コードが設定されているバッファを指定するようにリターン・アドレスを変更することによって機能していたという側面があるためです。ASLRとDEPを用いることで、メモリー領域を完全に実行不可にすることはできないまでも、指定するアドレスの特定は難しくなります。

開発、コンパイラ、オペレーティング・システムのレベルで対策を講じていても、脆弱性が見過ごされて攻撃の余地が残る場合があります。バッファ・オーバーフローが存在する最初の兆候の段階でエクスプロイトが成功することもあります。このような場合には、遂行しなければならない重要な課題が2つあります。最初に、脆弱性を特定し、コードベースを変更して問題を解決する必要があります。次は、コードの脆弱なバージョンのすべてを、パッチを適用した新しいバージョンに確実に置き換えることが目標になります。それには、インターネットに接続し、該当のソフトウェアを実行するすべてのシステムに及ぶ自動更新から始めることが理想的です。

しかし、自動更新で十分にカバーされることを前提にするわけにはいきません。組織または個人がインターネットへの制限付きアクセスでシステム上のソフトウェアを使用する場合があります。その場合には手動による更新が必要です。そのためには、該当ソフトウェアを使用している可能性があるすべての管理者に更新に関する情報を配信し、パッチを簡単にダウンロードできるようにする必要があります。パッチの作成と配布は脆弱性の発見後できるだけ早急に行い、ユーザーやシステムが脆弱性にさらされる時間を最短に抑えることをお勧めします。

コンパイラやオペレーティング・システムの安全なバッファ処理関数および適切なセキュリティ機能を使用することにより、バッファ・オーバーフローに対する堅牢な防衛手段を構築できます。こうした手順を踏まえた上でなお、エクスプロイトを防ぐには欠陥を着実に特定するステップが重要です。ソースコードの各行を丹念に調べてバッファ・オーバーフローの可能性を探す作業は骨が折れます。しかも、人間の目視では常に見落としが生じる可能性があります。そこで、開発時のセキュリティ脆弱性の検出専用に、コード品質を強化するための静的解析ツール(linterに似た機能)が開発されました。

たとえば、Coverity Code Advisorはバッファ・オーバーフローの可能性に対するレッドフラグ(危険信号)を特定します。特定されたレッドフラグをトリアージ(優先順位付け)して個別に修正できるため、コードベースを手動で探す必要がありません。これらのツールと定期的なコードレビュー、バッファ・オーバーフローへの対処方法に関する知識を組み合わせることにより、コードの開発が完了する前の段階でバッファの欠陥の大部分を特定し、緩和することが可能になります。

 

開発に品質とセキュリティを組み込みます。

 

この著者によるその他の情報