デバッガーの学習、パート2

記事最初の部分では 、統合されたDelphiデバッガーを使用する際の微妙な違いを調べました。もちろん、すべてではありませんが、開発者にとって最も必要なものです。 タスクは少し違って見えます。ソースコードの例を使用して、内部からの作業を検討してください。 API関数の説明に飽きさせず、デバッグのすべての段階を噛まないために、TFWDebugerCoreクラスを例として使用して、その機能を説明します。 あまり重要ではない点のいくつかは省略します。必要に応じて、このクラスのコードを見ることでそれらを明確にすることができます。

デバッガーの作業に既に精通している場合-大丈夫です。記事で言及されている作業のいくつかの側面が興味を引く可能性があります。

デバッガーの独立した実装に一度も遭遇したことがないが、それに興味がある場合は、少なくとも次のリンクから始める必要があります。 デバッグとエラー処理
これを使用すると、例外の構造処理、デバッグ情報の操作、ミニダンプなど、デバッグの主な側面について学習できます。 実行可能ファイルのイメージ、ヘッダー、セクション、プロセスメモリカード、RVAとVAなどの操作など。
しかし、これは、このすべてのキッチンを理解したい場合のみです。

その一部のみをよりシンプルな言語で説明するようにします。そうすれば、突然興味を持つようになり、もちろんアプリケーション保護を実装する場合は、少なくともデバッガーの複雑さを理解する必要があります。 (それ以外の場合はどうですか?)。

記事のテキストには多くのコードが含まれますが、イベントのデバッグ時に各構造のすべてのパラメーターを考慮するわけではありません。これにはMSDNがあります。 作業に必要なデバッガーについてのみ説明し、デバッグエンジンを独立して実装しているときに発生する可能性が最も高い微妙な違いをいくつか明らかにします。

あなたから、少なくともアセンブラの最小限の知識を持つことが望ましい。 悲しいかな、この記事はそれなしではできません。



デバッガーをプロセスにアタッチすることから始めましょう。


プロセスに接続する前の最初のステップは、デバッグ特権を取得することです。 これは、次のような単純なコードで実行されます。

function SetDebugPriv: Boolean; var Token: THandle; tkp: TTokenPrivileges; begin Result := false; if OpenProcessToken(GetCurrentProcess, TOKEN_ADJUST_PRIVILEGES or TOKEN_QUERY, Token) then begin if LookupPrivilegeValue(nil, PChar('SeDebugPrivilege'), tkp.Privileges[0].Luid) then begin tkp.PrivilegeCount := 1; tkp.Privileges[0].Attributes := SE_PRIVILEGE_ENABLED; Result := AdjustTokenPrivileges(Token, False, tkp, 0, PTokenPrivileges(nil)^, PDWord(nil)^); end; end; end; 


次のステップは、すでに実行中のプロセスに参加するか、ゼロから新しいプロセスを開始するかです。

プロセスが既に実行されていて、参加したい場合は、プロセスのPIDを見つけて次のコードを実行するだけです。

 function TFWDebugerCore.AttachToProcess(ProcessID: DWORD; SentEntryPointBreakPoint: Boolean): Boolean; begin Result := False; if FProcessInfo.ProcessID <> 0 then Exit; FSetEntryPointBreakPoint := SentEntryPointBreakPoint; FProcessInfo.ProcessID := ProcessID; Result := DebugActiveProcess(ProcessID); end; 


確かに、このコードは少なくとも2つの理由で常に正常に実行されるとは限りません。

実際、Windowsでは、2つのデバッガーで同時にプロセスに接続することは不可能であり、デバッガーが必要なプロセスに既に接続されている場合、DebugActiveProcess関数の呼び出しは成功せず、GetLastErrorはエラーコードERROR_INVALID_PARAMETERを返します。

2番目の理由は、必要なプロセスがデバッガーよりも高い特権で開始されていることです。 この場合、DebugActiveProcess関数の呼び出しも失敗し、GetLastErrorはエラーコードERROR_ACCESS_DENIEDを返します。

2番目の場合、必要な特権でデバッガーを実行することにより、このエラーを回避できます。

2番目のオプションは、次のコードでプロセスを開始することにより、デバッガーをプロセスにアタッチすることです。

 function TFWDebugerCore.DebugNewProcess(const FilePath: string; SentEntryPointBreakPoint: Boolean): Boolean; var PI: TProcessInformation; SI: TStartupInfo; begin Result := False; if FProcessInfo.ProcessID <> 0 then Exit; FSetEntryPointBreakPoint := SentEntryPointBreakPoint; ZeroMemory(@SI, SizeOf(TStartupInfo)); SI.cb := SizeOf(TStartupInfo); Result := CreateProcess(PChar(FilePath), nil, nil, nil, False, DEBUG_PROCESS or DEBUG_ONLY_THIS_PROCESS, nil, nil, SI, PI); if Result then begin FProcessInfo.ProcessID := PI.dwProcessId; FProcessInfo.CreatedProcessHandle := PI.hProcess; FProcessInfo.CreatedThreadHandle := PI.hThread; end; end; 


ここではすべてが簡単です。上記のコードでは、プロセスをDEBUG_PROCESSフラグで開始し、さらにDEBUG_ONLY_THIS_PROCESSフラグを指定して、デバッグされたプロセスによって作成されたプロセスをデバッグしないことを示しています。
プロセスを開始した後、そのパラメーターを覚えておいてください(有用)。

デバッガーとしてプロセスに参加するとすぐに、このプロセスは単独で動作を停止し、各動作のチームを待機します-次に何をすべきか。 これを行うために、彼はデバッグイベントを生成し、それらに反応するまで待機します。

WaitForDebugEvent関数を呼び出してデバッグプロセスからデバッグイベントを取得できます。その後、ContinueDebugEvent関数を呼び出して必要なアクションを実行し、制御を返すことができます。その後、次のイベントを待つ必要があります。
つまり 大体、デバッグイベントループを実装する必要があります。

MSNDでは、次のデバッグイベントループの実装を推奨しています。

デバッガーのメインループの記述

デバッガでもほぼ同じように実装します。

 procedure TFWDebugerCore.RunMainLoop; var DebugEvent: TDebugEvent; CallNextLoopIteration: Boolean; ThreadIndex: Integer; begin CallNextLoopIteration := False; repeat ContinueStatus := DBG_CONTINUE; if not WaitForDebugEvent(DebugEvent, MainLoopWaitPeriod) then begin if GetLastError = ERROR_SEM_TIMEOUT then begin DoIdle; if FProcessInfo.ProcessID = 0 then Exit; CallNextLoopIteration := True; Continue; end else begin DoMainLoopFailed; Break; end; end; case DebugEvent.dwDebugEventCode of CREATE_THREAD_DEBUG_EVENT: DoCreateThread(DebugEvent); CREATE_PROCESS_DEBUG_EVENT: DoCreateProcess(DebugEvent); EXIT_THREAD_DEBUG_EVENT: DoExitThread(DebugEvent); EXIT_PROCESS_DEBUG_EVENT: begin DoExitProcess(DebugEvent); Break; end; LOAD_DLL_DEBUG_EVENT: DoLoadDll(DebugEvent); UNLOAD_DLL_DEBUG_EVENT: DoUnLoadDll(DebugEvent); OUTPUT_DEBUG_STRING_EVENT: DoDebugString(DebugEvent); RIP_EVENT: DoRip(DebugEvent); EXCEPTION_DEBUG_EVENT: begin ThreadIndex := GetThreadIndex(DebugEvent.dwThreadId); case DebugEvent.Exception.ExceptionRecord.ExceptionCode of EXCEPTION_BREAKPOINT: ProcessExceptionBreakPoint(ThreadIndex, DebugEvent); EXCEPTION_SINGLE_STEP: ProcessExceptionSingleStep(ThreadIndex, DebugEvent); EXCEPTION_GUARD_PAGE: ProcessExceptionGuardPage(ThreadIndex, DebugEvent); else CallUnhandledExceptionEvents(ThreadIndex, CodeDataToExceptionCode( DebugEvent.Exception.ExceptionRecord.ExceptionCode), DebugEvent); end; end; end; CallNextLoopIteration := ContinueDebugEvent(DebugEvent.dwProcessId, DebugEvent.dwThreadId, ContinueStatus); until not CallNextLoopIteration; end; 


デバッグのプロセスでは、絶えずこのサイクル内にいて、既知のイベントを処理します。 デバッグループの各反復でのWaitForDebugEvent関数は、DEBUG_EVENT構造体を返します。 この構造体のdwDebugEventCodeパラメーターに基づいて、受信したイベントのタイプ、イベントが発生したプロセスIDとスレッド、およびこの構造体の最後のフィールドによって結合の形で表示される各イベントのパラメーターを識別できます。

 PDebugEvent = ^TDebugEvent; _DEBUG_EVENT = record dwDebugEventCode: DWORD; dwProcessId: DWORD; dwThreadId: DWORD; case Integer of 0: (Exception: TExceptionDebugInfo); 1: (CreateThread: TCreateThreadDebugInfo); 2: (CreateProcessInfo: TCreateProcessDebugInfo); 3: (ExitThread: TExitThreadDebugInfo); 4: (ExitProcess: TExitProcessDebugInfo); 5: (LoadDll: TLoadDLLDebugInfo); 6: (UnloadDll: TUnloadDLLDebugInfo); 7: (DebugString: TOutputDebugStringInfo); 8: (RipInfo: TRIPInfo); end; {$EXTERNALSYM _DEBUG_EVENT} TDebugEvent = _DEBUG_EVENT; DEBUG_EVENT = _DEBUG_EVENT; {$EXTERNALSYM DEBUG_EVENT} 


各イベントには独自のパラメーターセットがありますが、それらについては後ほど詳しく説明します。

いずれかのイベントがコードで処理されない場合、ContinueStatusパラメーターをDBG_CONTINUEに設定してContinueDebugEvent関数を呼び出し、デバッグされたプロセスを実行し続けるだけです。

ニュアンス:WaitForDebugEventがエラーを返した場合(タイムアウトなど)、ContinueDebugEventを呼び出さないでください。エラーも返します。 この時点で、彼らはしばしばつまずきます。あなた自身のデバッガの実装でそれを考慮することを忘れないでください。

これまでのところ、すべてが非常に単純です。次に、どのイベントが私たちに与えるかを見てみましょう。

CREATE_PROCESS_DEBUG_EVENT:


デバッグの開始時にデバッガーが受け取る最初のイベント。 プロセスを自分で開始するか、DebugActiveProcessを呼び出してプロセスに参加するかは関係ありません。作業を開始します。 このイベントのパラメーターは、 DebugEvent.CreateProcessInfo構造(CREATE_PROCESS_DEBUG_INFO構造)に保存されます。

