'Exec Shield' ── Linuxの新たなセキュリティ機構

このたび、Linux/x86向けにカーネル・ベースの新たな セキュリティ機構「exec-shield」のソース・コードを初めて公開 する運びとなった。対象となるカーネルのバージョンは2.4.21-rc1で、 GPL/OSLのライセンスのもとで配布される。 http://redhat.com/~mingo/exec-shield/からパッチをダウンロードしていただきたい。

さて、exec-shieldはスタック、バッファ、関数ポインタのオー バフロー防止機構を提供することにより、データ構造への上書きや コードの書き込みを利用した攻撃に対抗するものである。本パッチ を適用すると、いわゆる「シェルコードを利用した攻撃」も難しく なる。本パッチは透過的に動作するのでアプリケーシ ョンの再コンパイルは必要ない。

背景:

よく知られた事実だが、x86のページテーブル・エントリでは、 いわゆる実行可能ビットがサポートされていない。PROT_EXEC PROT_READが独立しておらず、読み取り 実行の制御を1つのフラグで兼用しているのだ。つまり、 アプリケーション・レベルで(メモリマップの際にPROT_EXEC フラグを立てないようにして)メモリ領域を実行可能でないと マークしても、x86ではPROT_READフラグが立っていると その領域を実行できてしまうのである。

さらに、x86のELF ABIがプロセス・スタックを実行可能とマーク するため、ページテーブルで実行可能ビットをサポートするCPUでも スタックを実行可能とマークせざるをえないだ。

この問題と取り組むために、これまでもさまざまなカーネル・パッチ が考案されてきた。Solar Designerの「non-exec stack patch」は そうしたパッチの代表格である。これらのパッチは概ねx86の セグメント・アドレッシング機能を利用して、コード・セグメントの 限界点(リミット)をスタック・フレーム直下の固定アドレスに設定 するようなしくみになっている。exec-shieldは、このコード・セグ メント・リミットで、スタックだけでなく仮想メモリもできるだけ 広くカバーしようとするものである。

実装:

exec-shield機構はカーネル側からトランスペアレントに働き、 アプリケーションの実行可能マップを常に監視して「最大実行可能 アドレス」を適切に維持する。このアドレスを「execリミット」 と呼ぶ。スケジューラはexecリミットを使用してコンテキスト・ スイッチングのたびにコード・セグメント・デスクリプタを更新 する。システムの各プロセス(あるいはスレッド)のexecリミット は必ずしも同じでないので、スケジューラはユーザ・コード・セグ メントを動的に設定して常に適切なコード・セグメント・リミット が使われるようにする。カーネルはユーザ・セグメント・デスクリ プタの値をキャッシュするため、コンテキスト・スイッチングの パスで生じるオーバーヘッドはごくわずかで、GDTに無条件に6バ イト書き込んでも、せいぜい2、3サイクル消費するにすぎない。 また、カーネルは、いわゆるASCII-armor領域(x86では0〜16MBの アドレス)にすべてのPROT_EXECマップを再マップする。 これらのアドレスが特徴的なのは、ASCIIベースのオーバフローを 利用してもそこにジャンプできないことだ。たとえば、次のような 長いURLが入力されるとオーバフローするバグを持つアプリケーションが あったとする。

http://somehost/buggy.app?realyloooooooooooooooooooong.123489719875

この場合、攻撃者が使うのはASCII文字(値1〜255)だけである。 そこで、すべての実行可能アドレスがASCII-armor領域にあれば、 URL攻撃を利用して実行可能コードへジャンプすることは不可能となる。 つまり、攻撃が成功することはない(URL文字列中に\0を含めること ができないからだ)。つい最近あった、sendmailの脆弱性を狙った remote root攻撃もASCIIベースのオーバフローを利用したものであった。

