SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

japan.internet.com翻訳記事

別のプロセスにコードを割り込ませる3つの方法

プロセス割り込みのためのチュートリアル

  • X ポスト
  • このエントリーをはてなブックマークに追加

おわりに

 最後に、ここまで触れずにいたいくつかの点をまとめておきます。

種類OSプロセス
1. フックWin9xおよびWinNTUSER32.DLLにリンクしているプロセスのみ【1】
2. CreateRemoteThreadLoadLibraryWinNTのみ【2】すべてのプロセス【3】(システムサービスを含む【4】)
3. CreateRemoteThreadWriteProcessMemoryWinNTのみすべてのプロセス(システムサービスを含む)
  1. 当然ながら、メッセージキューを持たないスレッドにはフックできません。また、SetWindowsHookExはシステムサービスに対しては機能しません(USER32.DLLにリンクしている場合も同様)。
  2. Win9xにはCreateRemoteThreadVirtualAllocExがありません(実際にはWin9xでもこれらの関数をエミュレートできますが、詳細は割愛します)。
  3. すべてのプロセス=Win32の全プロセス+csrss.exe
  4. smss.exe、os2ss.exe、autochk.exeなどのネイティブアプリケーションはWin32 APIを使用せず、kernel32.dllにもリンクしません。唯一の例外は、Win32サブシステム本体であるcsrss.exeです。csrss.exeはネイティブアプリケーションですが、その一部のライブラリ(~winsrv.dll)は、kernel32.dlをはじめとするWin32 DLLを必要とします。
  5. システムサービス(lsass.exe、services.exe、winlogon.exeなど)やcsrss.exeにコードを割り込ませる場合は、OpenProcessを使用してリモートプロセスへのハンドルを開く前に、AdjustTokenPrivilegesを使用してプロセスの権限をSeDebugPrivilegeに設定します。

 これで注意すべき点はほとんど説明しましたが、もう1つ覚えておいてほしいことがあります。それは、割り込ませたコードのせいでターゲットプロセスがダウンする可能性があるということです。特に、そのコードに何か問題があるときは危険です。力にはそれなりの責任が伴う、ということを覚えておいてください。

 本稿で紹介したサンプルの多くはパスワードに関するものだったので、さらに興味のある方はZhefu Zhangの記事「Super Password Spy++」も読んでみてください。Zhefu Zhangの記事では、Internet Explorerのパスワードフィールドからパスワードを取得する方法などを説明しています。さらに、そのような攻撃からパスワードコントロールを保護する方法も紹介しています。

 最後になりましたが、記事を書いたり発表したりする身としては、読者からの反響が何よりの励みになります。おもしろいと思われた方は、ぜひコメントをお寄せください。さらに、本稿の内容に間違いやバグがある場合、こうすればもっと良くなるというアイデアがある場合、よくわからない部分がある場合なども、お気軽にご意見をお寄せください。

謝辞

 まずCodeGuru読者の皆さんに感謝します。最初は1200ワード程度だった記事がこのように6000ワード級の長編になったのは、主に皆さんからの質問のおかげです。しかし、特に誰か1人を挙げるとすればRado Pichaでしょう。本稿のかなりの部分は、彼の提案と解説に基づいています。また、私のつたない英文に手を入れ、読みやすい原稿に仕上げてくれたSusan Mooreにも感謝したいと思います。

付録

