'Exec Shield' ── Linuxの新たなセキュリティ機構
さて、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