一般に、このイベントのハンドラーは次のようになります。

 procedure TFWDebugerCore.DoCreateProcess(DebugEvent: TDebugEvent); begin //     FProcessInfo.AttachedFileHandle := DebugEvent.CreateProcessInfo.hFile; FProcessInfo.AttachedProcessHandle := DebugEvent.CreateProcessInfo.hProcess; FProcessInfo.AttachedThreadHandle := DebugEvent.CreateProcessInfo.hThread; FProcessInfo.EntryPoint := DWORD(DebugEvent.CreateProcessInfo.lpStartAddress); AddThread(DebugEvent.dwThreadId, FProcessInfo.AttachedThreadHandle); //  BreakPoint     if FSetEntryPointBreakPoint then SetBreakpoint(FProcessInfo.EntryPoint, 'Process Entry Point Breakpoint'); if Assigned(FCreateProcess) then begin FCreateProcess(Self, GetThreadIndex(DebugEvent.dwThreadId), DebugEvent.CreateProcessInfo); DoResumeAction(GetThreadIndex(DebugEvent.dwThreadId)); end; end; 


その中で、プロセスのパラメーターを覚えているだけでなく、プロセスのメインスレッドのIDとハンドルを内部リストに追加します。 これらのデータは後で役立ちます。

ここで、プロセスエントリポイント(エントリポイント)を決定し、その値をDebugEvent.CreateProcessInfo.lpStartAddresパラメーターに記録し、必要に応じて、そのアドレスにブレークポイント(以降BPと呼びます)を設定し、実行するプロセスを開始します。 少し粗い場合は、このアクションを実行することにより、F7ボタンを押したときのDelphiデバッガーの動作をシミュレートします。

エントリポイントとは:ローダーがプロセスを作成するとき、プロセスが開始する瞬間まで、多くの準備アクションが実行されます。 アプリケーションのメインスレッドの作成、スタックの設定、環境/スレッドブロックの処理、ライブラリのロード、TLSコールバックの実行など。 これらすべてが完了した後にのみ、ローダーは、プログラマーによって実装されたコードが既に開始されているエントリポイントに制御を直接転送します。 このポイントのアドレスは、PEファイルのヘッダーに直接格納されます。PEファイルの構造を表示するアプリケーション(PEiDやPeExplorerなど)から取得できます。または、ファイルの先頭にあるTImageDosHeader構造を読み取ることでこの値を読み取ることができ、その_lfanewフィールドはオフセットされますTImageNtHeadersを起動します。その後、TImageNtHeaders構造自体を読み取り、TImageNtHeaders.OptionalHeader.AddressOfEntryPointフィールドの値を確認します。

空のプロジェクトをコンパイルして、F7を押してから[CPU-View]タブに移動すると、次のようになります。

画像

エントリポイントアドレスが判明しました:0x0043E2D4。 ここで、PEiDが結果のアプリケーションについて何を伝えているかを見てみましょう。

画像

彼は、エントリポイントの値は0x0003E2D4だと言います。

AddressOfEntryPointパラメーターに格納された値はRVA(相対仮想アドレス)として表されるため、デバッガーで見た数と一致しませんが、それでもここではすべてが正しいです。 このアドレッシングの特徴は、モジュール(hInstance)のロードアドレスを考慮しないことです。 RVAアドレスからVA(仮想アドレス)を取得するには、それにhInstanceモジュールを追加する必要があります。

微妙な違いがあります。これはアプリケーションにのみ当てはまりますが、ライブラリーでは少し異なります。 彼らにとっては、セクションのアドレスに集中する必要があります。 詳細については、このデモ「ファイルプロパティブックマークの実装」を参照してください。
DebugHlp.pasモジュールでは、ImageRvaToVa()関数の実装が提供されます。これにより、アドレスをキャストするためのルールを明確に調べることができます。

アプリケーションの場合、ベースダウンロードアドレスは常に、Image Baseパラメーターのリンカー設定で指定された値に等しく、デフォルトでは0x00400000です。 これらの2つの数値を追加すると、必要な0x0043E2D4が取得されます。

LOAD_DLL_DEBUG_EVENT:


CREATE_PROCESS_DEBUG_EVENTの直後に、 DebugEvent.LoadDll構造体(LOAD_DLL_DEBUG_INFO構造体)のパラメーターを使用して、ライブラリ読み込みイベントの受信を開始します

一般的なケースでは、Delphiデバッガーでライブラリのロードを観察できます。これにより、イベントログにロードの通知が表示されます。

画像

このイベントを受け取ると、モジュールをロードするためにBPがインストールされている場合、Delphiデバッガーはロード直後に中断されます。

画像

このコードを使用して、モジュールのロードについてユーザーに通知することもできます。

 procedure TFWDebugerCore.DoLoadDll(DebugEvent: TDebugEvent); begin if Assigned(FLoadDll) then begin FLoadDll(Self, GetThreadIndex(DebugEvent.dwThreadId), DebugEvent.LoadDll); DoResumeAction; end; CloseHandle(DebugEvent.LoadDll.hFile); end; 


この場合、イベントを発生させることに加えて、ロードされたライブラリのハンドルをすぐに閉じますが、それはもう必要ありません(このバージョンのデバッガー実装では)。

ニュアンスは次のとおりです。DebugEvent.LoadDll.lpImageNameパラメーターに格納されている、ロードされたライブラリへのパスを持つアドレスはアドレス空間にないため、ReadProcessMemoryを介して読み取る必要があります。
2番目のニュアンス:この値は、パスに関するデータが配置されているバッファーへのポインターでもあります。 少なくとも2回読む必要があります。
3番目の注意事項:パスは、AnsiiとUnicodeエンコーディングの両方である可能性があります。
さて、おやつについては、4番目の注意事項:データを読み取れない場合があります:)

ロード可能なライブラリへの有効なパスを取得するために、TFWDebugerCoreクラスは、これらすべてのポイントを考慮するGetDllNameメソッドを提供します。

実装を検討してください。
TFWDebugerCoreクラスは、外部のOnLoadDllイベントを呼び出してライブラリのロードを通知します。ここで、次のコードを記述します。

 procedure TdlgDebuger.OnLoadDll(Sender: TObject; ThreadIndex: Integer; Data: TLoadDLLDebugInfo); const FormatStrKnownDLL = 'Load Dll at instance %p handle %d "%s"'; FormatStrUnknownDLL = 'Load unknown Dll at instance %p handle %d'; var DllName: AnsiString; IsUnicodeData: Boolean; begin FCore.ContinueStatus := DBG_EXCEPTION_NOT_HANDLED; IsUnicodeData := Data.fUnicode = 1; DllName := FCore.GetDllName(Data.lpImageName, Data.lpBaseOfDll, IsUnicodeData); if DllName <> '' then begin if IsUnicodeData then Writeln(Format(FormatStrKnownDLL, [Data.lpBaseOfDll, Data.hFile, PWideChar(@DllName[1])])) else Writeln(Format(FormatStrKnownDLL, [Data.lpBaseOfDll, Data.hFile, PAnsiChar(@DllName[1])])); end else Writeln(Format(FormatStrUnknownDLL, [Data.lpBaseOfDll, Data.hFile])); end; 


ここでは、TFWDebugerCore.GetDllName()メソッドを呼び出し、(fUnicodeパラメーターに焦点を当てて)データをコンソールに出力します。

GetDllNameメソッドの実装は次のとおりです。

 function TFWDebugerCore.ReadData(AddrPrt, ResultPtr: Pointer; DataSize: Integer): Boolean; var Dummy: DWORD; begin Result := ReadProcessMemory(FProcessInfo.AttachedProcessHandle, AddrPrt, ResultPtr, DataSize, Dummy) and (Integer(Dummy) = DataSize); end; function TFWDebugerCore.ReadStringA(AddrPrt: Pointer; DataSize: Integer): AnsiString; begin SetLength(Result, DataSize); if not ReadData(AddrPrt, @Result[1], DataSize) then Result := ''; end; function GetMappedFileNameA(hProcess: THandle; lpv: Pointer; lpFilename: LPSTR; nSize: DWORD): DWORD; stdcall; external 'psapi.dll'; function TFWDebugerCore.GetDllName(lpImageName, lpBaseOfDll: Pointer; var Unicode: Boolean): AnsiString; var DllNameAddr: Pointer; MappedName: array [0..MAX_PATH - 1] of AnsiChar; begin if ReadData(lpImageName, @DllNameAddr, 4) then Result := ReadStringA(DllNameAddr, MAX_PATH); if Result = '' then begin if GetMappedFileNameA(FProcessInfo.AttachedProcessHandle, lpBaseOfDll, @MappedName[0], MAX_PATH) > 0 then begin Result := PAnsiChar(@MappedName[0]); Unicode := False; end; end; end; 


つまり、最初に、デバッグされたプロセスのアドレス空間(ReadData + ReadStringA)からデータを読み取ってライブラリパスを取得しようとします。うまくいかない場合は、GetMappedFileNameA関数を呼び出してこのデータを取得します。 シンボリックリンクを使用してデータを返すため、良好な結果を通常のパスに戻す必要がありますが、この場合はコードを過度に複雑にしないためにこれを行いませんでした。

CREATE_THREAD_DEBUG_EVENT


デバッグされたアプリケーションで新しいスレッドが作成されたときに、このイベントを受け取ります。 このイベントのパラメーターは、 DebugEvent.CreateThread構造体(CREATE_THREAD_DEBUG_INFO構造体)に保存されます。

すべてのパラメーターのうち、最も関心があるのはDebugEvent.CreateThread.hThreadです。これは内部リストに保存することが望ましいです。

ニュアンスは、ほとんどのイベントにはスレッドのIDのみに関するデータが含まれているため、それを使用する場合(たとえば、ハードウェアブレークポイントのインストール)、送信されたIDに対してOpenThread呼び出しを行う必要があります。 これらのアクションに煩わされないように、ThreadID = ThreadHandleのペアを独自のキャッシュに保持します。

このイベントのハンドラーコードは次のとおりです。

 procedure TFWDebugerCore.DoCreateThread(DebugEvent: TDebugEvent); begin AddThread(DebugEvent.dwThreadId, DebugEvent.CreateThread.hThread); if Assigned(FCreateThread) then begin FCreateThread(Self, GetThreadIndex(DebugEvent.dwThreadId), DebugEvent.CreateThread); DoResumeAction; end; end; 


スレッドのパラメーターを保存し、外部ハンドラーを呼び出すことに加えて、その中には何もありません。

OUTPUT_DEBUG_STRING_EVENT:


イベントは、デバッグされたアプリケーションがOutputDebugString関数の呼び出しに何かを通信しようとしているときに生成されます。 このイベントのパラメーターは、 DebugEvent.DebugString構造(OUTPUT_DEBUG_STRING_INFO構造)に保存されます。

イベントハンドラは簡単です。

 procedure TFWDebugerCore.DoDebugString(DebugEvent: TDebugEvent); begin if Assigned(FDebugString) then begin FDebugString(Self, GetThreadIndex(DebugEvent.dwThreadId), DebugEvent.DebugString); DoResumeAction; end; end; 


