SEH (Stractured Exception Handling)

ここではWindowsのSEHに関する技術情報をまとめています。主にその利用法というよりは、内部的な実装、脆弱性、保護方法などを中心としてまとめています。32bit CPU x86での処理を前提としています。

さらに詳しいSEHオーバーライトExploitに関するレポートはこちらの「SEHオーバーライトの防御機能とそのExploit可能性」をご覧ください。

基本

SEHはWindowsが提供している例外処理機構である。Windows自体はプラットフォームとしてその基本機能を提供し、その利用方法については各言語環境、コンパイラによって異なる。通常プログラマから見た場合には、コンパイラがインターフェースとなり、具体的なWindowsとのやりとりやコードはコンパイラが生成する。たとえば、Visual C++ではSEHを__try,__exceptキーワードを用いてそのインターフェースを提供している。このためSEHを利用する上でコンパイラの果たす役割がかなり大きい。

Windowsが提供する"生の"SEHは以下のような機能である。

プロセス内(のあるスレッド)で例外が発生するとWindowsはTEB(スレッド環境ブロック。常にFSレジスタが示すセグメントのオフセット0x00000000にある。)の先頭にあるNT_TIB構造体の先頭値を見る。この値はEXCEPTION_REGISTRATION_RECORD構造体へのポインタである。


struct EXCEPTION_REGISTRATION_RECORD{
	EXCEPTION_REGISTRATION_RECORD *_next;
	POINTER_TO_EXCEPTION_HANDLER _exception_handler; //例外ハンドラへのポインタ。引数等の詳細は後述
}
	

この構造体の_nextメンバは次のEXCEPTION_REGISTRATION_RECORDへのポインタとなっている。つまりこの構造体は一方向のリストになっている。

次に、Windowsはこの_exception_handlerをリストをたどりながら順番に呼び出し、そのハンドラが今起きた例外を処理するかどうかを確かめる(例外ハンドラの戻り値で確認)。もし、処理しないとわかれば、_nextの示す次のEXCEPTION_RECORDの例外ハンドラを呼び出す。そして最後までこれを繰り返す。最後のEXCEPTION_REGISTERATIONの_nextは0xffffffffになっている。

プログラムはこの構造体を作成し、例外ハンドラアドレスアドレスを代入し、TEBの先頭、つまりFS:0x00000000にその構造体アドレスを登録、さらに今までに作られていたリストを今作った要素の後ろにくっつけるという処理をすることで、例外発生時にそのハンドラが呼ばれるようになる。

例外ハンドラを登録する場合、基本的には以下のような処理をプログラムは行う。

  1. スレッドスタック上に例外ハンドラアドレスをpush (_exception_handler)
  2. スレッドスタック上に現在のFS:0x00000000の値をpush (_next)
  3. FS:00000000にESP(現在のスタックポインタ)を代入する。(ESPは今スタック上に作ったEXCEPTION_RECORDを指している。)

アセンブリでは


push  _handler
 mov  eax fs:[00000000]
push  eax
 mov  fs:[00000000], esp
	

例外ハンドラを順番に3つ登録していった場合、スタック上のEXCEPTION_RECORDとTEBとの関係は以下のようになっている。

seh1.png

例外ハンドラの関数プロトタイプは以下のようになっており、この形で例外ハンドラを作れば、いろいろな情報をWindowsが引数として与えてくれるため、その内容からどのような例外処理をするかを決めることができる。


typedef EXCEPTION_DISPOSITION (*ExceptionHandler)( IN EXCEPTION_RECORD ExceptionRecord,
                                                   IN PVOID EstablisherFrame,
                                                   IN PCONTEXT ContextRecord,
                                                   IN PVOID DispatcherContext );
	

後述のSEH Overwritingでは上のプロトタイプの中のEstablisherFrameが重要な役割を果たす。この値は、この例外ハンドラアドレスを保持しているスタック上のEXCEPTION_RECORDへのポインタである。

SEHとC++例外

SEHはWindowsが提供している機構であり、C++の例外とは本質的には関係はない。C++例外は仕様であり、その実装方法については書かれていないため、それはプラットフォームにより異なる。Visual C++はSEHを利用してC++例外処理を実装している。ただし二つを混同して使うべきではない。SEHそのものの(つまり__try,__exceptを使った)グローバルアンワインドでは、スタック上のオブジェクトのデストラクタは呼び出されない。

試しに以下のようなコードを作成して関数を呼び出してみる。VS2008では/EHscまたは/Ehaオプション付きではコンパイルできないため、はずしてコンパイル、実行する。func_SEHからreturnする時、MutexLockのデストラクタは呼ばれていないことが分かる。


class MutexLock{
public:
	MutexLock(){ /* Lock Mutex  */ };
	~MutexLock(){ std::cout << "Destuctor Called. Mutex is released properly."; /* Release Mutext */}

};

