Run-TimeErrorChecksとBufferSecurityCheck

Windowsには様々なセキュリティ機能が備わっている、しかしそれらすべてを使いこなしている人は想像以上に少ない。
自分もまた使いこなしているとはとてもじゃないが口にできない。
それでも、理解を深めようと努力することはできる。

というわけで、今回はRun-Time Error ChecksとBuffer Security Checkというセキュリティ機能について解説したいと思います。
一応続き物なので、初めての人は前回の記事からどうぞ。

今回も本記事を読む前にサンプルコード集をDLしてください。
rao_exploit.zip
ちなみにraoはreturn address overwriteの頭文字です。

exploit1:return address overwrite攻撃

前回スレッドスタック(以後スタック)上には様々な情報が配置されると書きましたが、その中にreturnアドレスがあったのを覚えているでしょうか?
returnアドレスとは、関数呼び出し時にスタック上に積まれる呼び出し元のアドレスのことで、関数の実行が終わった後に元の処理を再開するために使われます。
ですが、スタック上にあるということは当然ローカル変数のバッファオーバーフローによる影響を受けます。
そう、スタック上のreturnアドレスを利用した攻撃が存在するのです。

return address overwrite攻撃とは

return address overwrite攻撃とは、SEH overwriteと同様スタック上に配置されたreturnアドレスを任意のアドレスで上書きし、そこを実行させることにより任意のコード実行を行う攻撃を指します。
SEH overwriteとの主な違いは

  • returnするまでクラッシュさせずに処理を継続させる必要がある
  • 例外ハンドラとして実行されるわけではないのでSafeSEHの影響を受けない
  • 同様にSoftwareDEPの影響も受けない

となります。

おそらく説明だけではピンと来ないかと思うので、さっそくためしてみましょう。

実際に試してみよう

今回のソースコードはこちら。

#include <cstdio>

#include <Windows.h>

void call() {
  ::MessageBoxA(NULL, "message", "title", MB_OK);
  ::exit(0);
}

int main(const unsigned int argc, const char * const * argv) {
  FILE * const fp = ::fopen("file", "rb");
  unsigned int size;
  char s[256];
  ::fread(&size, 4, 1, fp);
  ::fread(s, 1, size, fp);
  for (unsigned int i = 0; i < size; i++) {
    ::printf("%02x ", s[i]);
  }
  ::fclose(fp);
  return 0;
}

前回同様、ファイルの先頭4byteを長さとして本体を読み込むもののバッファは256byteしかとっていないのでバッファオーバーフローが発生しうるコードです。
callはどこからも実行されていないのも以前と同じ。
今回はさらにSafeSEHのオプション付きコンパイルされており、可能ならDEPを有効にしてもかまいません。

では実行してみましょう。
exploit1のtest.exeを実行してみれば、callはどこからも呼ばれていないにもかかわらずmessageとメッセージボックスが上がったかと思います。
これが今回問題となるreturn address overwrite攻撃になります。

ちなみにfileの中身はreturnアドレスまでバッファオーバーフローさせてcallのアドレスで上書きしているだけ。
例外を吐かせるわけにはいかないので、fpやsizeを上書きしてしまうなら調整が必要でしたが上書き範囲外だったので無視しています。
※説明はしていませんが以降のexploitではfpやsizeでクラッシュしないよう調整を行っています。

exploit1まとめ

return address overwrite攻撃とはスタック上のreturnアドレスを上書きして任意コードを実行させる攻撃。
SafeSEHやDEPでは防げない。

次項では一部の開発者間ではバッファオーバーフローに有効とされているRun-Time Error Checksを使用して防いでみましょう。

exploit1_RunTimeErrorChecks:バッファオーバーフローの検出

バッファオーバーフローには実は弱点があります。
始点から目標とするメモリーまでの間全てを上書きしなければならないという点です。
それを利用したバッファオーバーフロー検出手段として、Run-Time Error Checksというものがあるのです。

Run-Time Error Checksとは