つまり ロード可能なライブラリへのパスを読み取るのと同じ原則に従って、送信された文字列を読み取る必要がある外部ハンドラを呼び出すだけです。

たとえば、次のように:

 procedure TdlgDebuger.OnDebugString(Sender: TObject; ThreadIndex: Integer; Data: TOutputDebugStringInfo); begin if Data.fUnicode = 1 then Writeln('DebugString: ' + PWideChar(FCore.ReadStringW(Data.lpDebugStringData, Data.nDebugStringLength))) else Writeln('DebugString: ' + PAnsiChar(FCore.ReadStringA(Data.lpDebugStringData, Data.nDebugStringLength))); end; 


ハンドラーでは、Data.fUnicodeパラメーターに焦点を合わせて、バッファーに渡されるエンコードを調べ、デバッガーコアのReadStringX()の対応する関数を呼び出します。

UNLOAD_DLL_DEBUG_EVENT、EXIT_THREAD_DEBUG_EVENT、EXIT_PROCESS_DEBUG_EVENT、RIP_EVENT:


ライブラリのアンロード、スレッドのクローズ、プロセスの終了、デバッガーコアでのエラー。
これらの4つのイベントはスキップします。 それらについて特別なものは何もありません。 それらのそれぞれを受け取ると、外部ハンドラーが呼び出され、デバッガーによって保存された内部リストが消去されます。
それらを使用する場合、ニュアンスはありません。

EXCEPTION_DEBUG_EVENT:


上記の8つのイベントはすべて、原則として二次的なものです。 メインの作業は、EXCEPTION_DEBUG_EVENTイベントの到着後にのみ開始されます。
そのパラメーターはDebugEvent.Exception構造(EXCEPTION_DEBUG_INFO構造)に送られます。

このイベントの生成は、デバッグされたアプリケーションで特定の例外が発生したことを意味し、そのタイプはDebugEvent.Exception.ExceptionRecord.ExceptionCodeパラメーターで見つけることができます。 記事の最初の部分で、デバッグは構造化エラー処理(SEH)メカニズムを介して行われることを覚えていますか? 次に、これについてさらに詳しく検討します。

デバッグプロセスのほとんどの例外が指摘されています。 つまり、例外を取得しても、プログラム自体でエラーが発生したわけではありません。 おそらく、BPのインストールなど、アプリケーションでのデバッガーの介入が原因で例外が発生しました。

ニュアンス:アプリケーション自体でエラーが発生した場合、デバッグ例外の形式でもエラーを受け取ります。また、ユーザーエラーと誘導されたエラーを区別できるように、デバッガコードを実装する必要があります。

通常、デバッガーはBPを操作するための3つのメカニズムを提供します(実際、この機能は従来のBPではないため、BPを考慮してモジュールをロードしない場合)。

  1. コード行ごとの標準BP。
  2. メモリアドレスへのBP(メモリブレークポイントまたはDelphiの切り捨てられたデータプリアックポイント)。
  3. ハードウェアBP(Delphiでは使用不可)。


それらを使用するには、3種類の例外を処理するだけで十分です。

 EXCEPTION_DEBUG_EVENT: begin ThreadIndex := GetThreadIndex(DebugEvent.dwThreadId); case DebugEvent.Exception.ExceptionRecord.ExceptionCode of EXCEPTION_BREAKPOINT: ProcessExceptionBreakPoint(ThreadIndex, DebugEvent); EXCEPTION_SINGLE_STEP: ProcessExceptionSingleStep(ThreadIndex, DebugEvent); EXCEPTION_GUARD_PAGE: ProcessExceptionGuardPage(ThreadIndex, DebugEvent); else CallUnhandledExceptionEvents(ThreadIndex, CodeDataToExceptionCode( DebugEvent.Exception.ExceptionRecord.ExceptionCode), DebugEvent); end; end; 


これら3つの例外だけで十分な理由をより明確にするために、EXCEPTION_DEBUG_EVENTイベントを処理するためのロジックの分析に進む前に、各タイプのBPを設定するメカニズムを最初に検討する必要があります。

コード行にブレークポイントを実装する:

コード行へのBPのインストールは、デバッグされたアプリケーションのコードを変更することにより行われます。 古典的には、これはBPによって設定されたアドレスに命令「INT3」を意味する0xCCオペコードを書き込むことによって行われます。

他のオプション、たとえば0xCD03オペコードもあります。これは「INT3」命令でもあります。 2番目のオプションは、アンチデバッグのためにより多く使用され、ほとんどの場合、アプリケーション自体によってインストールされます。核_KiTrap03()はシングルバイトオペコードでのみ動作し、ダブルバイトオペコードをわずかに誤って処理するという事実でデバッガーの存在をキャッチしようとします。

しかし、これはすべて歌詞です。最初のオペコードに興味があります。

インストールされたBPのリストを保存するために、TFWDebugerCoreクラスは次の構造を使用します。

 //      ( ) TBreakpointType = ( btBreakpoint, // WriteProcessMemoryEx + 0xCC btMemoryBreakpoint // VirtualProtectEx + PAGE_GUARD ); //         TInt3Breakpoint = record Address: Pointer; ByteCode: Byte; end; TMemotyBreakPoint = record Address: Pointer; Size: DWORD; BreakOnWrite: Boolean; RegionStart: Pointer; RegionSize: DWORD; PreviosRegionProtect: DWORD; end; TBreakpoint = packed record bpType: TBreakpointType; Description: ShortString; Active: Boolean; case Integer of 0: (Int3: TInt3Breakpoint;); 1: (Memory: TMemotyBreakPoint); end; TBreakpointList = array of TBreakpoint; 


新しいBPを追加する前に、彼はTBreakpointレコードを初期化して必要なパラメーターを入力し、それをブレークポイントの一般リストに追加します。

コード行のBPの場合、0xCCオペコードで消去する前に、BPのアドレスとこのアドレスに保存されているバイトの値の2つの値のみを保存する必要があります。

デバッグされたアプリケーションにBPをインストールすると、次のようになります。

 function TFWDebugerCore.SetBreakpoint(Address: DWORD; const Description: string): Boolean; var Breakpoint: TBreakpoint; OldProtect: DWORD; Dummy: DWORD; begin ZeroMemory(@Breakpoint, SizeOf(TBreakpoint)); Breakpoint.bpType := btBreakpoint; Breakpoint.Int3.Address := Pointer(Address); Breakpoint.Description := Description; Check(VirtualProtectEx(FProcessInfo.AttachedProcessHandle, Pointer(Address), 1, PAGE_READWRITE, OldProtect)); try Check(ReadProcessMemory(FProcessInfo.AttachedProcessHandle, Pointer(Address), @Breakpoint.Int3.ByteCode, 1, Dummy)); Check(WriteProcessMemory(FProcessInfo.AttachedProcessHandle, Pointer(Address), @BPOpcode, 1, Dummy)); finally Check(VirtualProtectEx(FProcessInfo.AttachedProcessHandle, Pointer(Address), 1, OldProtect, OldProtect)); end; Result := AddNewBreakPoint(Breakpoint); end; 


最初に、構造が初期化され、BPのタイプが確立され、その説明とパラメーターが設定されます。通常は書き込み権限を持たないコード領域に書き込むため、対応する権利が設定され、アドレスBPにある元の値が読み取られ、BPOpcode定数で表される0xCC命令が書き込まれ、元のページ属性がVirtualProtectEx()の繰り返し呼び出しによって返されます。すべての最後で、エラーが発生していない場合、インストールされたBPのレコードがクラスの一般リストに配置されます。

さて、今から楽しみが始まります:

BPのインストール後、デバッグされたアプリケーションは、当社が記録したINT3命令への移行が発生するまで通常の動作を続けます。この時点で、EXCEPTION_DEBUG_EVENTイベントが例外コードEXCEPTION_BREAKPOINTで発生します。

例外パラメーターは、DebugEvent.Exception.ExceptionRecord(EXCEPTION_DEBUG_INFO構造体)構造体の 形式で渡されます

前に説明したように、BPはデバッグされたアプリケーション自体によってインストールできます。したがって、これらのパラメーターに注目して、どのようなBPが機能したかを把握する必要がありますか?

これを行うには、以前に保存されたブレークポイントのリストが必要です。それを実行し、DebugEvent.Exception.ExceptionRecord.ExceptionAddressパラメーターに格納されたアドレスをbtBreakpointタイプの各レコードのAddressフィールドと比較すると、BPをこのアドレスにインストールしたか、それとも私たちのものではないかを判断できます。

BPが本当に私たちのものであると判断した場合は、外部イベントを発生させ(ここにいるだけでなく、動作していることをユーザーに示すため)、それを処理した後、デバッグされたアプリケーションを復元します。

BPをインストールした場合の結果:BPをインストールする

と、実行可能コードの一部が失われます。

たとえば、初期コードは次のとおりでした。

画像

操作後、次のようになりました。

画像

最初のステップは、元の命令の意味を復元することです。

これをより便利にするために、TBreakpoint構造には、ブレークポイントの状態を示すActiveパラメーターがあります。このパラメーターに注目して、TFWDebugerCoreクラスはアクティビティを認識し、状態を切り替えるためにToggleInt3Breakpointメソッドを実装します。このメソッドでは、フラグに応じてBPのオンとオフを切り替え、消去されたバイトをその場所に戻します。

 procedure TFWDebugerCore.ToggleInt3Breakpoint(Index: Integer; Active: Boolean); var OldProtect: DWORD; Dummy: DWORD; begin CheckBreakpointIndex(Index); if FBreakpointList[Index].bpType <> btBreakpoint then Exit; if FBreakpointList[Index].Active = Active then Exit; Check(VirtualProtectEx(FProcessInfo.AttachedProcessHandle, FBreakpointList[Index].Int3.Address, 1, PAGE_READWRITE, OldProtect)); try if Active then Check(WriteProcessMemory(FProcessInfo.AttachedProcessHandle, FBreakpointList[Index].Int3.Address, @BPOpcode, 1, Dummy)) else Check(WriteProcessMemory(FProcessInfo.AttachedProcessHandle, FBreakpointList[Index].Int3.Address, @FBreakpointList[Index].Int3.ByteCode, 1, Dummy)); finally Check(VirtualProtectEx(FProcessInfo.AttachedProcessHandle, FBreakpointList[Index].Int3.Address, 1, OldProtect, OldProtect)); end; FBreakpointList[Index].Active := Active; end; 


実装は、BPを再インストールするときに、バイト値を再度読み取る必要がないという点を除いて、BPを設定するコードとほぼ同じです(既にわかっているため)。

ここで注意点:デバッグしたアプリケーションを今実行すると、エラーが発生します。そして、すべて「INT3」命令がすでに実行されており、0x452220の場所に着用したバイトを戻したとしても、デバッグされたプログラムは、「mov ebp、esp」命令があり、どこからではなくアドレス0x452221から実行を継続しますデバッグ例外がスローされました。

