この記事はrogy Advent Calendar 2015 の14日目の記事となります。
はじめに
さて今回書く内容についてですが、最近ロ技研ではLinux が流行っているらしいので、あえて
Windows
に関するちょっとテクニカルなお話をしたいと思います。
Windowsといえばおそらく大多数の人が初めて触れたOSであると思いますが、技術的な視点から触れる機会は少ないのではないでしょうか。今回はそんなWindowsに普段とは違った一面を感じていただければと思います。
DLLインジェクションとAPIフック
今回紹介する内容は一言でいうと上の見出しのようになるのですが、「なんのこっちゃ」と感じる人がほとんどだと思うので1つずつ説明することにします。
DLLインジェクション
みなさんがこのブログを見る際に使っているブラウザやメールソフトなど、様々なアプリは「プロセス」という単位でPC上を動いています。こういったプロセスに、自分の書いたソースコードをDLL (ダイナミックリンクライブラリ) という形にして注入 (インジェクション) する技術のことをDLLインジェクションと呼びます。こうすることで対象のプロセス上で、自作した任意のコードが動かせることになり、事実上アプリを乗っ取ることができます。コンピューターウイルスなどがよく用いる手法だったりします。
APIフック
プログラム上でwindows特有の機能を使う場合には、必ずWindowsAPI と呼ばれる関数群を介することになりますが、このAPIの処理をこっそり独自の処理にすり替えてしまう技術がAPIフックと呼ばれるものです。これによって、例えばアプリがファイルを開くためにOpenFileといったWindows APIの関数を呼び出そうとしても、APIフックにより自前の処理に誘導してしまうことが可能になるわけです。
要するに、これらはいわゆる「ハッキング」に使われるような仕組みですね。次はこれらの詳細な仕組みや利用方法を実際のプログラムを通して見てみることにします。
実際に作ってみる
今回対象とするプログラムは、簡単のために次のように「進捗どうですか」をメッセージボックスで3回表示させるようなもの (TestEXE.exe)です。
このプログラムにDLLインジェクションとAPIフックを行い、「進捗どうですか」のメッセージを改変させてみようと思います。
必要なファイルは次の2つです。
- DLLを対象プロセスに注入するための「治具」の役割を果たすEXEファイル (InjectEXE.exe)
- DLL本体 (InjectDll.dll)
以下では2つのプログラムをソースコードを交えて解説していきます。
InjectEXE.exe
WindowsAPIにはLoadLibraryという、引数としてDLLのファイルパスを指定するとDLLを読み込んでくれる関数があります。対象プロセスがこの関数を実行してくれればDLLインジェクションが成功して万事解決というわけです。したがって、この実行ファイルの目的は
対象プロセスに注入したいDLLのファイルパスを書き込んでLoadLibraryを実行させる
ということになります。手順としては以下のような感じです。
- 実行されているプロセスから対象プロセスを探し出し、プロセスIDを取得する。
- 取得したIDを用いて対象プロセスを開き、DLLのパスを書き込むための領域を確保する。
- 確保した領域にDLLのパスを書き込む。
- DLLのパスを引数として対象プロセスにLoadLibraryを実行させる。
...以下淡々とコードの説明となるので、飛ばしたい方はこちらをクリックしてください
最初の手順に対応するコードの一部は次のようになります。
CreateToolhelp32SnapshotというAPIで現在動いているプロセスの情報を集め、Process32FirstとProcess32NextというAPIでプロセスを1つずつ調べていき、ターゲットのプロセスを見つけたらそのプロセスIDを記録するという処理です。
得られたプロセスIDから、OpenProcessを用いることで対象プロセスのハンドルを手に入れることができます。
続いてDLLのパスを書き込むための領域を確保する必要があるのですが、これに関してはVirtualAllocExを用いれば可能です。確保した領域にはWriteProcessMemoryを使ってDLLのパスを書き込みます。
最後に、対象プロセスにLoadLibraryを実行させればよいのですが、LoadLibraryの関数ポインタを取得するためにGetProcAddressというAPIを用いる必要があります。LoadLibraryはuser32.dllなるモジュールで定義されているので、GetProcAddressにuser32.dllとLoadLibraryの文字列を指定してあげれば良いです。
(本当はLoadLibraryの正体はLoadLibraryWのマクロなので (Unicode環境の場合)、文字列としてLoadLibraryWを指定する必要があります。)
ようやく対象プロセスにLoadLibraryを実行させる段階まで来ましたが、その際に使うAPIがCreateRemoteThreadです。これを用いることで、指定したプロセス上で指定した関数を実行することが可能になります。この引数に先ほど書き込んだDLLのファイルパスを指定してあげれば、めでたくDLLが対象プロセスに注入され、DLLインジェクションが成功します。
InjectDLL.dll
ようやくプロセス内にDLLが入り込めたので、続いての作業はDLL自身が担うこととなります。このDLLが行う仕事はAPIフックを行ってメッセージボックスを呼び出す関数 (MessageBoxW)の処理をすり替えることです。APIの関数のアドレスは、対象プログラムのexeファイルに存在するインポートセクションという領域で管理されているので、これを書き換えてしまえばAPIフックが実現できます。
ここでの手順は次のようになります。
- MessageBoxWを置き換える処理Hook_MessageBoxW関数を作成する。
- 対象プログラムのインポートセクションにアクセスし、MessageBoxWに対応する関数のアドレスを探す。
- アドレスをHook_MessageBoxWのアドレスに書き換える。
...この後延々とコードの説明が続くので、飛ばしたい方はこちらをクリックしてください。
まず、置き換え先の関数Hook_MessageBoxWの定義は、本来のMessageBoxWの定義に似せて、次のようにします。
ざっくりと説明すると、Hook_MessageBoxWでは、メッセージボックスの本文だけを「ぽぽぽぽぽぽぽぽ」に変えたMessageBoxWを実行しています。その際、return文の箇所を
のようにMessageBoxWをそのまま書いてしまうと、MessageBoxWをHook_MessageBoxWに置き換える処理のために無限ループが生じてしまうため、GetProcAddressを用いて、置き換える前のMessageBoxWを使用しています。
さて、DLLがプロセスに読み込まれた際、DLLにおけるmain関数に相当するDllmain関数が実行され、CreateThread関数によりapiHook関数が実行されるのですが、この関数の中でAPIフックに関する一連の処理が行われています。
apiHook関数では、まず対象プログラムのインポートセクションにアクセスすることから始めるのですが、その際にImageDirectoryEntryToDataというAPIを用いると便利です。ここではインポートされているモジュール名に、MessageBoxWを取り扱うモジュールであるuser32.dllが含まれているかを探しています。
モジュール名が見つかった後は、それぞれAPIの名前とアドレスを扱うインポートネームテーブルとインポートアドレステーブルを探し、MessageBoxWという文字列がインポートネームテーブルに存在するかを調べます。もし見つかれば対応するインポートアドレステーブルをHook_MessageBoxWのアドレスに書き換えてしまえばよいのですが、この領域は読み取り専用となっており、書き換えが不可能なので、VirtualProtectを用いて領域のアクセス権をいじってやればよいです。
これにて、MessageBoxWの処理がすべてHook_MessageBoxWに置き換わることとなったので、予想通り行けばすべてのメッセージボックスの本文が「ぽぽぽぽぽぽぽぽ」に書き換えられます。
ソースコードについて
全体のソースコードは以下に配置しています。https://github.com/j-i-k-o/rogyAdC/
本当はVisual Studioプロジェクトごと公開したほうが良いのですが、互換性とかで面倒くさそうなので後で考えます。
実演
1つ目のダイアログが「進捗どうですか」と表示されている間にInjectEXE.exeを実行すると、InjectDLL.dllが対象プロセスに注入され、APIフックによりMessageBoxWの処理がHook_MessageBoxWに置き換わります。その結果、2つ目以降のダイアログは次のようになります。
やったね。ちゃんとテキストが置き換わったね。
最後に
さて、長々とした説明となってしまいましたが、いかがでしたでしょうか。
今回はメッセージボックスの本文を変えるという単純なサンプルであったわけですが、注入するコード次第で様々なことができるのはとても面白いのではないでしょうか。例えば、いわゆる「ウイルス対策ソフト」がPCを監視するための方法にもこういった手法が使われていたりするそうです。
いずれにせよ、この記事を読んでくれた方が多少なりとも興味をもってもらえればと思います。
最後までお読みくださりありがとうございます!
Ordinalでインポートされた関数が存在する場合には
「if (IMAGE_SNAP_BY_ORDINAL(pINT->u1.Ordinal))continue;」の部分でcontinue文によりループの先頭に戻されるので、
「pIAT++;pINT++;」が実行されず無限ループします。