Run-Time Error Checksとは大きく分けて三つの機能があります。

  • 切り捨てが発生する整数キャストを検出
  • 未初期化の変数の使用を検出
  • スタックオーバーフローを検出

今回は必要ないのでスタックオーバーフローの検出以外は説明しませんが、興味のある方は「/RTC msdn」などでググるといいでしょう。

さてどのようにスタックオーバーフローを検出するのかというと、スタックオーバーフローが発生しそうな変数、今回の場合はsのすぐ後ろに未使用の領域をとり、そこを0xCCCCCCCCで埋めておき、関数を抜ける直前にチェック、0xCCCCCCCCから変化していたらバッファオーバーフローが発生したとしてアプリを強制終了させるというものです。

では今回も試してみましょう。

実際に試してみよう

今回はバッファオーバーフローの検出を行う/RTCsオプションをつけただけでソースコードに変更はありません。
ではさっそくexploit1_RunTimeErrorChecksのtest.exeを実行してみましょう。

どうでしょう、ちゃんと強制終了したでしょうか。
デバッグ情報付きでコンパイルしたうえで逆アセンブルしてみるとmain関数を抜ける直前で_RTC_CheckStackVarsという関数を呼び出しています。
その中では変数sの次のメモリーが0xCCCCCCCCであることをチェックしようとして失敗し、そこでエラーを吐いて強制終了する、という流れです。

exploit1_RunTimeErrorChecksまとめ

Run-Time Error Checksを使えば単純なバッファオーバーフローは検出できる。

ですが、実はRun-Time Error Checksは処理速度を大幅に低下させるなどの副作用があり、デバッグ専用のオプションとされリリースするアプリで使用することは非推奨となっています。
なぜ処理速度が低下するのかというと、変数の数に応じて仕込むデータの量が多くなるため、関数によっては仕込むのと検証に必要な時間が無視できないレベルまで伸びるためです。
また、「単純な」とついていることからわかるようにRun-Time Error Checksは簡単に回避が可能です。
次項ではそのあたりについて触れることにしましょう。

exploit2:Run-Time Error Checksは騙せる

実はRun-Time Error Checksはセキュリティー上の防御機能として設計されたわけではなく、あくまでコーディングミスによるバッファオーバーフローを検出しようとして作られたものです。
そのためセキュリティー機能としては致命的な弱点を持っているのです。

Run-Time Error Checksの弱点とは

Run-Time Error Checksの説明で「上書きチェック用に埋められるデータは0xCCCCCCCCである」と書きました。
これはRun-Time Error Checksを誰がどう使おうと変動することはありません、常に0xCCCCCCCCです。
そしてデータが配置される位置も当然変動することはありません。
つまり、Run-Time Error Checksが動作していると分かっていれば、そこを0xCCCCCCCCで埋めてしまえば容易にチェックを潜り抜けることが可能になるのです。

実際に試してみよう

今回もソースコードに変更はありません、当然/RTCsのオプションもついたままで、読み込むfileの中身を一部0xCCCCCCCCに書き換えただけになります。
ではexploit2のtest.exeを実行してみましょう。

どうでしたか?
おそらくmessageとメッセージボックスが表示されたかと思います。

exploit2まとめ

Run-Time Error Checksはセキュリティ機能ではなくデバッグ支援用のバッファオーバーフロー検出に過ぎない。
適切な個所を0xCCCCCCCCで埋めてしまえばRun-Time Error Checksによる検出回避は容易。

どうでしたでしょうか?
一部ではRun-Time Error Checksは処理速度低下のデメリットが大きいだけでセキュリティ向上するのだから有効にするべきだとする人もいるそうですが
このようにいとも簡単に突破できてしまう以上、処理速度を犠牲にしてまで有効にするべき機能ではないと私は考えます。
もちろんそれはRun-Time Error Checksを上回るセキュリティ機能が存在するからという話でもありますが。

次項では、そのRun-Time Error Checksを上回るセキュリティ機能、Buffer Security Checkについてです。

