ハードコアの例外。 動的に割り当てられたコードでの実行処理の機能

画像



最新バージョンのOSは、実行可能コードにセキュリティ制限を課しています。 そのような状況では、挿入されたコードまたは手動で投影された画像で例外メカニズムを使用することは、ニュアンスの一部がわからない場合、重要な作業になります。 この記事では、x86 / x64 / IA64プラットフォーム用のWindows OSのユーザーモード例外マネージャーの内部デバイスと、システム制限の回避策を実装するためのオプションについて説明します。



__try



実際には、外部プロセスに埋め込まれたコードで本格的な例外処理を実装する必要があるタスクが発生したか、別のPE-packer / cryptorを作成し、展開されたイメージで例外が機能することを確認するとします。 何らかの方法で、それはすべて、例外を使用するコードがシステムローダーによって投影されたイメージの外側で実行されるという事実に要約されます。これは、困難の主な原因になります。 問題のデモンストレーションとして、現在のAPプロセス内の新しい領域に独自のイメージをコピーするコードの簡単な例を考えてみましょう。



void exceptions_test() { __try { int *i = 0; *i = 0; } __except (EXCEPTION_EXECUTE_HANDLER) { /*       */ MessageBoxA(0, " ", "", 0); } } void main() { /*    */ exceptions_test(); /*        */ PVOID ImageBase = GetModuleHandle(NULL); DWORD SizeOfImage = RtlImageNtHeader(ImageBase)->OptionalHeader.SizeOfImage; PVOID NewImage = VirtualAlloc(NULL, SizeOfImage, MEM_COMMIT, PAGE_EXECUTE_READWRITE); memcpy(NewImage, ImageBase, SizeOfImage); /*   */ ULONG_PTR Delta = (ULONG_PTR) NewImage - ImageBase; RelocateImage(NewImage, Delta); /*  exceptions_test    */ void (*new_exceptions_test)() = (void (*)()) ((ULONG_PTR) &exceptions_test + Delta); new_exceptions_test(); }
      
      





exceptions_testプロシージャでは、nullポインターにアクセスしようとすると、例外フィルターの代わりにtry-except MSVC拡張機能でラップされ、EXCEPTION_EXECUTE_HANDLERを返すスタブがあります。これにより、exceptブロックでコードがすぐに実行されます。 最初の呼び出しであるexceptions_testが期待どおりに実行されると、例外がキャッチされ、メッセージボックスが表示されます。 しかし、コードを新しい場所にコピーしてexceptions_testのコピーを呼び出した後、例外は処理されなくなり、アプリケーションは特定のOSバージョンに特有の未処理の例外に関するメッセージで単に「クラッシュ」します。 この動作の特定の理由は、テストが実行されたプラットフォームによって異なります。テストを決定するには、例外をディスパッチするメカニズムを理解する必要があります。





未処理の例外



例外ディスパッチ



プラットフォームと例外の種類に関係なく、ユーザーモードのスケジューリングは常にntdllモジュールのKiUserExceptionDispatcherポイントから開始されます。この制御はカーネルKiDispatchExceptionから転送されます(例外がユーザーモードから発生し、デバッガーによって処理されなかった場合)。 前の例では、例外の両方の場合(exceptions_testの実行と新しいアドレスでのコピー中)に制御がディスパッチャーに転送されます。これは、ntdll!KiUserExceptionDispatcherにブレークポイントを設定することで確認できます。 KiUserExceptionDispatcherコードは非常に単純で、次のようになります。



 VOID NTAPI KiUserExceptionDispatcher (EXCEPTION_RECORD *ExceptionRecord, CONTEXT *Context) { NTSTATUS Status; if (RtlDispatchException(ExceptionRecord, Context)) { /*  ,    */ Status = NtContinue(Context, FALSE); } else { /*   ,         */ Status = NtRaiseException(ExceptionRecord, Context, FALSE); } ... RtlRaiseException(&NestedException); }
      
      





ここで、EXCEPTION_RECORDは例外に関する情報を含む構造体であり、CONTEXTは例外が発生したときのスレッドコンテキストの状態の構造体です。 どちらの構造もMSDNに文書化されていますが、おそらく既にご存じでしょう。 このデータへのポインターはntdll!RtlDispatchExceptionに渡され、実際のディスパッチが実行されますが、例外処理のメカニズムは32ビットシステムと64ビットシステムで異なります。



x86



