セキュリティのギャップはどこから来ますか?

                                              「セキュリティホールがあります。」
                                              「まあ、少なくとも何かは私たちにとって安全です。」
                                                                                  逸話


画像 Windowsユーザーの場合、「重要なセキュリティ更新プログラム」のインストールを報告する毎月第2火曜日のポップアップウィンドウに精通している必要があります。 マイクロソフトは、オペレーティングシステムの脆弱性を絶えず修正するために多大な努力を払っていますが、それだけの価値があります。サイバー攻撃の数が日々増加している世界では、コンピューターの防御における単一の抜け穴は、潜在的な攻撃者に開かれたままではありません。



66192nd SVN ReactOS改訂メーリングリストに関する最近の議論は、カーネルコードに重大な脆弱性を導入することがいかに簡単かを示しました。 このケースは、単純ではあるがセキュリティ関連のエラーの例として使用するとともに、本当に安全なシステムを取得したい場合にカーネルコードを公開する必要がある対策のいくつかを説明するために使用します。



脆弱性を見つける



このコードを見て、その内容を見てみましょう。



NTSTATUS APIENTRY NtUserSetInformationThread(IN HANDLE ThreadHandle, IN USERTHREADINFOCLASS ThreadInformationClass, IN PVOID ThreadInformation, IN ULONG ThreadInformationLength) { [...] switch (ThreadInformationClass) { case UserThreadInitiateShutdown: { ERR("Shutdown initiated\n"); if (ThreadInformationLength != sizeof(ULONG)) { Status = STATUS_INFO_LENGTH_MISMATCH; break; } Status = UserInitiateShutdown(Thread, (PULONG)ThreadInformation); break; } [...] }
      
      





これはNtUserSetInformationThread関数の小さな部分です。これはwin32k.sysのシステムコールで、ユーザープログラムから(多かれ少なかれ)直接呼び出すことができます。 ここで、ThreadInformationはデータを持つ特定のブロックへのポインターであり、ThreadInformationClassパラメーターはこのデータの解釈方法を示します。 UserThreadInitiateShutdownの場合、ブロックには4バイトの整数が必要です。 転送されたバイト数はThreadInformationLengthに格納され、簡単にわかるように、コードは実際に正確に「4」を確認します。そうでない場合、STATUS_INFO_LENGTH_MISMATCHエラーで実行が中断されます。 ただし、これらのパラメーターは両方ともユーザープログラムから直接取得されるため、この関数を呼び出す悪意のあるブックマークは何でも渡すことができます。



それでは、UserInitiateShutdownに渡されるThreadInformationで何が起こるかを見てみましょう。



 NTSTATUS UserInitiateShutdown(IN PETHREAD Thread, IN OUT PULONG pFlags) { NTSTATUS Status; ULONG Flags = *pFlags; [...] *pFlags = Flags; [...] /* If the caller is not Winlogon, do some security checks */ if (PsGetThreadProcessId(Thread) != gpidLogon) { // FIXME: Play again with flags... *pFlags = Flags; [...] } [...] *pFlags = Flags; return STATUS_SUCCESS; }
      
      





この関数のかなりの部分はまだ実装されていないため、上記で発生することはすべて、ユーザーが指し示した4バイト値の読み取りおよび書き込みサイクルのほんの一部です。



では、問題は何ですか?



さて、未確認のポインターを逆参照するだけでDoS攻撃(サービス拒否)が可能になります。悪意のあるプログラムは、権限を持たずにコンピューターをシャットダウンする可能性があります。 たとえば、プログラムは単純にヌルポインターを渡すことができるため、この脆弱性を悪用できます。 UserInitiateShutdownは前述のポインターを逆参照します。これにより、通常はカーネル開発者の間で「バグチェック」と呼ばれるBSODが発生します。 この場合、呼び出し元はメモリに書き込むことができます(ここでは、これが任意のポインターであることを思い出します-カーネル領域を参照することさえできます!)。 一見、指定されたメモリ領域から読み取られた値をそこに書き戻すことはそれほど悪くはありません。 しかし、実際には十分な問題をもたらす可能性があります。 メモリの一部のセクションは頻繁に高輝度で変化し、以前にそこに保存されていた値を復元すると、たとえば、暗号化アルゴリズムの乱数ジェネレーターのエントロピーレベルが低下したり、メモリページのテーブルが古いバージョンで書き換えられたりする可能性があります。より多くのメモリにアクセスできるようになり、システムを侵害するために使用できます。 しかし、これらは道筋に沿って生まれた単なる例です-標的を絞った攻撃者は、目標を達成するための最良のソリューションを数か月間考え出すことができ、このようなセキュリティの小さな欠陥が、誰かにとって十分であることが判明するかもしれません、あなたの車からすべての秘密を引き出し、それを完全に制御します。 もちろん、関数が完全に実装されると、Flags変数が変更されてから書き戻され、任意のメモリ(カーネル)を変更できるようになります。



このすべてを知って、何を修正できますか?



この種の問題から保護するために、NTカーネルにはプローブ(チェック)とSEH(構造化例外処理、構造化例外処理)の2つのメカニズムがあります。 メモリチェックにより、多数の問題が解消され、アプリケーションから受け取ったポインタがユーザーのメモリスペースを本当に参照していることを確認できます。 すべてのポインターパラメーターに対してこのようなチェックを実行すると、ユーザーレベルのプログラムがこの方法でカーネルメモリにアクセスできないという確信が得られます。 ただし、これはnullやその他の無効なポインターからは保存しません。 そして、ここで2番目のメカニズムが救いになります。SEH:疑わしいポインター(つまり、ユーザープログラムから受け取った)のデータへの各アクセスを例外処理ユニットでラップすると、ポインターが無効であってもコードが安定したままになります。 この場合のカーネルレベルのコードは、保護されたコードが例外(無効なポインターの使用によるアクセス違反など)をスローするたびに呼び出される例外ハンドラーを提供します。 例外ハンドラは、利用可能な情報(例外コードなど)を収集し、メモリをクリアするために必要なすべてのアクションを実行し、ほとんどの場合、エラーコードとともにユーザーに制御を返します。



修正されたソースを見てみましょう( r66223 commit ):



 ULONG CapturedFlags = 0; ERR("Shutdown initiated\n"); if (ThreadInformationLength != sizeof(ULONG)) { Status = STATUS_INFO_LENGTH_MISMATCH; break; } /* Capture the caller value */ Status = STATUS_SUCCESS; _SEH2_TRY { ProbeForWrite(ThreadInformation, sizeof(CapturedFlags), sizeof(PVOID)); CapturedFlags = *(PULONG)ThreadInformation; } _SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER) { Status = _SEH2_GetExceptionCode(); } _SEH2_END; if (NT_SUCCESS(Status)) Status = UserInitiateShutdown(Thread, &CapturedFlags); /* Return the modified value to the caller */ _SEH2_TRY { *(PULONG)ThreadInformation = CapturedFlags; } _SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER) { Status = _SEH2_GetExceptionCode(); } _SEH2_END;
      
      







安全でないThreadInformationポインターへのすべての呼び出しは、_SEH2_TRYブロック内で実行されるようになりました。 それらで発生する例外は、_SEH2_EXCEPTブロックのコードによって制御されます。 さらに、ポインターを初めて参照する前に、ProbeForWriteの呼び出しが行われ、無効なポインター(たとえば、カーネル領域に属する)または書き込み保護されたメモリが検出されると、例外STATUS_ACCESS_VIOLATIONまたはSTATUS_DATATYPE_MISALIGNMENTが発生します。 最後に、導入された変数CapturedFlagsに注意してください。この変数は、UserInitiateShutdownに渡されます。 このようなトリックは、安全でないパラメーターを使用して操作を単純化します。関数内のpFlagsにアクセスするたびにSEHを使用しないように、この値はNtUserSetInformationThreadによって信頼できる領域に保存され、UserInitiateShutdownが機能するときにユーザーメモリに書き戻されます。 これにより、カーネル領域から入力への安全なポインター(CapturedFlagsへのポインター)を受け取るため、UserInitiateShutdown自体を編集する必要がなくなります。 これらすべての対策の結果-この機能は、システムを損なうリスクなしに、正確に、そして非常にではなく、絶対に任意のユーザーデータのセットで動作できるようになりました。 仕事は完了です!



これからどのような教訓を学ぶべきでしょうか?



明らかに、開発段階でも警戒心が高まると、将来セキュリティリスクになる可能性のあるコード行に気付くことができます。 率直に言って、それらがなければ、確かに多くのセキュリティ上の問題があるので、それらの多くを許可してはいけません。 将来、すべてが計画どおりに進んだ場合、それらを徐々に探して修正し、Windows Update Centerから火曜日に届くような定期的な更新をリリースします。



限界ノート。 Alex Ionescuが正しく述べたように、Windows自体には同じ関数NtUserSetInformationThreadに脆弱性があります。 さらに、彼によると、Surface RTのようなあらゆるジェイルブレイクデバイスに対して、まだ閉じられておらず、積極的に悪用されています。 2012年に、マテウシュ「ジョオロ」ユルチク(マテウシュユルチク)という名前の有名なセキュリティ研究者(偶然、IRCで私たちとよく付き合っています)によって最初に説明されました。 このトピックに関する彼の記事はブログで見つけることができます: j00ru.vexillium.org/?p=1393



- 翻訳者からのメモ:

すべてのタイプミス、エラー、および不正確さをプライベートメッセージで報告してください。

翻訳に参加: Postscripteral-tarakanoffAlexey BraginMabu



All Articles