2番目のニュアンス:確かにこの方向に夢を見ることができます:「さて、考えてみてください-「ebpをプッシュ」は失敗しました、スタックは浮き上がりましたが、私たちはデバッガーであり、何かが来てすべてを修正できる場合©」。つまり、1つの点を除いて、基本的に正しいです。

はい、この場合、デバッガーとしてスタックヘッダーを正しく移動し、満たされていない命令に注意を払うことはできませんが、アンチデバッグ(より正確には逆アセンブラーを難読化することを目的とし、デバッガーはそれに気付かない)として頻繁に使用されるトリックがあります指示の途中。

それが何であるか:

このテクニックは、逆アセンブラがマシンコードを常に正しく解釈できるわけではないという事実に基づいています。

例として、かなりの数の開発者が「長いNOP」などの概念を知っています。さらに、1年半前にはIntelのマニュアルにはありませんでしたが、NOP(コードの整列に使用される空の命令)は0x90オペコードの形式でのみ表示されると言われていました。したがって、ほとんどの逆アセンブラーはこのオペコードでのみ機能します。はい、しかし、なぜ大多数-私はまだ長い足を正しく認識する逆アセンブラーに会っていません。

逆アセンブラは正常に認識できないため、次のトリックを実行できます。

たとえば、3バイトのNOP(オペコード$ 0F、$ 1F、$ 00)を取得して、このコードを記述します。

 asm db $0F, $1F, $00 xor eax, eax inc eax neg eax end; 


そして、ここに逆アセンブラーが示すものがあります:

画像

すべての命令オペコードは正しく、正しく実行されますが、逆アセンブラーは最初に書いたものを絶対に示しません。

まあ、または2番目の例では、既に直接ジャンプを使用しています。ここでの考え方は、次の命令の開始前に、絶対左バイトが書き込まれ、その前に命令コード「jmp +1」が書き込まれ、プログラムがこのガベージバイトをスキップして目的のコードに直接移動するという事実にあります。それは平凡なことのように思えますが、逆アセンブラーは非常に混乱しています。

例として、次のコードを記述します。

 asm db $EB, $01 // jmp +1 (  xor  " ") db $B8 //   ""  xor eax, eax //   inc eax neg eax not eax sub edx, eax imul eax, edx nop nop end; 


さて、逆アセンブラーが私たちに何を示しているのか見てみましょう:

画像

期待通りの完全なゴミ。

BPに戻り、プッシュ$ 00452245命令の開始時に0x452220ではなく、さらに9バイトでインストールされたと想像してみましょう。

この命令は、シングルバイトの「プッシュepb」とは異なり、5バイトの形式で表示されます。消去されたバイトの値の回復が発生した後でも、コードを実行し続けると、命令の先頭からではなく、その途中から開始します。ここでは、逆アセンブラではなく、デバッガ自体が間違っています。方法:

画像

つまり元の「push $ 00452245」の代わりに、「inc epb」および「and al、[ebp + $ 00]」という命令が実行されますが、これはここではありませんでした。そして、いくつかの指示の後、予想されたもの、つまりエラーを取得します。

したがって、消去されたバイトをその場所に戻すだけでは十分ではなく、デバッグされたアプリケーションに正しいアドレスからプログラムを続行させる必要があります。次の反復で実行される命令のアドレスには、EIP(拡張命令ポインター)レジスタが格納されます。このレジスタへのアクセスは、割り込みが発生したデバッグ済みプロセスのスレッドコンテキストを介して実行されます。 EIPレジスタの値を変更するには、GetThreadContext関数を呼び出してコンテキストを取得し、Context.Eipパラメーターの現在の値を減らしてから、SetThreadContext関数を呼び出して新しい値を書き込む必要があります。

微妙な違いがあります:EXCEPTION_DEBUG_EVENTイベントが発生すると、DebugEvent.dwThreadIdパラメーターでスレッドIDのみを取得し、GetThreadContext()およびSetThreadContext()関数は作業にスレッドハンドルを必要とします。もちろん、OpenThread関数を呼び出すことで必要な値を取得できますが、この場合、ThreadID = ThreadHandleの形式で保存されたスレッドハンドルの保存リストがあるため、これを行う必要はありません。

これですべてが正しく行われ、消去されたバイトが復元され、命令の正しいアドレスが設定されたように見えます。実行のためにプログラムを実行することもできますが、もう1つあります。そして、0xCCオペコードを削除した後、BPレコードはデバッガーリストにのみ残っていましたが、デバッグされたアプリケーションには実際にはないため、以前にインストールされたBPをどうしますか?今プログラムを実行すると、現在の命令が実行され、停止することなく次の命令に移動し、他のBPまたはエラーが発生するまでプログラムコードの実行を続けます。

そのため、タスクが表示されたので、BPを削除したばかりの命令を実行した直後に、何らかの方法でアプリケーションを制御をデバッガーに転送する必要があります。成功すれば、BPを正当な場所に戻すことができます。

たとえば、現在の命令のサイズを計算し、次の命令の先頭に新しい一時的なBPを配置できますが、長さの逆アセンブラーを記述し、次の命令の先頭にBPをインストールできる瞬間を考慮する必要があります。実際、これは正しい決定ではありません。

この場合の正しい解決策は、プロセッサをトレースモードにすることです。
プロセッサフ​​ラグは、このモードを有効にします。このフラグが有効な場合、各命令は「INT1」割り込みをトリガーし、デバッグされたプロセスで例外をスローし、デバッガーは例外コードEXCEPTION_SINGLE_STEPでEXCEPTION_DEBUG_EVENTイベントを制御します。

このフラグは、EIPレジスタの値を変更したスレッドコンテキストを介して有効にできます。フラグの状態は、Context.EFlagsパラメーターによって制御されます。TFフラグは、このパラメーターの8番目のビットに格納されます。つまり 有効化するために単純化するには、次のようにします。

 const EFLAGS_TF = $100; // 8-  ... Context.EFlags := Context.EFlags or EFLAGS_TF; 


トレースニュアンス:「INT1」割り込みの特性は、TFフラグをリセットすることです。 つまりBPを復元するために1つの命令のみを実行する必要がある場合、TFフラグを元の状態に復元することを心配する必要がないため、この動作は完全に適しています。ただし、各命令のシーケンシャルトレースに関心がある場合は、各EXCEPTION_SINGLE_STEPハンドラーでTFフラグを個別に再発生する必要があります。このモードは後で検討します。

要約すると、コードアドレスでBPをインストールおよび処理するためのアルゴリズムは次のとおりです。



これで、DelphiデバッガーでF7ボタンを押したときに実際に実行されるアクションの数が最小限になりました:)

さて、これはTFWDebugerCoreクラスでの実装方法です。

EXCEPTION_BREAKPOINT例外ハンドラー:

 procedure TFWDebugerCore.ProcessExceptionBreakPoint(ThreadIndex: Integer; DebugEvent: TDebugEvent); var ReleaseBP: Boolean; BreakPointIndex: Integer; begin ReleaseBP := False; BreakPointIndex := GetBPIndex( DWORD(DebugEvent.Exception.ExceptionRecord.ExceptionAddress)); if BreakPointIndex >= 0 then begin if Assigned(FBreakPoint) then FBreakPoint(Self, ThreadIndex, DebugEvent.Exception.ExceptionRecord, BreakPointIndex, ReleaseBP) else CallUnhandledExceptionEvents(ThreadIndex, ecBreakpoint, DebugEvent); ToggleInt3Breakpoint(BreakPointIndex, False); SetSingleStepMode(ThreadIndex, True); if ReleaseBP then RemoveBreakpoint(BreakPointIndex) else FRestoreBPIndex := BreakPointIndex; end else CallUnhandledExceptionEvents(ThreadIndex, ecBreakpoint, DebugEvent); end; 


その中で、まずExceptionAddressパラメーターによって内部リストでBPインデックスを探します。
外部イベントを発生させます。
ToggleInt3Breakpointメソッドを呼び出して、BPをオフにします。
EIPを編集し、SetSingleStepModeメソッドを呼び出してトレースを有効にします。
外部イベントハンドラーのユーザーがこのBPを削除したいと言った場合、RemoveBreakpointを呼び出して削除します。
実行を継続する必要がある場合は、EXCEPTION_SINGLE_STEPハンドラーの現在のBPのインデックスを記憶します。この変数に焦点を当てると、デバッグされたプロセスでBPが復元されます。

SetSingleStepModeメソッドのコードは次のとおりです。

 procedure TFWDebugerCore.SetSingleStepMode(ThreadIndex: Integer; RestoreEIPAfterBP: Boolean); var Context: TContext; begin ZeroMemory(@Context, SizeOf(TContext)); Context.ContextFlags := CONTEXT_FULL; Check(GetThreadContext(FThreadList[ThreadIndex].ThreadHandle, Context)); if RestoreEIPAfterBP then Dec(Context.Eip); Context.EFlags := Context.EFlags or EFLAGS_TF; Check(SetThreadContext(FThreadList[ThreadIndex].ThreadHandle, Context)); end; 


ここではすべてが非常に簡単です。CONTEXT_FULLフラグを設定することで完全なスレッドコンテキストを取得します。
必要に応じて、EIPレジスタを編集
し、TFフラグをオンにします。
そして、新しいコンテキストを割り当てます。

RemoveBreakpointメソッドはさらに簡単です。

 procedure TFWDebugerCore.RemoveBreakpoint(Index: Integer); var Len: Integer; begin ToggleBreakpoint(Index, False); Len := BreakpointCount; if Len = 1 then SetLength(FBreakpointList, 0) else begin FBreakpointList[Index] := FBreakpointList[Len - 1]; SetLength(FBreakpointList, Len - 1); end; end; 


ここでは、BPがオフになるだけで、BPデータがデバッガーリストから削除されます。

これまでのところ、例外ハンドラEXCEPTION_SINGLE_STEPのコードは提供していません。BPの復元だけに使用されるわけではありません。記事の最後に、すべてのニュアンスが考慮されるときに表示します。

メモリアドレスにBPを実装する:


次のタイプのBPは、デバッグされたアプリケーションのメモリ内のデータ変更を制御するために使用されます。メモリブレークポイント(以降MBP)として知られています。

次のように実装されます。アプリケーションメモリ全体は、一覧表示して属性を取得できるページのセットとして表示されます。 (デモアプリケーションを参照してください:プロセスメモリカード)。あるアドレスにMBPを配置する場合、このアドレスが属するページの境界を計算し、VirtualProtectEx関数を呼び出してPAGE_GUARDフラグを設定する必要があります。