x86プラットフォームの主なメカニズムは、スタック上にあり、常にNT_TIB.ExceptionListからアクセス可能な例外ハンドラーの単一リンクリストに基づく構造化例外処理(SEH)です。 このメカニズムの基本はさまざまな作品で繰り返し説明されているため(「有用な資料」ボックスを参照)、繰り返しはせず、タスクと交差する点にのみ焦点を当てます。





ダンプチェーンSEH



事実、SEHでは、ハンドラーのリストのすべての要素がスタック上にある必要があります。つまり、バッファーがスタック上でオーバーフローすると、それらの要素が上書きされる可能性があります。 エクスプロイトの作成者によって悪用されたもの:ハンドラーへのポインターはシェルコードの実行に必要なアドレスで書き換えられましたが、ポインターとリストの次の要素も書き換えられたため、ハンドラーチェーンの整合性が侵害されました。 SEHを使用したプログラムへの攻撃に対する回復力を高めるために、MicrosoftはSafeSEH(PEファイルのIMAGE_DIRECTORY_ENTRY_LOAD_CONFIGディレクトリにある「安全な」ハンドラーのアドレスを持つテーブル)、SEHOP(フレームチェーンの整合性の簡単なチェック)、および対応するディスパッチディスパッチプロセス中に実行されるシステムポリシーDEPチェック。



Windows 8.1のntdll.dllライブラリのx86バージョンのメインRtlDispatchExceptionディスパッチプロシージャの簡略化された擬似コードは、次のように(いくつかの仮定を付けて)表すことができます。



 void RtlDispatchException(...) // NT 6.3.9600 { /*   Vectored Exception Handlers */ if (RtlpCallVectoredHandlers(exception, 1)) return 1; ExceptionRegistration = RtlpGetRegistrationHead(); /* ECV (SEHOP) */ if (!DisableExceptionChainValidation && !RtlpIsValidExceptionChain(ExceptionRegistration, ...)) { if (_RtlpProcessECVPolicy != 2) goto final; else RtlReportException(); } /*   ,     */ while (ExceptionRegistration != EXCEPTION_CHAIN_END) { /*    */ if (!STACK_LIMITS(ExceptionRegistration)) { ExceptionRecord->ExceptionFlags |= EXCEPTION_STACK_INVALID; goto final; } /*   */ if (!RtlIsValidHandler(ExceptionRegistration, ProcessFlags)) goto final; /*    */ RtlpExecuteHandlerForException(..., ExceptionRegistration->Handler); ... ExceptionRegistration = ExceptionRegistration->Next; } ... final: /*   Vectored Continue Handlers */ RtlpCallVectoredHandlers(exception, 1); }
      
      





提示された擬似コードから、ディスパッチスケジューリング中にSEHハンドラーに制御を正常に転送するには、次の条件を満たす必要があると結論付けることができます。



  1. SEHフレームのチェーンは正しい必要があります(ntdll!FinalExceptionHandlerハンドラで終了)。 検証は、プロセスでSEHOPを有効にして行われます。
  2. SEHフレームは積み重ねる必要があります。
  3. SEHフレームには、有効なハンドラーへのポインターが含まれている必要があります。


情報



ベクトル化された例外処理の場合、ディスパッチャではチェックが行われないため、プログラムでSEHサポートに煩わされる必要がない場合、VEHは適切なツールになります。




例外フィルターの呼び出しスタック



最初の2つのポイントですべてが明確であり、それらを実行するために追加の手順が必要ない場合、ハンドラーの「有効性」をより詳細に確認する手順を調べます。 ハンドラーチェックはntdll!RtlIsValidHandler関数によって実行され、Vista SP1の擬似コードは、2008年に米国のBlack Hatカンファレンスで一般公開されました。 いくつかの不正確さは含まれていましたが、これは彼が数年間、あるリソースから別のリソースへのコピーアンドペーストの形でさまようことを止めませんでした。 それ以降、この関数のコードは大幅な変更を受けておらず、Windows 8.1用のバージョンの分析により、次の擬似コードを作成できました。



 BOOL RtlIsValidHandler(Handler) // NT 6.3.9600 { if (/* Handler    */) { if (DllCharacteristics&IMAGE_DLLCHARACTERISTICS_NO_SEH) goto InvalidHandler; if (/*   .Net ,  ILonly  */) goto InvalidHandler; if (/*   SafeSEH */) { if (/*    LdrpInvertedFunctionTable (  ),      */) { if (/* Handler    SafeSEH */) return TRUE; else goto InvalidHandler; } return TRUE; } else { if (/* ExecuteDispatchEnable  ImageDispatchEnable    ExecuteOptions  */) return TRUE; if (/* Handler      */) { if (ExecuteDispatchEnable) return TRUE; } else if (ImageDispatchEnable) return TRUE; } InvalidHandler: RtlInvalidHandlerDetected(...); return FALSE; }
      
      





