プロセスメモリカード

プログラムで使用できるメモリがどのように使用されているのか、実際にソフトウェアが動作するこれらの2ギガバイトまたは3ギガバイトの仮想メモリに正確に何が配置されているのか疑問に思ったことはありませんか?

理由を尋ねますか?
32ビットアプリケーションの場合、AWEを使用しないと2〜3ギガバイトを超えることはできませんが、それでも自分のリソースを制御することが望ましいです。 しかし、それがなくても、単にそれを把握するために...

以前の記事で、デバッグ中のアプリケーションのメモリが変更されたデバッガーの動作について説明しました。 この記事は、この資料の続きです。 そして、それはデバッガとは関係ありませんが、デバッグプロセスは最も直接的です...

プログラマーがデバッグ中にメモリを操作する方法を見てみましょう(特に、サードパーティのアプリケーションをデバッグするとき、言い換えると、リバースするとき)。

1.原則として、最も頻繁に行われる操作は、アプリケーションメモリ内の値を検索することです。残念ながら、この機能は何らかの理由でDelphiデバッガーでは提供されません(実際、MS VC ++など)。
2.システム構造(PEB / TEB / SEHChain / Unwind / PEファイルのディレクトリなど)の変更は、構造のフィールドが占有し、読み取り可能な形式で表示されるアドレスにマッピング解除されると、はるかに簡単になります。
3.プロセスメモリの変更の追跡(ほとんどの機能は提供されず、一般的なデバッガーのプラグインとして実装されます)。 実際、必要なデータ変更がここで行われているかどうかを理解するために、メモリカードの2つの画像を比較するだけで十分なのに、なぜ青みがかった色にトレースするのでしょうか。

はい、実際、多くのユースケースがあります。

ただし、歌詞がない場合、デバッグに使用できるプロセスメモリカードに関する多少の正気な情報を表示するユーティリティはほとんどありません。

最も便利な実装はOllyDebug 2からですが、残念ながら、64ビットでデータを表示しません(まだ待っています)。


Mark RussinovichのVMMapは、Microsoftによって署名された純粋に装飾的なプロパティを実行しますが、表示されたデータを実際に適用することは困難です。


ProcessHackerは優れたツールですが、その作成者はメモリデータの出力を操作するタスクを設定していなかったため、彼が表示する情報は一般的に最も単純であると言えます。


まあ、私はIDA Proのメモリーカードを長年使ってきたのに慣れていません(私は快適ではありません):)

ただし、有効なメモリカードが役に立つのは、デバッグだけではありません。 特に、仕事では、メモリカードを使用して、ユーザーから送信されたエラーログと重要なセクションのダンプを分析し、それに関する情報をEurekaLogに統合します。

この記事では、プロセスメモリカードを個別に作成し、デバッグと分析に必要なデータに関する情報をその中に配置する方法を段階的に説明します。

1.利用可能な地域のリストを取得する


すべての仮想プロセスメモリはページの形式で表示されます。
ページは小さく(4096バイト)、大きいです。 ( MSDNで詳細をご覧ください
ほとんどの場合、連続したページには同じ属性があります。

地域とは何ですか?
大まかに( MSDNに基づいて )VirtualQuery関数に渡されたアドレスで始まる同じ属性を持つすべてのページのセットです。

最も簡単な形式では、次のコードを使用してプロセスの領域のリストを取得できます。

program Project1; {$APPTYPE CONSOLE} {$R *.res} uses Windows, SysUtils; var MBI: TMemoryBasicInformation; dwLength: NativeUInt; Address: PByte; begin Address := nil; dwLength := SizeOf(TMemoryBasicInformation); while VirtualQuery(Address, MBI, dwLength) <> 0 do begin Writeln( 'AllocationBase: ', IntToHex(NativeUInt(MBI.AllocationBase), 8), ', BaseAddress: ', IntToHex(NativeUInt(MBI.BaseAddress), 8), ', RegionSize: ', MBI.RegionSize); Inc(Address, MBI.RegionSize); end; Readln; end. 

たとえば、最初にアドレスnilを最初のパラメーターとして渡しました。 関数を呼び出した後、MBI変数は次の値を取ります。


領域のサイズは10,000ドル(64 kb)です。これは、状態(State)がMEM_FREE(10,000ドル)であり、セキュリティ属性PAGE_NO_ACCESS(1)がProtectパラメーターで設定されているアドレス0から始まる16ページに相当します。

次のようにコードを書き直すと:

 function ExtractAccessString(const Value: DWORD): string; const PAGE_WRITECOMBINE = $400; begin Result := 'Unknown access'; if (Value and PAGE_EXECUTE) = PAGE_EXECUTE then Result := 'E'; if (Value and PAGE_EXECUTE_READ) = PAGE_EXECUTE_READ then Result := 'RE'; if (Value and PAGE_EXECUTE_READWRITE) = PAGE_EXECUTE_READWRITE then Result := 'RWE'; if (Value and PAGE_EXECUTE_WRITECOPY) = PAGE_EXECUTE_WRITECOPY then Result := 'RE, Write copy'; if (Value and PAGE_NOACCESS) = PAGE_NOACCESS then Result := 'No access'; if (Value and PAGE_READONLY) = PAGE_READONLY then Result := 'R'; if (Value and PAGE_READWRITE) = PAGE_READWRITE then Result := 'RW'; if (Value and PAGE_WRITECOPY) = PAGE_WRITECOPY then Result := 'Write copy'; if (Value and PAGE_GUARD) = PAGE_GUARD then Result := Result + ', Guarded'; if (Value and PAGE_NOCACHE) = PAGE_NOCACHE then Result := Result + ', No cache'; if (Value and PAGE_WRITECOMBINE) = PAGE_WRITECOMBINE then Result := Result + ', Write Combine'; end; function ExtractRegionTypeString(Value: TMemoryBasicInformation): string; begin Result := ''; case Value.State of MEM_FREE: Result := 'Free'; MEM_RESERVE: Result := 'Reserved'; MEM_COMMIT: case Value.Type_9 of MEM_IMAGE: Result := 'Image'; MEM_MAPPED: Result := 'Mapped'; MEM_PRIVATE: Result := 'Private'; end; end; Result := Result + ', ' + ExtractAccessString(Value.Protect); end; var MBI: TMemoryBasicInformation; dwLength: NativeUInt; Address: PByte; begin Address := nil; dwLength := SizeOf(TMemoryBasicInformation); while VirtualQuery(Address, MBI, dwLength) <> 0 do begin Writeln( 'AllocationBase: ', IntToHex(NativeUInt(MBI.AllocationBase), 8), ', BaseAddress: ', IntToHex(NativeUInt(MBI.BaseAddress), 8), ' - ', ExtractRegionTypeString(MBI)); Inc(Address, MBI.RegionSize); end; 

...次に、VirtualAlloc関数を使用して地域化の原則を明確に確認できます。


たとえば、2番目と3番目の領域のアクセス属性(読み取りレコード)は同じですが、AllocationBaseが異なります。 AllocationBaseは、VirtualAllocを介してメモリを割り当てるときにページに割り当てられるため、別の領域でページを結合します。

2.フローに関するデータを収集します


受け取った領域に、それらが格納するものに関する情報を記入し始める時が来ました。そして、フロー(スレッド—好きなものは何でも)から始めます。

スレッドのリストを取得するためのコードは簡単です-CreateToolhelp32Snapshotを使用。

 const THREAD_GET_CONTEXT = 8; THREAD_SUSPEND_RESUME = 2; THREAD_QUERY_INFORMATION = $40; ThreadBasicInformation = 0; ThreadQuerySetWin32StartAddress = 9; STATUS_SUCCESS = 0; var hSnap, hThread: THandle; ThreadEntry: TThreadEntry32; TBI: TThreadBasicInformation; TIB: NT_TIB; lpNumberOfBytesRead: NativeUInt; ThreadStartAddress: Pointer; begin //      hSnap := CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, GetCurrentProcessId); if hSnap <> INVALID_HANDLE_VALUE then try ThreadEntry.dwSize := SizeOf(TThreadEntry32); if Thread32First(hSnap, ThreadEntry) then repeat if ThreadEntry.th32OwnerProcessID <> GetCurrentProcessId then Continue; Writeln('ThreadID: ', ThreadEntry.th32ThreadID); //   hThread := OpenThread(THREAD_GET_CONTEXT or THREAD_SUSPEND_RESUME or THREAD_QUERY_INFORMATION, False, ThreadEntry.th32ThreadID); if hThread <> 0 then try //   ThreadProc() if NtQueryInformationThread(hThread, ThreadQuerySetWin32StartAddress, @ThreadStartAddress, SizeOf(ThreadStartAddress), nil) = STATUS_SUCCESS then Writeln('ThreadProcAddr: ', IntToHex(NativeUInt(ThreadStartAddress), 1)); //     if NtQueryInformationThread(hThread, ThreadBasicInformation, @TBI, SizeOf(TThreadBasicInformation), nil) = STATUS_SUCCESS then begin Writeln('Thread Environment Block (TEB) Addr: ', IntToHex(NativeUInt(TBI.TebBaseAddress), 1)); //      // TIB (Thread Information Block)   if ReadProcessMemory(GetCurrentProcess, TBI.TebBaseAddress, @TIB, SizeOf(NT_TIB), lpNumberOfBytesRead) then begin Writeln('Thread StackBase Addr: ', IntToHex(NativeUInt(TIB.StackBase), 1)); Writeln('Thread StackLimit Addr: ', IntToHex(NativeUInt(TIB.StackLimit), 1)); end; end; finally CloseHandle(hThread); end; until not Thread32Next(hSnap, ThreadEntry); finally CloseHandle(hSnap); end; Readln; end. 

手順:

  1. CreateToolhelp32Snapshot / Thread32First / Thread32Nextを使用して、アプリケーションからアクティブなスレッドのリストを取得します。
  2. 詳細については、OpenThreadを呼び出して取得したスレッドへのハンドルが必要です。
  3. NtQueryInformationThreadを使用して、作業を開始したスレッドプロシージャのアドレスと、スレッドに関する基本情報をTThreadBasicInformation構造の形式で取得します。
  4. この構造のうち、関心があるのは1つのフィールドだけです-TebBaseAddressには、ストリーム環境ブロックのアドレス、いわゆる TEB(スレッド環境ブロック)。
  5. ReadProcessMemoryを呼び出すことにより(これはアプリケーションにとって冗長ですが)、TEBアドレスのデータ、つまりその最初のパラメーターであるNT_TIB構造体を読み取ります。

NT_TIB宣言は次のようになります。

  PNT_TIB = ^_NT_TIB; _NT_TIB = record ExceptionList: Pointer; StackBase, StackLimit, SubSystemTib: Pointer; case Integer of 0: ( FiberData: Pointer ); 1: ( Version: ULONG; ArbitraryUserPointer: Pointer; Self: PNT_TIB; ) end; NT_TIB = _NT_TIB; PPNT_TIB = ^PNT_TIB; 

まあ、またはこのように、もう少し説明すると:


残りのフィールドは不要です。

まあ、しかし、どのように必要ではありませんか?
もちろん必要ですが、今のところそれらは私たちにとって冗長です。
ところで、この構造の少し時代遅れの説明を見ることができるリンクはここにあります: スレッド環境ブロック

このコードは、次の図を表示します。


そして、それはVMMapで見られるでしょう。


この図は、VMMapがTEBに関する情報を表示しなかったことを示しています。

ところで、上記のコードの一部の関数と構造体は、標準のDelphiソースで宣言されていません。これらの宣言は、この記事の一部であるデモ例で確認できます。 しかし、これはMSDNに文書化されていないという意味ではありません:)