exec-shieldが有効な状態で、ASCII-armor領域に再リンクされた 「cat」バイナリを実行すると、次のメモリ・レイアウトが得られる。

  $ ./cat-lowaddr /proc/self/maps
  00101000-00116000 r-xp 00000000 03:01 319365     /lib/ld-2.3.2.so
  00116000-00117000 rw-p 00014000 03:01 319365     /lib/ld-2.3.2.so
  00117000-0024a000 r-xp 00000000 03:01 319439     /lib/libc-2.3.2.so
  0024a000-0024e000 rw-p 00132000 03:01 319439     /lib/libc-2.3.2.so
  0024e000-00250000 rw-p 00000000 00:00 0
  01000000-01004000 r-xp 00000000 16:01 2036120    /home/mingo/cat-lowaddr
  01004000-01005000 rw-p 00003000 16:01 2036120    /home/mingo/cat-lowaddr
  01005000-01006000 rw-p 00000000 00:00 0
  40000000-40001000 rw-p 00000000 00:00 0
  40001000-40201000 r–p 00000000 03:01 464809     locale-archive
  40201000-40207000 r–p 00915000 03:01 464809     locale-archive
  40207000-40234000 r–p 0091f000 03:01 464809     locale-archive
  40234000-40235000 r–p 00955000 03:01 464809     locale-archive
  bfffe000-c0000000 rw-p fffff000 00:00 0

このレイアウトで、最上位の実行可能アドレスは0x01003fff である。つまり、実行可能アドレスはすべてASCII-armor領域 に含まれる。

これは、スタックだけでなく、mmap()した領域の多くとmalloc()で確保 したヒープ領域も実行可能でなくなることを意味する(一部のデータ領域 は依然として実行可能だが、ほとんどの領域はそうでない)。

ASCII-armor領域の最初の1MBはNULLポインタの間接参照対策のために 手付かずで残されいるため、XFree86などが16ビット・エミュレーション のマッピングに使用できる。

exec-shieldが有効でないときのメモリ・レイアウトと比べてみよう。

  
  08048000-0804b000 r-xp 00000000 16:01 3367       /bin/cat
  0804b000-0804c000 rw-p 00003000 16:01 3367       /bin/cat
  0804c000-0804e000 rwxp 00000000 00:00 0
  40000000-40012000 r-xp 00000000 16:01 3759       /lib/ld-2.2.5.so
  40012000-40013000 rw-p 00011000 16:01 3759       /lib/ld-2.2.5.so
  40013000-40014000 rw-p 00000000 00:00 0
  40018000-40129000 r-xp 00000000 16:01 4058       /lib/libc-2.2.5.so
  40129000-4012f000 rw-p 00111000 16:01 4058       /lib/libc-2.2.5.so
  4012f000-40133000 rw-p 00000000 00:00 0
  bffff000-c0000000 rwxp 00000000 00:00 0

このレイアウトでは、実行可能な領域はASCII-armor領域内に 存在せず、しかもexecリミットは0xbfffffff(3GB)である。 つまり、ユーザ空間のすべてのマッピングが含まれることになる。

なお、カーネルは共有ライブラリをいちいちASCII-armor領域に 再配置するが、そのバイナリ・アドレスはリンク時に決定される。 ASCII-armor領域にアプリケーションを再リンクする手間を省くた めにArjan Van de Venが作成したパッチ (binutils-2.13.90.0.18-elf-small.patch)があり、 このパッチで追加されたldの新しいフラグ、 “ld -melf_i386_small“(あるいは “gcc -Wl,-melf_i386_small“)によってASCII-armor 領域への再リンクが行われる(このパッチもexec-shieldと同じ URLから入手できる)。

オーバーヘッド:

本パッチは、効率を第一に考えた。PROT_MMAP システム・コールごとに監視のためのごくわずかのオーバーヘッド (2サイクル)が生じるほかは、コンテキスト・スイッチングごとに 2、3サイクル消費するだけである。

制限:

この機構は、あらゆるタイプの攻撃に対応するものではない。

たとえば、オーバーフローを利用してローカル変数が上書きされ た場合、その影響で制御の流れが変わって障害が生じることはあり 得る。しかし、スタック内のリターン・アドレスやヒープ内の関数 ポインタを狙った純粋なオーバーフロー攻撃はすべて阻止できると 思う。また、exec-shieldはシェルコードの実行もほぼ抑えるので、 これ以外の攻撃もかなり難しくなる。