A)なぜKERNEL32.DLLとUSER32.DLLは常に同じアドレスにマッピングされるのか

 おそらく、その方が速度を最適化するのに便利だとMicrosoftのプログラマが考えたからです。その理由を考えてみましょう。

 一般的に、実行可能ファイルは.relocセクションなどのいくつかのセクションから成ります。

 リンカがEXEまたはDLLファイルを作成するときは、そのファイルがメモリのどこにマッピングされるかを仮定します。これが、仮定のロード/基底アドレス、望ましいロード/基底アドレスと呼ばれるものです。イメージ内のすべての絶対アドレスは、このリンカーが仮定したロードアドレスに基づきます。何らかの理由でイメージがこのアドレスにロードされなかった場合は、イメージ内のすべての絶対アドレスをPE(portable executable)ローダーが修正しなければなりません。ここで.relocセクションの出番です。このセクションには、リンカーが仮定したロードアドレスと実際のロードアドレスとの違いを考慮する必要があるイメージ内のすべての場所のリストが含まれています(もっとも、コンパイラによって生成される命令の大部分は何らかの相対アドレス指定を使用しているので、再配置が必要な場所はそれほど多くありません)。一方、イメージをリンカーの望ましい基底アドレスにそのままロードできる場合は、.relocセクションは完全に無視されます。

 しかし、kernel32.dll、user32.dllとそのロードアドレスはどういうしくみで常に同じアドレスに配置されるのでしょうか。

 すべてのWin32アプリケーションはkernel32.dllを必要とし、その大部分はさらにuser32.dllも必要とするので、この2つのDLLを常に望ましい基底アドレスにマッピングしておけば、すべての実行可能プログラムのロード時間を短縮できます。そのため、ローダーはkernel32.dllとuser32.dllの(絶対)アドレスを決して修正しないのだと思われます。

 これを具体的な例で考えてみましょう。

 App.exeのイメージ基底アドレスを、KERNEL32(/base:"0x77e80000")またはUSER32(/base:"0x77e10000")の望ましい基底アドレスに設定しました。App.exeがUSER32からのインポートを行わない場合は、単純にLoadLibraryを使用してロードします。その後、App.exeをコンパイルして実行してみました。すると、「Illegal System DLL Relocation」というエラーボックスがポップアップされ、App.exeのロードに失敗しました。

 これはなぜでしょうか? 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以上では次のようになっています。

  • 必ず基底アドレスにマッピングされるDLL:kernel32.dll、user32.dll、ntdll.dll
  • すべてのWin32アプリケーション(+csrss.exe)で使用されるDLL:kernel32.dll、ntdll.dll
  • すべてのプロセスで(ネイティブアプリケーションでも)使用される唯一のDLL:ntdll.dll

B)/GZコンパイラスイッチ

 デバッグビルドでは、/GZコンパイラスイッチが既定で有効になっています。このスイッチを使用すると、いくつかのエラーをキャッチすることができます(詳しくはマニュアルを参照してください)。ところで、このスイッチは実行可能ファイルに対してどのような影響を与えるのでしょうか。

 /GZを有効にすると、コンパイラは、生成する実行可能ファイル内のすべての関数の末尾に追加コードを書き込みます。この追加コードには、その関数の中でESPスタックポインタが変化していないことを確認するための関数呼び出しなどが含まれます。ここでの問題は、ThreadFuncに関数呼び出しが追加されるという点です。これはエラーにつながります。リモートプロセス内のThreadFuncが、リモートプロセス内には存在しない(少なくとも同じアドレスにはない)関数を呼び出すことになるからです。

C)静的関数とインクリメンタルリンク

 インクリメンタルリンクは、アプリケーションをビルドするときのリンク時間を短縮するために使われます。通常のリンクを行った実行可能ファイルとインクリメンタルリンクを行った実行可能ファイルとの違いは、後者では、個々の関数呼び出しがリンカーから出された追加のJMP命令を通じて実行されるという点です(ただし、staticとして宣言した関数は例外です)。これらのJMP命令により、リンカーはその関数を参照するすべてのCALL命令を更新しなくても、関数をメモリ内のあちこちに移動できるようになります。しかし、このJMP命令が問題を引き起こします。これにより、ThreadFuncAfterThreadFuncが実コードではなくJMP命令を指すようになるからです。したがって、次の方法でThreadFuncのサイズを計算すると、

const int cbCodeSize = ((LPBYTE) AfterThreadFunc
                      - (LPBYTE) ThreadFunc);

 実際には、それぞれThreadFuncAfterThreadFuncを指す2つのJMP命令の間の「距離」を計算することになってしまいます(通常はこれらの命令が並んで配置されますが、ここでは考えません)。たとえば、ThreadFuncがアドレス004014C0にあり、対応するJMP命令が00401020にあるとします。

:00401020   jmp  004014C0
 ...
:004014C0   push EBP          ; real address of ThreadFunc
:004014C1   mov  EBP, ESP
 ...

 このとき、