ストリームのTEBを使用する場合、ToolHelp32.dll関数を使用する必要はなく、FSセグメントレジスタ(またはx64の場合はGS)を使用する必要があるため、コードは大幅に簡素化されます。
たとえば、そのような関数は、TEBアドレスを取得するためによく見つかります。

 function GetCurrentTEB: NativeUInt; asm {$IFDEF WIN64} // mov RAX, qword ptr GS:[30h] //   ,      64-  DB $65, $48, $8B, $04, $25, $30, 0, 0, 0 //    ,    mov RAX, qword ptr GS:[abs $30] {$ELSE} mov EAX, FS:[18h] {$ENDIF} end; 

この場合、TEB構造体のパラメーターNtTIB.Selfにアクセスします。このパラメーターは、先頭からオフセット0x18(64ビットTEBの場合は0x30)にあります。

しかし、私たちは続けます...
受信したデータの一部ですが、これは私たちが入手できるすべての情報ではありません。

各スレッドのスタック上には、try..finally / exceptブロックを入力すると自動的に生成されるSEHフレームと、プロシージャコールのスタックがあります。 これらのデータを手元に用意して、地域を参照してより視覚的な形式で表示するとよいでしょう。

ここでは、このような簡単な手順でSEHフレームのプロモーションを扱います。

 procedure GetThreadSEHFrames(InitialAddr: Pointer); type EXCEPTION_REGISTRATION = record prev, handler: Pointer; end; var ER: EXCEPTION_REGISTRATION; lpNumberOfBytesRead: NativeUInt; begin while ReadProcessMemory(GetCurrentProcess, InitialAddr, @ER, SizeOf(EXCEPTION_REGISTRATION), lpNumberOfBytesRead) do begin Writeln('SEH Frame at Addr: ', IntToHex(NativeUInt(InitialAddr), 1), ', handler at addr: ', IntToHex(NativeUInt(ER.handler), 1)); InitialAddr := ER.prev; if DWORD(InitialAddr) <= 0 then Break; end; end; 

パラメータとして最初のEXCEPTION_REGISTRATION構造体を指すTEB.TIB.ExceptionList値を受け取ると、これらの構造体のチェーンに沿って実行され、この構造体のprev値にガイドされ、前のEXCEPTION_REGISTRATION構造体のアドレスが含まれます。 また、突然発生した場合、ハンドラパラメータには例外ハンドラのアドレスが含まれます。

次のようになります。


さて、CallStackは次の手順を受け取ります:

 procedure GetThreadCallStack(hThread: THandle); var StackFrame: TStackFrame; ThreadContext: PContext; MachineType: DWORD; begin // ThreadContext   ,   VirtualAlloc //         //     ERROR_NOACCESS (998) ThreadContext := VirtualAlloc(nil, SizeOf(TContext), MEM_COMMIT, PAGE_READWRITE); try ThreadContext^.ContextFlags := CONTEXT_FULL; if not GetThreadContext(hThread, ThreadContext^) then Exit; ZeroMemory(@StackFrame, SizeOf(TStackFrame)); StackFrame.AddrPC.Mode := AddrModeFlat; StackFrame.AddrStack.Mode := AddrModeFlat; StackFrame.AddrFrame.Mode := AddrModeFlat; StackFrame.AddrPC.Offset := ThreadContext.Eip; StackFrame.AddrStack.Offset := ThreadContext.Esp; StackFrame.AddrFrame.Offset := ThreadContext.Ebp; MachineType := IMAGE_FILE_MACHINE_I386; while True do begin if not StackWalk(MachineType, GetCurrentProcess, hThread, @StackFrame, ThreadContext, nil, nil, nil, nil) then Break; if StackFrame.AddrPC.Offset <= 0 then Break; Writeln('CallStack Frame Addr: ', IntToHex(NativeUInt(StackFrame.AddrFrame.Offset), 1)); Writeln('CallStack Handler: ', IntToHex(NativeUInt(StackFrame.AddrPC.Offset), 1)); Writeln('CallStack Stack: ', IntToHex(NativeUInt(StackFrame.AddrStack.Offset), 1)); Writeln('CallStack Return: ', IntToHex(NativeUInt(StackFrame.AddrReturn.Offset), 1)); end; finally VirtualFree(ThreadContext, SizeOf(TContext), MEM_FREE); end; end; 

確かに、Delphiデバッガーとは異なり、スタックフレームが生成されたプロシージャに関するデータを出力し、残りはスキップします。
StackWalk(またはStackWalk64)関数は、スタックフレームに関する情報を一覧表示します。

ここで注意点:このコードを自分で適用すると、1つのスタックフレームのみをトレースでき、その後に出口があります( デモアプリケーションで確認できます )。

これは次の理由で発生します:StackWalk関数を適切にトレースするには、現在のスタックフレーム(x64のEBPおよびESP / RBPおよびRSP)のパラメーターと、実際には現在のコードアドレス(x64のEIPまたはRIPレジスタ)を指定する必要があります。 このデータを自分で取得する場合、GetThreadContext関数を呼び出したときにこれが発生し、この関数を終了した後にスタックのスピンを開始します。この関数では、3つすべてのパラメーターが有効になります。 このため、この関数を呼び出して自身をトレースすることはできません。
この点を考慮することが望ましい...

64ビットOSの下での32ビットプロセスのスレッドに関する情報の受信について詳しく説明します。32ビットおよび64ビットのバリアントを少し後で説明しますが...

3.ヒープに関するデータを収集します


Delphiアプリケーション自体は、原則として、ヒープを使用しません。これは、C ++アプリケーションの特権ですが、ヒープはまだ存在しています。 通常、それらは、必要に応じてさまざまなサードパーティライブラリによって作成および使用されます。

ヒープデータを取得する際のニュアンスは、各ヒープを構成するHeapEntry要素が数千になる可能性があることであり、2番目のニュアンスは、Heap32Next関数が各呼び出しでリスト全体を再構築し、かなり敏感な遅延(最大で数十秒)。

