別のプロセスにコードを割り込ませる3つの方法完成図 ![]() 目次
はじめにCodeGuruのサイトにはパスワードスパイのチュートリアルがいくつか投稿されていますが、それらはいずれもWindowsフックを利用しています。このようなユーティリティを作成するには、他に方法はないのでしょうか? 実はあります。しかし別の方法を説明する前に、この問題について簡単に復習しておきましょう。 コントロールのコンテンツを読み取るには、それが自作アプリケーション内にある場合でもない場合でも、コントロールに対して ::SendMessage( hPwdEdit, WM_GETTEXT, nMaxChars, psBuffer ); という関数を別のプロセスのアドレス空間内で実行する必要があります。 一般的には、この問題を解決するには次の3つの方法が考えられます。
1. Windowsフックのテクニックデモアプリケーション:HookSpy、HookInjEx Windowsフックの主な役割は、あるスレッドのメッセージトラフィックを監視することです。一般的には、次の種類があります。
フックされる側のスレッドが別のプロセスに属している場合(上記2-1および2-2の場合)は、フックする側のプロシージャはダイナミックリンクライブラリ(DLL)内になければなりません。フックプロシージャがDLL内にあるときは、そのDLLが、フック対象スレッドのアドレス空間へとマッピングされます。このときWindowsは、フックプロシージャだけでなくDLL全体をマッピングします。この機能により、Windowsフックを利用して別のプロセスのアドレス空間にコードを割り込ませることができます。 本稿では、フックについてこれ以上詳しく説明しません(詳しく知りたい方は、MSDNの 1. DLLはいつマッピングされるか あるスレッドのキュー化されていないすべてのメッセージを監視するフック( 2. パフォーマンスの低下を防ぐにはフックをインストールすると、システム全体のパフォーマンスに影響が出ることがあります(特にシステムワイドフックの場合)。しかし、DLLマッピングメカニズムとして主にスレッド固有フックを使用しており、メッセージのトラップを行っていない場合には、この弱点を簡単に克服できます。次のコード例を見てください。
BOOL APIENTRY DllMain( HANDLE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved )
{
if( ul_reason_for_call == DLL_PROCESS_ATTACH )
{
// Increase reference count via LoadLibrary
char lib_name[MAX_PATH];
::GetModuleFileName( hDll, lib_name, MAX_PATH );
::LoadLibrary( lib_name );
// Safely remove hook
::UnhookWindowsHookEx( g_hHook );
}
return TRUE;
}
さて、これで何が起きるでしょうか。 まず、Windowsフックを通じてこのDLLをリモートプロセスにマッピングします。その後、DLLが実際にマッピングされた直後に、 ここで1つ疑問が浮かんできます。終了時にはどうやってDLLをアンロードすればよいのでしょうか?このスレッドは既にフック解除されているので、
こうすると、リモートプロセスに対してDLLをマッピング/マッピング解除する間だけフックを使用することになるので、とりあえずフック対象スレッドのパフォーマンスに影響が出ることはありません。言い換えると、これは次に説明する では、この方法はいつ使用すればよいのでしょうか。 たとえば、DLLを長期間リモートプロセスにマッピングしておく必要があるが(別のプロセスに属するコントロールをサブクラス化するときなど)、ターゲットプロセスの邪魔はできるだけしたくない場合は、この方法を使用します。サンプルのHookSpyアプリケーションではこの方法を使用しませんでした。このアプリケーションではDLLの使用期間が短く、パスワードを取得する間しか使用しないからです。この方法の使用例を示すためには、HookInjExという別のサンプルを用意しました。HookInjExでは、explorer.exeに対してDLLをマッピング/マッピング解除し、Windowsの[スタート]ボタンをサブクラス化しています。具体的には、[スタート]ボタンの左クリックと右クリックを入れ替えます。 本稿の最初で紹介しているダウンロードパッケージに、HookSpyおよびHookInjExの実行可能ファイルとソースファイルが含まれています。 2. CreateRemoteThread+LoadLibraryのテクニックデモアプリケーション:LibSpy 一般的には、 まず、 ’’HINSTANCE LoadLibrary( LPCTSTR’’ lpLibFileName // address of filename of library module ); ’’BOOL FreeLibrary( HMODULE’’ hLibModule // handle to loaded library module ); これらの宣言を、
’’DWORD WINAPI ThreadProc(
LPVOID’’ lpParameter // thread data
);
見てのとおり、どの関数も同じ呼び出し規則を使用しており、いずれも32ビットパラメータを受け取ります。また、戻り値のサイズも同じです。したがって、 しかし、これには2つの問題があります(詳しくは下記の
1番目の問題は、実はあらかじめ解決されています。 2番目の問題も、簡単に解決することができます。 したがって、
また、使用し終わったらすべてのハンドルをクローズすることも忘れないでください。手順4と手順8で作成したスレッドへのハンドルと、手順1で取得したリモートプロセスへのハンドルをクローズします。 LibSpyのソースの一部を次に示します。上記の手順が実際にどのように実装されているかを見てみてください。コードを単純化するために、エラー処理とUnicodeサポートの部分は省いています。 HANDLE hThread; char szLibPath[_MAX_PATH]; // The name of our "LibSpy.dll" // module (including full path!); void* pLibRemote; // The address (in the remote process) // where szLibPath will be copied to; DWORD hLibModule; // Base address of loaded module (==HMODULE); // initialize szLibPath //... // 1. Allocate memory in the remote process for szLibPath // 2. Write szLibPath to the allocated memory pLibRemote = ::VirtualAllocEx( hProcess, NULL, sizeof(szLibPath), MEM_COMMIT, PAGE_READWRITE ); ::WriteProcessMemory( hProcess, pLibRemote, (void*)szLibPath, sizeof(szLibPath),NULL ); // Load "LibSpy.dll" into the remote process // (via CreateRemoteThread & LoadLibrary) hThread = ::CreateRemoteThread( hProcess, NULL, 0, (LPTHREAD_START_ROUTINE )::GetProcAddress( ::GetModuleHandle("Kernel32"), "LoadLibraryA"), pLibRemote, 0, NULL ); ::WaitForSingleObject( hThread, INFINITE ); // Get handle of the loaded module ::GetExitCodeThread( hThread, &hLibModule ); // Clean up ::CloseHandle( hThread ); ::VirtualFreeEx( hProcess, pLibRemote, sizeof(szLibPath),MEM_RELEASE ); 実際に割り込ませるコード( // Unload "LibSpy.dll" from the target process // (via CreateRemoteThread & FreeLibrary) hThread = ::CreateRemoteThread( hProcess, NULL, 0, (LPTHREAD_START_ROUTINE )::GetProcAddress( ::GetModuleHandle("Kernel32"), "FreeLibrary"), (void*)hLibModule, 0, NULL ); ::WaitForSingleObject( hThread, INFINITE ); // Clean up ::CloseHandle( hThread ); プロセス間通信ここまでは、リモートプロセスにDLLを割り込ませる方法についてのみ説明してきました。しかし、たいていの場合は、割り込んだDLLが元のアプリケーションと何らかの方法で通信を行う必要があります(ローカルアプリケーションではなくリモートプロセスにDLLをマッピングしているのだということを思い出してください!)。本稿のサンプルのパスワードスパイについて考えてみましょう。このDLLは、実際にパスワードを含んでいるコントロールへのハンドルを知らなければなりません。当然ながら、この値をコンパイル時にハードコーディングすることはできません。同様に、このDLLはパスワードを取得した後、それを表示するためにアプリケーションに送信しなければなりません。 そのためにはさまざまな方法があり、たとえばファイルマッピング、 本稿の最初で紹介しているダウンロードパッケージに、LibSpyの実行可能ファイルとソースファイルが含まれています。 3. CreateRemoteThread+WriteProcessMemoryのテクニックデモアプリケーション:WinSpy コードを別のプロセスのアドレス空間にコピーし、そのプロセスのコンテキスト内で実行するには、リモートスレッドと まず、 HANDLE CreateRemoteThread( HANDLE hProcess, // handle to process to create thread in LPSECURITY_ATTRIBUTES lpThreadAttributes, // pointer to security // attributes DWORD dwStackSize, // initial thread stack size, in bytes LPTHREAD_START_ROUTINE lpStartAddress, // pointer to thread // function LPVOID lpParameter, // argument for new thread DWORD dwCreationFlags, // creation flags LPDWORD lpThreadId // pointer to returned thread identifier ); MSDNに記載されている
このテクニックの手順をまとめると次のようになります。
以上の規則に従わないと、ほぼ必ずターゲットプロセスがクラッシュします。ターゲットプロセスとローカルプロセスで同じアドレスが使用されている保証はない、という点だけはよく覚えておいてください(付録Fを参照)。 GetWindowTextRemote(A/W) リモートのエディットコントロールからパスワードを取得するために必要な機能はすべて int GetWindowTextRemoteA ( HANDLE hProcess, HWND hWnd, LPSTR lpString ); int GetWindowTextRemoteW ( HANDLE hProcess, HWND hWnd, LPWSTR lpString ); パラメータ
戻り値コピーした文字数が返されます。 それでは、 INJDATAtypedef LRESULT (WINAPI *SENDMESSAGE)(HWND,UINT,WPARAM,LPARAM); typedef struct { HWND hwnd; // handle to edit control SENDMESSAGE fnSendMessage; // pointer to user32!SendMessageA char psText[128]; // buffer that is to receive the password } INJDATA; ThreadFuncstatic DWORD WINAPI ThreadFunc (INJDATA *pData) { pData->fnSendMessage( pData->hwnd, WM_GETTEXT, // Get password sizeof(pData->psText), (LPARAM)pData->psText ); return 0; } // This function marks the memory address after ThreadFunc. // int cbCodeSize = (PBYTE) AfterThreadFunc - (PBYTE) ThreadFunc. static void AfterThreadFunc (void) { }
このテクニックを使ってリモートコントロールをサブクラス化する方法デモアプリケーション:InjectEx 今度はもう少し複雑な話をしましょう。別のプロセスに属しているコントロールをこのテクニックでサブクラス化するにはどうすればいいか、という話です。 これを実現するには、まず次の2つの関数をリモートプロセスにコピーする必要があります。
問題は、リモートの ソリューション1次の図を見てください。 仮想アドレス空間 ![]() リモートプロセス内では static LRESULT CALLBACK NewProc( HWND hwnd, // handle to window UINT uMsg, // message identifier WPARAM wParam, // first message parameter LPARAM lParam ) // second message parameter { INJDATA* pData = (INJDATA*) NewProc; // pData points to // NewProc; pData--; // now pData points to INJDATA; // recall that INJDATA in the remote // process is immediately before NewProc; //----------------------------- // subclassing code goes here // ........ //----------------------------- // call original window procedure; // fnOldProc (returned by SetWindowLong) was initialised by // (the remote) ThreadFunc and stored in (the remote) INJDATA; return pData->fnCallWindowProc( pData->fnOldProc, hwnd,uMsg,wParam,lParam ); } しかし、これでもまだ問題があります。1行目を見てください。 INJDATA* pData = (INJDATA*) NewProc; こうすると、ハードコーディングされた値(ローカルプロセス内のオリジナルの この問題をC/C++で解決することはできませんが、インラインでアセンブラを利用すれば解決できます。修正後の static LRESULT CALLBACK NewProc( HWND hwnd, // handle to window UINT uMsg, // message identifier WPARAM wParam, // first message parameter LPARAM lParam ) // second message parameter { // calculate location of the INJDATA struct // (remember that INJDATA in the remote process // was placed immediately before NewProc) INJDATA* pData; _asm { call dummy dummy: pop ecx // <- ECX contains the current EIP sub ecx, 9 // <- ECX contains the address of NewProc mov pData, ecx } pData--; //----------------------------- // subclassing code goes here // ........ //----------------------------- // call original window procedure return pData->fnCallWindowProc( pData->fnOldProc, hwnd,uMsg,wParam,lParam ); } さて、何がどうなったでしょうか。 ほとんどのプロセッサには、次に実行する命令のメモリロケーションを指す特殊なレジスタがあります。これがいわゆる命令ポインタで、IntelおよびAMDの32ビットプロセッサでは「 これで ここで、
Address OpCode&Params Decoded instruction
--------------------------------------------------
:00401000 55 push ebp ; entry point of
; NewProc
:00401001 8BEC mov ebp, esp
:00401003 51 push ecx
:00401004 E800000000 call 00401009 ; *1* call dummy
:00401009 59 pop ecx ; *2*
:0040100A 83E909 sub ecx, 00000009 ; *3*
:0040100D 894DFC mov [ebp-04], ecx ; mov pData, ECX
:00401010 8B45FC mov eax, [ebp-04]
:00401013 83E814 sub eax, 00000014 ; pData--;
.....
.....
:0040102D 8BE5 mov esp, ebp
:0040102F 5D pop ebp
:00401030 C21000 ret 0010
こうすれば、
これが、本稿のサンプルInjectExで使用しているソリューションです。InjectExでは、HookInjExと同様に、[スタート]ボタンの左クリックと右クリックを入れ替えます。 ソリューション2 この問題の解決策は、リモートプロセスのアドレス空間内で static LRESULT CALLBACK NewProc( HWND hwnd, // handle to window UINT uMsg, // message identifier WPARAM wParam, // first message parameter LPARAM lParam ) // second message parameter { INJDATA* pData = 0xA0B0C0D0; // a dummy value //----------------------------- // subclassing code goes here // ........ //----------------------------- // call original window procedure return pData->fnCallWindowProc( pData->fnOldProc, hwnd,uMsg,wParam,lParam ); } このコード中の この Address OpCode&Params Decoded instruction -------------------------------------------------- :00401000 55 push ebp :00401001 8BEC mov ebp, esp :00401003 C745FCD0C0B0A0 mov [ebp-04], A0B0C0D0 :0040100A ... .... .... :0040102D 8BE5 mov esp, ebp :0040102F 5D pop ebp :00401030 C21000 ret 0010 したがって、コンパイル済みのコード(16進形式)は ここで、次の作業を行います。
【1】コンパイル済みコード内で たとえばUNIXという単語を4バイトに格納する場合を考えてみましょう。ビッグエンディアン方式では、これを「UNIX」として格納します。リトルエンディアン方式では、これを「XINU」として格納します。 【2】悪意のある人物は同様の方法で実行可能ファイルのコードを書き換えます。しかし、一度メモリにロードしてしまえば、プログラムが自身のコード(実行可能ファイルの CreateRemoteThread+WriteProcessMemoryのテクニックはどこで使用すべきか 本稿の最初で紹介しているダウンロードパッケージに、WinSpyおよびInjectExの実行可能ファイルとソースファイルが含まれています。 おわりに最後に、ここまで触れずにいたいくつかの点をまとめておきます。
これで注意すべき点はほとんど説明しましたが、もう1つ覚えておいてほしいことがあります。それは、割り込ませたコードのせいでターゲットプロセスがダウンする可能性があるということです。特に、そのコードに何か問題があるときは危険です。力にはそれなりの責任が伴う、ということを覚えておいてください。 本稿で紹介したサンプルの多くはパスワードに関するものだったので、さらに興味のある方はZhefu Zhangの記事「Super Password Spy++」も読んでみてください。Zhefu Zhangの記事では、Internet Explorerのパスワードフィールドからパスワードを取得する方法などを説明しています。さらに、そのような攻撃からパスワードコントロールを保護する方法も紹介しています。 最後になりましたが、記事を書いたり発表したりする身としては、読者からの反響が何よりの励みになります。おもしろいと思われた方は、ぜひコメントをお寄せください。さらに、本稿の内容に間違いやバグがある場合、こうすればもっと良くなるというアイデアがある場合、よくわからない部分がある場合なども、お気軽にご意見をお寄せください。 謝辞まずCodeGuru読者の皆さんに感謝します。最初は1200ワード程度だった記事がこのように6000ワード級の長編になったのは、主に皆さんからの質問のおかげです。しかし、特に誰か1人を挙げるとすればRado Pichaでしょう。本稿のかなりの部分は、彼の提案と解説に基づいています。また、私のつたない英文に手を入れ、読みやすい原稿に仕上げてくれたSusan Mooreにも感謝したいと思います。 付録A)なぜKERNEL32.DLLとUSER32.DLLは常に同じアドレスにマッピングされるのかおそらく、その方が速度を最適化するのに便利だとMicrosoftのプログラマが考えたからです。その理由を考えてみましょう。 一般的に、実行可能ファイルは リンカがEXEまたはDLLファイルを作成するときは、そのファイルがメモリのどこにマッピングされるかを仮定します。これが、仮定のロード/基底アドレス、望ましいロード/基底アドレスと呼ばれるものです。イメージ内のすべての絶対アドレスは、このリンカーが仮定したロードアドレスに基づきます。何らかの理由でイメージがこのアドレスにロードされなかった場合は、イメージ内のすべての絶対アドレスをPE(portable executable)ローダーが修正しなければなりません。ここで しかし、kernel32.dll、user32.dllとそのロードアドレスはどういうしくみで常に同じアドレスに配置されるのでしょうか。 すべてのWin32アプリケーションはkernel32.dllを必要とし、その大部分はさらにuser32.dllも必要とするので、この2つのDLLを常に望ましい基底アドレスにマッピングしておけば、すべての実行可能プログラムのロード時間を短縮できます。そのため、ローダーはkernel32.dllとuser32.dllの(絶対)アドレスを決して修正しないのだと思われます。 これを具体的な例で考えてみましょう。 App.exeのイメージ基底アドレスを、KERNEL32( これはなぜでしょうか? Win 2000、Win XP、およびWin 2003上のローダーがプロセスを作成するときには、kernel32.dllとuser32.dllが望ましい基底アドレスにマッピングされているかどうかを確認し、されていない場合はハードエラーを発生させます(これらのDLL名はローダーにハードコーディングされています)。WinNT 4では、ole32.dllについても確認しました。WinNT 3.51以前は、このようなチェックが存在しなかったため、kernel32.dllとuser32.dllはどこにでもマッピングされる可能性がありました。なお、ntdll.dllだけは必ず基底アドレスに配置されます。ローダーはこのモジュールをチェックしませんが、ntdll.dllが基底アドレスにない場合は、そもそもプロセスを作成できません。 まとめると、WinNT 4以上では次のようになっています。
B)/GZコンパイラスイッチ デバッグビルドでは、 C)静的関数とインクリメンタルリンク インクリメンタルリンクは、アプリケーションをビルドするときのリンク時間を短縮するために使われます。通常のリンクを行った実行可能ファイルとインクリメンタルリンクを行った実行可能ファイルとの違いは、後者では、個々の関数呼び出しがリンカーから出された追加の const int cbCodeSize = ((LPBYTE) AfterThreadFunc - (LPBYTE) ThreadFunc); 実際には、それぞれ :00401020 jmp 004014C0 ... :004014C0 push EBP ; real address of ThreadFunc :004014C1 mov EBP, ESP ... このとき、 WriteProcessMemory( .., &ThreadFunc, cbCodeSize, ..); という関数は、実際の しかし、この D)ThreadFuncでローカル変数を4Kbまでしか使用できないのはなぜかローカル変数は必ずスタックに格納されます。たとえば、ある関数内に256バイトのローカル変数がある場合は、その関数(より正確には関数のプロローグ)に入ると、スタックポインタが256バイト減少します。たとえば次の関数は、 void Dummy(void) { BYTE var[256]; var[0] = 0; var[1] = 1; var[255] = 255; } コンパイルすると次のようになります。
:00401000 push ebp
:00401001 mov ebp, esp
:00401003 sub esp, 00000100 ; change ESP as storage for
; local variables is needed
:00401006 mov byte ptr [esp], 00 ; var[0] = 0;
:0040100A mov byte ptr [esp+01], 01 ; var[1] = 1;
:0040100F mov byte ptr [esp+FF], FF ; var[255] = 255;
:00401017 mov esp, ebp ; restore stack pointer
:00401019 pop ebp
:0040101A ret
この例でスタックポインタ( マニュアルでは、スタックプローブと 「 この説明の「領域の量だけがメモリに入ります」という部分で考え込んだ人も多いのではないでしょうか。こうしたコンパイラオプション(とその解説)は、ときどき非常にわかりにくくてイライラさせられます。何がどうなっているのかを実際に見てみれば、それほど難しいことではないのですが。たとえば、12Kbのローカル変数を必要とする関数の場合は、スタック上のメモリが次のようにして「割り当て」られます(正確には「コミット」されます)。 sub esp, 0x1000 ; "allocate" first 4 Kb test [esp], eax ; touches memory in order to commit a ; new page (if not already committed) sub esp, 0x1000 ; "allocate" second 4 Kb test [esp], eax ; ... sub esp, 0x1000 test [esp], eax スタックポインタが4Kbずつ変更され、各ステップの後でスタックの下部が さらに次の説明を読むと、 「新しいスレッドは、コミット済みメモリと予約済みメモリの両方から成る専用のスタック領域を受け取ります。既定では、各スレッドは1Mbの予約済みメモリと1ページのコミット済みメモリを使用します。システムは、必要に応じて予約済みスタックメモリから1ページブロックをコミットします。」(MSDNの「CreateThread > dwStackSize > Thread Stack Size」を参照) では、 E)4つ以上のcaseステートメントを含むswitchブロックを分割するのはなぜかここでも、例を見ながら説明していきたいと思います。次のような関数があるとします。 int Dummy( int arg1 ) { int ret = 0; switch( arg1 ) { case 1: ret = 1; break; case 2: ret = 2; break; case 3: ret = 3; break; case 4: ret = 0xA0B0; break; } return ret; } この関数は、コンパイルすると次のようになります。
Address OpCode&Params Decoded instruction
--------------------------------------------------
; arg1 -> ECX
:00401000 8B4C2404 mov ecx, dword ptr [esp+04]
:00401004 33C0 xor eax, eax ; EAX = 0
:00401006 49 dec ecx ; ECX --
:00401007 83F903 cmp ecx, 00000003
:0040100A 771E ja 0040102A
; JMP to one of the addresses in table ***
; note that ECX contains the offset
:0040100C FF248D2C104000 jmp dword ptr [4*ecx+0040102C]
; case 1: eax = 1;
:00401013 B801000000 mov eax, 00000001
:00401018 C3 ret
; case 2: eax = 2;
:00401019 B802000000 mov eax, 00000002
:0040101E C3 ret
; case 3: eax = 3;
:0040101F B803000000 mov eax, 00000003
:00401024 C3 ret
; case 4: eax = 0xA0B0;
:00401025 B8B0A00000 mov eax, 0000A0B0
:0040102A C3 ret
:0040102B 90 nop
; Address table ***
:0040102C 13104000 DWORD 00401013 ; jump to case 1
:00401030 19104000 DWORD 00401019 ; jump to case 2
:00401034 1F104000 DWORD 0040101F ; jump to case 3
:00401038 25104000 DWORD 00401025 ; jump to case 4
1つ1つの
この方法は
このような場合は、 これまでに、どうして では、本題に戻りましょう。 アドレス
Opcode Instruction Description
FF /4 JMP r/m32 Jump near, absolute indirect,
address given in r/m32
なんと、データベース F)そもそもリモートプロセスがクラッシュするのはなぜかリモートプロセスがクラッシュするときは、必ず次のいずれかの理由によります。
どの場合でも、 参考資料
著者紹介Robert Kuster(Robert Kuster)
最新トップニュース
|
|