ニュアンス:確かにページアドレスを計算することはできませんが、単に必要なアドレスでVirtualProtectExを呼び出しますが、ページアドレス指定の特性は、他のすべてのアドレスを変更せずにページの小さなセクションの保護を変更できないことです。セキュリティ属性は常にページ全体に割り当てられます。したがって、1バイトだけで変更を追跡しようとすると、それに隣接するバイトにアクセスするときに、デバッガーも通知を受け取ります。

2番目の警告:ほとんどのデバッガーでは、同じページ内に同時に2つ以上のMBPをインストールすることはできません。この動作は、次の点が原因である可能性が最も高くなります。MBPをインストールする場合、MBPを削除するときにページの保護フィールドの現在の状態を戻す必要があります。ページでMBPが既に設定されている場合、その保護属性が変更されます。この点を回避するために、TFWDebugerCoreクラスでは次のアプローチが使用されます。新しいMBPをインストールするとき、このページを制御する別のMBPがあるかどうかが最初にチェックされます。見つかった場合、PreviosRegionProtectパラメーターの値を取ります。MDRがない場合、この値はVirtualProtectExを呼び出すことによって取得されます。

MVRインストールコードは次のようになります。

 function TFWDebugerCore.SetMemoryBreakpoint(Address: Pointer; Size: DWORD; BreakOnWrite: Boolean; const Description: string): Boolean; var Breakpoint: TBreakpoint; MBI: TMemoryBasicInformation; Index: Integer; begin Index := GetMBPIndex(DWORD(Address)); if (Index >= 0) and (FBreakpointList[Index].bpType = btMemoryBreakpoint) then begin MBI.BaseAddress := FBreakpointList[Index].Memory.RegionStart; MBI.RegionSize := FBreakpointList[Index].Memory.RegionSize; MBI.Protect := FBreakpointList[Index].Memory.PreviosRegionProtect; end else Check(VirtualQueryEx(DebugProcessData.AttachedProcessHandle, Address, MBI, SizeOf(TMemoryBasicInformation)) > 0); ZeroMemory(@Breakpoint, SizeOf(TBreakpoint)); Breakpoint.bpType := btMemoryBreakpoint; Breakpoint.Description := ShortString(Description); Breakpoint.Memory.Address := Address; Breakpoint.Memory.Size := Size; Breakpoint.Memory.BreakOnWrite := BreakOnWrite; Breakpoint.Memory.RegionStart := MBI.BaseAddress; Breakpoint.Memory.RegionSize := MBI.RegionSize; Check(VirtualProtectEx(FProcessInfo.AttachedProcessHandle, Address, Size, MBI.Protect or PAGE_GUARD, Breakpoint.Memory.PreviosRegionProtect)); if Index >= 0 then Breakpoint.Memory.PreviosRegionProtect := MBI.Protect; Result := AddNewBreakPoint(Breakpoint); end; 


MBPの場合、BPとは異なり、より多くのパラメーターを保存する必要があります。制御領域のアドレスとそのサイズに加えて、MBP自体を構成するパラメーター-BreakOnWriteが格納されます。これは、外部イベントをトリガーする条件を管理します(制御領域からデータを読み取るとき、または書き込むとき)。ページの先頭のアドレスとそのサイズも保存されます。

MVRのインストール後、プログラムを実行することができます。監視対象ページへのアクセスが発生するとすぐに、EXCEPTION_DEBUG_EVENTイベントがEXCEPTION_GUARD_PAGE例外コードとともに生成されます。

ここにもニュアンスがあります。ページにPAGE_GUARDフラグを設定すると、最初にアクセスしたときに例外が発生し、このフラグは削除されます。つまり、BPとは異なり、MVRの切断を個別に行う必要はありません。しかし、小さな問題があります。前述したように、MDRが設定されているアドレスだけでなく、ページがアクセスされるたびにEXCEPTION_GUARD_PAGE例外が発生するため、デバッガーはPAGE_GUARDフラグを復元して、確立されたMVRが正常に機能し続ける必要があります。

ハンドラコードは次のようになります。

 procedure TFWDebugerCore.ProcessExceptionGuardPage(ThreadIndex: Integer; DebugEvent: TDebugEvent); var CurrentMBPIndex: Integer; function CheckWriteMode: Boolean; begin Result := not FBreakpointList[CurrentMBPIndex].Memory.BreakOnWrite; if not Result then Result := DebugEvent.Exception.ExceptionRecord.ExceptionInformation[0] = 1; end; var MBPIndex: Integer; ReleaseMBP: Boolean; dwGuardedAddr: DWORD; begin ReleaseMBP := False; dwGuardedAddr := DebugEvent.Exception.ExceptionRecord.ExceptionInformation[1]; MBPIndex := GetMBPIndex(dwGuardedAddr); if MBPIndex >= 0 then begin CurrentMBPIndex := MBPIndex; while not CheckIsAddrInRealMemoryBPRegion(CurrentMBPIndex, dwGuardedAddr) do begin CurrentMBPIndex := GetMBPIndex(dwGuardedAddr, CurrentMBPIndex + 1); if CurrentMBPIndex < 0 then Break; end; if CurrentMBPIndex >= 0 then begin MBPIndex := CurrentMBPIndex; if Assigned(FBreakPoint) and CheckWriteMode then FBreakPoint(Self, ThreadIndex, DebugEvent.Exception.ExceptionRecord, MBPIndex, ReleaseMBP) else CallUnhandledExceptionEvents(ThreadIndex, ecGuard, DebugEvent); end else CallUnhandledExceptionEvents(ThreadIndex, ecGuard, DebugEvent); FBreakpointList[MBPIndex].Active := False; SetSingleStepMode(ThreadIndex, False); if ReleaseMBP then RemoveBreakpoint(MBPIndex) else FRestoreMBPIndex := MBPIndex; end else CallUnhandledExceptionEvents(ThreadIndex, ecGuard, DebugEvent); end; 


最初に、デバッガーは、例外が発生したために呼び出しが発生したアドレスを受け取ります。このアドレスは2番目のパラメーターとしてExceptionRecord.ExceptionInformation配列に格納され、操作フラグはこの配列の最初のパラメーターです。ゼロはアドレスでの読み取り試行を意味し、1はアドレスでの読み取り試行を意味します。
次に、CheckIsAddrInRealMemoryBPRegionを呼び出して適切なMDRを検索し、アドレスがMDRによって制御されるゾーンにあるかどうかを確認します。

適切なものが見つかった場合、BreakOnWriteパラメーターがチェックされます。
このパラメーターの値は、最初のパラメーターExceptionInformationの値と比較されます。 BreakOnWriteが有効な場合、ユニットがExceptionInformationにある場合にのみ外部イベントが呼び出され、それ以外の場合、BreakOnWriteが無効な場合、イベントは常に呼び出されます。

すべてのチェックの後、コードはBP処理と同様に実装されます。BP処理との唯一の違いは、この場合、EIPレジスタの値を編集する必要がないことです。これを行うには、2番目のパラメーターとしてSetSingleStepModeメソッドにFalseが渡されます。

同様に、キャプチャされたMVPの回復は、FRestoreMBPIndexインデックスに基づいてEXCEPTION_SINGLE_STEPハンドラーで発生します。

次のコードは、MBPのアクティビティを切り替える役割を果たします。

 procedure TFWDebugerCore.ToggleMemoryBreakpoint(Index: Integer; Active: Boolean); var Dummy: DWORD; begin CheckBreakpointIndex(Index); if FBreakpointList[Index].bpType <> btMemoryBreakpoint then Exit; if FBreakpointList[Index].Active = Active then Exit; if Active then Check(VirtualProtectEx(FProcessInfo.AttachedProcessHandle, FBreakpointList[Index].Memory.Address, FBreakpointList[Index].Memory.Size, FBreakpointList[Index].Memory.PreviosRegionProtect or PAGE_GUARD, Dummy)) else Check(VirtualProtectEx(FProcessInfo.AttachedProcessHandle, FBreakpointList[Index].Memory.Address, FBreakpointList[Index].Memory.Size, FBreakpointList[Index].Memory.PreviosRegionProtect, Dummy)); FBreakpointList[Index].Active := Active; end; 


MVRの削除は、BPと同じ方法で行われます。

原則として、BPとMVRにはいくつかのニュアンスを除いて、主要な違いはありません。それらはそれぞれのタスクに適用するだけです。

たとえば、時々MBPがトレーサーとして使用されます。この機能を実装するには、ICBMをコード領域に設定するだけで、デバッガーの起動後、リモートアプリケーションの現在のEIPの変更に関する通知の受信を開始します。かなり便利なことですが、同情はDelphiデバッガーでは実現できません。

このような使用の例を記事の最後に示します。次に、最後の3番目のタイプのブレークポイントに進みましょう。

ハードウェアブレークポイントの実装:


いわゆるハードウェアブレークポイント(以降HBP)。十分に強力なデバッグツール。 BPおよびMVRとは異なり、これらのブレークポイントはデバッグされたアプリケーションのメモリを変更しません。唯一の悪い点は、それらの数が非常に少なく、各スレッドに4つのピースしかないことです。

しかし、他のHBPとは異なり、デバッグされたアプリケーションを制御するためのかなり柔軟な条件を提供します。

比較:
BP-実行可能コードへのアクセスのみを追跡(実行モードなど);
MVR-読み取りまたは読み取り/書き込みモードでアクセスを追跡できます。
-条件をより正確に設定できます。書き込み、読み取り/書き込み、IOモード(入力/出力ポートへのアクセス)と実行モードのモードを区別します。

つまりHBPは、BP(実行モード)およびMBP(レコード-読み取り/書き込みモード)の両方の動作をエミュレートできます。確かに、MVRとは異なり、メモリ領域の広い範囲を制御することはできません。固定サイズ1、2、または4バイトのブロックでのみ機能します。

HBP設定は各スレッドのコンテキストに保存されます。このDRレジスタが使用されるため、CONTEXT_DEBUG_REGISTERSフラグが指定されたときにアクセスされます。
それらの6つがあります。 Dr0..Dr3、Dr6、Dr7。 (Dr4およびDr5は予約済みです)。
最初の4つのレジスタには、各HBPのアドレスが格納されます。レジスターDr7は、各HBPのパラメーターを微調整するために使用されます。レジスターDr6は、4つのHBPのいずれかの操作後に結果を読み取るのに役立ちます。

TFWDebugerCoreクラスは、HBPに関する情報を次の構造の形式で保存します。

 THWBPIndex = 0..3; THWBPSize = (hsByte, hdWord, hsDWord); THWBPMode = (hmExecute, hmWrite, hmIO, hmReadWrite); THardwareBreakpoint = packed record Address: array [THWBPIndex] of Pointer; Size: array [THWBPIndex] of THWBPSize; Mode: array [THWBPIndex] of THWBPMode; Description: array [THWBPIndex] of ShortString; Active: array [THWBPIndex] of Boolean; end; 