この不快な機能についてはすでに書います。
確かに、その記事では、原則自体を示すためだけにコードはかなり荒く、私たちにとってはうまくいきませんが、よりコームされたバージョンが私たちに合っています:

 const RTL_HEAP_BUSY = 1; RTL_HEAP_SEGMENT = 2; RTL_HEAP_SETTABLE_VALUE = $10; RTL_HEAP_SETTABLE_FLAG1 = $20; RTL_HEAP_SETTABLE_FLAG2 = $40; RTL_HEAP_SETTABLE_FLAG3 = $80; RTL_HEAP_SETTABLE_FLAGS = $E0; RTL_HEAP_UNCOMMITTED_RANGE = $100; RTL_HEAP_PROTECTED_ENTRY = $200; RTL_HEAP_FIXED = (RTL_HEAP_BUSY or RTL_HEAP_SETTABLE_VALUE or RTL_HEAP_SETTABLE_FLAG2 or RTL_HEAP_SETTABLE_FLAG3 or RTL_HEAP_SETTABLE_FLAGS or RTL_HEAP_PROTECTED_ENTRY); STATUS_SUCCESS = 0; function CheckSmallBuff(Value: DWORD): Boolean; const STATUS_NO_MEMORY = $C0000017; STATUS_BUFFER_TOO_SMALL = $C0000023; begin Result := (Value = STATUS_NO_MEMORY) or (Value = STATUS_BUFFER_TOO_SMALL); end; function FlagToStr(Value: DWORD): string; begin case Value of LF32_FIXED: Result := 'LF32_FIXED'; LF32_FREE: Result := 'LF32_FREE'; LF32_MOVEABLE: Result := 'LF32_MOVEABLE'; else Result := ''; end; end; var I, A: Integer; pDbgBuffer: PRtlDebugInformation; pHeapInformation: PRtlHeapInformation; pHeapEntry: PRtrHeapEntry; dwAddr, dwLastSize: ULONG_PTR; hit_seg_count: Integer; BuffSize: NativeUInt; begin // ..  Heap32ListFirst, Heap32ListNext, Heap32First, Heap32Next //   , -   // RtlQueryProcessDebugInformation   ,     //      //    BuffSize := $400000; pDbgBuffer := RtlCreateQueryDebugBuffer(BuffSize, False); //       while CheckSmallBuff(RtlQueryProcessDebugInformation(GetCurrentProcessId, RTL_QUERY_PROCESS_HEAP_SUMMARY or RTL_QUERY_PROCESS_HEAP_ENTRIES, pDbgBuffer)) do begin //     , ... RtlDestroyQueryDebugBuffer(pDbgBuffer); BuffSize := BuffSize shl 1; pDbgBuffer := RtlCreateQueryDebugBuffer(BuffSize, False); end; if pDbgBuffer <> nil then try //       if RtlQueryProcessDebugInformation(GetCurrentProcessId, RTL_QUERY_PROCESS_HEAP_SUMMARY or RTL_QUERY_PROCESS_HEAP_ENTRIES, pDbgBuffer) = STATUS_SUCCESS then begin //       pHeapInformation := @pDbgBuffer^.Heaps^.Heaps[0]; //    ... for I := 0 to pDbgBuffer^.Heaps^.NumberOfHeaps - 1 do begin //     pHeapEntry := pHeapInformation^.Entries; dwAddr := DWORD(pHeapEntry^.u.s2.FirstBlock) + pHeapInformation^.EntryOverhead; dwLastSize := 0; A := 0; while A < Integer(pHeapInformation^.NumberOfEntries) do try hit_seg_count := 0; while (pHeapEntry^.Flags and RTL_HEAP_SEGMENT) = RTL_HEAP_SEGMENT do begin //     RTL_HEAP_SEGMENT, //       EntryOverhead dwAddr := DWORD(pHeapEntry^.u.s2.FirstBlock) + pHeapInformation^.EntryOverhead; Inc(pHeapEntry); Inc(A); Inc(hit_seg_count); //      if A + hit_seg_count >= Integer(pHeapInformation^.NumberOfEntries - 1) then Continue; end; //       ,     , //    +    if hit_seg_count = 0 then Inc(dwAddr, dwLastSize); //   if pHeapEntry^.Flags and RTL_HEAP_FIXED <> 0 then pHeapEntry^.Flags := LF32_FIXED else if pHeapEntry^.Flags and RTL_HEAP_SETTABLE_FLAG1 <> 0 then pHeapEntry^.Flags := LF32_MOVEABLE else if pHeapEntry^.Flags and RTL_HEAP_UNCOMMITTED_RANGE <> 0 then pHeapEntry^.Flags := LF32_FREE; if pHeapEntry^.Flags = 0 then pHeapEntry^.Flags := LF32_FIXED; //   Writeln('HeapID: ', I, ', entry addr: ', IntToHex(dwAddr, 8), ', size: ', IntToHex(pHeapEntry^.Size, 8), ' ', FlagToStr(pHeapEntry^.Flags)); //     dwLastSize := pHeapEntry^.Size; //     Inc(pHeapEntry); finally Inc(A); end; //     Inc(pHeapInformation); end; end; finally RtlDestroyQueryDebugBuffer(pDbgBuffer); end; Readln; end. 

つまり、RtlQueryProcessDebugInformation、RtlCreateQueryDebugBuffer、およびRtlQueryProcessDebugInformation関数を呼び出すことにより、現在のプロセスヒープに関する情報を含むバッファーが作成されます。 次に、格納されているデータの構造がわかっているので、このデータをループで取得します。
pDbgBuffer ^ .Heaps-ヒープリスト(THeapList32のアナログ)を保存し、レコード自体はpDbgBuffer ^ .Heaps ^ .Heaps [N] .Entries(THeapEntry32のアナログ)に保存されます。

このコードは次の情報を出力します。


原則として、デバッグ時にヒープを使用することはほとんどありませんが、この情報が役に立つ場合があります。

4.ダウンロードしたPEファイルのデータを収集します


次に、プロセスのアドレス空間にロードされた実行可能ファイルとライブラリに関する情報を取得します。 これを行うにはいくつかの方法がありますが(たとえば、PEB.LoaderDataを分析することによって)、それを簡単にしましょう。

原則として、PEファイルには別の領域が割り当てられます(少なくとも、私はPEが領域の上部と整列せずにロードされるように遭遇したことはありません)。したがって、最初の章のコードを使用し、領域の最初のページのデータを確認しますPEファイルに準拠するために、ロードされたすべてのライブラリと実行可能ファイルのリストを取得します。

次のコードは、指定されたアドレスに有効なPEファイルの存在を検出します。

 function CheckPEImage(hProcess: THandle; ImageBase: Pointer; var IsPEImage64: Boolean): Boolean; var ReturnLength: NativeUInt; IDH: TImageDosHeader; NT: TImageNtHeaders; begin Result := False; IsPEImage64 := False; if not ReadProcessMemory(hProcess, ImageBase, @IDH, SizeOf(TImageDosHeader), ReturnLength) then Exit; if IDH.e_magic <> IMAGE_DOS_SIGNATURE then Exit; ImageBase := Pointer(NativeInt(ImageBase) + IDH._lfanew); if not ReadProcessMemory(hProcess, ImageBase, @NT, SizeOf(TImageNtHeaders), ReturnLength) then Exit; Result := NT.Signature = IMAGE_NT_SIGNATURE; IsPEImage64 := (NT.FileHeader.Machine = IMAGE_FILE_MACHINE_IA64) or (NT.FileHeader.Machine = IMAGE_FILE_MACHINE_ALPHA64) or (NT.FileHeader.Machine = IMAGE_FILE_MACHINE_AMD64); end; 

もっと正確に言うと、彼は単にImageDosHeaderとImageNTHeaderの存在をチェックし、署名に焦点を合わせます。 原則として、99%のケースでこれで十分です。

3番目のパラメーターは単なる参考情報であり、PEファイルが64ビットかどうかを示します。

GetMappedFileName関数を呼び出すことにより、ダウンロードしたファイルへのパスを取得できます。

 function GetFileAtAddr(hProcess: THandle; ImageBase: Pointer): string; begin SetLength(Result, MAX_PATH); SetLength(Result, GetMappedFileName(hProcess, ImageBase, @Result[1], MAX_PATH)); end; 

