win32デバッグAPIの続きだ。このチュートリアルではデバッグ対象プログラムを編集する方法について解説する。
メインソース1 | メインソース2 | デバッグ対象ソース |
実行結果1 | 実行結果2 | 実行結果3 |
|
前章ではデバッグ対象プログラムの起動方法とデバッグイベントの処理方法を解説した。より使いやすくするためにはデバッグ対象プログラムを修正できなければならない。このためのAPIがいくつか用意されている。
- ReadProcessMemory
この関数で対象のプロセスのメモリを読み取ることができる。プロトタイプは以下のようになっている。
ReadProcessMemory proto hProcess:DWORD, lpBaseAddress:DWORD, lpBuffer:DWORD, nSize:DWORD, lpNumberOfBytesRead:DWORD
- hProcess
メモリを読みたいプロセスのハンドル- lpBaseAddress
読み始めたいプロセスのアドレスで、例えば、40100hから4バイト読みたければ、この変数を401000hにする。- lpBuffer
読み取ったデータを受け取るためのバッファ- nSize
読み取りたいバイト数- lpNumberOfBytesRead
実際に読み込まれたバイト数を格納するバッファ。必要なければNULLを指定すればよい- WriteProcessMemory
ReadProcessMemory関数と対をなす関数で、対象プロセスのメモリに書き込むことができ、関数の引数はReadProcessMemory関数と同様である。
次の2つのAPI関数は少々説明が必要だ。ウィンドウズのようなマルチタスクOSでは、同時に複数のプログラムを起動できる。その方法はいくつかあるが、Windowsでは各スレッドにある一定の時間(タイムスライス)を割り振り、その時間分スレッドの処理を行う。一定の時間が経過したら優先度の高いスレッドを選び、そのスレッドに対して、ある一定時間処理を行う。
Windowsは、処理するスレッドを切り替える瞬間、処理していたスレッドのレジスタ値を保存しておき、今度またこのスレッドを再開するときには以前保存しておいた環境をそっくりそのまま移し変え、処理を再開する。保存しておいたレジスタの値を総称してコンテキストと呼んでいる。
本題に戻ろう。デバッグイベントが発生したとき、Windowsはデバッグ対象プログラムを停止させ、コンテキストを保存する。デバッグ対象プログラムは停止しているので、値が変わることはない。ちなみにそれらの値を取得するさいは、GetThreadContext関数を、変更したければSetThreadContext関数をCALLすればよい。
これら2つのAPIは非常に強力で、仮想デバイスドライバのような危険極まりないものである。というのも、デバッグ対象プログラムが停止中にコンテキストの値をいかようにも変更できるため、注意して使用しなければならない。例えば、停止中のプログラムのEIPレジスタ値を変更した後、プログラムを再開すれば簡単にふっとばせる。こんなことは普通の環境下ではできないだろう。
GetThreadContext proto hThread:DWORD, lpContext:DWORD
- hThread
コンテキストを取得したスレッドハンドル
- lpContext
CONTEXT構造体のアドレスで、この関数が成功したら値が格納されている。
SetThreadContext関数も引数は同じである。ではCONTEXT構造体について解説しよう。
- CONTEXT STRUCT
ContextFlags dd ? ;---------------------------------------------------------------------------------------------------------- ; ContextFlagsがCONTEXT_DEBUG_REGISTERSだった場合 ;----------------------------------------------------------------------------------------------------------- iDr0 dd ? iDr1 dd ? iDr2 dd ? iDr3 dd ? iDr6 dd ? iDr7 dd ? ;---------------------------------------------------------------------------------------------------------- ; ContextFlagsがCONTEXT_FLOATING_POINTだった場合 ;----------------------------------------------------------------------------------------------------------- FloatSave FLOATING_SAVE_AREA <> ;---------------------------------------------------------------------------------------------------------- ; ContextFlagsがCONTEXT_SEGMENTSだった場合 ;----------------------------------------------------------------------------------------------------------- regGs dd ? regFs dd ? regEs dd ? regDs dd ? ;---------------------------------------------------------------------------------------------------------- ; ContextFlagsがCONTEXT_INTEGERだった場合 ;----------------------------------------------------------------------------------------------------------- regEdi dd ? regEsi dd ? regEbx dd ? regEdx dd ? regEcx dd ? regEax dd ? ;---------------------------------------------------------------------------------------------------------- ; ContextFlagsがCONTEXT_CONTROLだった場合 ;----------------------------------------------------------------------------------------------------------- regEbp dd ? regEip dd ? regCs dd ? regFlag dd ? regEsp dd ? regSs dd ? ;---------------------------------------------------------------------------------------------------------- ; ContextFlagsがCONTEXT_EXTENDED_REGISTERSだった場合 ;----------------------------------------------------------------------------------------------------------- ExtendedRegisters db MAXIMUM_SUPPORTED_EXTENSION dup(?) CONTEXT ENDS見てわかるように、これらの構造体は本物のレジスタとうりふたつだ。どのレジスタグループを必要とするのかによりContextFlagsメンバの値を決めなければならない。
例えば、全てのレジスタを読み書きしたいのなら、ContextFlagsにCONTEXT_CONTROLを指定しなければならない。CONTEXT構造体を使用するに当たって一つだけ注意事項がある。それは、この構造体をDWORD境界にアラインしなければなら無いことだ。さもないと、WindowsNTでは奇妙な現象に遭遇する。
なので以下のように、この構造体を宣言するすぐ上の行で "align dword" と記述しなければならない。
align dword MyContext CONTEXT <>
|
始めの例はDebugActiveProcess関数の使用方法の紹介だ。まず、win.exe というターゲット実行ファイルを起動させる。その実行ファイルはウィンドウが出現する前に無限ループに入ることになっている。
その後、デバッグプログラムを起動させ、win.exeにアタッチする。そして無限ループを脱出するようにwin.exeを修正し、ウィンドウを出現させる。
.386 .model flat,stdcall option casemap:none include \masm32\include\windows.inc include \masm32\include\kernel32.inc include \masm32\include\comdlg32.inc include \masm32\include\user32.inc includelib \masm32\lib\kernel32.lib includelib \masm32\lib\comdlg32.lib includelib \masm32\lib\user32.lib .data AppName db "Win32 Debug Example no.2",0 ClassName db "SimpleWinClass",0 SearchFail db "Cannot find the target process",0 TargetPatched db "Target patched!",0 buffer dw 9090h .data? DBEvent DEBUG_EVENT <> ProcessId dd ? ThreadId dd ? align dword context CONTEXT <> .code start: invoke FindWindow, addr ClassName, NULL .if eax!=NULL invoke GetWindowThreadProcessId, eax, addr ProcessId mov ThreadId, eax invoke DebugActiveProcess, ProcessId .while TRUE invoke WaitForDebugEvent, addr DBEvent, INFINITE .break .if DBEvent.dwDebugEventCode==EXIT_PROCESS_DEBUG_EVENT .if DBEvent.dwDebugEventCode==CREATE_PROCESS_DEBUG_EVENT mov context.ContextFlags, CONTEXT_CONTROL invoke GetThreadContext,DBEvent.u.CreateProcessInfo.hThread, addr context invoke WriteProcessMemory, DBEvent.u.CreateProcessInfo.hProcess, context.regEip ,addr buffer, 2, NULL invoke MessageBox, 0, addr TargetPatched, addr AppName, MB_OK+MB_ICONINFORMATION .elseif DBEvent.dwDebugEventCode==EXCEPTION_DEBUG_EVENT .if DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEPTION_BREAKPOINT invoke ContinueDebugEvent, DBEvent.dwProcessId,DBEvent.dwThreadId, DBG_CONTINUE .continue .endif .endif invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId, DBG_EXCEPTION_NOT_HANDLED .endw .else invoke MessageBox, 0, addr SearchFail, addr AppName,MB_OK+MB_ICONERROR .endif invoke ExitProcess, 0 end start ;-------------------------------------------------------------------- ; The partial source code of win.asm, our debuggee. It's actually ; the simple window example in tutorial 2 with an infinite loop inserted ; just before it enters the message loop. ;---------------------------------------------------------------------- ...... mov wc.hIconSm,eax invoke LoadCursor,NULL,IDC_ARROW mov wc.hCursor,eax invoke RegisterClassEx, addr wc INVOKE CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\ WS_OVERLAPPEDWINDOW,CW_USEDEFAULT, CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,\ NULL,NULL, hInst,NULL mov hwnd,eax jmp $ <---- Here's our infinite loop. It assembles to EB FE invoke ShowWindow, hwnd,SW_SHOWNORMAL invoke UpdateWindow, hwnd .while TRUE invoke GetMessage, ADDR msg,NULL,0,0 .break .if (!eax) invoke TranslateMessage, ADDR msg invoke DispatchMessage, ADDR msg .endw mov eax,msg.wParam ret WinMain endp
|
invoke FindWindow, addr ClassName, NULLこのプログラムは、DebugActiveProcess関数をCALLしてデバッグ対象プログラムにアタッチするのだが、その際にプロセスIDが必要になる。そのプロセスIDはGetWindowThreadProcessId関数をCALLして取得するのだが、今度はその際にウィンドウハンドルが必要になる。よってまず始めにウィンドウハンドルを取得しなければならない。
ウィンドウクラス名を指定して、FindWindow関数をCALLすると、ウィンドウハンドルを取得できる。NULLが返ってこれば、そのクラス名のウィンドウは現在無いことを意味する。
.if eax!=NULL invoke GetWindowThreadProcessId, eax, addr ProcessId mov ThreadId, eax invoke DebugActiveProcess, ProcessIdプロセスIDを取得したあと、DebugActiveProcess関数をCALLし、デバッグイベントを待つためのデバッグループに入る。
.if DBEvent.dwDebugEventCode==CREATE_PROCESS_DEBUG_EVENT mov context.ContextFlags, CONTEXT_CONTROL invoke GetThreadContext,DBEvent.u.CreateProcessInfo.hThread, addr contextCREATE_PROCESS_DEBUG_INFO構造体を取得するときにはデバッグ対象プログラムは停止しており、そのプログラムをいかようにもすることができる。この例では、デバッグ対象プログラムのループ命令( 0EBh 0FEh )を何もしない命令である NOPs(90h 90h) に置き換えてみよう。
まず始めに命令コードのアドレスを取得する必要がある。デバッグ対象プログラムは、アタッチした時には既にループに入っているので、EIPレジスタは常に有効な命令コードのアドレスを保持しており、そのEIPレジスタの値を取得すればよい。よって、GetThreadContext関数をCALLすればゴールだ。CONTEXT構造体の "control" レジスタメンバを取得するため、ContextFlagsメンバにCONTEXT_CONTROLをセットした後、GetThreadContext関数をCALLする。
invoke WriteProcessMemory, DBEvent.u.CreateProcessInfo.hProcess, context.regEip ,addr buffer, 2, NULLようやくEIPレジスタの値を取得できたので、WriteProcessMemory関数をCALLして"jmp $" 命令をNOP命令に置き換えよう。 これにより、デバッグ対象プログラムが無限ループから脱出する。
その後、ユーザにメッセージを出力し、ContinueDebugEvent関数をCALLしてデバッグ対象プログラムを再開する。再開したプログラムはループせずに、ウィンドウを表示し、今度はメッセージループに入る。他のサンプルは、少し違った方法でデバッグ対象プログラムを無限ループから脱出させている。
....... ....... .if DBEvent.dwDebugEventCode==CREATE_PROCESS_DEBUG_EVENT mov context.ContextFlags, CONTEXT_CONTROL invoke GetThreadContext,DBEvent.u.CreateProcessInfo.hThread, addr context add context.regEip,2 invoke SetThreadContext,DBEvent.u.CreateProcessInfo.hThread, addr context invoke MessageBox, 0, addr LoopSkipped, addr AppName, MB_OK+MB_ICONINFORMATION ....... .......このサンプルでは、"jmp $"命令をNOP命令に置き換えるのではなく、EIPレジスタの値をGetThreadContext関数により取得し、regEipに2を加え、実行コードを"スキップ"させている。
結果、デバッグ対象プログラムの処理が再開するときには、"jmp $"命令を飛び越えた状態になっており、ループを脱出している。Get/SetThreadContext関数の威力はどうだろうか? 今回の例ではEIPレジスタだけだったが、同様に他のレジスタも変更することができ、もちろんデバッグ対象プログラムへそれを反映させることができる。 しかも、デバッグ対象プログラムにブレークポイントを置くための int 3h 命令さえも可能なのだ。