exploit2_BufferSecurityCheck:return address overwriteにとって最大の天敵

Run-Time Error Checksの弱点について説明した時、0xCCCCCCCC固定ではなく乱数にすればよいのではないか、と思った方もいるのではないでしょうか?
そう、それを実現した機能がBuffer Security Checkになります。

Buffer Security Checkとは

Run-Time Error Checksでは0xCCCCCCCC固定なのと処理速度が低下するのが問題だと書きました。
その両方に対応したのがBuffer Security Checkであり、これ単独で実質上return address overwriteを完璧に防ぐことのできるセキュリティ機能となります。

原理は簡単で、起動時に乱数を生成してグローバル領域に保持、バッファオーバーフローしうる関数の開始時点でreturnアドレスの直後にその乱数列とスタックの先頭アドレスをxorしたものを入力しておく、その後returnする直前でその値が変わっていないことを検証し、検証に失敗すればそこで強制終了するというものです。

Run-Time Error Checksに比べて優れている点は二点。
第一に、乱数計算は事前に済ませておく、データ挿入も検証も関数呼び出し一回につき一回だけ、と出来るだけ軽量な処理で実行できるということ。
第二に、乱数を利用しているため起動するごとに別の値となり、32bitアプリケーションであれば1/4,294,967,296の確率でしか突破できない堅牢さ。
この二点です。

実際に試してみよう

今回もソースコードに変更なし、コンパイルオプションとして新たにBuffer Security Checkを有効にする/GSを追加しています。
ではexploit2_BufferSecurityCheckのtest.exeを実行してみましょう。
どうでしたか?
うまくいっていれば攻撃は見事阻止されアプリは強制終了したかと思います。

デバッグ情報付きでコンパイルしたうえで逆アセンブルしてみると、まず最初に__security_init_cookieで__security_cookieに乱数値を設定、main関数最初で__security_cookieとスタックの先頭アドレスをxorしたものをセット、return直前に__security_check_cookie関数を呼び出して検証しているのが分かります。

総まとめ

バッファオーバーフローでreturnアドレスを上書きすれば任意コード実行が可能。
Run-Time Error Checksを有効にすれば単純なバッファオーバーフローなら検出して防御できる。
だが上書きチェックに埋めておくデータは0xCCCCCCCC固定なので、そうと分かれば検出回避は容易。
Buffer Security Checkなら乱数でチェックされるのでほぼ完全に阻止できる。

というわけで、みなさんBuffer Security Check(/GSオプション)は有効にしましょう。
Run-Time Error Checksはデバッグには便利ですが、セキュリティー的には大した効果がないので、リリースバイナリではかける必要はありません。
これでreturn address overwrite攻撃対策は完璧ですね。

終わりに

前回と今回でバッファオーバーフローを用いた二大攻撃手段であるSEH overwriteとreturn address overwriteの詳細と対策を述べました。
まだまだ攻撃方法はいろいろありますが、この二つが防げるだけでもだいぶ安全性が違うので、みなさんもぜひ導入しましょう。

とはいえ、前回の/SAFESEHと違って/GSはデフォルトでONなので知らず知らずに使っている開発者さんも多いかと思います。
そういう場合はラッキーと思いつつ、このオプション一個で左右されるセキュリティの大きさに思いをはせるなどしてみるのもいいかもしれません。

さて、次回は攻撃方法を特に限定することなく全般的に攻撃を困難にするASLR(アドレス空間レイアウトランダマイズ)の解説をしようかと思います。
ではまた!

SafeSEHとSoftwareDEP

SafeSEHという言葉を聞いたことがあるでしょうか?
DEP、もしくはSoftwareDEPという言葉は?
どちらもWindowsのセキュリティー機能の名称ですが、聞いたことぐらいはあっても具体的になにをどうしてアプリケーション守っているのかまでは知らない人が多いかと思います。

そこで今回は、実際にバッファオーバーフローの脆弱性をつく攻略コードを作成しながら、SafeSEHやSoftwareDEPがどのようにしてそれらの攻撃からアプリケーションを守ってくれるのかを紹介したいと思います。

