Programming対策は今もまだ決着していない前置きバッファオーバーフローなどの脆弱性があってもそれだけでは任意コード実行には繋がりません。メモリを書き換えるだけではなく、制御を奪い処理を変えてやる必要があります。Programming(以降ROP)があります。Protectionの機能です。今回はROPとは何かを実物を交えて解説し、そこからCallerCheckやSimExecの対策の内容、およびROPがどのようにしてそれらを攻略していったかを解説していきます。※本記事はx86アセンブリの基礎知識を必要とします。)攻撃対象アプリとして以下を用意しました(ASLR/BufferSecurityCheck無効、DEP有効)サンプルの1フォルダにもNoGuard/target.cppおよびtarget.exeとして入っています。0;}ハイライトしている箇所を見るとバッファオーバーフローが発生しています。argv/size(再利用)sizeを72にしてreturnアドレスの部分を上書きしてやれば任意のアドレスを実行できそうです。任意のアドレスが実行できたとして、どうすれば任意の処理を行わせることが出来るのでしょうか?そこで登場するのがROPです。ROPとは要するに既存のコードを利用してスタック上でプログラミングする技術のことです。ROPの大きな特徴としてDEPが有効であっても問題なく機能するというものがあります。これは実行されるのはあくまでスタックが指している先でありスタック上は読み取りアクセスしかされないことに起因しています。自由度も高く、目的となる処理を含むイメージ(今回はntdll.dll)の存在するアドレスさえわかれば大抵の処理は実装出来ます。実際に試してみようNoGuard.exeを実行しdata.binが出力されたのを確認したあとtarget.exeを実行してみてください。電卓が起動したかと思います。ret;のアドレスWinExecのアドレスExitProcessのアドレスfilenameのアドレスSW_SHOW(0x05)0x00000000このコードは要するに以下を実現するためのコードです。SW_SHOW);::ExitProcess(0);NoGuard.exeの動作に必要な情報は以下の通りです。0x77080000ntdll.dllの中身では順に解説していきましょう。ret;と命令が並んでいるコード領域のアドレスのことです。DEPを回避する必要があるので.exeか.dllのコード領域内から見つけなければいけませんが、条件はそれだけなので関数の途中は当然のこと、場合により4byteの数値定数が偶然命令と同一になっている場合でも利用できます。ret;に移動するとなります。このように何か+ret命令のセットを次々実行していき任意の処理を実行するわけです。ちなみにntdll.dllのベースアドレスなどが必要なのは、これらのコードの探索をntdll.dllの中から行っているためです。逆に言えば他の領域から見つけることが出来るのであればそちらでも何も問題はありません。以降を疑似コードで表現すると以下の通り。*/);ExitProcess(0);※直にeaxにfilenameを代入するコードはWindows7のntdll.dllからは見つからなかったため、一度ecxを経由するなど手間をかけています。ProgrammingまとめROPはバッファオーバーフローなどから任意コード実行につなげる技術スタック上だけでプログラミングできる何か+retの命令を見つけてきて継ぎ接ぎする形で実現DEPは回避できるこのようにして任意コード実行を行うのがROP、次はこれにCallerCheckで対策を行います。CallerCheck一般的なアプリであれば関数は基本的にcall命令で呼びます。戻り先アドレスの1つ前を見ればよっぽどのことが無い限りcall命令が見つかります。逆にROPはretで移っていくためそのような性質を持ちません。CallerCheckはその点に着目した技術で、戻り先アドレスの1つ前の命令が正しくcallであることをチェックし、そうでないならクラッシュします。Protectionから有効にする必要があります。Protectionと入力>プログラム設定>プログラムを追加してカスタマイズ>ファイルパスここで先ほどのtarget.exeを指定した上でCallerCheckを有効にしてから起動してみましょう。無事電卓起動が阻止され強制終了するはずです。なったのかCallerCheckは戻り先アドレスの一個前の命令が正しいcallでない場合はそこで強制終了する技術と書きました。現実にはこのチェックをいつどこでやるかが問題となりますが、CallerCheckではROPで攻撃されやすい関数一覧を保持しており、それらの関数の頭部分でチェックするようにしています。今回はWinExecの呼び出しでチェックがかかり、call命令ではなかったため実行前にクラッシュしたわけです。MapViewOfFileFromAppCallerCheckまとめCallerCheckは戻り先アドレスの直前の命令が正しいcallであるかを調べる対象関数は決まっている今回はWinExecが引っかかっている次はCallerCheck回避を試みていきます。気づいている人も多いでしょうがCallerCheckはROPでcall命令を呼んでやれば回避できます。ret;が見つかればいいわけで、実際に見つかります。いうわけでROPを書き換えるとこうなります。ret;のアドレスfilenameのアドレスSW_SHOW(0x05)ExitProcessのアドレス0x00000000実際にやってみようBypassCallerCheck.exeで生成したdata.binをtarget.exeを実行してみましょう、無事(?)電卓が起動したはずです。ROPでCallerCheck回避まとめ実際にcall命令を使えばCallerCheckは回避できるROPでcall命令を使うことは難しくはない仕組みがバレれば回避も余裕な類に思えますね。CallerCheckは他にもjmpでの呼び出しなども弾くのですが、これは一般的なプログラミングテクニックでも使用されるため問題なのではないかとの意見もあるようです。次はSimExecによる更なる対策を行います。SimExecCallerCheckを回避するためにcall命令を呼びましたが、そのcall命令の呼び出し元は偽装できるでしょうか?またさらにその前は?すぐ上で書いたようにcall一回呼ぶためにもretを伴うレジスタ操作を何度か行っており、それら全てをcall命令ごしに行うことはとても現実的ではありません。そこで20命令分(※EMETは15命令)までret後を先読みし、その範囲内で見つけたret全てでCallerCheckを行おうという考えが生まれました、それがSimExecです。Protectionから有効にする必要があります。target.exeを対象にSimExecを有効にして起動してみましょう。無事電卓の起動に失敗したはずです。なったのかCallerCheck対策は1度callを挟んでいるとは言え20命令の中でExitProcessの先頭へのretを含みます。そこでCallerCheckと同様のチェックが行われ、call命令が見つからなかったためROPだと見破ることができています。ret;を使用したROPで回避するのは難しい次はSimExec回避を試みていきます。実際にcall命令で呼んだかを見ているのではなくret後に到達する20命令を見ているだけです。ret;のアドレスがあるのでこのret部分に連続で20回retしてやれば回避できます。いうわけでROPを書き換えるとこうなります。ret;のret部分へのアドレスExitProcessのアドレス0x00000000※WinExecの呼び出しがcallのままなので厳密には20回retしてやる必要はありません。実際にやってみようBypassSimExec.exeで同じようにdata.binを生成しtarget.exeを実行してみましょう、今回も無事(?)に電卓が起動したはずです。ROPでSimExec回避まとめcall命令実行後のふりしたret命令20回で回避できてしまう回避は別に難しくはないこちらも仕組みがバレれば回避は余裕な類に思えますね。一応バッファーオーバーフローの長さに制限があるパターンでは対策になりえますが、長さに負荷をかけたいならもっと別のアプローチの方が賢そうです。ROP対策の是非CallerCheckやSimExecはROP対策として導入されたものではありますが、現状ROP対策として有効に機能しているとはとても言えない状態です。Protection)ではROPに対して必要十分な対策を行うことはできません。有効なものはASLRによりアドレスを出来る限り秘匿すること。次に有効なのがBufferSecurityCheckやSafeSEHなどの技術でそもそもROPを始めさせないことです。自分に見えている範囲の話になりますが、日々新たなROP対策技術が開発されているとはいえ、今回のように詳細が分かれば対策は容易なものがほとんどであり、ROP対策はいたちごっこに近い状況だと感じています。ロードされる全てのバイナリで有効にしなければ機能しないものも多く、OSやコンパイラの協力が壁となるのもその状況を加速させていると言えるでしょう。現状ではですが、ROP対策にはROP狙い撃ち対策ではなく総合的なセキュリティーで対策するほうが賢いのかもしれません。実はCallerCheck/SimExecには大きなデメリットが存在しています。1903でのVirtualProtectを10,000,000回実行した計測結果を載せておきます。140.72%次にWIndows7+EMETで同様の計測をした結果。610.23%Win7(というよりはおそらくはEMET)側は論外とも呼べる速度差、Win10側もボトルネックでの採用はためらわれる程度の速度差が出ています。対象関数の数は多く、それらを内部的に使うAPIにも波及することを考えると範囲はかなり広くなると予想され、影響はそれなりに大きいと予想されます。ProtectionではGetProcAddressも対象に入っているので気を付けてください。※EMETではGetProcAddressは対象外。このデメリットがある機能が前述程度の対策で回避されると考えると、私は費用対効果が割に合わないと感じました。終わりに今回はどちらかというとROP回でした。CallerCheckとSimExecはMSからも動的有効化APIが何年も作りかけのまま放置されていたりと不遇な扱いで、おそらくは内部的にも似た評価なのだと思われます。実際に有用かや有効にするべきかの判断は読者の皆様方にゆだねます。