Programming Field - プログラミング Tips

かなり強引な方法でDLLのインポートを横取りする(未参照を防ぐ)

(いきなりですが)例えば、Visual C++.NET 2005 (Visual Studio.NET 2005) 以降では、C/C++のランタイムライブラリ内でWindows 98以降の関数である「IsDebuggerPresent」関数を呼び出すようになったので、これでビルドされたアプリケーションは必然的にWindows 95で動作しなくなります。しかし、この関数自体は「デバッガの下でアプリケーションが実行されているか」を返すだけの関数なので、特に判定する必要が無ければFALSEを返すようにしておいても何の問題もありません。

そこで、この関数がある「kernel32.dll」へ処理を行かせずに、以下の方法で自前で「IsDebuggerPresent」の関数処理をしてしまいます。

1つ目のやり方はC/C++のみで書いたコードで、メモリを書き換えて関数処理を横取りしています。

// 装飾された名前のアドレスを作るための仮定義
// (これだけでインポートを横取りしている)
// ※ 関数内部の処理は何でも構いませんが、書き換えても問題ないように
//  ある程度の内容を含んでいる必要があります。
EXTERN_C int WINAPI _imp__IsDebuggerPresent()
    { return PtrToInt((void*) &_imp__IsDebuggerPresent); }
// 実際に横取り処理を行う関数
EXTERN_C BOOL WINAPI Cover_IsDebuggerPresent()
    { return FALSE; }
// 関数が実際に呼び出されたときに備えて
// 横取り処理関数を呼び出させるための下準備
EXTERN_C void __stdcall DoCover_IsDebuggerPresent()
{
    DWORD dw;
    DWORD_PTR FAR* lpdw;
    // 横取り関数を設定するアドレスを取得
    lpdw = (DWORD_PTR FAR*) &_imp__IsDebuggerPresent;
    // このアドレスを書き込めるように設定
    // (同じプログラム内なので障害なく行える)
    // ※ ページサイズ単位で保護フラグを書き換えるため、
    //  この関数が実行不可能にならないように「EXECUTE」を入れる
    VirtualProtect(lpdw, sizeof(DWORD_PTR), PAGE_EXECUTE_READWRITE, &dw);
    // 横取り関数を設定
    *lpdw = (DWORD_PTR)(FARPROC) Cover_IsDebuggerPresent;
    // 読み書きの状態を元に戻す
    VirtualProtect(lpdw, sizeof(DWORD_PTR), dw, NULL);
}
// アプリケーションが初期化される前に下準備を呼び出す
// ※ かなり早くに初期化したいときは、このコードを
//  ファイルの末尾に書いて「#pragma init_seg(lib)」を、
//  この変数宣言の手前に書きます。
//  初期化を急ぐ必要が無い場合は WinMain 内から
//  DoCover_IsDebuggerPresent を呼び出して構いません。
EXTERN_C int s_DoCover_IsDebuggerPresent
    = (int) (DoCover_IsDebuggerPresent(), 0);

_imp__」が重要で、これがついた関数はインポートライブラリに定義される、DLLの呼び出しを行う前のワンクッションとなる、関数のアドレスを所有した変数(メモリアドレス)のポインタです。呼び出し規約だけはそろえられて名前が装飾されるので、「__stdcall」により変数の名前が「__imp__IsDebuggerPresent@0」となります。

2つ目のやり方は、アセンブリコード(x86 系統)を使った、もっとすっきりした横取り方です。アセンブラが使える人はこちらを利用するとコードも小さくなりお得です。(※ Visual Studio.NET 2005 や Visual C++.NET 2005 (Express 含む) で使用できるアセンブラツールがMicrosoftのページで配布されています。→「Microsoft Macro Assembler 8.0 (MASM) パッケージ (x86 用)」)

.386
.model flat, stdcall
.data
    __imp__IsDebuggerPresent@0 dd IsDebuggerPresent
    EXTERNDEF __imp__IsDebuggerPresent@0 : DWORD
.code
    IsDebuggerPresent PROC
        xor eax, eax
        ret
    IsDebuggerPresent ENDP
end

DWORD は、64ビットコードではQWORDにする必要があります。が、64ビットコードでIsDebuggerPresentを回避する必要はありません。。

アセンブリコードの場合、C/C++ ではやりたくても出来なかった装飾無しの変数の宣言が出来るため、その変数に関数のアドレスを初期化しておき、実態の関数を適当に宣言するだけ、ということになります。

この手法はどんな関数にも応用が効きますが、注意する点は、上記の C/C++ の例において、Cover_IsDebuggerPresent に当たる関数で、引数と呼び出し規約、戻り値をそろえ、安全な値を返すことです。

なお、このコードを Visual C++.NET 2005 以降で開発しているアプリケーションに置けば、(Windows 98/NT 以降の関数を使わない限り) Windows 95 でも実行できるようになります。(関数が関数なので、動作にも影響はありません。ちなみに VirtualQuery は Windows 95 でも使用できます。このコードは Windows 95 でテストをしました。)

※ この手法は危険が伴う場合があるので注意してください。何か致命的な問題が起きても責任は取れません...
※ 以前は「_imp__XXX」を横取り関数として、引数をそろえて定義していましたが、後に「_imp__XXX」が関数ではなく、関数のアドレスを所有した変数(メモリアドレス)のポインタであった(この言い方は便宜上です)ことが分かったため、現在のコードに変えています。
※ コードの小さくなるアセンブリコード版を使いたくても使い方がわからないという人のために、上記のコードだけをアセンブリしたオブジェクトファイルのパッケージを用意しました。「Imp.zip (1,495 bytes)」をダウンロードしてください。なお、これをインポートしてもkernel32.dllの「IsDebuggerPresent」が参照されている場合は、「プロジェクトの設定」などでライブラリを指定する際、「Imp.lib」の後ろに「kernel32.lib」を指定してみるか、または、「/defaultlib:Imp.lib」を使用してみてください。
※ 上のオブジェクトデータの使用も含めて、正しくインポートを横取りするには、横取りされる側の定義が含まれるライブラリまたはオブジェクトファイルを、横取りする側のものより後に記述してリンクする必要があります。
※ 「Visual Studio 2008」での方法は「Windows 95 で動くプログラムを作る (VS2008編)」をご覧ください。

-- 更新履歴 --
2010/01/04 VirtualProtectにおける保護フラグを「PAGE_EXECUTE_READWRITE」に変更しました。指摘してくださったTrueRoadさんありがとうございます。
2009/10/28 VS2008 に関するリンクを掲載しました。
2007/02/09 インポート時の注意書きを追加しました。
2006/11/29 アセンブリコード版を追加しました。またしてもryojiさんありがとうございます。
2006/10/03 _imp__XXX 関数の呼び出され方がDebug版とRelease版で異なっていたので、それにあわせて修正しました。ryojiさんありがとうございます。

最終更新日: 2010/01/04