void func_SEH(){
	// SEHでアンワインド( Visual Studio 2008では/EHscまたはEhaオプション付きではコンパイルできない
	__try{
		MutexLock a; // This may be like "MutexLock a(mutex)"
		int* b = 0;
		*b = 1; // Exception!
	}
	__except(EXCEPTION_EXECUTE_HANDLER){
		
	}
	return;
}

同様にC++例外も試す。こちらはしっかりとデストラクタに書いたメッセージが表示される。(/EHscオプション付きでコンパイルすることを忘れずに)

	
void func_CPP(){
	// C++例外でアンワインド
	try{
		MutexLock a; // This may be like "MutexLock a(mutex)"
		int* b = 0;
		*b = 1; // Exception!
	}
	catch(...){
	
	}
	
	return;
}		

例外処理でもあり、動きもにているため混同しがちだが全くことなるものであり、かつ対等な関係にはない。つまり、SEHはWindowsの提供する機能であり、C++言語環境から見ればより下位の層に実装されていることになる。C++例外はVisual C++ではこのSEHを内部的には利用して実装されているということである。Linux(gcc)などのC++例外は当然このようなSEHの提供を受けていないので別の方法で実装されている。

SEH Overwiting Expoit

基本の部分で書いたように、SEHはスタックを利用してその処理を行っている。当然ローカルスタック上のバッファオーバーフローによって、その中身が書き換えられる可能性がある。その場合例外ハンドラアドレスを自由なアドレスに設定できる。バッファオーバーフローを突いて、意図的に例外ハンドラの書き換えをする攻撃が可能となってしまう。これがどれくらい問題なのかはこれから説明する。ポイントとしては、リターンアドレスの書き換えよりも確実性の高い攻撃コードを作成することができることである(理由は後述)

Exploit動作概要は以下の通り

  1. バッファオーバーフローを利用して、EXCEPTION_REGISTRATION_RECORDの_exception_handlerのアドレスを書き換える
  2. このとき、アドレスの示す先は、pop,pop,retの3命令の並ぶ場所にする。
  3. 例外を発生させる(どんな形でもよいため、比較的簡単。プログラムが参照するスタック上のポインタ変数を0にしておくなどでよい)
  4. Windowsが_exception_handlerを呼び出す(FSレジスタ経由でスタック上の書き換えたハンドラアドレスを取得するため、特に問題なく実行される)
  5. 呼び出されたとき、ESP+8の位置にはEXCEPTION_REGISTRATION_RECORDのアドレスが入っている。(_exception_handlerの関数プロトタイプからEXCEPTION_REGISTRATION_RECORDへのポインタがebp+8の位置に存在することが分かる)
  6. pop,pop,retの3命令が実行される
  7. この3命令が実行されたとき、リターンアドレスはスタック上のEXCEPTION_EXCEPTION_RECORDの場所を指しており、_nextの値をコードとして実行しようとする。
  8. この値は、一番最初のバッファオーバーフロー部分で書き換えることのできる部分なので、自由なコードを置いておけばよい。ただし、その後の_exception_handlerの値は利用しているため、4バイト分しか使える領域はない。おそらく攻撃をするならば、他の場所へジャンプする命令になる。

seh2.png

このExploitの最大利点はpop,pop,retという命令のある場所を安定的に見つけやすいということである。関数の終わりなどにどこにでも存在するようなパターンである。リターンアドレスの書き換えでは、スタックを実行するには、そのリターン先をjmp espというような比較的特殊な命令でなくてはスタック上のコードを実行できないが、この方法は例外ハンドラの書き換え後のアドレスを安定的に見つけられる。

ただし、最近ではスタック上のコードが実行しにくくなっているため攻撃はしにくくなっているというのも確かなことではあり、一方でリターンアドレスの書き換えでは、Return-into-libc, Return-oriented programmingなどの方法があるため、スタックが実行できないのであれば、こちらの方が簡単かもしれない。

しかし、この方法の攻撃者から見た利点は他にもある。最近では/GSオプション(スタックガード)付きでコンパイルされているモジュールが多いと思うが、SEH Overwritingではこの保護を回避できてしまう。/GSオプションは関数からのリターン前に、スタック保存したクッキー(ランダムな値)が関数実行前と同じかどうかをチェックするが、SEH Overwritingはこのチェックが入る前に例外が発生すればその時点で任意のコードの実行ができてしまう。これもまたこれが大きな問題となる理由である。

SafeSEH

SafeSEHは上記のExploitの保護手段のひとつである。バイナリをSAFESEHオプション付きでリンクし作成することで保護することができる。SAFESEHオプションの付いたバイナリ(EXEまたはDLL)は 例外ハンドラのリストを内部に持っており、それ以外の場所を例外ハンドラとして呼べないようにしている。つまり、例外が発生したとき、Windowsはその例外ハンドラリストの中に呼ぼうとしている_exception_handlerの値があるかどうかを確認するということである。