だが、オーバーフローがexec-shield自体で発生した場合は (つまり、ASCII-armor領域内のいずれかの共有ライブラリ・オブ ジェクトのデータ・セクションで発生すると)まだ攻撃される 余地がある。

exec-shieldは攻撃を抑える防壁の1つであって、それだけで100% 完璧な保護を与えるものではない。セキュリティを確保するには、 何重にも対策を講じることが重要なのだ。

付け入る隙をできるだけ与えないように、exec-shieldコードは トランポリンに頼らないことにした。トランポリンを利用した execリミットの侵犯が起こる余地はまずない。gccのトランポリンを 前提とするアプリケーションはバイナリ単位のELFフラグを使用して スタック・コードを再度makeする必要がある(このELFフラグは Solar Designerのnon-exec stack patchで使われているものと同じで、 既存のnon-exec-stack処理系との互換性に配慮した)。

exec-shield機構は、x86のPROT_READによる実行許可 を前提とした変則的なアプリケーションをあぶりだしてくれる。 その1つの例が、XFree86モジュール・ローダだ。この問題は rawhide.redhat.comの最新のXFree86では解決されている。XFree86の バグフィックスをすぐインストールできない人のために、本パッチでは 次の回避オプションを用意した。

    echo 1 > /proc/sys/kernel/X-workaround

これで、アプリケーション(たとえば、X)を使用するiopl() ごとにexec-shieldが無効になる。他のアプリケーション(sendmailなど) ではexec-shieldは依然として有効である。この回避オプションはデフォルト でオフになっている。この問題を解決するには、Xをアップグレードするか、 「chkstk」ユーティリティを使ってXのスタックを強制的に実行可能にする ことを強くお勧めする。

使用方法:

exec-shield-2.4.21-rc1-B6カーネル・パッチを2.4.21-rc1カーネル に適用し、再コンパイル後、カーネルをインストールして再起動すれば 終わりである。

起動時のカーネル・コマンド・ライン・オプションとして新たに exec-shield=が設けられた。これはセキュリティ・レベルに応じて 次の4つの値を取る。

   exec-shield=0    – 常に無効
   exec-shield=1    – 明示的に有効にしたバイナリ以外はデフォルトで無効
   exec-shield=2    – 明示的に無効にしたバイナリ以外はデフォルトで有効
   exec-shield=3    – 常に有効

現在のパッチはexec-shield=2がデフォルトである。次のように して/procに値を書き込めば、実行時にセキュリティ・レベルを変更 することもできる。

   echo 0 > /proc/sys/kernel/exec-shield

重要:セキュリティに関係のアプリケーションがexec-shieldが無効な 間に開始された場合は、実行可能なスタックを持つことになるので、 exec-shieldを再び有効にするときそれらを再起動する必要がある。

Solar Designerのchstk.cコードの修正版もアップロードした。 これにはELFフラグ「enable non-exec stack」の変更に必要が次のオプション が設けられている。

  $ ./chstk
  使用方法: ./chstk OPTION FILE…
  バイナリのスタック領域の実行可能フラグを管理する

    -e    実行パーミッションをオンにする
    -E    非実行パーミッションをオンにする
    -d    実行パーミッションをオフにする
    -D    非実行パーミッションをオフにする
    -v    現在のフラグの状態を表示する

つまり、2つの明確に区別されたフラグが存在する。1つは実行可能 なスタックを強制するもので、もう1つは実行可能でないスタックを 強制するものだ。両方のフラグがオフの場合はシステムのデフォルト が使われる。

したがって、exec-shieldのセキュリティ・レベルを1にしておき、 バイナリごとにnon-execスタックを有効にするようなことが可能である。 具体的には、起動時にexec-shield=1を指定し、各バイナリに対して 個別に次のコマンドを実行する。

   ./chstk -E /usr/sbin/sendmail

(本番の環境をexec-shieldカーネルに移行する場合は、 この方法を取るのがよいだろう。)

ご意見、提案、評価をお聞かせ願いたい。

Ingo