特定の各スレッドの4つのHBPにはすべて独自のものがあるため、BPクラスの一般リストには保存されません。
最初に、ペアID = hThreadHandleの形式で個別のリストにスレッドに関するデータを格納することを言ったことを思い出してください。実際、このリストは次のとおりです。

 TThreadData = record ThreadID: DWORD; ThreadHandle: THandle; Breakpoint: THardwareBreakpoint; end; TThreadList = array of TThreadData; 


つまりこれらの2つのパラメーターに加えて、各スレッドには、それに属するHBPの設定を記述する独自の構造があります。

インストール、状態の変更、HBPの削除は非常に簡単です。

インストールは次のようになります。

 procedure TFWDebugerCore.SetHardwareBreakpoint(ThreadIndex: Integer; Address: Pointer; Size: THWBPSize; Mode: THWBPMode; HWIndex: THWBPIndex; const Description: string); begin if ThreadIndex < 0 then Exit; FThreadList[ThreadIndex].Breakpoint.Address[HWIndex] := Address; FThreadList[ThreadIndex].Breakpoint.Size[HWIndex] := Size; FThreadList[ThreadIndex].Breakpoint.Mode[HWIndex] := Mode; FThreadList[ThreadIndex].Breakpoint.Description[HWIndex] := ShortString(Description); FThreadList[ThreadIndex].Breakpoint.Active[HWIndex] := True; UpdateHardwareBreakpoints(ThreadIndex); end; 


構造を初期化し、UpdateHardwareBreakpointsメソッドを呼び出すだけです。

状態の変更は、次のコードによって実装されます。

 procedure TFWDebugerCore.ToggleHardwareBreakpoint(ThreadIndex: Integer; Index: THWBPIndex; Active: Boolean); begin if ThreadIndex < 0 then Exit; if FThreadList[ThreadIndex].Breakpoint.Active[Index] = Active then Exit; FThreadList[ThreadIndex].Breakpoint.Active[Index] := Active; UpdateHardwareBreakpoints(ThreadIndex); end; 


Activeフラグを変更して、UpdateHardwareBreakpointsを再度呼び出します。

さて、削除:

 procedure TFWDebugerCore.DropHardwareBreakpoint(ThreadIndex: Integer; Index: THWBPIndex); begin if ThreadIndex < 0 then Exit; if FThreadList[ThreadIndex].Breakpoint.Address[Index] = nil then Exit; FThreadList[ThreadIndex].Breakpoint.Address[Index] := nil; UpdateHardwareBreakpoints(ThreadIndex); end; 


HBPアドレスをリセットし、UpdateHardwareBreakpointsを再度呼び出します。

微妙な違いは、UpdateHardwareBreakpointsメソッドにあります。
その主なタスクは、Dr0-Dr3レジスタにアクティブなHBPのアドレスを入力し、Dr7レジスタを正しく初期化することです。

ここでそれをいじる必要があります。

このレジスタは、各HBPの設定を定義するビットフラグのセットであり、正式にはすべてが次のようになります

。最も古い4ビット(31-28)は、Dr3レジスタの設定を保存します。
次のよう

になります。4つ上位2ビット(LENi)は、監視対象のHBPメモリのサイズを担当します。
00-1バイト
01-2バイト
10-このビットの組み合わせは使用されません。
11-4バイト

4の下位2ビット(RWi)は、HBPの動作モードを設定する役割を果たします
00-実行
01-書き込み
10-IO読み取り/書き込み
11-読み取り/書き込み

したがって、Dr3レジスタからのHBPが4バイトから始まる書き込みに応答するようにする場合Dr3で指定された値から、Dr7レジスタの上位4ビットは1101

ようになります。次の4ビット(27-24)は、Dr2レジスタのHBPの設定に使用されます。
ビット23-20はDr1を、最後にビット19-16はレジスタDr0を参照します。

Dr7レジスタのビット13(GD-グローバルデバッグレジスタアクセス検出)-デバッグレジスタ内のデータの整合性を管理します。たとえば、デバッグ中のプログラムがこれらのレジスタに値を保存することを突然決定した場合、デバッガーはこれについて通知されます。

Dr7レジスタのビット9(GE-グローバル完全一致データブレークポイント一致)-グローバルHBPでの作業を含みます。
Dr7レジスタのビット8(LE-Local Exactデータブレークポイントの一致)-ローカルHBPでの作業を有効にします。

タスクを切り替えるとLEビットがリセットされ、詳細はIntelのマニュアルに記載されています。

グローバルモードまたはローカルモードのHBPを含む各レジスタのGiフラグとLiフラグのペアとして表される残りの8ビット(7-0)があります。

ビット7(Gi-グローバルブレークポイントイネーブル)-Dr3レジスタのグローバルモードを有効にします;
ビット6(Li-ローカルブレークポイントイネーブル)-Dr3レジスタ5-4のローカルモードを有効にします;
Dr1のDr2
3-2とDr0の1-0は同じ

ですか?

さて、ここに写真があります:

画像

ソースコードの形式では、すべてが非常に単純に見えます。

 procedure TFWDebugerCore.UpdateHardwareBreakpoints(ThreadIndex: Integer); const DR7_SET_LOC_DR0 = $01; DR7_SET_GLB_DR0 = $02; DR7_SET_LOC_DR1 = $04; DR7_SET_GLB_DR1 = $08; DR7_SET_LOC_DR2 = $10; DR7_SET_GLB_DR2 = $20; DR7_SET_LOC_DR3 = $40; DR7_SET_GLB_DR3 = $80; DR7_SET_LOC_ON = $100; DR7_SET_GLB_ON = $200; DR7_PROTECT = $2000; DR_SIZE_BYTE = 0; DR_SIZE_WORD = 1; DR_SIZE_DWORD = 3; DR_MODE_E = 0; DR_MODE_W = 1; DR_MODE_I = 2; DR_MODE_R = 3; DR7_MODE_DR0_E = DR_MODE_E shl 16; DR7_MODE_DR0_W = DR_MODE_W shl 16; DR7_MODE_DR0_I = DR_MODE_I shl 16; DR7_MODE_DR0_R = DR_MODE_R shl 16; DR7_SIZE_DR0_B = DR_SIZE_BYTE shl 18; DR7_SIZE_DR0_W = DR_SIZE_WORD shl 18; DR7_SIZE_DR0_D = DR_SIZE_DWORD shl 18; DR7_MODE_DR1_E = DR_MODE_E shl 20; DR7_MODE_DR1_W = DR_MODE_W shl 20; DR7_MODE_DR1_I = DR_MODE_I shl 20; DR7_MODE_DR1_R = DR_MODE_R shl 20; DR7_SIZE_DR1_B = DR_SIZE_BYTE shl 22; DR7_SIZE_DR1_W = DR_SIZE_WORD shl 22; DR7_SIZE_DR1_D = DR_SIZE_DWORD shl 22; DR7_MODE_DR2_E = DR_MODE_E shl 24; DR7_MODE_DR2_W = DR_MODE_W shl 24; DR7_MODE_DR2_I = DR_MODE_I shl 24; DR7_MODE_DR2_R = DR_MODE_R shl 24; DR7_SIZE_DR2_B = DR_SIZE_BYTE shl 26; DR7_SIZE_DR2_W = DR_SIZE_WORD shl 26; DR7_SIZE_DR2_D = DR_SIZE_DWORD shl 26; DR7_MODE_DR3_E = DR_MODE_E shl 28; DR7_MODE_DR3_W = DR_MODE_W shl 28; DR7_MODE_DR3_I = DR_MODE_I shl 28; DR7_MODE_DR3_R = DR_MODE_R shl 28; DR7_SIZE_DR3_B = DR_SIZE_BYTE shl 30; DR7_SIZE_DR3_W = DR_SIZE_WORD shl 30; DR7_SIZE_DR3_D = $C0000000; //DR_SIZE_DWORD shl 30; DR_On: array [THWBPIndex] of DWORD = ( DR7_SET_LOC_DR0, DR7_SET_LOC_DR1, DR7_SET_LOC_DR2, DR7_SET_LOC_DR3 ); DR_Mode: array [THWBPIndex] of array [THWBPMode] of DWORD = ( (DR7_MODE_DR0_E, DR7_MODE_DR0_W, DR7_MODE_DR0_I, DR7_MODE_DR0_R), (DR7_MODE_DR1_E, DR7_MODE_DR1_W, DR7_MODE_DR1_I, DR7_MODE_DR1_R), (DR7_MODE_DR2_E, DR7_MODE_DR2_W, DR7_MODE_DR2_I, DR7_MODE_DR2_R), (DR7_MODE_DR3_E, DR7_MODE_DR3_W, DR7_MODE_DR3_I, DR7_MODE_DR3_R) ); DR_Size: array [THWBPIndex] of array [THWBPSize] of DWORD = ( (DR7_SIZE_DR0_B, DR7_SIZE_DR0_W, DR7_SIZE_DR0_D), (DR7_SIZE_DR1_B, DR7_SIZE_DR1_W, DR7_SIZE_DR1_D), (DR7_SIZE_DR2_B, DR7_SIZE_DR2_W, DR7_SIZE_DR2_D), (DR7_SIZE_DR3_B, DR7_SIZE_DR3_W, DR7_SIZE_DR3_D) ); var Context: TContext; I: THWBPIndex; begin if ThreadIndex < 0 then Exit; ZeroMemory(@Context, SizeOf(TContext)); Context.ContextFlags := CONTEXT_DEBUG_REGISTERS; for I := 0 to 3 do begin if not FThreadList[ThreadIndex].Breakpoint.Active[I] then Continue; if FThreadList[ThreadIndex].Breakpoint.Address[I] <> nil then begin Context.Dr7 := Context.Dr7 or DR7_SET_LOC_ON; case I of 0: Context.Dr0 := DWORD(FThreadList[ThreadIndex].Breakpoint.Address[I]); 1: Context.Dr1 := DWORD(FThreadList[ThreadIndex].Breakpoint.Address[I]); 2: Context.Dr2 := DWORD(FThreadList[ThreadIndex].Breakpoint.Address[I]); 3: Context.Dr3 := DWORD(FThreadList[ThreadIndex].Breakpoint.Address[I]); end; Context.Dr7 := Context.Dr7 or DR_On[I]; Context.Dr7 := Context.Dr7 or DR_Mode[I, FThreadList[ThreadIndex].Breakpoint.Mode[I]]; Context.Dr7 := Context.Dr7 or DR_Size[I, FThreadList[ThreadIndex].Breakpoint.Size[I]]; end; end; Check(SetThreadContext(FThreadList[ThreadIndex].ThreadHandle, Context)); end; 


コードの前にある定数のブロックに注意を払わない場合、Dr7レジスタの初期化は3行のみで実装されます。

 Context.Dr7 := Context.Dr7 or DR_On[I]; Context.Dr7 := Context.Dr7 or DR_Mode[I, FThreadList[ThreadIndex].Breakpoint.Mode[I]]; Context.Dr7 := Context.Dr7 or DR_Size[I, FThreadList[ThreadIndex].Breakpoint.Size[I]]; 