ただし、この保護も回避する方法がいくつかある。

例外ハンドラアドレスが/SAFESEHオプションなしでリンクされた実行イメージ(EXE,DLL)の実行可能ページに内にある場合、それはチェックされない。これはつまりメインとなるEXEが/SAFESEHオプション付きで作成されていたとしても、他のDLLが/SAFESEHオプションなしでリンクされていた場合には、そちらのアドレスを攻撃に利用される可能性があるということである。また、DEPがOffである場合には、実行イメージ外のアドレス(ヒープやメモリマップデータ)を指定したとしてもチェックされずに実行される。

上記の動作はDEPや各プロセス情報のフラグによって変化する。NtSetInformationProcessなどで挙動は変えることができるが、デフォルトでは上のような動きになる。

ただし、ここで言っているDEPはSoftware DEPと呼ばれている物である。Software DEPは例外ハンドラ呼び出し時に追加的に行われるチェック機能を指している。追加的に行われるチェックは主に二つである。一つは実行可能属性のないメモリを例外ハンドラが指していないかどうか、そして二つめが、実行可能イメージ内のアドレスを指しているかどうかのチェックである。(注意:いろいろなところにSoftware DEPという言葉の説明があるが、一部混乱があるようである。筆者もどの機能のことをSoftware DEPと言うのかははっきりと分かっていないが、このような機能と考えるのが分かりやすいと考えこのような機能を指すとしてある。一部ではSafeSEHと同じ物を指しているという物もあれば、SafeSEHを含んだ例外処理に追加されたチェック処理全体を言うと書いてあるところもある。ここではSoftwareDEPは例外ハンドラリストのチェックのことだけを指すと考えた。)

SEHOP ( SEH Overwrite Prevention )

SEHOPはWindowsに組み込まれたSEH Overwriteingの保護機能である。 この機能はVista SP1および2008 Serverから導入され、Windows 2008 ServerではデフォルトでOnになっており、Vista SP1およびWindows 7ではOffになっている。また、Windows7ではVista SP1同様デフォルトでOffになっている。

SEHOPがOnであるとき、Windowsはスレッド開始時に、確認用のEXCEPTION_REGISTRATION_RECORDを必ず挿入する。Windowsは例外発生時にEXCEPTION_REGISTRATION_RECORDの リンクリストをたどり、最終的に確認用EXCEPTION_REGISTRATION_RECORDにたどり着くかどうかを確認する。たどり着かなければ、SEH Overwritingが発生している可能性が高いため、例外ハンドラーは呼び出さない。これによりSafeSEHでは得られない保護機能が得られる。具体的にはSafeSEHはコンパイルし直さなくてはいけないが、SEHOPは動的にチェックをするためその必要がない。モジュール外のアドレスを例外ハンドラとして指定する意味がなくなるが、SHEOPではそのような場合でもチェックできる。

ただし、一部のアプリケーションではSEHOPとの互換性がとれないものがあり、それらについては問題を起こす可能性がある。CygwinやSkypeなどで問題が起きるという報告があるようである。

スタック保護技術とその回避

以下に各保護機能に対する代表的なスタックオーバーフローExploitが成功するかどうかを○、×であらわしてある。○は攻撃が成功することを意味し、×は攻撃することができないことを意味する。各保護機能はそれぞれ単独で機能している場合である。

NX bit(HW-DEP On)SafeSEH(SW-DEP On)SafeSEH(SW-DEP Off)/GS optionSEHOP
リターンアドレス書き換え + jmp esp × ×
リターンアドレス書き換え + Return into Libc ×
SEH Overwirting + スタック実行 × × ×
SEH Overwriting + Return into Libc × ×

ここではSafeSEHの保護機能においてはSoftware DEPがOnであっても、NX bitは利用されていないことに注意してほしい。ここではSoftware DEPのみがOnであることを意味している。またSafeSEHはプロセス内のすべての実行可能モジュールに適用されている場合を考えている。△とした場所は、Software DEPが有効でないとSEH Overwrite発生時の 例外ハンドラとして指定できるアドレス範囲がかなり増え、攻撃可能である可能性がかなり高いためこのようにした。

SEH Overwrite + Return into Libcという攻撃手法については、直接言及されてる資料は今のところなかった。可能なのではないかと考えここでは載せてみた。例外ハンドラが呼ばれる時点でESPアドレスには例外情報が乗っているため、攻撃者が自由に書き換えたスタックで任意のコードを実行できるわけではないが、うまいことESPを移動させる(スタックのクリア)+ return命令またはretn 0xXXXXなどの命令の場所が見つかれば、あとは攻撃者の任意のスタックでコードを実行できるため、可能であるのではないかと考えた。

参考文献