WriteProcessMemory( .., &ThreadFunc, cbCodeSize, ..);

 という関数は、実際のThreadFuncではなく、JMP 004014C0命令(およびそれ以降のcbCodeSizeの範囲内に含まれるすべての命令)をリモートプロセスにコピーします。したがって、リモートスレッドが最初に実行するのはJMP 004014C0命令になります。これは、そのリモートスレッドにとってだけでなく、プロセス全体にとっても最初の命令になります。

 しかし、このJMP命令の規則には例外があります。staticとして宣言した関数は、インクリメンタルリンクを行った場合でも直接呼び出されるのです。これが、規則4ThreadFuncAfterThreadFuncstaticとして宣言するか、インクリメンタルリンクを無効にするよう指示している理由です(インクリメンタルリンクのその他の側面については、Matt Pietrekの記事「Remove Fatty Deposits from Your Applications Using Our 32-bit Liposuction Tools」を参照のこと)。

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

 この例でスタックポインタ(ESP)がどのように変化しているかに注目してください。では、4Kb超のローカル変数を必要とする関数の場合はどうなるでしょうか? その場合は、スタックポインタが直接変更されません。その代わりに、別の関数(スタックプローブ)が呼び出され、その関数によって適切に変更されます。しかし、この追加の関数呼び出しがThreadFuncの動作を妨げます。リモートのThreadFuncが、存在しないものを呼び出そうとしてしまうからです。

 マニュアルでは、スタックプローブと/Gsコンパイラオプションについて次のように書かれています。

/Gssizeオプションは、スタックプローブを制御するための高度な機能です。スタックプローブとは、コンパイラがすべての関数呼び出しに挿入するコード群です。スタックプローブがアクティブになると、該当する関数のローカル変数を保存するために必要なスタック領域のサイズ分だけがメモリに入ります。
関数のローカル変数を保存するために必要なスタック領域がnバイトを超えると、その関数のスタックプローブがアクティブになります。nの既定値は1ページ分(80×86プロセッサでは4Kb)に相当します。この値によってWin32用アプリケーションとWindows NTの仮想メモリマネージャ間の連系動作を調整できるため、実行時にプログラムスタックで使用できるメモリ量を増やすことができます。」

 この説明の「領域の量だけがメモリに入ります」という部分で考え込んだ人も多いのではないでしょうか。こうしたコンパイラオプション(とその解説)は、ときどき非常にわかりにくくてイライラさせられます。何がどうなっているのかを実際に見てみれば、それほど難しいことではないのですが。たとえば、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ずつ変更され、各ステップの後でスタックの下部がtest命令によって「タッチ」されていることに注目してください。これにより、別のページを割り当て(コミット)する前に、スタックの下部を含んでいるページがコミットされます。

 さらに次の説明を読むと、

「新しいスレッドは、コミット済みメモリと予約済みメモリの両方から成る専用のスタック領域を受け取ります。既定では、各スレッドは1Mbの予約済みメモリと1ページのコミット済みメモリを使用します。システムは、必要に応じて予約済みスタックメモリから1ページブロックをコミットします。」(MSDNの「CreateThread > dwStackSize > Thread Stack Size」を参照)

 /Gsについての説明の中で、アプリケーションとWindows NT仮想メモリマネージャとの連係動作でスタックプローブを使用することに言及している理由がわかります。

 では、ThreadFuncと4Kbの制限に話を戻しましょう。

 /Gsを使用すればスタックプローブルーチンの呼び出しを妨げることができますが、マニュアルでは、そうしないよう推奨されています。さらに、スタックプローブの有効/無効は#pragma check_stack指令で切り替えることができると書かれています。しかし、このプラグマはスタックプローブに何も影響を与えないようです(マニュアルが間違っているのか、それとも私が何か別の要素を見逃しているのでしょうか?)。ともかく、CreateRemoteThreadWriteProcessMemoryのテクニックは、短いコードを割り込ませるときにのみ使用することをお勧めします。そうすれば、ローカル変数で何バイトも消費することはまずなくなり、4Kbの制限に近づくこともないからです。

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

 switch-caseがどのように実装されているかに注目してください。

 1つ1つのcaseステートメントを個別に検討するのではなく、アドレステーブルを作成しています。その上で、アドレステーブルでのオフセットを計算し、適切なcaseステートメントにジャンプします。一見したところ、これは優れた処理方法のように見えます。たとえば50個のcaseステートメントを含むswitchブロックの場合は、上記のトリックを使わないと、最後のcaseステートメントに到達するまでにCMP命令とJMP命令を50回実行しなければなりません。しかし、アドレステーブルを利用すれば、テーブルを1回参照するだけでどのcaseステートメントにもジャンプできます。コンピュータアルゴリズムと時間計算量(time complexity)の用語で言えば、O(2n)のアルゴリズムをO(5)のアルゴリズムで置き換えたことになります。これは次のことを意味しています。

  1. Oは、最悪ケースの時間計算量を表します。
  2. オフセットを計算し、テーブルルックアップを行い、最終的に適切なアドレスにジャンプするためには、5つの命令が必要です。

 この方法はcase定数が1、2、3、4と連続しているから可能だったのではないかと思う人もいるかもしれません。しかしこのソリューションは、オフセット計算を少し複雑にするだけで、たいていの実世界の例に応用することができます。ただし次の2つの例外があります。

  • caseステートメントが3つ以下の場合
  • case定数どうしの関連性がまったくない場合(例:"case 1""case 13""case 50""case 1000"など)

 このような場合は、CMP命令とJMP命令を使って1つ1つのcase定数を個別に検討しなければならないので、結果コードが非常に長くなります。つまり、この場合の結果コードは、普通のif-else ifシーケンスを使ってコーディングしたのと実質的に同じになります。

 これまでに、どうしてcaseステートメントには定数式しか指定できないのか疑問に思ったことがある人もいるでしょうが、これでその理由がわかったと思います。上記のようなアドレステーブルを作成するためには、この値がコンパイル時に確定していなければならないのです。

 では、本題に戻りましょう。

 アドレス0040100CJMP命令に注目してください。Intelのマニュアルでは、16進のOpCode FFについて次のように説明されています。