まあ、DR7_SET_LOC_ON定数で表されるLEビットを含めることは別です。

次に、HBPの処理について説明します。

BPがトリガーされると、コードEXCEPTION_BREAKPOINTを受け取りました。
MVRがトリガーされたとき、コードはEXCEPTION_GUARD_PAGEでした。
そして、HBPの中断時に、コードEXCEPTION_SINGLE_STEPを使用してEXCEPTION_DEBUG_EVENTイベントを生成します。これは、とりわけBPおよびMBPの状態を復元するために使用されます(したがって、記事の冒頭で実装を引用しませんでした)。

EXCEPTION_SINGLE_STEPを受信すると、最初のものとして実装されたHBPハンドラーは次のように呼び出されます。

 function TFWDebugerCore.ProcessHardwareBreakpoint(ThreadIndex: Integer; DebugEvent: TDebugEvent): Boolean; var Index: Integer; Context: TContext; ReleaseBP: Boolean; begin ZeroMemory(@Context, SizeOf(TContext)); Context.ContextFlags := CONTEXT_DEBUG_REGISTERS; Check(GetThreadContext(FThreadList[ThreadIndex].ThreadHandle, Context)); Result := Context.Dr6 and $F <> 0; if not Result then Exit; Index := -1; if Context.Dr6 and 1 <> 0 then Index := 0; if Context.Dr6 and 2 <> 0 then Index := 1; if Context.Dr6 and 4 <> 0 then Index := 2; if Context.Dr6 and 8 <> 0 then Index := 3; if Index < 0 then begin Result := False; Exit; end; ReleaseBP := False; if Assigned(FHardwareBreakpoint) then FHardwareBreakpoint(Self, ThreadIndex, DebugEvent.Exception.ExceptionRecord, Index, ReleaseBP); ToggleHardwareBreakpoint(ThreadIndex, Index, False); SetSingleStepMode(ThreadIndex, False); if ReleaseBP then DropHardwareBreakpoint(ThreadIndex, Index) else begin //   HWBP    , //  ..     //  ProcessExceptionSingleStep,   HWBP   //        HWBP if (FRestoredThread >= 0) and (FRestoredHWBPIndex >= 0) then ToggleHardwareBreakpoint(FRestoredThread, FRestoredHWBPIndex, True); FRestoredHWBPIndex := Index; FRestoredThread := ThreadIndex; end; end; 


そのタスクは、どの特定のHBPが中断を引き起こしたかを判断し、外部イベントをトリガーし、BPおよびMVRハンドラーですでに示されているアルゴリズムとほぼ同様のファイナライズアルゴリズムを実行することです。

HBP番号を決定するには、スレッドコンテキストからDr6レジスタの値を読み取る必要があります。
このレジスタの下位4ビットは、対応するDrXレジスタが機能している場合に値1をとるフラグです。

すべてが非常に簡単です。必要なNVRを決定した後、外部イベントを呼び出し、NVRをオフにして、プロセッサをトレースモードにし(EIPを編集せずに)、NVRを削除するか、そのインデックスを2つの変数に保存します。EXCEPTION_SINGLE_STEPハンドラーはNVRの状態を復元します

さて、論理的な結論に達したようです。
EXCEPTION_SINGLE_STEPハンドラー自体の実装を示すためだけに残ります。

次のようになります。

 procedure TFWDebugerCore.ProcessExceptionSingleStep(ThreadIndex: Integer; DebugEvent: TDebugEvent); var Handled: Boolean; begin //  HWBP Handled := ProcessHardwareBreakpoint(ThreadIndex, DebugEvent); //    - HWPB   HWBP if not Handled and (FRestoredThread >= 0) and (FRestoredHWBPIndex >= 0) then begin ToggleHardwareBreakpoint(FRestoredThread, FRestoredHWBPIndex, True); FRestoredThread := -1; FRestoredHWBPIndex := -1; end; //   if FRestoreBPIndex >= 0 then begin CheckBreakpointIndex(FRestoreBPIndex); if FBreakpointList[FRestoreBPIndex].bpType = btBreakpoint then ToggleInt3Breakpoint(FRestoreBPIndex, True); FRestoreBPIndex := -1; end; //  M if FRestoreMBPIndex >= 0 then begin CheckBreakpointIndex(FRestoreMBPIndex); if FBreakpointList[FRestoreMBPIndex].bpType = btMemoryBreakpoint then ToggleMemoryBreakpoint(FRestoreMBPIndex, True); FRestoreMBPIndex := -1; end; //         //     if ResumeAction <> raRun then begin CallUnhandledExceptionEvents(ThreadIndex, ecSingleStep, DebugEvent); //          DoResumeAction(ThreadIndex); end; end; 


彼の仕事は、HBPの停止により例外が生成されたかどうかを最初に判断することです。これが当てはまる場合、ToggleHardwareBreakpointを呼び出すことにより、HBPはその場所に戻ります。
BPまたはMVP処理後にトレースフラグがオンになったために例外が発生した場合、変数FRestoreBPIndexおよびFRestoreMBPIndexは、その場所に戻す必要があるブレークポイントのインデックスを示します。
そのタイプに応じて、ToggleInt3BreakpointまたはToggleMemoryBreakpointメソッドが呼び出されます。

練習:


デバッガー実装の説明でこれを終了しますが、時間がかかります-実際に見せたい点がいくつかあります。
飛行機についてのジョークのように:「今、このカヌー全体を空中に持ち上げようとします」:)

このためには、2つのアプリケーションを実装する必要があります。

最初に、デバッグを試みます。新しいVCLプロジェクトを作成し、「test_app」という名前で保存してから、プロジェクトをコンパイルします。

次に、デバッガアプリケーションを作成します。このために、2つのボタン(デバッグプロセスを開始および停止する)と、すべての情報が表示されるTMemoまたはTRichEditの十分なフォームがあります。

私たちは書きます:

 type TdlgDebuger = class(TForm) Panel1: TPanel; btnStart: TButton; btnStop: TButton; edLog: TRichEdit; procedure btnStartClick(Sender: TObject); procedure btnStopClick(Sender: TObject); private FCore: TFWDebugerCore; FNeedStop: Boolean; procedure Writeln(const Value: string = ''); end; ... procedure TdlgDebuger.btnStartClick(Sender: TObject); var Path: string; begin FNeedStop := False; //        Path := ExtractFilePath(ParamStr(0)) + '..\test_app\test_app.exe'; FCore := TFWDebugerCore.Create(50); try btnStart.Enabled := False; btnStop.Enabled := True; if not FCore.DebugNewProcess(Path, True) then RaiseLastOSError; FCore.RunMainLoop; finally FCore.Free; btnStart.Enabled := True; btnStop.Enabled := False; end; Writeln; Writeln('Debug stop'); end; procedure TdlgDebuger.Writeln(const Value: string); begin edLog.Lines.Add(Value); end; procedure TdlgDebuger.btnStopClick(Sender: TObject); begin FNeedStop := True; end; 

次のようなものが表示されるはずです。

画像

「開始」ボタンをクリックします。すべてが正しく完了したら、テストアプリケーションが起動します。

この場合、メインアプリケーションは、マウスとキーボードのコマンドに実質的に応答しなくなります。

実際には、起動後にTFWDebugerCore.RunMainLoopデバッグサイクル内に配置されるため、メッセージキューフェッチサイクルの実行が妨げられます。

テストアプリケーションを閉じます。これにより、デバッガーはデバッグサイクルを終了し、ウィンドウを操作できるようになります。

良い方法では、デバッガーを別のスレッドで実行する必要があります(むしろ、良い方法ではありませんが、これは正しいアプローチです)が、起動しなくても、TFWDebugerCoreクラスのOnIdleイベントを閉じることで通常の方法で作業できます。

 procedure TdlgDebuger.OnIdle(Sender: TObject); begin if FNeedStop then FCore.StopDebug else Application.ProcessMessages; end; 


Application.ProcessMessagesを呼び出しても、アプリケーションのプロセスが遅くなることはありません。

次に、Delphiデバッガーが表示する形式で、プロセスに関するデバッグ情報を取得してみましょう。これを行うには、OnCreateProcessハンドラーとOnLoadDllハンドラーを接続します。

最初に、以下を記述します。

 procedure TdlgDebuger.OnCreateProcess(Sender: TObject; ThreadIndex: Integer; Data: TCreateProcessDebugInfo); var T: TThreadData; begin T := FCore.GetThreadData(ThreadIndex); Writeln(Format('CreateThread ID: %d', [T.ThreadID])); Writeln(Format('ProcessStart ID: %d', [FCore.DebugProcessData.ProcessID])); end; 


第二に、このコード:

 procedure TdlgDebuger.OnLoadDll(Sender: TObject; ThreadIndex: Integer; Data: TLoadDLLDebugInfo); const FormatStrKnownDLL = 'Load Dll at instance %p handle %d "%s"'; FormatStrUnknownDLL = 'Load unknown Dll at instance %p handle %d'; var DllName: AnsiString; IsUnicodeData: Boolean; begin FCore.ContinueStatus := DBG_EXCEPTION_NOT_HANDLED; IsUnicodeData := Data.fUnicode = 1; DllName := FCore.GetDllName(Data.lpImageName, Data.lpBaseOfDll, IsUnicodeData); if DllName <> '' then begin if IsUnicodeData then Writeln(Format(FormatStrKnownDLL, [Data.lpBaseOfDll, Data.hFile, PWideChar(@DllName[1])])) else Writeln(Format(FormatStrKnownDLL, [Data.lpBaseOfDll, Data.hFile, PAnsiChar(@DllName[1])])); end else Writeln(Format(FormatStrUnknownDLL, [Data.lpBaseOfDll, Data.hFile])); end; 


その後、デバッグアプリケーションを再度起動し、[開始]ボタンを押します。

次の

画像

ようになります。

トレース実装:


それでは、デバッガで作業してみましょう。まず、トレースの2つのオプションを検討します。1つ目はTFフラグ、2つ目はMBPです。プログラムのエントリポイントから最初の40バイトをトレースします。すぐにそれらがどのように見えるか見てみましょう:

画像

良いトレースを開始するには、プロセスが初期化されるのを待つ必要があります。その後、BP / MVRなどを安全に設定できます。これを行うには、アプリケーションのエントリポイントにBPをインストールするようデバッガーに指示する必要があります。DebugNewProcess関数の2番目のパラメーターがこれを担当します。既にTrueに設定されており、このBPを処理するためだけに残っています。これを行うには、トレースモードを設定するOnBreakPointハンドラーを接続します。

 procedure TdlgDebuger.OnBreakPoint(Sender: TObject; ThreadIndex: Integer; ExceptionRecord: Windows.TExceptionRecord; BreakPointIndex: Integer; var ReleaseBreakpoint: Boolean); begin //      Writeln(Format('!!! --> Breakpoint "%s"', [FCore.BreakpointItem(BreakPointIndex).Description])); //   (    ) ReleaseBreakpoint := True; //    FCore.ResumeAction := raTraceInto; //     FStepCount := 0; end; 