それでは、通常のコンソールアプリケーションに何を読み込んでいるかを見てみましょう。

 var MBI: TMemoryBasicInformation; dwLength: NativeUInt; Address: PByte; IsPEImage64: Boolean; begin Address := nil; dwLength := SizeOf(TMemoryBasicInformation); while VirtualQuery(Address, MBI, dwLength) <> 0 do begin if CheckPEImage(GetCurrentProcess, MBI.BaseAddress, IsPEImage64) then begin Write(IntToHex(NativeUInt(MBI.BaseAddress), 8), ': ', GetFileAtAddr(GetCurrentProcess, MBI.BaseAddress)); if IsPEImage64 then Writeln(' (x64)') else Writeln(' (x32)'); end; Inc(Address, MBI.RegionSize); end; Readln; end. 

次の図が表示されます。


32ビットアプリケーションの64ビットライブラリ? はい、それは簡単です:)

32ビットアプリケーション、オペレーティングシステムWindows 7 x64があります。 写真に示されていることから判断すると、4つの64ビットライブラリは32ビットプロセスで静かに動作し、動作しますが、ここでは珍しいことはありません。これはいわゆるWow64( 64ビットWindowsでのWin32エミュレーション )です。

しかし、32ビットストリームとヒープの64ビットアナログがどこから来るかはすぐに明らかになります。

さて、良い方法で、各PEファイルのセクションのアドレスを取得して、より明確に表示できるようにする必要があります。 すべてのセクションはページの先頭に配置され、互いに交差しません。

このようにしましょう:

 procedure GetInfoFromImage(const FileName: string; ImageBase: Pointer); var ImageInfo: TLoadedImage; ImageSectionHeader: PImageSectionHeader; I: Integer; begin if MapAndLoad(PAnsiChar(AnsiString(FileName)), nil, @ImageInfo, True, True) then try ImageSectionHeader := ImageInfo.Sections; for I := 0 to Integer(ImageInfo.NumberOfSections) - 1 do begin Write( IntToHex((NativeUInt(ImageBase) + ImageSectionHeader^.VirtualAddress), 8), ': ', string(PAnsiChar(@ImageSectionHeader^.Name[0]))); if IsExecute(ImageSectionHeader^.Characteristics) then Write(' Execute'); if IsWrite(ImageSectionHeader^.Characteristics) then Write(' Writable'); Writeln; Inc(ImageSectionHeader); end; finally UnMapAndLoad(@ImageInfo); end; Writeln; end; 

ここでは、MapAndLoad関数の呼び出しを使用します。この関数は、ファイルの読み込みとヘッダーの確認に加えて、NtMapViewOfSectionを呼び出してセクションのアライメントを実行します。

もちろん、独自のプロセスでは、この関数を呼び出すことは冗長です。 必要なPEファイルはすでにプロセスのアドレススペースにロードされていますが、 他のプロセスを操作するためにより汎用的なコードが必要な場合、このアプローチを使用します。

MapAndLoadは、64ビットプロセスが32ビットPEファイルを読み込むことができるため(これは32ビットプロセスでは機能しませんが)優れており、この機能は後で便利になります。

コードの要点は次のとおりです。MapAndLoadを実行すると、TLoadedImage構造体が格納され、そのSectionsパラメーターがTImageSectionHeader構造体の配列を指します。 これらの各構造には、ライブラリのロードアドレスからのオフセットであるVirtualAddressフィールドがあります。 このフィールドの値をhInstanceライブラリに追加すると、セクションのアドレスが取得されます。

IsExecuteおよびIsWrite関数は、セクションの特性を確認し、セクションに実行可能コード(IsExecute)または変更可能なデータ(IsWrite)が含まれている場合にTrueを返します。 次のようになります。

 function IsExecute(const Value: DWORD): Boolean; begin Result := False; if (Value and IMAGE_SCN_CNT_CODE) = IMAGE_SCN_CNT_CODE then Result := True; if (Value and IMAGE_SCN_MEM_EXECUTE) = IMAGE_SCN_MEM_EXECUTE then Result := True; end; function IsWrite(const Value: DWORD): Boolean; begin Result := False; if (Value and IMAGE_SCN_CNT_UNINITIALIZED_DATA) = IMAGE_SCN_CNT_UNINITIALIZED_DATA then Result := True; if (Value and IMAGE_SCN_MEM_WRITE) = IMAGE_SCN_MEM_WRITE then Result := True; end; 

このコードの結果、次のように表示されます。


確かに、このコードには別の小さなニュアンスがあります。
前の図に見られるように、GetMappedFileName関数は、ダウンロードしたファイルへのパスを次の形式で返します。「\ Device \ HarddiskVolume2 \ Windows \ System32 \ wow64cpu.dll」、MapAndLoad関数には「C:\ Windows \ System32 \ wow64cpu」の正規化パスが必要です。 dll。

次のコードは、使い慣れた外観にパスをもたらす役割を果たします。

 function NormalizePath(const Value: string): string; const OBJ_CASE_INSENSITIVE = $00000040; STATUS_SUCCESS = 0; FILE_SYNCHRONOUS_IO_NONALERT = $00000020; FILE_READ_DATA = 1; ObjectNameInformation = 1; DriveNameSize = 4; VolumeCount = 26; DriveTotalSize = DriveNameSize * VolumeCount; var US: UNICODE_STRING; OA: OBJECT_ATTRIBUTES; IO: IO_STATUS_BLOCK; hFile: THandle; NTSTAT, dwReturn: DWORD; ObjectNameInfo: TOBJECT_NAME_INFORMATION; Buff, Volume: string; I, Count, dwQueryLength: Integer; lpQuery: array [0..MAX_PATH - 1] of Char; AnsiResult: AnsiString; begin Result := Value; //     ZwOpenFile RtlInitUnicodeString(@US, StringToOleStr(Value)); //   InitializeObjectAttributes FillChar(OA, SizeOf(OBJECT_ATTRIBUTES), #0); OA.Length := SizeOf(OBJECT_ATTRIBUTES); OA.ObjectName := @US; OA.Attributes := OBJ_CASE_INSENSITIVE; //  ZwOpenFile   ,     //    , : // \SystemRoot\System32\ntdll.dll // \??\C:\Windows\System32\ntdll.dll // \Device\HarddiskVolume1\WINDOWS\system32\ntdll.dll //        NTSTAT := ZwOpenFile(@hFile, FILE_READ_DATA or SYNCHRONIZE, @OA, @IO, FILE_SHARE_READ or FILE_SHARE_WRITE or FILE_SHARE_DELETE, FILE_SYNCHRONOUS_IO_NONALERT); if NTSTAT = STATUS_SUCCESS then try //  ,      NTSTAT := NtQueryObject(hFile, ObjectNameInformation, @ObjectNameInfo, MAX_PATH * 2, @dwReturn); if NTSTAT = STATUS_SUCCESS then begin SetLength(AnsiResult, MAX_PATH); WideCharToMultiByte(CP_ACP, 0, @ObjectNameInfo.Name.Buffer[ObjectNameInfo.Name.MaximumLength - ObjectNameInfo.Name.Length {$IFDEF WIN64} + 4{$ENDIF}], ObjectNameInfo.Name.Length, @AnsiResult[1], MAX_PATH, nil, nil); Result := string(PAnsiChar(AnsiResult)); //     ZwOpenFile  //    \Device\HarddiskVolume\- //        SetLength(Buff, DriveTotalSize); Count := GetLogicalDriveStrings(DriveTotalSize, @Buff[1]) div DriveNameSize; for I := 0 to Count - 1 do begin Volume := PChar(@Buff[(I * DriveNameSize) + 1]); Volume[3] := #0; //         //     QueryDosDevice(PChar(Volume), @lpQuery[0], MAX_PATH); dwQueryLength := Length(string(lpQuery)); if Copy(Result, 1, dwQueryLength) = string(lpQuery) then begin Volume[3] := '\'; if lpQuery[dwQueryLength - 1] <> '\' then Inc(dwQueryLength); Delete(Result, 1, dwQueryLength); Result := Volume + Result; Break; end; end; end; finally ZwClose(hFile); end; end; 

これはすでに非常に古いコードであり、通常のパスに使用するために常に使用しています。その本質は、次のタイプのパスの本質です。


...修正された「\ Device \ HarddiskVolume1 \ WINDOWS \ system32 \ ntdll.dll」を取得します。
これは、ZwOpenFile + NtQueryObjectを呼び出すことによって実行されます。その後、システム内のすべてのディスクが単純にソートされ、それぞれに対してQueryDosDeviceが呼び出され、同じ形式でパスが返されます。その後、パスが比較され(一致する場合)、対応するディスクラベルが転送されたパスに置き換えられます。

しかし、これは歌詞です。
自分に完全に満足するには、PEファイルのディレクトリも表示することをお勧めします。これにより、インポートテーブル、UNWINDが置かれている場所などをすぐに確認できます。

