エクスプロイトを書きつつ学ぶWindowsセキュリティー機能 ~CFG export suppression~

今回はControl Flow Guard(解説記事)の派生技術をご紹介。

忙しい人向けCFG export suppressionまとめ:

  • dllのexport関数のアドレスを正規の手段以外で得てcallするとクラッシュする機能
  • 正規の手段は以下の通り
    • 静的リンク(Import Address Tableなど)経由
    • GetProcAddress関数
    • dllにexport以外で関数ポインタなどで受け渡す機能が備わっていた
  • 基本的な機構はControl Flow Guardと一緒
    • 具体的にはコンパイラが動的なcallやjmpの直前にチェック関数を挿入している
  • 有効化するにはexeとdllのすべてで/guard:cfを指定し、exeのリンカに /guard:exportsuppress を指定する

前置き

Windowsの32bitアプリケーションはアドレスランダマイズが不足しており危険だという話はPCセキュリティ界隈では比較的よく聞く話です。

その中でも特に脆弱なのがdllのアドレスランダマイズで、過去の記事でも触れたとおり比較的軽微なメモリ漏洩でも関数のアドレスが推測可能になるのが現状です。

そういった現状を踏まえ任意コード実行の脆弱性を少しでも難しくするために生み出された技術、それが今回紹介するCFG export suppression(以降CFG ES)です。

以降はサンプルコード前提で進みます。

cfg_es.zip

注意事項

本機能を有効にする/guard:exportsuppressは2019/09/17現在、MSDN上では非公開なオプションであり正式に提供されている機能ではない可能性があります。

今後正式に提供されるとしても挙動など変更になっている可能性を念頭においてご利用ください。

攻撃:情報漏洩と任意コード実行

話をシンプルにするためにexport関数のアドレスを漏洩する関数を持つdllを用意します。

// ただのexport関数
extern "C" __declspec(dllexport) void proc() {
   std::cout << "Arbitrary code execution\n";
}

// procのアドレスを漏洩する脆弱な関数
extern "C" __declspec(dllexport) __declspec(guard(ignore)) unsigned int leak() {
   return reinterpret_cast<unsigned int>(proc);
}

leak関数はprocの関数を漏洩しています。

関数ポインタの取得ではない旨を示すために__declspec(guard(ignore))するのを忘れずに。
※関数ポインタの取得とみなされるとCFG ESの対象外となるため

攻撃対象のproc関数はただ標準出力にメッセージを出すだけの関数です。

攻撃側は既に脆弱性を突かれている前提でleak関数の返り値を実行するのを直に実装します。

int main(const int argc, const char * const * const argv) {
   const HMODULE dll = ::LoadLibraryA("dll.dll");
   unsigned int addr = reinterpret_cast<unsigned int (*)()>(::GetProcAddress(dll, "leak"))();
   reinterpret_cast<void (*)()>(addr)();
   std::cin.get();
   return 0;
}

実際に上記のソースコードと実行ファイルがフォルダ1に入れてあるので実行してみましょう、特に問題もなくproc関数が実行できてしまいます。

ではCFG ESで対抗しましょう。

防御:CFG ES

/guard:cf が有効でないなら有効にします。
※本アプリでは最初から有効になっています。

次にexe側のリンカオプションで /guard:exportsuppressを設定するだけです。

今回も設定済みの物をフォルダ2に入れてあるので実行すると、今回はクラッシュしました。

「proc関数はGetProcAddressされていないのにアドレスを知っているのはおかしい、攻撃だな!」

との判断により強制終了された結果です。

このチェックによりエラーとされない方法は以下の通りです。

  • GetProcAddressによりアドレスを取得する
  • 静的リンクして関数を呼び出す
  • dll内部で関数ポインタとして取得されたものを受け取る
  • /guard:cfを解除する(セキュリティが低下します)

一般的な使い方をしていれば回避する方が難しいのがよくわかるかと思います。

上記以外で不正に取得したアドレスでcallやjmpすることが難しくなるというわけですね。

このチェックは/guard:cfの機能により行われるために/guard:cfも有効でないといけません。

まとめ

  • CFG ESは/guard:cfと/guard:exportsuppressをつければ有効になる
  • 有効になると不正に入手したexport関数アドレスは実行できない

実用性

GetProcAddressを呼び出してしまえば許可されてしまうため、100%の防御というよりは攻撃者の手札を減らす類のもの、場合により気休め程度の効果にしかならないことも。

リンクするdllすべてでControl Flow Guardが有効でなければ効果が薄いのと、一定より古いOSではそもそも無視される問題があり頼りづらい。

Buffer Overflowなど任意コード実行を目指す攻撃は動的アドレスへのcallやjmpの利用を目指すことがあるため、そういった攻撃を防げるのは水際防御としては有効そう。

とはいえreturnアドレスなど他にも利用できるものは多いため、やはり単独で使用できるものではなくSafeSEHBuffer Security Checkなどの技術との併用は必須。

構造上Control Flow Guardとの併用が必須であり、Control Flow GuardはJITエンジンなど食い合わせが悪い機能が多く採用が難しいという問題がある、採用する際は十分テストを行い問題ないことを確認したい。

メモリや計算量にオーバーヘッドが存在するため速度がダイレクトに響く分野での採用も難しい。

総じてメリットは小さくデメリットは大きい傾向がある技術、採用はほかの技術を優先したい。

終わりに

Control Flow Guardの派生技術ということで取り上げましたがいかがだったでしょうか。

CFG ESのかかったバイナリは手元ではEdgeしか発見できていないレア物だったりもします。

無条件で使えるものではないのは確かですが、ここまで誰も使わないほど有用性が低い技術でもないと思うので、少しでも採用アプリが増えてくれたらうれしいんですがどうでしょうね?

ではまた。