Delphi 3分ハッキング。
この投稿はDelphi / Appmethod Advent Calendar 2014の3日目の記事です。
♪例のテーマ
はい、それでは本日はDelphiをハックしていきたいと思います。IDEのハックも季節的にとっても旬でおいしいんですが、今日はRTLやVCLをハックしてみましょう。
材料はこちらになります。
Windows PC | 適量 |
Delphi | 適量 |
準備
まず下ごしらえです。usesにWinapi.Windowsユニットを追加します。このユニットスコープであるWinapiはXE以前では実装されてないので記述の必要はありません。
次に関数を定義します。
procedure Patch(OldAddr, NewAddr: Pointer);
宣言部はこのような形でいいでしょう。お好みに応じて手続き名は変更してください。実装内容は少々アセンブラの知識が必要になります。利用する上で必須ではないですが、アセンブラの知識は多少なりとも知っておくとアプリケーションに味がぐっと染み込みやすくなります。
そしてすでに作っておいたものがこちらになります。
procedure Patch(OldAddr, NewAddr: Pointer); type TJmp = packed record OpCode: Byte; Offset: Longint; end; PJmp = ^TJmp; const OP_JMP = $E9; var oldProtect: DWORD; begin VirtualProtect(OldAddr, SizeOf(TJmp), PAGE_EXECUTE_READWRITE, oldProtect); PJmp(OldAddr)^.OpCode := OP_JMP; PJmp(OldAddr)^.Offset := NativeInt(NewAddr) - NativeInt(OldAddr) - SizeOf(TJmp); VirtualProtect(OldAddr, SizeOf(TJmp), oldProtect, oldProtect); FlushInstructionCache(GetCurrentProcess, OldAddr, SizeOf(TJmp)); end;
調理
はい、それではこれを使って実際にハックしていきます。今日は誰でも簡単に使える、Random関数をハックしてみましょう。
Random関数はご存知の通り、0から渡した値-1の間でランダムに値を返してくれる関数ですね。しかしその性質上、アプリケーションを立ち上げる度に違う値が返ってくることになり、テストの際など不便なこともあるかと思います。これを毎回同じ値を返すようにハックしてみましょう。
まず同じ値を返すような偽Random関数を定義します。簡単に、このような形でいいでしょう。
function DummyRandom(const ARange: Integer): Integer; begin Result := 100; end;
次に先ほどのPatch手続きを使い、Random関数を呼ぶとDummyRandom関数が実行されるように差し替えます。処理を記述する場所はinitialization節などが良いですね。
initialization Patch(@Random, @DummyRandom); end.
はい、これでRandom関数を呼び出すと常に100が返ってくるようにハックすることができました。固定された値ではなく、例えば中身をメルセンヌツイスターなどにして、Random関数を良質な乱数ジェネレータに置き換えるといったアレンジもおいしそうです。
皆さんもご家庭で、あるいは職場でお試し下さい。
ちゃんちゃかちゃかちゃかちゃっちゃっちゃー
(真面目な)解説とおまけ
Patch手続きは、実行時に自作の関数とRTL/VCLの関数を入れ替えるような働きをする手続きです。上記の例のように、置き換え先と同じ引数、同じ戻り値を持った関数を定義すれば簡単に置き換え可能です。アセンブラが分かる方には説明するまでもないと思いますが、JMP命令で自作関数に飛ばしてるだけですね。いわゆる自己書き換えコードです。
心臓部はただのx86アセンブラなのでWindowsだけでなくOSXでも利用可能ですが、メモリ保護の設定でWinAPIを使っているので、そこはmprotect関数などに変更する必要があります。
さすがにこれだけではつまらないので、もう少しまともなサンプルも用意してみました。
JVNVU#97910946
皆さん記憶に新しいと思いますが、今年はDelphiに脆弱性が見つかった、なんてことがありましたよね。この問題、XE5以降には修正プログラムがあり、それ以前のバージョンでも自前でソースを修正すれば対応可能ということですが、ソースコードの付属していないStarterは対処不可能です…不可能なんですよね。
ということで、上記コードの応用編として、ソースコードが無くても脆弱性を修正可能なユニットとツールを作ってみました。
使い方としては、まず修正用ユニットであるjvnvu97910946fixを適当にusesします。基本的にはどこでもいいですが、dprの一番最初に書けば間違いないと思います。
次にプロジェクトオプションのビルドイベントを設定するんですが、この中の「ビルド後イベント」に次のように記述します。
"[exeを置いてあるパス]\patchwork.exe" "$(OUTPUTPATH)"
[exeを置いてあるパス]についてはパスの通ったところにexeを置いておけば不要です。
そして一番重要な設定として、mapファイルが生成されるようにします。設定は「Delphiコンパイラ→リンク→マップファイル」にありますが、これをパブリック以上にしてください。オフやセグメントでは動作しません。
以上の設定が終われば、あとは普通にビルドするだけです。これで脆弱性対策済みの修正ユニット内の関数がVCLの脆弱性のある関数に代わって実行されるようになります。
ツールの動作原理としては、単にmapファイルから関数アドレスを取得してPatch手続き用の変数に代入しているだけです。実行時にパッチ先の各関数アドレスが取れればこのようなツールは不要なんですが、宣言が無くimplementation下だけにしかない関数のアドレスはこのような形でしか取れません。
正直な話、静的にパッチをするだけなら実行時に書き換えずとも、ビルド時に全て書き換えてしまうことも可能なんですが、それをやると応用でも何でも無くなってしまうのでこういう形になりました(´◔◡◔`)
ちなみにこのユニットとツールですが、Starter以外の環境でもちょっとだけ利点はあって、TBitmapを内部で利用してるソースコードを配布したり複数の環境でコンパイルされることが想定される場合、その環境が脆弱性の対策が行われているかどうかは分からないですよね。ですがこれらを使用することで、環境に依存せず穴を確実に塞ぐことができます。
その場合ソースと一緒にツールも配布して、ビルド後イベントに、
$(PROJECTDIR)patchwork.exe
というように書いておけば、どの環境でもパッチが当たるようになります。
以上を3分でやりましょう(無茶ぶり)