これはかなり単純なコードで行われます:

 procedure EnumDirectoryes(ImageBase: Pointer; ImageInfo: TLoadedImage; AddrStart, AddrEnd: NativeUInt); const DirectoryStr: array [0..14] of string = ('export', 'import', 'resource', 'exception', 'security', 'basereloc', 'debug', 'copyright', 'globalptr', 'tls', 'load_config', 'bound_import', 'iat', 'delay_import', 'com'); var I: Integer; dwDirSize: DWORD; DirAddr: Pointer; ReadlDirAddr: NativeUInt; begin for I := 0 to 14 do begin DirAddr := ImageDirectoryEntryToData(ImageInfo.MappedAddress, True, I, dwDirSize); if DirAddr = nil then Continue; ReadlDirAddr := NativeUint(ImageBase) + NativeUint(DirAddr) - NativeUint(ImageInfo.MappedAddress); if (ReadlDirAddr >= AddrStart) and (ReadlDirAddr < AddrEnd) then Writeln( IntToHex(ReadlDirAddr, 8), ': directory "', DirectoryStr[I], '"'); end; end; 

TLoadedImage構造体を手元に置いて、ImageDirectoryEntryToData関数を呼び出すだけでアドレスを取得できますが、PEファイルが表示されるアドレスにバインドされます。それを実際のアドレスに変換するには、現在のアドレスから画像が表示されるアドレスを減算し、ファイルの先頭からオフセットを取得して、ImageBaseライブラリに既に追加する必要があります。

結果はこの写真です:


たとえば、msctf.dllライブラリの「.text」セクションには、インポート/エクスポート/保留中のインポートディレクトリなどがあることがすぐにわかります。
リソースディレクトリは「.rsrc」セクションにあります。また、relocは本来あるべき場所ですが、「bound_import」ディレクトリはスキームから除外されます。

はい、確かに、このディレクトリはライブラリのどのセクションにも直接配置されていません。通常、PEヘッダーの直後にあります(ただし、セクションの間にある場合もあります)。このディレクトリは、主にOSの一部であるプログラムとライブラリにある「タイドインポート」のメカニズムを提供します。

その本質は、インポートされた関数のすべてのアドレスがコンパイル段階で実行可能ファイルに縫い付けられるため、関数アドレスを検索して通常のインポートテーブルを実行して不要なジェスチャーを実行する必要はありません。
ただし、リンクされたインポートセクションで宣言されたライブラリが変更されるとすぐに、アプリケーションを再コンパイルする必要があるため、オーバーヘッドも適切です。

5.プロセス環境ユニット(PEB)+ KUSER_SHARED_DATA


ストリーム、ヒープ、実行可能ファイルのデータを手元に置いて、情報を読み取り可能な形式で表示する小さなユーティリティを作成できますが、他に何を追加できますか?

少なくとも、プロセス環境ブロックから情報を送受信することを強くお勧めします。

ProcessBasicInformationフラグ(定数0)を指定してNtQueryInformationProcess関数を呼び出すことでアクセスできます。この場合、手はPROCESS_BASIC_INFORMATION構造を持ち、PebBaseAddressフィールドにはPEBアドレスが含まれます。

ただし、これは、プロセスのビット(要求元と情報の要求元)のビットが一致する場合にのみ関連します。 32ビットアプリケーションに適用されている64ビットアプリケーションからこの関数を呼び出すと、ネイティブ32ビットアプリケーションではなく、64ビットPEBのアドレスが取得されます。

64ビットアプリケーションからWow64PEBにアクセスするには(それを呼び出しましょう)、ProcessWow64Informationパラメーター(定数は26)とバッファーサイズがSizeOf(ULONG_PTR)に等しいNtQueryInformationProcess関数を呼び出す必要があります。この場合、PROCESS_BASIC_INFORMATION構造の代わりに、関数は32ビットPEBへのポインターを返し、そこからReadProcessMemoryを使用して必要な情報を読み取ります。

PEBとは何ですか?
大まかに言って、これは十分に文書化された構造ではなく、そのほとんどはシステムが直接使用するデータを保存するように設計されています。しかし、これは通常のアプリケーションの開発者にとって面白くないという意味ではありません。特に、この構造には以下のような多くの興味深いフィールドが含まれます。デバッガがプロセスに接続されているかどうかを示すBeingDebuggedフラグ。プロセスにロードされたモジュールに関する情報を含むPEB_LDR_DATAへのポインター。残りの多くは、プログラマーにとって、特に自分の目的でそれを使用する方法を知っている人にとって非常に有用な情報です:)

この構造は次のようになります(Windows7 x86 / 64の宣言):

  PPEB = ^TPEB; TPEB = record InheritedAddressSpace: BOOLEAN; ReadImageFileExecOptions: BOOLEAN; BeingDebugged: BOOLEAN; BitField: BOOLEAN; { BOOLEAN ImageUsesLargePages : 1; BOOLEAN IsProtectedProcess : 1; BOOLEAN IsLegacyProcess : 1; BOOLEAN IsImageDynamicallyRelocated : 1; BOOLEAN SkipPatchingUser32Forwarders : 1; BOOLEAN IsPackagedProcess : 1; BOOLEAN IsAppContainer : 1; BOOLEAN SpareBits : 1; } Mutant: HANDLE; ImageBaseAddress: PVOID; LoaderData: PVOID; ProcessParameters: PRTL_USER_PROCESS_PARAMETERS; SubSystemData: PVOID; ProcessHeap: PVOID; FastPebLock: PRTLCriticalSection; AtlThunkSListPtr: PVOID; IFEOKey: PVOID; EnvironmentUpdateCount: ULONG; UserSharedInfoPtr: PVOID; SystemReserved: ULONG; AtlThunkSListPtr32: ULONG; ApiSetMap: PVOID; TlsExpansionCounter: ULONG; TlsBitmap: PVOID; TlsBitmapBits: array[0..1] of ULONG; ReadOnlySharedMemoryBase: PVOID; HotpatchInformation: PVOID; ReadOnlyStaticServerData: PPVOID; AnsiCodePageData: PVOID; OemCodePageData: PVOID; UnicodeCaseTableData: PVOID; KeNumberOfProcessors: ULONG; NtGlobalFlag: ULONG; CriticalSectionTimeout: LARGE_INTEGER; HeapSegmentReserve: SIZE_T; HeapSegmentCommit: SIZE_T; HeapDeCommitTotalFreeThreshold: SIZE_T; HeapDeCommitFreeBlockThreshold: SIZE_T; NumberOfHeaps: ULONG; MaximumNumberOfHeaps: ULONG; ProcessHeaps: PPVOID; GdiSharedHandleTable: PVOID; ProcessStarterHelper: PVOID; GdiDCAttributeList: ULONG; LoaderLock: PRTLCriticalSection; NtMajorVersion: ULONG; NtMinorVersion: ULONG; NtBuildNumber: USHORT; NtCSDVersion: USHORT; PlatformId: ULONG; Subsystem: ULONG; MajorSubsystemVersion: ULONG; MinorSubsystemVersion: ULONG; AffinityMask: ULONG_PTR; {$IFDEF WIN32} GdiHandleBuffer: array [0..33] of ULONG; {$ELSE} GdiHandleBuffer: array [0..59] of ULONG; {$ENDIF} PostProcessInitRoutine: PVOID; TlsExpansionBitmap: PVOID; TlsExpansionBitmapBits: array [0..31] of ULONG; SessionId: ULONG; AppCompatFlags: ULARGE_INTEGER; AppCompatFlagsUser: ULARGE_INTEGER; pShimData: PVOID; AppCompatInfo: PVOID; CSDVersion: UNICODE_STRING; ActivationContextData: PVOID; ProcessAssemblyStorageMap: PVOID; SystemDefaultActivationContextData: PVOID; SystemAssemblyStorageMap: PVOID; MinimumStackCommit: SIZE_T; FlsCallback: PPVOID; FlsListHead: LIST_ENTRY; FlsBitmap: PVOID; FlsBitmapBits: array [1..FLS_MAXIMUM_AVAILABLE div SizeOf(ULONG) * 8] of ULONG; FlsHighIndex: ULONG; WerRegistrationData: PVOID; WerShipAssertPtr: PVOID; pContextData: PVOID; pImageHeaderHash: PVOID; TracingFlags: ULONG; { ULONG HeapTracingEnabled : 1; ULONG CritSecTracingEnabled : 1; ULONG LibLoaderTracingEnabled : 1; ULONG SpareTracingBits : 29; } CsrServerReadOnlySharedMemoryBase: ULONGLONG; end; 

ところで、この構造をMSDNで公式に入手可能な構造と比較してください

Window 2000 / XP / 2003では、小さな変更がありますが、それほど重大ではありません。
私は各フィールドをペイントしません、PEBで働く人はすでに知っていますか?まさに彼らが必要とするものですが、いくつかの分野ではあなたの注意を引くでしょう。

だから:


まあなど-あなたは長い間続けることができます。