上記の擬似コードでは、条件のチェック順序が変更されています(元の条件では、2回チェックされる条件と、ネストされた関数でチェックされる条件があります)。 疑似コードを分析した後、検証が成功するには、ハンドラーが属する条件セットの1つを満たす必要があると結論付けることができます。





この場合、領域の属性でMEM_IMAGEフラグが設定されている場合(属性はNtQueryVirtualMemory関数によって取得されます)、コンテンツはPE構造に対応している場合、メモリ領域は方法と見なされます。 プロセスフラグは、KPROCESS.KEXECUTE_OPTIONSからNtQueryInformationProces関数によって取得されます。 受信した情報に基づいて、x86プラットフォームで動的に割り当てられたコードで例外サポートを実装するために、少なくとも3つの方法を区別できます。



  1. プロセスのImageDispatchEnableフラグを設定/置換します。
  2. メモリ領域のタイプをMEM_IMAGEに置き換えます(SafeSEHのないPEイメージの場合)。
  3. すべてのチェックをバイパスする独自の例外マネージャーを実装します。


これらの各オプションについて、以下で詳しく検討します。 また、SafeSEHサポートについても言及する必要があります。これは、たとえば正規の法的PEパッカーまたはプロテクターを作成する場合に必要になる場合があります。 実装するには、マップされたイメージに関するレコード(SafeSEHへのポインターを含む)をntdllグローバルテーブルに手動で追加する必要があります!LdrpInvertedFunctionTable、このテーブルで直接機能する関数はntdll.dllライブラリによってエクスポートされず、手動で検索する意味が少しあります: OSでは、テーブル自体へのポインタが必要です。 何らかの方法でポインタを見つけたら、安全な変更のためにテーブルへのアクセスをブロックすることにも注意する必要があります。 別の方法として、ファイルをアンパッカーのいずれかのセクションにアンパックし、アンパックされたファイルからメインイメージにSafeSEHテーブルを転送します。 残念ながら、これらおよびその他の手法の詳細な説明はこの記事の範囲を超えています; SafeSEHサポートを含まないオプションはここで考慮されます(ところで、このテーブルは常に単純にリセットできます)。



ExecuteOptionsプロセスの置換



ExecuteOptions(KEXECUTE_OPTIONS)-プロセスのDEP設定を含むKPROCESSカーネル構造の一部。 構造の形式は次のとおりです。



 typedef struct _KEXECUTE_OPTIONS { UCHAR ExecuteDisable : 1; UCHAR ExecuteEnable : 1; UCHAR DisableThunkEmulation : 1; UCHAR Permanent : 1; UCHAR ExecuteDispatchEnable : 1; UCHAR ImageDispatchEnable : 1; UCHAR Spare : 2; } KEXECUTE_OPTIONS, PKEXECUTE_OPTIONS;
      
      







DEPが有効なプロセスのExecuteOptions



ユーザーレベルでのこれらの設定(フラグ)の値は、情報クラスパラメーターが0x22(ProcessExecuteFlags)に等しいNtQueryInformationProcess関数によって取得されます。 フラグは、NtSetInformationProcess関数によって同じ方法で設定されます。 Vista SP1以降、DEPが有効になっているプロセスの場合、Permanentフラグがデフォルトで設定されます。これにより、プロセスの初期化後に設定を変更できなくなります。 NtSetInformationProcessからカーネルモードで呼び出されるKeSetExecuteOptionsプロシージャのフラグメントは、これを確認します。



 @PermanentCheck: ; KeSetExecuteOptions +2Fh mov al, [edi+6Ch] ; current KEXECUTE_OPTIONS mov byte ptr [ebp+arg_0+3], al test al, 8 ; test Permanent jnz short @Fail ;  0C0000022h (STATUS_ACCESS_DENIED)
      
      





したがって、ユーザーモードでは、DEPがアクティブになっている場合、ExecuteOptionsを変更できません。 ただし、フックをNtQueryInformationProcessに設定することにより、RtlIsValidHandlerを単純に「トリック」するオプションが残っています。この場合、フラグは必要なフラグに置き換えられます。 このようなインターセプトをインストールすると、システムによってロードされたモジュールの外部にあるコードが例外的に動作可能になります。 インターセプターコードの例:



 NTSTATUS __stdcall xNtQueryInformationProcess(HANDLE ProcessHandle, INT ProcessInformationClass, PVOID ProcessInformation, ULONG ProcessInformationLength, PULONG ReturnLength) { NTSTATUS Status = org_NtQueryInformationProcess(ProcessHandle, ProcessInformationClass, ProcessInformation, ProcessInformationLength, ReturnLength); if (!Status && ProcessInformationClass == 0x22) /* ProcessExecuteFlags */ *(PDWORD)ProcessInformation |= 0x20; /* ImageDispatchEnable */ return Status; }
      
      