前置き

まずは何を置いてもサンプルコード集をDLしてください。
seh_exploit.zip

本記事内で紹介する技術は悪用すればウイルスを作成することもできる危険な技術に当たります。
今回はウイルス作成が主目的ではないので実用性ができるだけ低い攻略コードを用いていますが、危険なことに変わりはありません。
悪用ダメ、絶対。

また本記事を読んだり、サンプルを実行することで発生したいかなる被害でも作者は一切責任を負いません。
全て自己責任でお願いします。

本記事は
Nothing but Programming様
および
株式会社フォティーンフォティ技術研究所様の「SEHオーバーライトの防御機能とそのExploit可能性」
を参考にさせていただきました。
より詳しい情報はそちらをどうぞ。

exploit1:SEHオーバーライト攻撃

WindowsにはSEH(Structured Exception Handling)という例外を処理するための機構が備わっています。
簡単に説明すると、例外発生時に呼び出す例外ハンドラのリスト、およびそれを用いた例外処理機能のことを指します。
ここで問題なのは、そのリストがスレッドスタック(以降スタックと呼称)の上、要するにローカル変数やリターンアドレスといった情報を配置するのと同じ場所に配置されていること。
なぜなら、ローカル変数のバッファオーバーフローで容易に書き換えられてしまうためです。

SEHオーバーライト攻撃とは

正式名称をSEH overwrite、その名の通りスタック上に構築されたSEHのリストを上書きし、例外発生時の呼び出し先を任意のコードに変更する攻撃を指します。
手順としては

  • バッファオーバーフローの脆弱性を利用してSEHのリストを上書きする
  • 何かしらの手段で例外を発生させる
  • SEHのリストをたどり例外ハンドラを実行=>書き換えた任意のアドレスのコードを実行

となります。
※ここから任意コードの実行につなげるにはさらに一工夫必要な場合が多いのですが今回は省略。

実際にやってみよう

ここでいったんサンプル内のexploit1を開いて、中のtest.exeを実行してみてください。
黒窓でずらずらと表示された後、「message」とメッセージボックスで表示されて、OKを押すと終了したかと思います。

これだけでは何のことやらさっぱりわからないと思うので、test.exeのソースコードを開いてみましょう。
以下のようになっているはずです。

#include &lt;cstdio&gt;

#include &lt;Windows.h&gt;

void call() {
  ::MessageBoxA(NULL, "message", "title", MB_OK);
  ::exit(0);
}

int main(const unsigned int argc, const char * const * argv) {
  FILE * const fp = ::fopen("file", "rb");
  unsigned int size;
  char s[256];
  ::fread(&amp;size, 4, 1, fp);
  ::fread(s, 1, size, fp);
  for (unsigned int i = 0; i &lt; size; i++) {
    ::printf("%02x ", s[i]);
  }
  ::fclose(fp);
  ::getchar();
  return 0;
}

ちょっと読めばすぐわかるかと思いますが、call関数はコード中のどこからも呼び出されてはいません。
かといってMessageBoxのAPIを呼び出している箇所はほかになく、なぜあのような動作になったのかわからないかと思います。

このアプリは、ファイルを開き、先頭4byteのデータ長を読み込んで、そのデータ長分だけファイルから読み込みそれを16進数で表示するアプリです。
ですが、データ長が256以下であることを前提としているため、256超のデータ長を与えることでバッファオーバーフローが発生してしまう脆弱性を持ったアプリでもあります。

そう、つまりcallが実行されてしまった秘密は読み込んでいるファイルにあるのです。

どうしてcallが実行されてしまったのか

test.exeを逆アセンブルすると、sの先頭アドレスは0x0018FE14、sizeのアドレスは0x0018FF1C、SEHにより最初に実行される関数ポインターが格納されているのが0x0018FF70、callのアドレス0x0042B390がとなります。
※コンパイル環境により異なります、この数値はzip内のexeファイルの物