これらのフィールドのほとんどは、プロセスのアドレス空間でページを占有します。たとえば、ProcessParametersは通常、ローダーによって作成されたヒープの1つに配置され、環境変数もその領域のどこかに配置されます。

このすべてを視覚化する場合(そして、私はこれにつながる)、最終アプリケーションに表示するものがあるように、このデータを手元に用意する必要があります。

同意します。バイナリデータの特定のブロックではなく、この形式の何かを持っている方がはるかに快適です。


ただし、KUSER_SHARED_DATAもあります。
これはシステムが使用する構造体でもあり、常に同じGetTickCountまたはIsProcessorFeaturePresentを呼び出してそれに対応します。
たとえば、NtSystemRootがその中にあり、繰り返しになりますが、すべてをリストする理由は、見やすくなっています。



しかし、おそらく、私たちは今のところここで停止します...

6. TRegionData


これで理論的な部分は終わり、すべてを実践する時が来ました。

まず、地域に関する情報を保存する方法を決定する必要があります。記事の準備として、共通の名前空間「MemoryMap」に割り当てられた一連のクラスを作成しましたデモ例の一部としてそれらを見つけることができます

重要!!!
このクラスのセットは、Delphi XE4の革新を考慮して開発されました; Delphiの古いバージョンでは、パフォーマンスはテストされておらず、保証されていません。


各領域の情報は、「MemoryMap.RegionData.pas」モジュールに実装されているTRegionDataクラスによって保存されます。

おおよそ次のようになります(プロジェクトの開発過程で、クラス宣言が変更される場合があります)。

  TRegionData = class private FParent: TRegionData; FRegionType: TRegionType; FMBI: TMemoryBasicInformation; FDetails: string; FRegionVisible: Boolean; FHiddenRegionCount: Integer; FTotalRegionSize: NativeUInt; FHeap: THeapData; FThread: TThreadData; FPEBData: TSystemData; FSection: TSection; FContains: TList; FDirectories: TList; FShared: Boolean; FSharedCount: Integer; FFiltered: Boolean; protected ... public constructor Create; destructor Destroy; override; property RegionType: TRegionType read FRegionType; property MBI: TMemoryBasicInformation read FMBI; property Details: string read FDetails; property RegionVisible: Boolean read FRegionVisible; property HiddenRegionCount: Integer read FHiddenRegionCount; property Parent: TRegionData read FParent; property TotalRegionSize: NativeUInt read FTotalRegionSize; property Heap: THeapData read FHeap; property Thread: TThreadData read FThread; property SystemData: TSystemData read FPEBData; property Section: TSection read FSection; property Directory: TList read FDirectories; property Contains: TList read FContains; end; 

順序:

各地域は、原則として、1つのタイプのデータをそれ自体に保存します。
つまりヒープ、ストリームのスタック、PEファイルには、独自のページ領域が割り当てられます。
RegionTypeプロパティは、リージョンタイプの格納を担当します。これは、次のように宣言された列挙型です。

  //   TRegionType = ( rtDefault, rtHeap, //     rtThread, //      TEB rtSystem, //     (PEB/KUSER_SHARED_DATA  ..) rtExecutableImage //     PE  ); 

VirtualQueryExを呼び出して取得した領域パラメーターは、MBIフィールドに保存されます。

地域の簡単な説明は、詳細に保存されます。表示されているPEファイルへのパス(存在する場合)、ストリームIDの文字列の説明など、何でも保存できます

。以下の3つのパラメーターは、ツリー構造を編成するために使用されます。
リージョンの1つはルートノード(ルート)で、残りは子ノードです。
RegionVisibleフラグは、リージョンがルートノードかどうかを示します。
HiddenRegionCountプロパティには、サブリージョン(AllocationBaseがBaseAddressルートに等しい)の数が含まれています。
さて、Parentパラメーターにはルートへのリンクが格納されます。
最適な方法ではありません。古典的なツリーを整理することは可能ですが、現時点ではやり直しする時間はありません。おそらく後で:

TotalRegionSizeには、ルートを含むすべてのサブリージョンの合計サイズが含まれます。

領域にヒープが含まれる場合、その最初の要素に関するデータは、次の構造であるヒープパラメータに配置されます。

  THeapEntry = record Address: ULONG_PTR; Size: SIZE_T; Flags: ULONG; end; THeapData = record ID: DWORD; Wow64: Boolean; Entry: THeapEntry; end; 

領域内にある残りのヒープ要素は、[含む]フィールドに配置されます。

一般に、Containsフィールドには多くのタイプのデータを含めることができます。

  TContainItemType = (itHeapBlock, itThreadData, itStackFrame, itSEHFrame, itSystem); TContainItem = record ItemType: TContainItemType; function Hash: string; case Integer of 0: (Heap: THeapData); 1: (ThreadData: TThreadData); 2: (StackFrame: TThreadStackEntry); 3: (SEH: TSEHEntry); 4: (System: TSystemData); end; 

次はThreadフィールドで、領域が独自のデータを保存するために使用するスレッドに関する情報を保存します。

 type TThreadInfo = (tiNoData, tiExceptionList, tiStackBase, tiStackLimit, tiTEB, tiThreadProc); type TThreadData = record Flag: TThreadInfo; ThreadID: Integer; Address: Pointer; Wow64: Boolean; end; 

領域内に多くのフローデータがある場合(たとえば、SEHフレームまたはCallStackフローのリスト)、それらは[含む]フィールドにも配置されます。

システム構造(PEB / TEB構造のフィールドなど)からのデータは、データアドレスとその説明からのレコードであるSystemDataフィールドに配置されます。
また、このデータは[含む]フィールドに配置できます。

領域がPEファイルのセクションのいずれかに属する場合、セクションデータはSectionパラメーターに配置されます。さて、ファイルディレクトリのリストは[ディレクトリ]フィールドに配置されます。

一言で言えば、このようなものです。次に、プロセスメモリカード上のデータを表すために、領域のリストを取得し、各領域のTRegionDataクラスのインスタンスを作成し、作成されたオブジェクトのフィールドを必要な情報で初期化する必要があります。

TMemoryMapクラスはこれに責任があります...

7. TMemoryMap


このクラスは、モジュール「MemoryMap.Core.pas」に実装されています。
そのタスクは、文字通り3つの主要な段階に削減されます。

  1. 指定されたアプリケーションのメモリ内の選択されたすべての領域のリスト、スレッド/ヒープ/ロードされたイメージなどのデータを取得します。
  2. TRegionDataリストを作成し、そのフィールドに受信した情報を入力します。
  3. データの保存/読み込み、データのフィルタリング。

実際には、すべてが少し複雑に見えます。
情報を収集する基本的な手順は次のとおりです。

 function TMemoryMap.InitFromProcess(PID: Cardinal; const ProcessName: string): Boolean; var ProcessLock: TProcessLockHandleList; begin Result := False; FRegions.Clear; FModules.Clear; FFilter := fiNone; ProcessLock := nil; //     FProcess := OpenProcess( PROCESS_QUERY_INFORMATION or PROCESS_VM_READ, False, PID); if FProcess = 0 then RaiseLastOSError; try FPID := PID; FProcessName := ProcessName; //    FProcess64 := False; {$IFDEF WIN64} if not IsWow64(FProcess) then FProcess64 := True; {$ELSE} //    32 ,    64- //   if Is64OS and not IsWow64(FProcess) then raise Exception.Create('Can''t scan process.'); {$ENDIF} //     if SuspendProcessBeforeScan then ProcessLock := SuspendProcess(PID); try FSymbols := TSymbols.Create(FProcess); try FPEImage := TPEImage.Create; try FWorkset := TWorkset.Create(FProcess);; try //        GetAllRegions; finally FWorkset.Free; end; {$IFDEF WIN64} //       32   AddWow64HeapsData; {$ENDIF} //     AddThreadsData; //     AddHeapsData; //    Process Environment Block AddPEBData; //     PE  AddImagesData; finally FPEImage.Free; end; finally FSymbols.Free; end; finally if SuspendProcessBeforeScan then ResumeProcess(ProcessLock); end; //  SortAllContainsBlocks; //      CalcTotal; //    UpdateRegionFilters; finally CloseHandle(FProcess); end; end; 

最初の4つの章でGetAllRegions / AddThreadsData / AddHeapsDataおよびAddImagesDataプロシージャのサンプルコードを提供しましたが、それには焦点を当てませんが、残りは処理したいと思います。

プロセスを開いた後の最初のステップは、プロセスのビット数を決定することです。
これは、プロセスのビットサイズ(現在および情報を受け取る)が一致しない場合、いくつかの追加手順を実行する必要があるためです。