トレースはOnSingleStepイベントの生成を通じて発生するため、これも実装します。

 procedure TdlgDebuger.OnSingleStep(Sender: TObject; ThreadIndex: Integer; ExceptionRecord: Windows.TExceptionRecord); begin //      Inc(FStepCount); Writeln(Format('!!! --> trace step №%d at addr 0x%p', [FStepCount, ExceptionRecord.ExceptionAddress])); //        if FStepCount > 10 then FCore.ResumeAction := raRun else FCore.ResumeAction := raTraceInto; end; 


結果は次のようになります

画像

。StepInトレースを実行しました。0x00409FF4で発生したトレースの5番目のステップはこれを明確に示しています。これは0x00409C53で呼び出される_InitExe()関数の始まりです。トレースはかなり遅いプロセスであり、_InitExe()関数から制御が戻るのを待ちませんでした。デモンストレーションのために、私は数十ステップに制限しました。

2番目のトレースモードは、MVRのインストールです。
それを実証するには、OnPageGuardイベントをブロックする必要があり、エントリポイントに到達したら、制御されたメモリ範囲を0にしてSetMemoryBreakpointメソッドを呼び出します。この場合、デバッガーはMBPによって監視されているページを認識しますが、このMBPのOnBreakPointハンドラーは呼び出されません。このトレースオプションの実装はあなたの裁量に任されています。ヒントをお伝えします。デバッグイベントハンドラーからRemoveBreakpointメソッドを呼び出さないことを強くお勧めします(インデックスは消えます)。 、またはいずれかのハンドラーで使用可能なRemoveCurrentBreakpointプロシージャ。おそらく、TFWDebugerCoreクラスの次の実装では、この動作が修正されます。しかし、当分の間、そのようなオプションは行くでしょう。

もちろん、トレースは良いのですが、記事の実際の部分では説明したくなかったので、デモにトレースの例はありません。

デバッグ文字列の受信:

開始するには、アプリケーションがOutputDebugString関数を使用してデバッガーに送信する文字列の受信を表示する必要がありました。これを行うには、テストアプリケーションにボタンを配置し、たとえば、ハンドラーで次のコードを記述します。

 // //    // ============================================================================= procedure TForm1.btnDebugStringClick(Sender: TObject); begin OutputDebugString('Test debug string'); end; 


次に、デバッガーで、次のコードを実装してOnDebugStringイベントを閉じます。

 procedure TdlgDebuger.OnDebugString(Sender: TObject; ThreadIndex: Integer; Data: TOutputDebugStringInfo); begin if Data.fUnicode = 1 then Writeln('DebugString: ' + PWideChar(FCore.ReadStringW(Data.lpDebugStringData, Data.nDebugStringLength))) else Writeln('DebugString: ' + PAnsiChar(FCore.ReadStringA(Data.lpDebugStringData, Data.nDebugStringLength))); end; 


デバッグされたアプリケーションでデバッガーを実行し、ボタンをクリックします。 「テストデバッグ文字列」というメッセージがログに表示されますか?はいの場合、すべてが正しく行われました:)

例外処理:

BPをアンチデバッグアプリケーションとして使用したことを思い出しましたか?次に、同じオプションについて検討してみましょう。テストアプリケーションで、別のボタンを追加し、次のコードを記述します。

 // //       // ============================================================================= procedure TForm1.btnExceptClick(Sender: TObject); begin try asm int 3 end; ShowMessage('Debugger detected.'); except ShowMessage('Debugger not found.'); end; end; 


原則として、これは反デバッグではありませんが、奇妙なことに、このような原始的なスキームでさえ、一部のリバーサが焼け付くことがあります。

このメソッドの本質は次のとおりです。記事の冒頭で、デバッグサイクルの例を示しました。その中で、各反復で、ContinueDebugEvent関数が呼び出されるContinueStatusパラメーターがDBG_CONTINUE定数で初期化されました。これはどういう意味ですか?これは、発生した例外をデバッガが正常に処理したことを示すシグナルであり、それをさらにいじる価値はありません。

さて、これが上記のコードの例で何を意味するのか:「INT3」命令を呼び出すことにより、例外を発生させます。アプリケーションの通常の操作中、この例外を処理する人はいないため、例外が発生すると、exception..endハンドラーへの遷移が発生します。デバッガーの下にいる場合、彼はこの例外をキャッチし、アプリケーションハンドラーは呼び出されません。

確認して、アプリケーションを起動し、このコードのボタンをクリックすると、正直に表示されます-デバッガーの下にあります。

このコードを克服するのも同じくらい簡単です。OnUnknownBreakPointイベントをブロックするだけで十分です(int3はブレークポイントであり、私たちによって設定されていないため、このイベントでキャッチされます)。イベントハンドラーで、次のコードを記述します。

 procedure TdlgDebuger.OnUnknownBreakPoint(Sender: TObject; ThreadIndex: Integer; ExceptionRecord: Windows.TExceptionRecord); var ApplicationBP: Boolean; begin ApplicationBP := (DWORD(ExceptionRecord.ExceptionAddress) > FCore.DebugProcessData.EntryPoint) and (DWORD(ExceptionRecord.ExceptionAddress) < $500000); Writeln; if ApplicationBP then begin Writeln(Format('!!! --> Unknown application breakpoint at addr 0X%p', [ExceptionRecord.ExceptionAddress])); Writeln('!!! --> Exception not handled.'); FCore.ContinueStatus := DBG_EXCEPTION_NOT_HANDLED; end else begin Writeln(Format('!!! --> Unknown breakpoint at addr 0X%p', [ExceptionRecord.ExceptionAddress])); Writeln('!!! --> Exception handled.'); FCore.ContinueStatus := DBG_CONTINUE; end; Writeln; end; 


BPがインストールされているアドレスに基づいて、すべてがシンプルです。アプリケーション本体での位置を決定します(アプリケーションのダウンロードアドレスから500,000ドルまでの範囲を大まかにします)。 BPがアプリケーションの本体にインストールされている場合、これはある種のアンチデバッグです。デバッガーにDBG_EXCEPTION_NOT_HANDLEDフラグを設定することで何をすべきかわからないことを伝えます。そうでない場合は、他の誰かがブレークポイントで遊んでいる情報を記録するだけです。

このようなジェスチャーの結果、アプリケーションによって人為的に発生した例外は処理されず、デバッガーが検出されなかったことを喜んで通知します:) スタックがオーバーフローするとどうなります

か:

さて、最後に見せたいのは、デバッガー側から見たスタックオーバーフローの様子です。次のように、以前の記事の1つからのオーバーフローの例を取り上げます。

 // //      // ============================================================================= procedure TForm1.btnKillStackClick(Sender: TObject); procedure T; var HugeBuff: array [0..10000] of DWORD; begin if HugeBuff[0] <> HugeBuff[10000] then Inc(HugeBuff[0]); T; end; begin try T; except T; end; end; 


このコードをテストアプリケーションに追加し、ボタンをクリックします。デバッガの応答は異なる場合がありますが、結果は常に同じです-デバッガは非常に悪くなります。この場合はどうなりますか?スタックオーバーフローを検出するメカニズムは非常に単純で、交差できない境界線は、PAGE_GUARDフラグでマークされた別のページで表されます。ええ、はい、これはMVRをセットアップするのと同じメカニズムですが、この場合は他の目的に使用されます。オーバーフローすると、EXCEPTION_STACK_OVERFLOWはデバッガーへの初期通知を受け取ります。原則として、ここで「オールを乾燥」してデバッガーをシャットダウンできますが、私たちは永続的であり、テストアプリケーションをさらに起動します。 PAGE_GUARDフラグのニュアンスは、最初の呼び出し後に削除されることを覚えている場合、同じケースです。このページに再度アクセスすると、EXCEPTION_ACCESS_VIOLATION以外は何もキャッチされず、ここでは本当に「すべて」であり、もうふらつくのは意味がありません。DBG_CONTROL_Cを設定してデバッグを停止するだけです(もちろん、永久的なサイクルを観察したい場合を除きます) AVの発行と)。

OnUnknownExceptionイベントにオーバーフローハンドラを実装します。TFWDebugerCoreは、これら2つの例外を個別のイベントとしてスローしません。その中に以下を書きます。

 procedure TdlgDebuger.OnUnknownException(Sender: TObject; ThreadIndex: Integer; ExceptionRecord: Windows.TExceptionRecord); var Cause: string; begin Writeln; case ExceptionRecord.ExceptionCode of EXCEPTION_STACK_OVERFLOW: begin Writeln('!!! --> Stack overflow detected. Probe to continue.'); FCore.ContinueStatus := DBG_CONTINUE; end; EXCEPTION_ACCESS_VIOLATION: begin { The first element of the array contains a read-write flag that indicates the type of operation that caused the access violation. If this value is zero, the thread attempted to read the inaccessible data. If this value is 1, the thread attempted to write to an inaccessible address. If this value is 8, the thread causes a user-mode data execution prevention (DEP) violation. The second array element specifies the virtual address of the inaccessible data. } case ExceptionRecord.ExceptionInformation[0] of 0: Cause := 'read'; 1: Cause := 'write'; 8: Cause := 'DEP violation'; else Cause := 'unknown cause'; end; Writeln(Format('!!! --> Access violation at addr 0x%p %s of address 0x%p', [ ExceptionRecord.ExceptionAddress, Cause, Pointer(PDWORD(@ExceptionRecord.ExceptionInformation[1])^) ])); Writeln('!!! --> Process Stopped.'); FCore.ContinueStatus := DBG_CONTROL_C; end; else Writeln(Format('!!! --> Unknown exception code %p at addr 0x%p', [ Pointer(ExceptionRecord.ExceptionCode), ExceptionRecord.ExceptionAddress ])); end; Writeln; end; 


次のようになります。

画像

要約すると:


基本的に、これがデバッガの実装について伝えたかったことのすべてです。明らかにされていないトピックがいくつかありますが、それらは第3部にあります。
記事が非常に膨大なものになったことを後悔していますが、残念ながら、それを小さな部分に分割することはできませんでした。少なくとも私は乾燥した事実ではなく、技術記事では通常省略されているニュアンスに最大限の注意を払うようにしました。
私は材料が有用である誰に人々があることを願っています:)

記事へのソースコードは、このリンクからダウンロードすることができます:http://rouse.drkb.ru/blog/dbg_part2.zip

さて、この記事の第三部では、我々は対立のデバッガアプリケーションことを見ていきます本当にデバッグしたくない:)

そして、これには本当にすべてがあります。

©Alexander(Rouse_)ベーグル
モスクワ、2012年11月

Source: https://habr.com/ru/post/J178159/


All Articles