一応この時点で必要な情報はすべて出ています。
ためしにfileの中をのぞいてみる、test.exeを逆アセンブルしてみるなどして、どのようにcallを呼ばせているのか調べてみるといいでしょう。

結論を言ってしまえば、0x160byte読み込ませて、その最後4byteをcallのアドレスを指すよう90 B3 42 00(リトルエンディアンなため)にして最初に呼び出される例外ハンドラをcallに変更。
0x108byte目から4byteを可能な限り大きな数値にしてforで範囲外アクセスの例外を発生させています。

逆アセンブル環境が整っているなら、ためしにtest.cppの中身を改変したうえで再コンパイルし、fileの中身を書き換えて同じことができるかを試してみるのもいいでしょう。
個人的にはollydbgとstirlingがお勧めです、前者は逆アセンブラを表示しながら実行ができ、後者はバイナリファイルをいじるのに便利です。

exploit1まとめ

SEHはローカル変数のすぐ隣に例外ハンドラを置いている。
バッファオーバーフローを利用すればローカル変数の近くは書き換え放題。
無対策だとバッファオーバーフロー脆弱性一個で任意のコード実行が可能。

言い方は悪いですが、何も防御手段を講じていない場合、バッファオーバーフローの脆弱性一個でこうも簡単に動作をいじることができてしまうのです。
ウイルスがなかなか撲滅されないわけですね。

ちなみに、「無対策だと」や「何も防御手段を講じていない場合」などとついている理由は後述します。
今は、任意コード実行なんて意外と簡単だということだけ覚えておいてください。

exploit1_safeseh:exploit1への対策

さて、前項で任意コード実行なんて簡単だと書きましたが、防御手段がないわけではありません。
もちろんバッファオーバーフローの脆弱性をなくしてしまうのが一番ですが、何百から巨大なもので何百万行にも及ぶプログラムコードから脆弱性0のアプリを作るのは現実的ではないことは大体予想がつくかと思います。
そこで登場する防御手段がSafeSEHです。

SafeSEHとはなにか

SafeSEHとは、SEHで呼び出しうるアドレスを事前に登録しておき、そこ以外が呼び出されそうになったら強制終了させる仕組みです。
動的な登録はできず、あくまで起動時のみの登録となるので、動的コード生成などとは相性が悪いですが、これがあるだけで前述したSEH overwriteは大体防ぐことができます。

実際に試してみよう

サンプルのexploit1_safesehを開いて、中のtest.exeを実行してみてください。
ソースコードは全く同じ、fileもtest.exeを逆アセンブルした情報をもとに攻略コードを仕込んであります。
しかし、今度はメッセージボックスが表示されることはなく、そのまま終了したかと思います。
違いはコンパイル時に/SAFESEHオプションをつけただけ。
SafeSEHの威力がよくわかったかと思います。

exploit1_safesehまとめ

SafeSEHというSEH overwriteを防ぐ防御機能がwindowsには備わっている。
/SAFESEHオプションをつけるだけで単純なSEH overwriteなら強制終了までしか届かない。

さて「大体」とか「単純な」とついていることからわかった人もいるかもしれませんが、SafeSEHとて万能ではありません。
次項ではSafeSEHがかかったexeファイルでSEH overwriteを行ってみましょう。

exploit2:無防備なDLL

前項ではSafeSEHによって事なきを得ましたが、実はSafeSEHはオプション付きでコンパイルされたバイナリとそれに紐づくスタックまでしか保護しません。
つまり、一つでもSafeSEHをかけていないdllなどが混ざるとそこからいくらでも任意コード実行が可能になります。
そこで今回はSafeSEHがかかっていないdllが混ざった場合にどうなるかを見ていきましょう。

アプリの改修

今度は一部機能をdllとして分離することになりました。
ソースコードは以下のようになります。

・dllmain.h

#ifdef DLL_EXPORT_FLAG
#define EXPORT __declspec(dllexport)
#else
#define EXPORT __declspec(dllimport)
#endif /* TEST_EXPORTS */