一般的なスキームは次のとおりです。
  1. 32ビットプロセスは、32ビットOSで完全に32ビットでデータを受信できます。
  2. 64ビットプロセスは、64ビットプロセスでデータを完全に受信できます。
  3. 32ビットプロセスは、64 ビットプロセスでデータを受信できません
  4. 32- 32- 64- , .
  5. 64- 32-, .

最初の2つのポイントですべてが明確な場合、他の3つのポイントがより詳細に検討されます。

32ビットプロセスが64ビットプロセスでデータを受信できない理由は簡単です。ポインターのサイズでは許可されず、さらにReadProcessMemoryはエラーERROR_PARTIAL_COPYを定期的に生成します。

ただし、64ビットOSの32ビットプロセスからデータを取得するのは、はるかに難しい作業です。
前述したように、32ビットアプリケーションでは、4つの64ビットライブラリがロードされ、ヒープ/スレッドが作成されます。

32ビットアプリケーションからヒープとストリームのリストを取得すると、32ビットに関連するデータのみが表示され、64ビットアナログのデータは取得できません。

64ビットプロセスから32ビットプロセスに関するデータを要求する場合も同様です。64ビットに関連するデータのみが返されます。この場合、それらを部分的に取得するオプションがありますが。
特に、32ビットPEBへのアクセスは、次の関数を呼び出すことによって行われます。

 const ProcessWow64Information = 26; ... NtQueryInformationProcess(FProcess, ProcessWow64Information, @FPebWow64BaseAddress, SizeOf(ULONG_PTR), @ReturnLength) 

32ビットTEBへのアクセスは、NtTIB.ExceptionListパラメーターに格納されている64ビットTEBからアドレスを読み取ることで取得できます。

  //  64  TEB  TIB.ExceptionList    Wow64TEB if not ReadProcessMemory(hProcess, TIB.ExceptionList, @WOW64_NT_TIB, SizeOf(TWOW64_NT_TIB), lpNumberOfBytesRead) then Exit; 

次のコードを使用して、CallStackプロモーションの32ビットストリームのコンテキストを取得できます。

 const ThreadWow64Context = 29; ... ThreadContext^.ContextFlags := CONTEXT_FULL; if NtQueryInformationThread(hThread, ThreadWow64Context, ThreadContext, SizeOf(TWow64Context), nil) <> STATUS_SUCCESS then Exit; 

または、Wow64GetThreadContext関数を呼び出します。

しかし、64ビットプロセスから32ビットヒープに関するデータを合法的に取得する方法がわかりません。現在適用している唯一のオプションは、コマンドを32ビットプロセスに送信することです。32ビットプロセスは、32ビットヒープに関するデータを収集し、64ビットに送り返します(これがAddWow64HeapsData関数のハンドラーです)。

プロセスのビット数の定義と、それが必要な理由がわかったので、次にSuspendProcess関数の呼び出しに移りましょう。

良い方法では、これは、リモートプロセスのデータが変更されて読み取られたときに無関係にならないようにするためにのみ必要です。ただし、通常、このクラスのセットは、自分のアプリケーションまたはデバッガーの下のアプリケーションの2つの場合に使用します。どちらの場合も、スレッドをフリーズする必要はありませんが、サードパーティのアプリケーションが分析されている場合、なぜそうではないのですか?

リモートプロセスをフリーズすると、3つのヘルパークラスが作成されます。

  1. TSymbols-次の章で彼について話します。
  2. TPEImage-このクラスには、第4章で説明されているPEファイルに関する情報を取得できるメソッドが含まれています。利便性のみのために作られました。
  3. TWorksetは、共有メモリに関する情報を取得するタスクを持つヘルパークラスです。

本質的に、TWorksetは次の形式の構造のリストを保存します。

  TShareInfo = record Shared: Boolean; SharedCount: Byte; end; 

これらの構造は辞書に保存され、それぞれが特定のページアドレスに関連付けられています。
パラメーターは簡単です:


このデータは次の方法で取得されます。この方法では、すべてがQueryWorkingSet関数を呼び出すことになります。

 procedure TWorkset.InitWorksetData(hProcess: THandle); const {$IFDEF WIN64} AddrMask = $FFFFFFFFFFFFF000; {$ELSE} AddrMask = $FFFFF000; {$ENDIF} SharedBitMask = $100; SharedCountMask = $E0; function GetSharedCount(Value: ULONG_PTR): Byte; inline; begin Result := (Value and SharedCountMask) shr 5; end; var WorksetBuff: array of ULONG_PTR; I: Integer; ShareInfo: TShareInfo; begin SetLength(WorksetBuff, $400000); while not QueryWorkingSet(hProcess, @WorksetBuff[0], Length(WorksetBuff) * SizeOf(ULONG_PTR)) do SetLength(WorksetBuff, WorksetBuff[0] * 2); for I := 0 to WorksetBuff[0] - 1 do begin ShareInfo.Shared := WorksetBuff[I] and SharedBitMask <> 0; ShareInfo.SharedCount := GetSharedCount(WorksetBuff[I]); try FData.Add(Pointer(WorksetBuff[I] and AddrMask), ShareInfo); except on E: EListError do ; else raise; end; end; end; 

この関数はULONG_PTR配列を返します。配列の各要素は次のようにデータを保存します。最初の5ビットはページセキュリティ属性を保存します。次の3ビットは、このページが利用可能なプロセスの数です。もう1ビットは、ページのアクセシビリティを示します。次に、ページ自体のアドレスが来ます。
詳細については、PSAPI_WORKING_SET_BLOCKを参照してください

実際、これは単なる情報クラスであり、それ以上でもそれ以下でもありません。

ただし、コードに戻ります。
次の手順は次のとおりです。

  1. GetAllRegionsは、最初の章のコードに類似しています。
  2. AddThreadsDataは、第2章のコードに類似しています。
  3. AddHeapsDataは、第3章のコードに類似しています。
  4. AddPEBData-第5章の構造データの出力。
  5. AddImagesDataは、第4章のコードに類似しています。

ご覧のとおり、面白い(ほぼ)すべてを既に説明しました:)

残りの手順は、UpdateRegionFiltersの呼び出しを除いて、興味深いものではありません。
つまり、実用的な機能を実行します。つまり、現在不要な領域をリストから除外します(たとえば、未割り当てのメモリがある領域を削除するなど)。
このプロパティは、Filterプロパティでフィルターが変更されたときに常に呼び出されます。

ただし、必要に応じて、クラス自体のコードからこれらすべてを確認できます。
作業は非常に簡単です。

 var AMemoryMap: TMemoryMap; M: TMemoryStream; I: Integer; begin try M := TMemoryStream.Create; try //   AMemoryMap := TMemoryMap.Create; try //     AMemoryMap.InitFromProcess(GetCurrentProcessId, ''); //  , AMemoryMap.SaveToStream(M); //           finally AMemoryMap.Free; end; //     -,      M.Position := 0; //   AMemoryMap := TMemoryMap.Create; try //   AMemoryMap.LoadFromStream(M); //     AMemoryMap.Filter := fiNone; //   //       AMemoryMap.ShowEmpty := True; //    for I := 0 to AMemoryMap.Count - 1 do Writeln(NativeUInt(AMemoryMap[I].MBI.BaseAddress)); finally AMemoryMap.Free; end; finally M.Free; end; except on E: Exception do Writeln(E.ClassName, ': ', E.Message); end; Readln; end. 

彼らが言うように、私は自分自身のために書いたので、このクラスでの作業は梨を砲撃するのと同じくらい簡単です:)

8. TSymbols-シンボルの操作


このクラスの本質は、プロセスの住所に関するより詳細な情報を取得することです。たとえば、第2章では、CallStackストリーム(またはSEHフレームハンドラー)を受け取りましたが、これらは単なるアドレスです。しかし、乾燥した数字の代わりにこの写真のようなものを見る方がはるかに興味深いです:


これは非常に簡単に行われます-SymGetSymFromAddr関数を呼び出すだけで十分ですが、いくつかの微妙な違いがあります。

最初にコードを見てみましょう。

 function TSymbols.GetDescriptionAtAddr(Address, BaseAddress: ULONG_PTR; const ModuleName: string): string; const BuffSize = $7FF; {$IFDEF WIN64} SizeOfStruct = SizeOf(TImagehlpSymbol64); MaxNameLength = BuffSize - SizeOfStruct; var Symbol: PImagehlpSymbol64; Displacement: DWORD64; {$ELSE} SizeOfStruct = SizeOf(TImagehlpSymbol); MaxNameLength = BuffSize - SizeOfStruct; var Symbol: PImagehlpSymbol; Displacement: DWORD; {$ENDIF} begin Result := ''; if not FInited then Exit; GetMem(Symbol, BuffSize); try Symbol^.SizeOfStruct := SizeOfStruct; Symbol^.MaxNameLength := MaxNameLength; Symbol^.Size := 0; SymLoadModule(FProcess, 0, PAnsiChar(AnsiString(ModuleName)), nil, BaseAddress, 0); try if SymGetSymFromAddr(FProcess, Address, @Displacement, Symbol) then Result := string(PAnsiChar(@(Symbol^).Name[0])) + ' + 0x' + IntToHex(Displacement, 4) else begin //        SymLoadModule(FProcess, 0, PAnsiChar(AnsiString(ModuleName)), nil, BaseAddress, 0); if SymGetSymFromAddr(FProcess, Address, @Displacement, Symbol) then Result := string(PAnsiChar(@(Symbol^).Name[0])) + ' + 0x' + IntToHex(Displacement, 4); end; finally SymUnloadModule(FProcess, BaseAddress); end; finally FreeMem(Symbol); end; if Result = '' then Result := ExtractFileName(ModuleName) + ' + 0x' + IntToHex(Address - BaseAddress, 1); end; 

