CPUのアーキテクチャから徹底解説 バッファオーバーフローで何が起こるか
第4回 バッファオーバーフロー脆弱性対策の歴史をひも解く
前回の記事では、バッファオーバーフローが起こるしくみを、コップに入れた水を例にとって解説しました。前回の記事をまだご覧になっていない方は、ぜひこの機会に当連載記事に目を通していただけますと幸いです。
前回記事の最後で少し触れましたが、バッファオーバーフローはスタックに記憶しているプログラムの制御情報を書き換えることが可能です。このことはプログラムが停止したり、予期しない動作をしたりするという、セキュリティ上の脆弱性につながります。また実はこのような脆弱性は古くから知られており、いまだに根本対策ができていません。
*たとえばスタックバッファオーバーフローに関する書籍が1999年に発刊されています。
そこで今回はバッファオーバーフローの理解を深めるために、スタックバッファオーバーフローを利用した攻撃と、その対策の歴史について説明いたします。なお今回の記事においてもプログラム並びに実行環境はC言語と「ARM Cortex-A シリーズ 」CPU(32bitモード) を想定します。
スタック上に置いたコードを実行させる仕組みの復習
前回記事では、バッファオーバーフローによって、制御情報を入力内容で上書きできてしまうことを述べました。制御情報の中には関数の戻り先アドレスが含まれています。バッファオーバーフロー攻撃の対策がまだ存在しなかったころ、攻撃者は戻り先アドレスを改ざんできることを思いつきました。攻撃者はプログラムに対して、不正なプログラム(シェルコードと呼ばれるシェルを起動するプログラム)と不正なプログラムの先頭アドレスを含む、バッファの大きさを超えるデータを入力し、バッファオーバーフローを発生させます。(図1:①)。 そうするとバッファから入力内容があふれます。あふれた入力内容が戻り先アドレスを上書きしていることに注目してください(図2:②)。上書きの結果として、CPUは関数の実行を終えた後、戻り先アドレスに戻るのではなく、上書きされたアドレスに「戻る」ことになります。そしてCPUは上書きされたアドレスにある、不正なプログラムを実行しはじめます(図1:③)。
NX bit により、スタック上のプログラムを実行不可能にして対策!
上記の攻撃は、スタック上に置かれたプログラムを実行できなければ成立しません。「NX bit」はスタックやヒープなどの領域を実行不可にすることが可能なメモリ管理に属する機能です(注釈:実はNX bit はARM CPUには存在せず、「XN(eXecute Never) bit」が存在します。どちらも同じようにしてスタックやヒープ領域を実行不可にする機能を提供するため、本記事ではより名前が知られているNX bitについて解説します。また最近のLinuxやWindowsはデフォルトでこの機能に対応しており、gccなどのコンパイラもデフォルトでこの機能を有効にしたプログラムを生成します)。
OSはNX bitを有効にしてコンパイルしたプログラムを実行するときに、スタックやヒープに対して、その場所が実行不可であることを示すNX bitを立てます(図2:①)(注釈:具体的には仮想アドレスと物理アドレスを対応づけるためのページテーブルにNX bitを立てますが、話がバッファオーバーフローから逸れるためページテーブルの詳細については省略します)。CPUはプログラムを実行するときに、NX bitが立った場所のプログラムを実行しません(図2:②)。これにより、バッファオーバーフロー攻撃でスタックに配置された不正なプログラムも実行できなくなり、図1のような攻撃は無効になりました。
NX bitを有効にしても、共有ライブラリ内の関数が不正実行されえてしまう!
NX bitによってスタックに置いたプログラムを実行させることはできなくなりました。しかし「制御情報を書き換えられる」という特徴は、そのまま残っています。そのため次はバッファオーバーフローによって攻撃者の「意図どおりの関数を実行させる攻撃」が登場しました。ほとんどのプログラムが使う共有ライブラリ内の関数を意図通りに呼び出すという攻撃です。
たとえば攻撃者がバッファオーバーフローによって共有ライブラリ内の関数 system() を不正使用する攻撃を考えます。system() は引数に与えられた文字列をシェルスクリプトのコマンドとして実行する関数です。また攻撃者はsystem() のアドレスを容易に推測できます。
*後述するASLRという対策が行われないケースでは、共有ライブラリは同じアドレスに配置されるためです。しかし話が本題から逸れてしまうので配置に関する詳細については省略します。
この攻撃は以下のように行います。
まず、バッファオーバーフロー攻撃によって関数の戻り先アドレスを、共有ライブラリ関数 system() のアドレスに上書きします(図3:①)。この結果、プログラムは攻撃者の意図するコマンドを実行することになります(図3:②)(注釈:実際には戻り先アドレスの上書き以外に、 system() の引数をレジスタにセットするための工夫が必要です。
*本記事では説明を簡易にするためこの工夫を省略します。
スタック、ヒープと共有ライブラリのアドレス配置をランダムにするASLRで対策
次に、このような攻撃に対応するため、スタック、ヒープと共有ライブラリのアドレス配置をランダムにするという方法が登場しました。ASLR(Address Space Layout Randomization)は、アドレス空間に配置されるスタック、ヒープのアドレスをランダムにするOSの機能です。
*LinuxやWindowsなどのOSではデフォルトでこの機能が有効になっています。
これらの対策によって、共有ライブラリの関数やスタック、ヒープのアドレスはプログラム実行のたびに配置する位置が変わります(図4:①)。
ASLRによって、OSはPICとしてコンパイルされた共有ライブラリを任意のアドレス空間に配置できます。
*PICとはPosition Independent Code、位置独立コードと呼ばれる、どのアドレスに配置しても実行可能なプログラムのことです。プログラムをPICとしてコンパイルするためにオプションを指定する必要があります。たとえばgccでは-fPICオプションです。
したがって攻撃者からみて、上書きすべきアドレスの内容がわかりにくくなり、バッファオーバーフロー攻撃がしづらくなります。
上記のsystem()を使ったバッファオーバーフロー攻撃の例では、事前にsystem()のアドレスがわかっていました。しかしASLRによりアドレスが不定になるため、前述の図3のような攻撃が難しくなります。(図4:②)
まだテキスト領域(プログラム本体)の関数を不正に実行させることも……
ASLRによって、バッファオーバーフロー攻撃による共有ライブラリの関数の不正実行は難しくなりました。しかしテキストセグメントに存在する関数の位置はまだ推測可能です。そのためにテキストセグメントの配置アドレスをランダムに変える PIE (Position Independent Executables) が登場しました。 PIEとは、プログラム本体を位置独立コードにしたものです。攻撃者から見て、PIEとしてコンパイルされたプログラムの関数は、どのアドレスに配置されたかわからなくなります(注釈:コンパイラによっては、コンパイルとリンクのときにオプションを指定する必要があります。例えばgccではコンパイル時は-fPIEオプション、リンク時は-pieオプションです)。そのためバッファオーバーフロー攻撃がしづらくなります。
ランダムに配置したアドレスを特定し、ASLR, PIEをすり抜けてしまう!
上記の対策でバッファオーバーフロー攻撃は難しくなりました。NX bitのためにスタック上のコード実行は殆ど不可能です。ASLRやPIEによって関数の不正実行も難しくなりました……
しかし難しくなっただけで、すり抜ける方法が存在します。この方法とは、ランダムに配置したアドレスを特定できてしまうことを利用したものです。たとえばInformation Leak、あるいはInformation Exposureと呼ばれる脆弱性を利用することで、実行中プログラムのアドレスマップを取得した場合、ASLRならびにPIEは無意味なものとなります。
*話が本題から逸れてしまうのでInformation LeakならびにInformation Exposureの詳細は省略します。
SSPで、制御情報の書き換えを検知し、BoF攻撃による関数の不正実行を防ぐ!
このような課題に対応するため、バッファオーバーフローによる制御情報の書き換えを検出するSSP(Steak Smashing Protection)が登場しました。SSPとは、制御情報の書き換えを検知し、バッファオーバーフロー攻撃によって関数が不正実行されることを防ぐ対策です(注釈:例えばgccなどのコンパイラは、デフォルトでSSP対策を施したプログラムを生成します)。この対策は制御情報とバッファの間に書き換え検知のための印「canary」を挿入することで実現します。これらのcanary挿入とチェックの処理は、コンパイラによってプログラムに挿入されます。
挿入されたプログラムの実行内容は次の通りです。関数呼び出しの時、スタックに記憶する制御情報とバッファとの間にcanary を記憶します(図5:①および②)。また関数から戻るときにはcanaryが書き換わっていないことをチェックします(図5:③)。 バッファオーバーフロー攻撃で制御情報を上書きする場合はcanaryも上書きしています(図5:④)。そのため関数から戻るときの canary チェックでバッファオーバーフローを検出できます(図5:⑤)。
おわりに
これまでバッファオーバーフロー攻撃とその対策の歴史について説明しました。概要は以下の通りです。
- バッファ上の不正プログラム実行 → NX bitによってバッファ上のプログラム実行を抑止
- 共有ライブラリの関数を不正実行 → 共有ライブラリのアドレスをASLRによってランダムに
- プログラム本体の関数を不正実行 → プログラム本体のアドレスをPIEによってランダムに
- 上記がすりぬけられる場合 → SSPによってバッファオーバーフローを検出する
しかし、これらの対策によってバッファオーバーフロー攻撃は完全に防がれたわけではありません。問題の元である、バッファがあふれるという問題はまだ残っています。また最後に説明したSSPにも、実はすり抜けの方法があります。最終回となる次回は、これらの対策をすり抜けてしまう攻撃について説明したいと思います。
著者
株式会社ATTC(エーティーティーシー) 尾關晃充
元々はWebアプリケーションのサーバサイドエンジニアだった。
ATTC入社後はセキュリティ製品開発に携わっている。
現在は「ATTC Control Flow Integrity」の開発担当を務め
セキュリティや脆弱性対策に取り組んでいる。
「CPUのアーキテクチャから徹底解説 バッファオーバーフローで何が起こるか」の最新話は、メールマガジンにてもご案内致しています。是非JAPANSecuritySummit Updateのメールマガジンにご登録ください。
メールマガジンの登録はこちらからお願いします。