EXPORT unsigned int xor(const unsigned int a, const unsigned int b);

・dllmain.cpp

#include &lt;cstdio&gt;

#include &lt;Windows.h&gt;

#define DLL_EXPORT_FLAG

#include "dllmain.h"

void call() {
  ::MessageBoxA(NULL, "message", "title", MB_OK);
  ::exit(0);
}

unsigned int xor(const unsigned int a, const unsigned int b) {
  return a ^ b;
}

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved) {
  return TRUE;
}

・test.cpp

#include &lt;cstdio&gt;

#include "dllmain.h"

int main(const unsigned int argc, const char * const * argv) {
  FILE * const fp = ::fopen("file", "rb");
  unsigned int size;
  char s[256];
  ::fread(&amp;size, 4, 1, fp);
  ::fread(s, 1, size, fp);
  const unsigned int key = 0xAA;
  for (unsigned int i = 0; i &lt; size; i++) {
    ::printf("%02x ", xor(s[i], key));
  }
  ::fclose(fp);
  ::getchar();
  return 0;
}

やってることはexploit1の時とほとんど変わっていません。
単に表示する内容をdllのxor関数を通した結果に変更しただけです。

callがdll側に移動していることからもわかるように、今度はdll側のアドレスをsehに組み込みます。

実際に試してみよう

サンプルのexploit2を開き、その中のtest.exeを実行してみましょう。
今回もcallに触れている関数がないにもかかわらず、callの内容が実行されたかと思います。

手順はexploit1とほぼ同じ、ただcallの存在する位置が/SAFESEHオプションのかかっていないdllの中に移動しただけです。
もちろんexe本体は/SAFESEHをかけています。

exploit2まとめ

SafeSEHはすべてにかけて初めて効果を発揮する、ということがよくわかったかと思います。
一応dllはアドレスがぶつかったらロードされるアドレスが変わるなどランダム要素がないわけではないのですが、ASLR(アドレス空間レイアウトランダマイズ)がかかっていない場合はほぼ固定と言い切ってもいいので、かなり簡単です。
次項では、この対策となります。

exploit2_safeseh:exploit2への対策

exploit2への対策は簡単。
要するに全部でSafeSEHのオプションを有効にしておくだけです。
幸いWindowsが提供するDllはすべてSafeSEHがかかっているので、外部ライブラリにだけ気を付けていればよいことになります。

実際に試してみよう

サンプルのexploit2_safesehを開きtest.exeを実行してみましょう。
今度もソースコードはexploit2と変わりませんが、/SAFESEHオプションがDLLでも有効なため、強制終了するまでで被害が抑えられています。

exploit2_safesehまとめ

SafeSEHをかけるなら全部でかけよう。
Windows付属のdllは元からSafeSEHかかっているので安心。

次項ではSafeSEHだけでは防げないSEH overwriteについて、です。

exploit3:SafeSEHでは防げないSEH overwrite

すべてのexeとdllにSafeSEHを施してSEH overwrite対策は万全、そう思ってはいませんか?
SafeSEHだけではすべてのSEH overwrite攻撃は防げません。

SafeSEHで防げないSEH overwriteとは

SafeSEHが禁止する例外ハンドラは、事前に登録されておらず、SafeSEHがかかったバイナリから紐づいている領域+スタック領域なのですが、実はこの定義に入らないメモリー領域が存在します。
そう、動的に確保する「ヒープ領域」(以降ヒープ)です。
C言語ではmallocで取得できるものがヒープに当たります。

ヒープはどのバイナリにも属していない扱いとなるためSafeSEHの対象外となり、ここにコードを置いて例外ハンドラとして登録できればSafeSEHがかかっていても任意コード実行ができてしまうわけです。

実際に試してみよう

今回はわかりやすさのためexploit1のコードにヒープ関連のコードを若干追加した形となります。

#include &lt;cstdio&gt;

#include &lt;Windows.h&gt;