アドレスが属する関数の名前の説明を正しく取得するには、関数が属するライブラリーへのパス、またはこのライブラリーがロードされるアドレスを知る必要があります(両方のパラメーターがコードで使用されます)。これらのパラメーターは、SymLoadModule関数に必要です。

2番目の注意点は、SymGetSymFromAddr関数の呼び出しが失敗する場合があることです。その理由は私には明らかではありませんが、インターネットはこの状況を定期的に説明し、その解決方法はSymUnloadModuleを呼び出さずにSymLoadModule関数を再度呼び出すことです。私はそのような奇妙な動作を理解していませんでした-しかし、それは本当に役立ちます。

最後の微妙な点は、この関数がこの情報が存在する場合にのみアドレスの有効な説明を返すことです(外部ファイルからの文字がロードされるか、それらが目的のモジュールの一部です)。

この情報はデバッグにはあまり重要ではありませんが、少し簡単になります。
たとえば、ここでは、標準のChromeブラウザストリームスタックのように見えます(CallStack + SEHフレーム):


シンボルが提供できるより有用な情報は、エクスポートされたライブラリ関数とその現在のアドレスのリストです。
TSymbolsクラスでは、この情報はGetExportFuncListプロシージャを呼び出すことで取得され、次のようになります。

 function SymEnumsymbolsCallback(SymbolName: LPSTR; SymbolAddress: ULONG_PTR; SymbolSize: ULONG; UserContext: Pointer): Bool; stdcall; var List: TStringList; begin List := UserContext; List.AddObject(string(SymbolName), Pointer(SymbolAddress)); Result := True; end; procedure TSymbols.GetExportFuncList(const ModuleName: string; BaseAddress: ULONG_PTR; Value: TStringList); begin SymLoadModule(FProcess, 0, PAnsiChar(AnsiString(ModuleName)), nil, BaseAddress, 0); try if not SymEnumerateSymbols(FProcess, BaseAddress, @SymEnumsymbolsCallback, Value) then begin SymLoadModule(FProcess, 0, PAnsiChar(AnsiString(ModuleName)), nil, BaseAddress, 0); SymEnumerateSymbols(FProcess, BaseAddress, @SymEnumsymbolsCallback, Value) end; finally SymUnloadModule(FProcess, BaseAddress); end; end; 

すべては、コールバック関数のアドレスを渡すSymEnumerateSymbolsを呼び出すことになります。
呼び出されると、SymbolNameパラメーターにはエクスポートされた関数の名前とSymbolAddressのアドレスが含まれます。

これは、ユーザーに次のサインを表示するのに十分です。


モジュール「MemoryMap.Symbols.pas」のSymSetOptionsおよびSymInitializeの省略された呼び出しを含む、このクラスの実装をより詳細に確認できます。

9. ProcessMemoryMap


さて、ここで記事の最後の部分に行きます。
前に言ったように、2つの方法でMemoryMapクラスセットを使用し

ます。1. OnAttachedFilesRequestハンドラーをオーバーラップしてEurekaLog出力に統合し、例外が発生した時点で現在のプロセスマップを追加して、 MEM_PRIVATEフラグを持つ特定のデータ)とストリームスタック、さらにPEBからの情報の一部。通常、これはエラーの原因を分析するのに十分です。
2.デバッグされたアプリケーションを分析するための代替ツールとして使用します。

2番目のオプションでは、MemoryMapクラスと直接連携する別のユーティリティが実装され、さらにいくつかの追加機能が追加されました。


そのソースコードについては説明しません。機能については少しだけ説明します。

フロントエンドから見ると、ほぼ1対1のVMMapに似ています。ただし、このようなインターフェイスは分析に最も便利であるため、これは当初計画されていました。

上部には、タイプごとにグループ化された地域に関する一般情報を含むリストがあり、これもフィルターです。

現時点では、次の機能を表しています

。1.指定されたアドレスのメモリの内容を表示します(Ctrl + Q)。


原則として、この機能はDelphiデバッガーの[CPUビュー]ウィンドウにありますが、このモードにはさらに多くの可能性があります。たとえば、PEBフィールドを見ると、データは異なる形式で表示されます。


プロセスパラメータブロックは次のようになります。


まあなど。 現時点では、ユーティリティは次の構造のデマップされたデータを表示できます。


このリストは最終的なものではなく、定期的に新しい構造が追加されます。

2.プロセスメモリ内のデータを検索します(Ctrl + F):


残念ながら、Delphiデバッガにはこの機能がありません。
Ansi、Unicode文字列、または単に抽象HEXバッファで検索できます。検索時には、検索の開始アドレスと、ページを検索する必要があることを示すフラグを指定できます。このフラグへのアクセスは読み取り専用です。
結果は、上記のメモリダンプを含むウィンドウとして表示されます。

3. 2枚のメモリカードのコンパレータ。設定に含まれています。

2つのメモリカードの違いを見つけて、テキストとして表示できます。


データではなく、カード自体のみが比較されます。 つまりあるアドレスで4バイトが変更された場合、この変更は表示されません。ただし、リージョンのサイズが変更された場合、束がなくなった場合、ファイルがアップロード/ダウンロードされた場合などです。 -これらはすべて比較結果に表示されます。
カードの現在の写真と以前に保存した写真を比較することも、F5ホットキーを使用して写真を更新することもできます。

4.メモリダンプ。

また、Delphiデバッガーの機能が欠落しています。指定された領域のメモリの内容または指定されたアドレスのデータをディスクに保存できます。

5.分析されたプロセスに読み込まれたすべてのライブラリから利用可能なすべてのエクスポートされた関数の出力(Ctrl + E)。


名前またはアドレスによる機能のクイック検索と同様に。

これまでのところ、私は個人的に現在の機能を十分に備えており、新しい機能は追加しませんでしたが、将来このユーティリティは開発されます。

ProcessMemoryMapは、オープンソースプロジェクトです。
最新の安定版リリースは、常に次のリンクから入手できます。http //rouse.drkb.ru/winapi.php#pmm2
最新のコード変更を含むGitHubリポジトリは、https //github.com/AlexanderBagel/ProcessMemoryMapにあります。

ソースコードへの直接リンク:https : //github.com/AlexanderBagel/ProcessMemoryMap/archive/master.zip
最新ビルドへの直接リンク:http : //rouse.drkb.ru/files/processmm_bin.zip

自己アセンブリには、Virtual TreeViewバージョン5以降のコンポーネントのインストールパッケージが必要です:http : //www.soft-gems.net/

アセンブリは、Delphi XE4以降使用して「Win32 /リリース」モードで実行され、このユーティリティの64ビットバージョンが自動的にアセンブルされ、接続されます(リソースとして)。
Delphiの古いバージョンでは、ProcessMemoryMapはテストされていません。

10.結論として


さて、この資料があなたの役に立つことを願っています。もちろん、すべての資料をより詳細に開示すると、記事の量が非常に増加するため、最上部のみに行きました。

そのため、ここにもう少し情報を見つけるために使用できるリンクがあります。

TEB / PEBシステム構造などに関する情報ここで見つけることができます:
http://processhacker.sourceforge.net/
http://redplait.blogspot.ru/
http://www.reactos.org/ru

PEファイルに関する情報:
http://msdn.microsoft.com/ en-us / magazine / ms809762.aspx

SEH情報:
http : //msdn.microsoft.com/en-us/library/ms680657(v=VS.85).aspx
http://www.microsoft.com/msj /0197/exception/exception.aspx
http://qxov.narod.ru/articles/seh/seh.html

すべてのデモ例のソースコードは、このリンクから取得できますDelphi Masters

フォーラムに、記事の作成に繰り返し協力してくれたことに感謝します。Dmitry別名「Ptiburdukovの兄弟」、Andrei Vasiliev別名「Inovet」、およびSergey別名「Cartman」に校正資料を提供してくれたことに個人的に感謝します。



がんばって。

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


All Articles