課題
add.dllは重大なバグを持っています.本当なら2つの整数値の和を返す関数が、実は引き算を計算しています.これを使うmymain.exeは間違った計算結果を延々と表示し続けるプログラムです.私はこのバグを修正したいのですが,mymain.exeはミッションクリティカルなプロセスであるために,安易に再起動できません.果たしてmymain.exeの動作を停止させないで,こっそりとDLLのバグを修正することはできるのでしょうか?
このように,プログラムの動作を停止させることなくメモリの内容を直接書き換えてバグを修正することをオンラインパッチと呼びます.パソコン環境では見たことはありませんが,通信系ソフトウェアのようなミッションクリティカルな現場では,このような修正をこっそり行うことはよくありました.全く実用性はありませんが,DLLインジェクションの技術習得のために,オンラインパッチをWindows環境で実践してみました.
条件
Visual Studio toolsがインストールされている状態で,コマンドプロンプトからコンパイラ(cl)を起動します.
ご注意
おそらくほとんどの方は64ビットOSをお使いのことと思います. Visual Studio toolsコマンドプロンプトには多くのショートカットが含まれますが,その中から"x64 native Tools コマンドプロンプト" を選択してください. x86を選ぶと32ビットの実行ファイルが生成されてしまいます.32ビット実行ファイルは64ビットDLLを呼び出すことができません.このため,64ビット環境で32ビット実行ファイルから"CreateRemoteProcess" APIを呼び出すとランタイムエラーになります.エラーコードが5(アクセスが拒否されました)なので,アクセス権の問題なのかな?と誤解しがちなのですが,実際には32/64ビット不整合によるエラーです.
ソースコード
ここから4つのC言語によるソースコードを取得してください.
git clone https://github.com/h-hata/dllinjection.git
短いプログラムばかりなので,コピペしていただいても構いません.
add.c <=バグのある関数を含むDLL
mymain.c <=add.dllを使って2秒毎に2つの整数の足し算を計算し続けるプログラム
newadd.c <=バグを修正した関数を含むDLL
inj.c <= mymain.exe + add.dllからなるプロセスにnewadd.dllを挿入するプログラム
ビルド方法
0)Visual Studio tools コマンドプロンプトを開く
>cl
と打ち込んで,コンパイラのバージョンが表示されることを確認してください.
1)バグありDLL add.c のビルド
>cl add.c /LD
add.dllが生成されます.
2)重要なプロセスである(とする)mymain.exeをビルドする
>cl mymain.c add.lib
mymain.exeが生成されます.add.libを一緒にリンクするのを忘れないでください.
3)バグ改修されたDLLをビルドする
>cl newadd.c /LD
newadd.dllが生成されます.
4)DLLインジェクションを実行するinj.exeをビルド
>cl inj.c
inj.exeが生成されます.
最終的に
add.dll mymain.exe newadd.dll inj.exeの4つが出来上がります.
実験
(1)mymain.exeの起動
このプログラムは2秒おきに,add.dllを使って15+3を繰り返し計算するプログラムです.しかしadd.dllにはバグが含まれているので,間違った答を延々に表示します.
195:15+3=12
196:15+3=12
197:15+3=12
198:15+3=12
コントロールCで終了しますが,実験では動作している状態でバグを改修します.
(2)mymain.exeのプロセスIDを取得する
コマンドプロンプトからmymain.exeを起動するとそのままずっと動作し続けるので,もう一つ別にコマンドプロンプト窓を開いて,ここからパッチを当てます.tasklistコマンドでパッチが当てられるmymain.exeのプロセスIDを取得します(セキュリティ機能が強化され少なくともWindows11では、2つめのコマンドプロンプト画面は管理者で実行する必要があります).
>tasklist /FI "IMAGENAME eq my*"
IMAGE NAME PID SESSION NAME SESSION# MEMORY
mymain.exe 860 Console 1 1,608 K
これでプロセスIDが860であることがわかります.このプロセスIDは,その時々で異なりますのでtasklistコマンドで取得できた値を次のステップで使ってください.
(3) パッチを当てる
injコマンドは第1パラメータに,パッチの宛先のプロセスIDを,第2パラメータに修正されたDLLのファイル名を指定します.動作中のプロセスに新たにnewadd.dllという新たなDLLを読み込ませる処理をDLLインジェクションといいます.今回の場合は以下のようになります.
>inj 860 newadd.dll
Cross Injection id=860
Wait for Remote Thread
Remote Thread initialization completed
パッチが成功すると上記のようなメッセージは表示され,実行中のmymain.exeはパッチが当てられた瞬間から正しい計算値を表示するようになります.
198:15+3=12
199:15+3=12
200:15+3=18 <===== mymain.exeを再起動することなくバグが修正された
201:15+3=18
202:15+3=18
この実験を行った人は、正常な計算結果が表示された後は2秒間隔だった計算タイミングが1秒間隔に短縮されたことに気づかれたでしょう.一定時間待つために,mymain.cはWindows APIのSleepを使っています.newadd.dllはadd関数に加えてSleep関数も上書きしています.DLLインジェクション後にはもともとのadd関数とSleep関数ではなく,newadd.dllにて書かれている関数がコールされるようになります.newadd.dllのSleepは,渡されたパラメータの値を2で割って,その値をパラメータとして本来のWindows APIのSleepを呼び出します.この方法を使うと,アプリケーションのソースコードがなくてもどのようなWindows APIが,どのようなコンテキストで,どのようなパラメータで呼び出されているのかを調査することができます.偽のWindows APIをたくさん含むDLLをターゲットのアプリケーションに注入して,DLLでは呼び出し記録をとった後本来のAPIを呼べばアプリケーションに気づかれずAPI呼び出し状況を調査できるのです.このような技術のことをAPIフックといいます.
ご注意 その2
DLLインジェクションは他のアプリケーションに任意のコードを実行させることができます.類似の手法としてスタックオーバフローが有名です.スタックオーバフローは,明らかに悪意のあるコードを注入するために使われますが,DLLインジェクションは必ずしも悪意で使われるものとは限りません.しかしキワモノ技術であることは確かです.
またAPIフックでAPIの呼び出し状況を探る作業もリバースエンジニアリングといって,場合によっては違法になる行為になりますのでご注意ください.
技術力が高まれば出来ることも増えてきます.それだけ高いモラルも要求されます.
どうしてこのようなことができるのか?
ソースコードを説明することは省略して,概要だけ説明します.DLLインジェクションは以下の2つの重要な技術から成り立っています.
1.DLLをターゲットプロセスに読み込ませる
2.ターゲットとなる関数の呼び先を,新たに呼び出したDLLに変更する
それぞれの原理は以下のとおりです.
DLLインジェクション(inj.exe)
inj.exeはnewadd.dllをmymain.exeプロセスに注入
DLLを読み込ませる方法には
1.ビルド時に必要なDLLを指定すると起動時にOSがロードしてくれる.アプリケーションは,すでにDLLが読み込まれているものとしていきなり関数を呼び出す.
2.ビルド時はDLLを指定せず,実行時にアプリケーションがDLLを指定して読み込む.
mymain.cをビルドするときにadd.libを一緒にリンクしましたが,これは1.の方法になります.mymain.exeにadd.dllが必要である旨書き込まれており,mymain.exeを起動するときWindowsがmymain.exeとadd.dllを同時にロードします.
一方2.の方法はアプリケーションがDLLのファイル名を指定してLoadLibraryというAPIを呼び出してロードする方法です.DLLインジェクションはこの2.の方法を使います.inj.exeは,まずターゲットとなるプロセス(mymain.exe)にメモリエリアを確保し,そこに新たに読み込ませたいDLL名を書き込みます.そして,そのアドレスをパラメータとして起動アドレスをLoadLibraryとした新たなスレッドを起動させます.他プロセスへのメモリ操作や,他プロセスでの新たなスレッドの生成は,そのためのWindows API(VirtualAllocExやCreateRemoteThread)が用意されています.これらのAPIの使い方はinj.cをご覧ください.
関数の呼び先を変更する(newadd.dllのDllMain関数)
自モジュール(mymain.exe)の中にある関数を呼び出すときと,DLLの関数を呼び出すのでは後者のほうが一手間かかります.DLLは起動時以降にロードされるために,ビルド時にはDLLの関数のアドレスはまだ決まっていません.そこでテーブルを作って,DLLがロードされるとそのテーブルにDLL名とそのDLLに含まれる関数の開始アドレスを登録します.mymain.exeはDLLの関数が呼び出されると,そのテーブルを検索して関数のアドレスを調べてそこにジャンプするのです.関数の呼び先変更は,このテーブルの書き換えで実現します.このテーブルはIATと呼ばれています.この操作は,DLLがロードされた後でなければ出来ませんので,newadd.dllのメイン関数であるDllMainの中で行っています.DllMainではIATでadd.dllを探し,そのなかのadd関数のエントリが書かれている場所を見つけ出します.そして,そのアドレスが書かれている箇所に,新しいadd関数のアドレスを上書きしています.
このように,関数の呼び先変更はIATがあるからこそできることです.DLLではなくmymain.exe本体に存在する関数はIATには含まれませんので,IATを使った呼び先変更はできません.
IATにどのような情報が書かれているのかを調べるためのアプリケーションとして,dumpmain.cというツールが同じレポジトリにあります.これは自プロセスのIATの内容を表示するものです.