void call() {
  ::MessageBoxA(NULL, "message", "title", MB_OK);
  ::exit(0);
}

int main(const unsigned int argc, const char * const * argv) {
  FILE * const fp = ::fopen("file", "rb");
  unsigned int size1;
  ::fread(&amp;size1, 4, 1, fp);
  char *s1 = reinterpret_cast&lt;char *&gt;(::VirtualAlloc(reinterpret_cast&lt;void *&gt;(0x00250000), size1, MEM_COMMIT, PAGE_READWRITE));
  ::fread(s1, 1, size1, fp);
  unsigned int size2;
  char s2[256];
  ::fread(&amp;size2, 4, 1, fp);
  ::fread(s2, 1, size2, fp);
  for (unsigned int i = 0; i &lt; size2; i++) {
    ::printf("%02x ", s1[i] ^ s2[i]);
  }
  ::fclose(fp);
  ::getchar();
  return 0;
}

動作としては、先頭4byteを読み込みそれをsizeとしてVirtualAllocAPIを使用してヒープを確保、そこに指定byteだけ読み込みます。
その後、再度4byte読み込み、その4byteの分だけローカル変数上に読み込むが、256byteを超えていたらバッファオーバーフローが発生する、といういつもの動作となっています。

ヒープからの確保がVirtualAllocなのは気にしないでください。

それではtest.exeを実行、する前に
DEPを有効にしている場合はDEPの例外設定にtest.exeを設定してください。
理由は後述しますが、DEPの管理下だと攻略コードは通らないようになっているためです。
また、アドレス指定でメモリーを確保している都合上、メモリーの確保に失敗して動作しないことがあります。
見慣れないエラーで落ちたらリトライしてみてください。

どうでしょうか?
ちゃんとSafeSEHされていたのにSEH overwrite攻撃が成功していたかと思います。

exploit3まとめ

SafeSEHをしていてもヒープ領域を使われるとSEH overwriteは防げない。

ではアプリ開発者はSEH overwrite攻撃に屈するしかないのでしょうか?
いいえ、ほぼ完全に防ぎきる方法が存在します。
次項はその方法について。

そしてSoftwareDEPへ

DEPという機能はご存知でしょうか?
WindowsXP SP2から入った機能で、Data Execution Preventionの略となります。

DEPとひとくくりにされていますが、その機能は大きく分けてHardwareDEPとSoftwareDEPの二つに分かれます。
HardwareDEPは強力な機能ですが、今回は関係がないので説明はまた今度にして、本題はSoftwareDEPです。

SoftwareDEPとは

SoftwareDEPとは、例外ハンドラがヒープ上を指していた場合にそこで強制終了させるというもの。
メモリーに実行可能属性をつけていようがSafeSEHをOFFにしていようが完全にシャットアウトするという、ある分野のソフトウェアに対しては正常動作すら阻害するというある意味ぶっ飛んだ機能です。

前述したDEPの例外設定にしてくれというのはこれに関係していて、要するにSoftwareDEPが有効ならばヒープを利用したSEH overwriteは完全に防御できるわけです。

実際に試してみよう

DEPが無効な方は有効にして(※要再起動)、有効な方はDEPの例外設定からtest.exeを外して実行してみましょう。
たとえCPUが未対応でHardwareDEPが無効になっていたとしても、exploit3のtest.exeの攻略コードはきっちり防がれるはずです。

総まとめ

SEH overwriteで攻撃されても、SafeSEHならバイナリに紐づく領域やスタック上で任意のコード実行は防げる。
DllがひとつでもSafeSEH未対応だと危ない。
それだけだとヒープ利用されたら防げない。
が、それもSoftwareDEPが有効なら防げる。

というわけで、みなさんDEPは可能な限り有効にしましょう。
アプリを配布する際も可能な限りSafeSEHをつけましょう。
dll形態で配布する場合はなおさらね。
※作者のは今までSafeSEHはVC++ではデフォルトONだと思っていたが違ったため大体ついてません、ごめんね!