メモリ属性の置換



プロセスフラグを置き換える別の方法は、ハンドラーが配置されているメモリ領域の属性を置き換えることです。 既に述べたように、RtlIsValidHandlerは割り当てられたメモリ領域のタイプをチェックし、MEM_IMAGEと一致する場合、その領域は画像と見なされます。 選択したVirtualAlloc領域にMEM_IMAGEを割り当てることはできません。このタイプは、正しいファイルハンドルが指定されている(NtCreateSection)セクションを表示するようにのみ設定できます。 ExecuteOptionsの置換と同様に、今回はNtQueryVirtualMemory関数をインターセプトする必要があります。



 NTSTATUS NTAPI xNtQueryVirtualMemory(HANDLE ProcessHandle, PVOID BaseAddress, INT MemoryInformationClass, PMEMORY_BASIC_INFORMATION MemInformation, ULONG Length, PULONG ResultLength) { NTSTATUS Status = org_NtQueryVirtualMemory(ProcessHandle, BaseAddress, MemoryInformationClass, Buffer, Length, ResultLength); if (!Status && !MemoryInformationClass) /* MemoryBasicInformation */ { if((UINT_PTR)MemInformation->AllocationBase == g_ImageBase) MemInformation->Type = MEM_IMAGE; } return Status; }
      
      





このメソッドは、PEイメージ全体を注入するときの例外、または手動でマップされたイメージに適しています。 さらに、このオプションは、DEPを部分的に無効にすることでプロセスのセキュリティを低下させないという理由だけで、前のオプションよりもいくぶん望ましいです(本当に追加のマルウェアが必要ですか?) ボーナスとして、このメソッドを使用すると、try-exceptおよびtry-finallyコンストラクトを使用して、CRTの最新バージョンでハンドラーの内部チェックを渡すことができます(これらのコンストラクトはCRTなしでも使用できます。詳細については、対応するボックスを参照してください)。 CRTでの検証は、_except_handler3から呼び出される__ValidateEH3RN関数によって実行され、領域の確立されたMEM_IMAGEタイプと、正しいPE構造を想定しています。



ネイティブ例外マネージャー



フックをインストールするためのオプションが何らかの理由で不適切または単に気に入らない場合は、さらに進んで、SEHディスパッチをコードで完全に置き換え、ベクターハンドラー内にSEHディスパッチャーの必要なロジックをすべて実装できます。 与えられた擬似コードRtlDispatchExceptionから、SEHチェーン処理が始まる前にVEHが呼び出されていることがわかります。 ベクターハンドラーで例外を制御し、それをどう処理し、どのハンドラーを呼び出すかを決定することを妨げるものは何もありません。 VEHハンドラーは1行だけでインストールされます。



 AddVectoredExceptionHandler(0, (PVECTORED_EXCEPTION_HANDLER) &VectoredSEH);
      
      





ここで、VectoredSEHはハンドラーであり、実際にはSEHディスパッチャーです。 このハンドラーの完全な呼び出しチェーンは、KiUserExceptionDispatcher-> RtlDispatchException-> RtlpCallVectoredHandlers-> VectoredSEHのようになります。 同時に、呼び出し元の関数の制御は返されない場合がありますが、スケジューリングの成功に応じて、NtContinueまたはNtRaiseException自体を呼び出します。 この記事に添付されている資料またはGitHubで、VEHを介してSEHを実装するための完全なソースコードを参照してください。 実装コードは完全に機能しており、ディスパッチロジックはシステムのロジックに対応しています。





ベクトルハンドラー内のSEHディスパッチャー



x64およびIA64



x64およびItaniumプラットフォーム用の64ビットバージョンのWindowsでは、x86バージョンとはまったく異なる例外処理方法が使用されます。 このメソッドは、例外が処理されるコードブロックの最初と最後のオフセットを含む、ディスパッチスケジューリングに必要なすべての情報を含むテーブルに基づいています。 したがって、これらのプラットフォーム用にコンパイルされたコードには、try-exceptブロックごとにハンドラーをインストールおよび削除する操作はありません。 静的例外テーブルは、PEファイルの例外ディレクトリにあり、次のようなRUNTIME_FUNCTION構造要素の配列です。



 typedef struct _RUNTIME_FUNCTION { ULONG BeginAddress; ULONG EndAddress; ULONG UnwindData; } RUNTIME_FUNCTION, *PRUNTIME_FUNCTION;
      
      





