Windowsのセキュリティ機能Control Flow Guard解説

Windows8.1 updateから入ったセキュリティー機能であるControl Flow Guard(以降GuardCF)について調べたのでまとめておきます。

本記事を書く上で、以下のサイトを参考にさせていただきました。
http://www.ffri.jp/blog/2015/01/2015-01-05.htm

IMAGE_OPTIONAL_HEADER32など本稿にあまり関係のない部分については特に解説しないので、必要なら別途ググるなどしてください。

GuardCFとは何か

ざっくりと説明すると「呼び出す関数が実行時に決定されるコードから呼び出せるアドレスをホワイトリストで管理し、それ以外を実行しようとしたらクラッシュする機能」です。
たとえばC++の仮想関数、dllを動的リンクした際のGetProcAddressの返り値、関数ポインターを経由した呼び出し、などなどが対象。
主に、仮想関数テーブルを上書きすることで仮想関数実行時に任意のアドレスを実行させる、俗にいうvtable overwriteを防ぐための機能です。

パスの解析やらは一切やっておらず、バグか脆弱性がない限りここではAは呼ばれないと自明な場合でも、Aがホワイトリストに入っていればAの呼び出しを許可してしまいます。
おそらくこれは速度的な都合か、キャストを駆使されると確実な追跡が難しいというあたりかと。

また、仕組み上GuardCFは.exeだけではなくリンクする.dllでもすべて有効になっていなければ効果が激減します。
win8.1 update以降であればwindows提供の.dllの大半はGuardCFがかかった状態で提供されていますが、サードパーティー製の.dllなどを使う場合、GuardCFが有効かチェックしてから使うとよいでしょう。

実際に作成してみたい場合、2015/04/01現在preview版であるVisualStudio2015の14.0.22609.0 D14REL以降を使う必要があります。
C/C++のコンパイルオプションに「/d2guard4」、Linkerのオプションに「/guard:cf」を追加してコンパイルすればGuardCFが有効な.exe/.dllを作成できます。
詳しくはFFRI様の記事を参照のこと。

次は具体的にどのようにして処理されているのかを説明しましょう。

具体的にはどういう仕組みなのか

まずコンパイル時に動的呼び出しされうるアドレスの一覧を作成する必要があります。
正式に資料に載っていたわけではなく動作から推測した内容ですが、仮想関数/dllexportした関数/&演算子などでアドレス取得された関数、のどれかに合致した物をすべてホワイトリストとして扱うようです。

次に実行時に呼び出すアドレスが確定するコードの直前に「guard_check_icallというホワイトリストに含まれるかを調べる関数」を呼び出すようコードを追加します。

次はリンクして.exeを生成する際にGuardCFにまつわる情報を付加します。
中身は、guard_check_icallで呼び出すアドレス格納位置、GuardCFのホワイトリスト、GuardCFの動作にまつわるフラグ、の3種になります。
これの詳細ついては後述します。

そして実行時、まず通常通り.exeをロードしますが、その際にguard_check_icallは通常のリロケーション処理に基づき、単にRETNするだけの関数へ向けます。
その後、win8.1 update以降である場合はGuardCF用情報を元に、guard_check_icallが呼び出す先のアドレスをどこに保持しているかを突き止め、それをntdll.guard_check_icall_fptrに変更します。
これにより、旧来のOS側の専用処理をいれることなく従来のロード処理だとチェックがスキップされ、win8.1 update以降でのみGuardCFが有効になるわけです。

次に実際のチェックです。
以下のうちどれかに合致すると実行してもよいと判断するようです。

  • 実行しようとしているアドレスが事前に各種.exeや.dllから集めたホワイトリストの中に含まれている
  • GuardCFがかかっていない.dll/.exe上のアドレスを指している
  • .dllや.exeに属さない動的確保したメモリー上で、なおかつそのメモリーに実行可能属性がついている

そして、実行するべきではないと判断した場合は強制的にクラッシュして、被害を最小化する、という動作になります。

.exe/.dllに付与されるGuardCF情報詳細

IMAGE_OPTIONAL_HEADER32内のDataDirectoryのIMAGE_DIRECTORY_ENTRY_LOAD_CONFIG(=10)に格納されています。
これはIMAGE_LOAD_CONFIG_DIRECTORY32構造体がそのまま入っていて、VC++2015以降であればGuard*というメンバーがあるのでそれを使います。
※GuardFlagsは0x58にあるのに14.0.22609.0 D14REL時点ではIMAGE_DATA_DIRECTORY上はsizeが0x40と出力されます、が、IMAGE_DATA_DIRECTORY.sizeが間違っているだけで、IMAGE_LOAD_CONFIG_DIRECTORY32.Sizeは正しい値なのでそちらを参照のこと。

以下各パラメーターの説明。

  • GuardCFCheckFunctionPointer:前述したguard_check_icallで呼び出す先のアドレスが格納されている場所の仮想アドレス、offsetではないのでベースアドレスの変動を考慮する必要がある
  • GuardCFFunctionTable:ホワイトリストの仮想アドレス、4byteのアドレスがGuardCFFunctionCount個分並んでいる、こちらもoffsetではない点に注意
  • GuardCFFunctionCount:ホワイトリストの数
  • GuardFlags:WinNT.h内のIMAGE_GUARD_*のフラグ群をorしたもの、ちゃんと調べたわけではないがIMAGE_GUARD_CF_FUNCTION_TABLE_PRESENTを入れるとGuardCF有効とみなされる模様

Q&A

その他雑多にいろいろ書いておきます。

    • Q結局どうすればいいの
    • A何も考えずに.exe/.dllすべてでGuardCFを有効にすればいいとおもいます
    • Qバイナリしか提供されていない.dllにGuardCFかかってなかったんだけど
    • Aどうしようもありません、あきらめましょう
    • Q GuardCFをかけることのデメリットは?
    • Aだいぶ安全側に倒した実装になっているので一般的には処理速度の低下だけだと思われます、また一部の人にとっては外部ツールによる機能拡張が大変になるというデメリットもあります。
    • Q処理速度にどれぐらい影響するの?
    • A自分も知りたいので計測して記事書いてください、見に行きますので。
    • Q IMAGE_OPTIONAL_HEADER32内の~あたりからさっぱりわからん
    • A IMAGE_DOS_HEADER、IMAGE_NT_HEADERS32、IMAGE_DATA_DIRECTORYあたりでググって出てきたページ片っ端から読む方が自分が説明するよりわかりやすいとの判断により説明していません。

終わりに

どうでしたでしょうか?
実はこれエクスプロイトを書きつつ学ぶWindowsセキュリティー機能 ~番外編SEHOP~の続編にするつもりだったんですが、/DYNAMICBASE:NOを指定するとなぜかGuardCFが働くなってしまい、どうしてもexploitがASLR回避に比重が置かれるため断念したという経緯で書かれたものになります。
そのためただ情報を列挙しただけで、読みづらい感じに……。
まぁそれでも人によっては役に立つ資料となるでしょう。

どうか、一人でも多くこういったセキュリティー機能に興味を持ってくれることを祈ります。
そして自分で書かなくてもググれば見つかる世の中になったらいいな。