Opcode    Instruction    Description
FF /4     JMP r/m32      Jump near, absolute indirect,
                         address given in r/m32

 なんと、データベースJMPは何らかの絶対アドレス指定を使用するというのです。つまり、この命令のオペランドの1つ(今回の例では0040102C)は絶対アドレスを表すということです。これ以上言う必要があるでしょうか? リモートのThreadFuncswitchブロックのアドレステーブルが0040102Cにあると盲目的に信じ込み、間違ったロケーションにジャンプするため、リモートプロセスがクラッシュしてしまいます。

F)そもそもリモートプロセスがクラッシュするのはなぜか

 リモートプロセスがクラッシュするときは、必ず次のいずれかの理由によります。

  1. ThreadFunc内の存在しない文字列を参照した。
  2. ThreadFunc内の命令が絶対アドレスを使用している(具体例については付録Eを参照)。
  3. ThreadFuncが存在しない関数を呼び出した(この呼び出しはコンパイラ/リンカーによって追加される場合もある)。この場合、逆アセンブラしたThreadFuncを見てみると、次のようになっている。
  4. :004014C0    push EBP         ; entry point of ThreadFunc
    :004014C1    mov EBP, ESP
     ...
    :004014C5    call 0041550     ; this will crash the
                                  ; remote process
     ...
    :00401502    ret
    
    /GZなどの禁止されているスイッチを有効にしたことでコンパイラがデータベースCALLを追加した場合、その呼び出し命令は、ThreadFuncの先頭か終わり近くに配置される。

 どの場合でも、CreateRemoteThreadWriteProcessMemoryのテクニックを使用するときには十分な注意が必要です。特にコンパイラ/リンカーオプションには注意してください。これらのオプションのおかげでThreadFuncに呼び出しが追加されることはよくあります。

参考資料

  1. Load Your 32-bit DLL into Another Process's Address Space Using INJLIB by Jeffrey Richter.MSJ May, 1994
  2. HOWTO: Subclass a Window in Windows 95; Microsoft Knowledge Base Article - 125680
  3. Tutorial 24: Windows Hooks by Iczelion
  4. CreateRemoteThread by Felix Kasza
  5. API hooking revealed by Ivo Ivanov
  6. Peering Inside the PE: A Tour of the Win32 Portable Executable File Format by Matt Pietrek, March 1994
  7. Intel Architecture Software Developer's Manual, Volume 2:Instruction Set Reference

この記事は参考になりましたか?

  • X ポスト
  • このエントリーをはてなブックマークに追加
japan.internet.com翻訳記事連載記事一覧

もっと読む

この記事の著者

japan.internet.com(ジャパンインターネットコム)

japan.internet.com は、1999年9月にオープンした、日本初のネットビジネス専門ニュースサイト。月間2億以上のページビューを誇る米国 Jupitermedia Corporation (Nasdaq: JUPM) のニュースサイト internet.comEarthWeb.com からの最新記事を日本語に翻訳して掲載するとともに、日本独自のネットビジネス関連記事やレポートを配信。

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

Robert Kuster(Robert Kuster)

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

  • X ポスト
  • このエントリーをはてなブックマークに追加
CodeZine(コードジン)
https://codezine.jp/article/detail/82 2005/11/18 18:08

おすすめ

アクセスランキング

アクセスランキング

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー

アクセスランキング

アクセスランキング