楽しい瞬間:システムレベルで、動的コードの例外サポートが実装されます。 コードが非イメージメモリ領域にある場合、またはコンパイラによって生成された例外テーブルがこのイメージにない場合、例外処理の情報は動的例外テーブル(DynamicFunctionTable)から取得されます。 リストへのポインターはntdll!RtlpDynamicFunctionTableに格納され、リストを操作するためのいくつかの関数がntdll.dllからエクスポートされます。 これらの関数のリストをすばやく分析すると、DynamicFunctionTableリスト項目の次の構造を取得できました。



 struct _DynamicFunctionTable { /* +0h */ PVOID Next; PVOID Prev; //       /* +10h */ PRUNTIME_FUNCTION Table;//   ,      ID|0x03 PVOID TimeCookie; // ZwQuerySystemTime /* +20h */ PVOID RegionStart; //   BaseAddress DWORD RegionLength; //   ()  /* +30h */ DWORD64 BaseAddress; PGET_RUNTIME_FUNCTION_CALLBACK Callback; /* +40h */ PVOID Context; //     DWORD64 CallbackDll; //   +58h,  DLL  /* +50h */ DWORD Type; // 1 — table, 2 — callback DWORD EntryCount; WCHAR DllName[1]; };
      
      







検索アルゴリズムRUNTIME_FUNCTION



要素は、RtlAddFunctionTableおよびRtlInstallFunctionTableCallback関数によって追加され、RtlDeleteFunctionTableによって削除されます。 これらの機能はすべて、MSDNに詳細に記載されており、非常に使いやすいです。 手動で表示されたばかりの画像に動的テーブルを追加する例:



 ULONG Size, Length; /*  ,  ,    */ PRUNTIME_FUNCTION Table = (PRUNTIME_FUNCTION) RtlImageDirectoryEntryToData(NewImage, TRUE, IMAGE_DIRECTORY_ENTRY_EXCEPTION, &Size); Length = Size/sizeof(PRUNTIME_FUNCTION); /*      DynamicFunctionTable */ RtlAddFunctionTable(Table, Length, (UINT_PTR)NewImage);
      
      





それはすべて、フックやカスタム例外マネージャー、システムチェックの回避策はありません。 DynamicFunctionTableはプロセスに対してグローバルであるため、レコードが追加されたコードが機能しており、削除する必要がある場合は、テーブルから対応するレコードも削除する必要があることに注意してください。 テーブルを追加する代わりに、APの特定の範囲のアドレスにコールバックを設定できます。これは、この領域のコードにRUNTIME_FUNCTIONレコードが必要になるたびに制御を受け取ります。 コールバックをインストールするバージョンの記事に添付されているソースコードを参照してください。





処理された例外



__最後に



ネイティブAPIを使用したWindowsでの低レベルプログラミングは、エラー処理の方法として例外を課しません。「特定のソフトウェア」の開発者は、単にそれらを単に無視するか、未処理の例外のフィルター設定またはVEHの使用に制限することがよくあります。 それにもかかわらず、例外は、プログラムのアーキテクチャが複雑になるほど、より大きな利益を引き出すことができる強力なメカニズムのままです。 また、この記事で説明した方法のおかげで、最も異常な状況でも例外を使用できます。



便利な資料





Windows Research Kernel(NT5.2カーネルソースコードの主要部分)を入手することもお勧めします。 WRKは大学や学術機関に配布されていますが、そのようなものを探す方法と場所を教えることは私にとってではありません。


CRTを使用しないTry-exceptおよびtry-finallyコンストラクト



例外ブロックとファイナライズブロックの構成を使用する場合は、プログラム内のプログラムを処理する必要があります。コンパイラーは、実際のハンドラーに置き換えます。x86プロジェクトの場合は__except_handler3、x64の場合は__C_specific_handlerです。 これらのプロシージャは独自のディスパッチを実行します。必要なハンドラを検索して呼び出し、スタックを昇格させます。 x86プロジェクトの場合、古いDDKのexpsup3.lib(DDKのntdll.libにも必要な機能が含まれています)を接続するだけでよく、x64の場合はさらに簡単です:__C_specific_handlerはntdll.dllの64ビットバージョンでエクスポートされます。正しいlibファイルを使用するだけです。


画像



Hacker Magazine#195で最初に発行されました。

投稿者:Teq



ハッカーを購読する




All Articles