終わりに

本記事ではSEH overwriteだけを扱いましたが、世の中にはもっといろいろな攻撃方法が存在します。
同時に防御方法も、HardwareDEPなどまだまだ説明していない機能がたくさんあります。
この記事を読んで少しでもそういうものに興味を持ってくれた方は、そういうことを考えて少しでもユーザーのセキュリティーにやさしいソフトウェア開発をしてくれたらうれしいです。

また次回があれば、今度はreturn address overwrite攻撃とバッファーセキュリティーチェックについてでも書こうかと思います。
ではまた!

VirtualDLLの仕組み

VirtualDLLというものを知っているだろうか?
自分も由来などは知らないが、asciiの記事によるとWindows7から導入されたDLLのAPI呼び出しを別のDLLへ転送するものとか。
ちょっと気になったので仕組みを色々調べてみた。

※ちなみにMSDNをVirtualDllで検索しても何も出てこない
なのでVirtualDllというのは非公式な造語の可能性がある
が、他にそれらしい名称が見つからなかったので本記事ではVirtualDllと呼称することにする。

VirtualDLLの本体はどこか

まずAPIの転送と聞くと、.defファイルに「#{API名} = #{転送先DLL名}.#{転送先API名}」と記述することで行えるエクスポート転送が思い当たる。
実際Kernel32.dllなどを覗いてみるとapi-ms-win-base-bootconfig-l1-1-0.dll(Windows8の場合)など、見覚えのない数個のDLLへの転送が行われている。
だが、実際に転送先のdllを覗いてみてもさらに自分自身へ転送して無限ループしているだけで実体に行き着かない。
実際にLoadLibraryしてみるとadvapi32.dllがロードされるのだが、api-ms-win-base-bootconfig-l1-1-0.dllの中にはadvapi32.dllなどという文字列は出てこないうえ、暗号化の類を施している形跡もない。
それどころか、中にはファイルとして存在しないdllもあり、通常のエクスポート転送だけではないことがうかがえる。

上記のasciiの記事によればapisetschema.dllを介して転送されるとある。
中をのぞいてみると、確かにms-win-base-bootconfig-l1-1-0(なぜか最初のapi-が削られている)や、advapi32.dllという文字列が見える。
どういうことかは不明だが、これを介して特殊な転送が行われているらしいことは確かなようだ。

※apisetschema.dllをwindows8 64bit上で32bitアプリから開く場合C:\windows\sysnative\apisetschema.dllでないと開けないことに注意
windows7ならばSystem32とSysWOW64の両方に存在するので問題はない

どのように転送しているか

apisetschema.dllを実際に調べてみると.textや.rsrcなどの通常のセクションに混ざって.apisetという見覚えのないセクションが含まれているのがわかる。
結論から言ってしまうとここがまさにVirtualDllの要なのだが、解析した道筋を書いていくと無駄に長いうえ対して得るものもないので
コードだけ示す。

ReadApiSetSchema.zip

簡単にいうと、転送元dll名と転送先dll名のペアがいくつも格納されているだけというシンプルな構造。
格納の仕方にいくつか癖があるが、それは重要な部分ではないので、気になる方は実際のソースコードを読んでみてください。

ちなみに、最初四文字がapi-かext-だと決まっているようで、apisetschema.dll内では省略されている。
別にそんなところ節約したってしょうがないのだから含めてしまえばいいと思うのだが、なにか別の事情があるのだろうか……?

まとめ

先頭がapi-かext-で始まるdllは、まずapisetschema内のテーブルを参照し
転送先が記述されている場合はそちらのdllをロード。
そうでない場合に通常のdllロードが行われる。
それがVirtualDllの仕組みのようだ。

逆にいえば、apisetschemaを書き換えることができれば、一部処理を自作dllに向けることも可能。
全アプリケーションに対してdllインジェクションしようとするなら、割と視野に入るかもしれない。
もっとも、そんなアプリケーションはよっぽどのことがない限りユーザーに嫌われますが。