APIコールインターセプトの実装

モニター しかし、すばらしい言葉は、たくさんの概念を組み合わせています。 たとえば、1861年に初めてこの言葉が戦艦USSモニターに適用されました。 少し後に、この言葉はディスプレイと呼ばれました。 しばらくすると、モニターの栄光のコホートには、パフォーマンスカウンターなど、私たちにとって馴染みのあるものが含まれ、それらの主なタスクは監視、つまり監視である、さまざまなソフトウェアの束になりました。

モニターのタスクは単純です-実際には、オブザーバーですが、マネージャーとして機能することもできますが、「モニター」を翻訳するためのオプションの1つはメンターです。 また、状況を分析して適切な結論を引き出すことができる一連のデータを提供するというタスクも明らかです。

現代のソフトウェアでは、モニターはほぼどこにでもあります。たとえば、同じPunto Switcherは合法的なモニターの典型的な例です。 ほとんどすべてのウイルス対策プログラムはモニター、プロファイラーであり、私はメインツールキットであるデバッガーについても話していません。デバッガーはモニターでもあります。

バリケードの裏側には、悪意のあるソフトウェアの山全体が表示されますが、その一部は監視を使用して主な目標を達成することも好みます。 しかし、それらについては今ではありません...

現時点では、インターネットには非常に多くのソフトウェア機能モニターの例がありますが、ほとんどの場合、インポートテーブルを編集することによってインターセプトされるか、インターセプトされた機能の開始時にインターセプターをインストールすると考えられています(いわゆるスプライシング)。 しかし、これらの資料は入手可能ですが、時には難解な言語で書かれているため、開発者を助けないこともあります。また、対象分野に精通していない開発者にとって理解できない、文脈から外れたコードの一部を表すことさえあります。

先月、インターセプターを正しく実装する方法を尋ねる人が何人か来て、例へのリンクは実際には役に立たなかったので、最初からすべてを噛まなければなりませんでした。 しかし今では、傍受技術に対処し始めたばかりの人々が遭遇する主な間違いに気付いています。

その結果、次回すべてを説明しないように、私はレビュー記事を作成し、「仕組み」について可能な限り簡単な言語ですべてを説明しようとすることにしました。


1.モニターの本質


モニターの主なタスクは、それが制御する機能への制御の移行について学習することができます。

このために、元の関数の呼び出しをインターセプトするためのさまざまなオプションが使用され、その制御の助けを借りて「インターセプトハンドラー」(または、より便利な「インターセプター」)に制御が転送されます。

インターセプトされた関数をどうするかは、インターセプターの実装にさらに依存します。 呼び出しのパラメーターをログに表示し、必要に応じて、他のパラメーター、たとえば、結果、呼び出しスタックのステータスなどを表示できます。 主なものは、コントロールをとることです。

私はあなたがほとんど自分で関数フックを書くことがほぼ保証されていると言うことができます。 これは、OOPの概念によって非常に促進されます。 仮想メソッドまたは動的メソッドをオーバーライドするクラスは、モニターの監視下で既に呼び出すことができます。 実際、オーバーライドによってブロックされたメソッドは、インターセプトされた関数の最終的なハンドラーであり、実際に内部でどのように機能するかを考える必要さえありません。元のメソッドを呼び出すことで問題が生じることはありません。 コンパイラはすでにすべてを行っています。

しかし、もう少し深く掘り下げなければなりません。 たとえば、静的メソッドでは、このようなフォーカスは通過しなくなり、そのようなオーバーラップが必要な場合は、すべて自分で行う必要があります。 ただし、監視に直接進む前に、傍受ハンドラーの実装を理解する必要があります。

2.傍受ハンドラーの正しい宣言


関数のインターセプトを開始する前に、その呼び出しのパラメーターと呼び出し規則を正確に知る必要があります。 つまり、たとえばMessageBoxAをインターセプトする場合、元の関数の代わりに機能するインターセプターは次の形式になっている必要があります。

function InterceptedMessageBoxA(hWnd: HWND; lpText, lpCaption: PAnsiChar; uType: UINT): Integer; stdcall; begin // TODO... end; 

つまり、インターセプトハンドラーのパラメーターと呼び出し規則(stdcall / cdeclなど)は、元の関数と正確に一致する必要があります。 これが行われない場合、ハンドラーを呼び出した後にエラーが発生することがほぼ保証されます。

主に便宜上、正しいハンドラー宣言が必要です。 もちろん書くことができ、ここにそのようなハンドラがあります:

 procedure InterceptedFunc; begin // TODO... end; 

ただし、この場合、アセンブラーの挿入を使用して、合意とパラメーターの数を考慮して、呼び出しパラメーターを取得し、呼び出しを正しく終了する必要があります。

クラスメソッドの形式で実装された関数/プロシージャのインターセプターの実装には、わずかなニュアンスがあります。

TApplication.MessageBoxをインターセプトするとしましょう。
インターセプトハンドラがクラスメソッドとして実装されている場合、その宣言は元の関数のようになります。

 function TTestClas.InterceptedApplicationMessageBox( const Text, Caption: PChar; Flags: Longint): Integer; begin // TODO... end; 

インターセプターが独立した関数である場合、その実装は少し異なります。

 function InterceptedApplicationMessageBox( Self: TObject; const Text, Caption: PChar; Flags: Longint): Integer; begin // TODO... end; 

ハンドラーの宣言におけるこのような違いは、クラスメソッドの最初のパラメーターが明示的に宣言されていない変数Selfであるという事実によるものです。

ところで、たとえば次のように、最初のインターセプターで特定の変数のクラス名を取得しようとすると、

 function TTestClas.InterceptedApplicationMessageBox( const Text, Caption: PChar; Flags: Longint): Integer; begin ShowMessage(Self.ClassName); end; 

...次に、TTestClassではなく、TApplicationというテキストが表示されます。 Selfパラメーターには、インターセプターが実装されているクラスに関するデータではなく、元のクラスに関するデータが含まれます。

原則として、インターセプトハンドラの宣言について知っておく必要があるのはこれだけです。これで、関数をインターセプトするさまざまな方法を検討することができます。

3.ウィンドウプロシージャのサブクラス化


長い間、技術的な部分を開始する場所を選択し、最終的には文書化された技術に専念することにしました。 確かに、なぜ別の自転車を発明するのか-簡単に曲げることができます。

VCLが本質的にAPIの単なるラッパーであることは、あなたにとって秘密ではないと思います。 フォーム上のほとんどの視覚要素とフォーム自体はウィンドウであり、他のパラメーターの中でもウィンドウプロシージャを持っています。 プログラマーがウィンドウの標準動作を変更する必要があるタスクを持っている場合、サブクラス化を適用し、ウィンドウプロシージャハンドラーを自分のものに置き換えて、必要な機能を実装します。

この手法は非常に一般的であり、Delphiで独自のコントロールを実装する場合、ほとんどの場合、グローバルTWinControl.MainWndProcハンドラーですべてのウィンドウのウィンドウプロシージャが重複する結果に遭遇します。 これは、メッセージ+メッセージ定数を指定することでコード内の特定のメッセージをブロックしたり、仮想WndProcおよびDefaultHandlerで作業したりすることができる、非常に便利なソリューションです。

このメソッドの一般的な説明は、次のリンクにあります。 ウィンドウのサブクラス化

実装アルゴリズムは、5つのポイントの形式で表すことができます。
  1. 元のウィンドウプロシージャのアドレスを取得する
  2. ハンドラーがアクセスできる場所に保存する
  3. 新しいウィンドウプロシージャハンドラーの割り当て
  4. インターセプターコールの処理(ログ記録/コールパラメーターの変更など)
  5. 必要に応じて、古いハンドラーのアドレスを取得して呼び出します。

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

 unit uSubClass; interface uses Windows, Messages, Classes, Controls, Forms, StdCtrls; type TForm1 = class(TForm) Button1: TButton; procedure Button1Click(Sender: TObject); end; var Form1: TForm1; implementation {$R *.dfm} function MainFormSubclassProc(hwnd: THandle; uMsg: UINT; wParam: WPARAM; lParam: WPARAM): LRESULT; stdcall; var OldWndProc: Pointer; begin if uMsg = WM_WINDOWPOSCHANGING then PWindowPos(lParam)^.flags := PWindowPos(lParam)^.flags or SWP_NOSIZE or SWP_NOMOVE; OldWndProc := Pointer(GetWindowLong(hwnd, GWL_USERDATA)); Result := CallWindowProc(OldWndProc, hwnd, uMsg, wParam, lParam); end; procedure TForm1.Button1Click(Sender: TObject); var OldWndProc: THandle; begin OldWndProc := GetWindowLong(Handle, GWL_WNDPROC); SetWindowLong(Handle, GWL_USERDATA, OldWndProc); SetWindowLong(Handle, GWL_WNDPROC, Integer(@MainFormSubclassProc)); MessageBox(Handle, '      ', PChar(Application.Title), MB_ICONINFORMATION); end; end. 

この例では、アプリケーションのメインフォームのウィンドウプロシージャが置き換えられます。
古いプロシージャのアドレスは、定数GWL_USERDATAを介してアクセスされるユーザーウィンドウバッファーに格納されます。
新しいMainFormSubclassProcハンドラーが呼び出されると、メッセージコードがチェックされます。 このメッセージがウィンドウのサイズ変更または座標に関するものである場合、フラグSWP_NOSIZEおよびSWP_NOMOVEを設定することにより、この機能がブロックされます。
このアプリケーションを実行してボタンをクリックし、メインフォームのサイズを変更してみてください。 あなたは成功しません。

小さなニュアンス :このコードでは、二重の重複チェックはありません。 もう一度ボタンをクリックすると、スタックオーバーフローに関するエラーが発生します。 これは、ウィンドウプロシージャの2番目のオーバーラップで、古いアドレスを受信すると、MainFormSubclassProcハンドラーの同じアドレスが返されるため、CallWindowProcの最初の呼び出しで無限ループに入るためです。 私たちは自分自身を呼び出し、その出口はオーバーフローによって発生します。

アプリケーション :この傍受オプションは、Windowsでのみ使用できます。 ダイアログでは、定数DWL_DLGPROCを使用する必要があります。

その結果 、モニターの概念のフレームワーク内で、制御された関数としての古いウィンドウプロシージャ、制御された関数に送られるすべてのデータをモニターする新しいMainFormSubclassProcハンドラーがモニターになります。 その中で、すべてのパラメーターをログに記録し、データを呼び出す前にデータを置き換えるか、まったく呼び出さないことで古いハンドラーの動作を制御し、独自の動作を実装できます。

残念ながら、このコードはアプリケーションのウィンドウでのみ機能します。 もちろん、より正確には、他の人のアプリケーションからウィンドウハンドルを取得し、上記と同じ方法で新しいインターセプターを割り当てることができますが、これは何も良い結果にならないからです。 この場合、アドレス空間にある新しいハンドラーのアドレスを示します。このアドレスの見知らぬ人には完全に異なるコードがあります(まあ、または一般的にこのメモリの一部は割り当てられないかもしれません)、それは別のプロセスの突然の死につながる可能性があります。

これを防ぐには、何らかの方法でインターセプターコードを他の誰かのプロセスに配置し、その後でのみ置換を行う必要があります。 これはいくつかの方法で行われますが、少し後でそれらに焦点を当てます...

4. VMTテーブルの編集による傍受。


原則として、これはインターセプトの非常にまれなオプションですが、考えられるすべての方法について説明することにしたので、それも考慮する必要があります。

おそらく、特定のコントロールの動作が完全に満足できる状況に出くわしたことがありますが、ここでは少し欠けています。 問題を解決するには、通常、対応する仮想メソッドをオーバーラップする問題クラスに相続人を記述し、コントロールの目的の動作を記述する必要があります。 しかし、それが非常に遅延している場合、これは、必要なメソッドを外部からブロックするだけで、継承者を実装せずに実現できます。

次の例では、フォームのサイズが変更されたときに呼び出されるTForm.CanResizeメソッドをオーバーライドする方法を示します。 コードのタスクは、フォームの幅が500ピクセルを超えて変更されないようにすることです。

もちろん、この例では、最初に目標を達成するために多数のマイナスがあり、OnResizeハンドラーをブロックできます。次に、CanResizeがTFormからオーバーラップするため、この変更はプロジェクト内のすべてのフォームに影響しますが、彼と例は彼のタスクですこの機会を示してください。

アプリケーション :このインターセプトオプションは、仮想クラスメソッドにのみ使用でき、独自のPEファイルのフレームワーク内でのみ使用できます。 このコードをライブラリに配置し、この方法でメインアプリケーションからメソッドをインターセプトしようとしても、TFormアプリケーションとTFormライブラリは異なるクラスであるため、何も機能しません。

コードは次のとおりです。

 unit uVMT; interface uses Windows, Classes, Controls, Forms, StdCtrls; type TForm1 = class(TForm) Button1: TButton; procedure Button1Click(Sender: TObject); end; var Form1: TForm1; implementation {$R *.dfm} function NewCanResizeHandler(Self: TObject; var NewWidth, NewHeight: Integer): Boolean; begin Result := True; if NewWidth > 500 then NewWidth := 500; end; procedure TForm1.Button1Click(Sender: TObject); var VMTAddr: PPointer; OldProtect: Cardinal; begin asm mov eax, Self mov eax, [eax] //    VMT  add eax, VMTOFFSET TForm.CanResize //     TForm.CanResize mov VMTAddr, eax end; //     VirtualProtect(VMTAddr, 4, PAGE_EXECUTE_READWRITE, OldProtect); try //    VMTAddr^ := @NewCanResizeHandler; finally //    VirtualProtect(Pointer(VMTAddr), 4, OldProtect, OldProtect); FlushInstructionCache(GetCurrentProcess, VMTAddr, 4); end; if Width > 500 then Width := 500; MessageBox(Handle, '     500 ', PChar(Application.Title), MB_ICONINFORMATION); end; end. 

より詳細に:

1.傍受ハンドラーの宣言は、この記事の2番目のセクションで説明したニュアンスを考慮して行われました(Selfパラメーターが表示されました)。

2. Delphiヘルプで説明されている、文書化されたVMTOFFSETディレクティブがインターセプトに使用されます。

ここに彼女の説明のある部分があります:
追加の2つのディレクティブにより、アセンブリコードは動的メソッドと仮想メソッドにアクセスできます:VMTOFFSETとDMTINDEX。
VMTOFFSETは、仮想メソッドテーブル(VMT)の先頭から、仮想メソッド引数の仮想メソッドポインターテーブルエントリのオフセットをバイト単位で取得します。 このディレクティブには、TExample.VirtualMethodなど、メソッド名をパラメーターとして含む完全に指定されたクラス名が必要です。

説明から明らかなように、そのタスクは、仮想メソッドテーブル(VMT)の先頭に対する仮想メソッドのアドレスへのオフセットを返すことです。 VMTテーブルの開始は、Selfパラメーターによって直接示されます。 つまり、これをコードの形式で表すと、次のようになります。

 VMT := Pointer(Self)^; 

3.アセンブラを使用してメソッドのアドレスへのポインタを挿入すると、仮想メソッドの新しいハンドラのアドレスに置き換えられます。

4.置換は、通常は書き込み権限のないメモリページにあるアプリケーションコードで行われるため(通常、これらは読み取り権と実行権です)、書き込み権限は新しいハンドラーの予約前に設定されます。

さて、例を実行してその動作を確認してください。

ご覧のとおり、実際には、最初の例とほぼ同じ手順をすべて実行しました。 監視されている関数のアドレスを受け取り、元のメソッドのパラメーターが管理されている新しいハンドラーのアドレスに置き換えます。 元のハンドラー( inheritedのアナログ)を呼び出すコードを提供しません。 それにもかかわらず、この傍受方法は、視野を広げるためだけに示されており、戦闘アプリケーションでの実装には非常に望ましくありません。

VMTの編集の原則を理解するのが難しい場合は、 Hallvard Vassbotnによるこの記事を読むことをお勧めします。メソッドはコンパイラー実装を呼び出します。

または、 Alexander Alekseevが親切に提供した翻訳: コンパイラによるメソッド呼び出しの実装

動的クラスメソッドをインターセプトするオプションについては検討しませんが、原則はほぼ同じです。

5.インポートテーブルの編集による傍受


インポートテーブルとは何ですか。 アプリケーションを作成し、関数のAPIを呼び出すと、アプリケーションは何らかの方法でこの関数のアドレスを計算して、制御を転送する必要があります。 ほとんどの関数は、たとえば次のように静的に宣言されます。

 {$EXTERNALSYM MessageBox} function MessageBox(hWnd: HWND; lpText, lpCaption: PChar; uType: UINT): Integer; stdcall; ... function MessageBox; external user32 name 'MessageBoxA'; 

これは、MessageBox関数の呼び出しの完全な宣言とそれを呼び出す合意を明示的に示します。 この情報は、コンパイラーが関数を呼び出すときにスタックを適切に調整するために必要です。 また、関数が実装されているライブラリの名前とエクスポートされるライブラリの名前も示します。

ご覧のとおり、関数のアドレスはここにありません。関数をエクスポートするライブラリは任意のアドレスにロードでき、ほとんどの場合、各プロセスで同じ関数のアドレスが異なるため、関数のアドレスはありません。 確かに、ここにはわずかなニュアンスがあります。user32.dll、kernel32.dll、およびntdll.dllライブラリは、すべてのプロセスに対して固定アドレス(少なくとも32ビットシステム)でロードされますが、とにかく特定の静的アドレスに依存しないでください。

アプリケーションをコンパイルすると、静的に宣言された関数に関する情報がインポートテーブルの本体に配置されます。 アプリケーションが起動されると、ローダーはこのテーブルを分析し、それに示されているライブラリをロードし、ライブラリエクスポートテーブルに基づいて、アプリケーションインポートテーブルの対応するフィールドに配置されている関数の実際のアドレスを見つけます。

インポートテーブルがどのように機能するかについての一般的な説明は、Matt Pitrekの記事「 Peering Inside the PE:A Tour of the Win32 Portable Executable File Format」にあります。
オブジェクトファイルのREおよびCOFF形式の RSDNで翻訳を利用できます
さらに情報が必要な場合は、次の記事をご覧ください: PE。 レッスン6. Iczelionに起因するインポートテーブル

この情報で何ができるのでしょうか? インターセプトハンドラーを割り当てるのは簡単です。ローダーによって計算されたアドレスをハンドラーのアドレスに変更するだけで、その後インターセプトは有効と見なされます。

アプリケーション :このインターセプトオプションは、静的リンクのあるAPI関数にのみ使用されます。

元の関数のアドレスを検索し、インターセプターのアドレスに置き換えるには、次の関数を記述します。

 function ReplaceIATEntry(const OldProc, NewProc: FARPROC): Boolean; var ImportEntry: PImageImportDescriptor; Thunk: PImageThunkData; Protect: DWORD; ImageBase: Cardinal; DOSHeader: PImageDosHeader; NTHeader: PImageNtHeaders; begin Result := False; if OldProc = nil then Exit; if NewProc = nil then Exit; ImageBase := GetModuleHandle(nil); //     DOSHeader := PImageDosHeader(ImageBase); NTHeader := PImageNtHeaders(DWORD(DOSHeader) + DWORD(DOSHeader^._lfanew)); ImportEntry := PImageImportDescriptor(DWORD(ImageBase) + DWORD(NTHeader^.OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress)); //     ... while ImportEntry^.Name <> 0 do begin Thunk := PImageThunkData(DWORD(ImageBase) + DWORD(ImportEntry^.FirstThunk)); // ...     ... while Pointer(Thunk^._function) <> nil do begin // ...      . if Pointer(Thunk^._function) = OldProc then begin //   if VirtualProtect(@Thunk^._function, SizeOf(DWORD), PAGE_EXECUTE_READWRITE, Protect) then try //   ... //Thunk^._function := DWORD(NewProc); // ...   . InterlockedExchange(Integer(Thunk^._function), Integer(NewProc)); Result := True; finally VirtualProtect(@Thunk^._function, SizeOf(DWORD), Protect, Protect); FlushInstructionCache(GetCurrentProcess, @Thunk^._function, SizeOf(DWORD)); end; end else Inc(PAnsiChar(Thunk), SizeOf(TImageThunkData32)); end; ImportEntry := Pointer(Integer(ImportEntry) + SizeOf(TImageImportDescriptor)); end; end; 

GetModuleHandle(nil)イメージベースコードが示すように、現在の実行可能ファイルの本文のインポートテーブルを修正します。 インターセプターアドレスの直接設定は、InterlockedExchangeを呼び出すことでアトミックに実行されます。 これはかなりデリケートなポイントですが、インターセプターのアドレスを変更するこの特定の方法の理由については少し後で止めます。

この関数を呼び出す例を書くだけです。

新しいプロジェクトを作成し、メインフォームにボタンを配置し、ReplaceIATEntry関数の実装を記述してから、次のコードを追加します。

 var OrigAddr: Pointer = nil; function InterceptedMessageBoxA(Wnd: HWND; lpText, lpCaption: PAnsiChar; uType: UINT): Integer; stdcall; type TOrigMessageBoxA = function(Wnd: HWND; lpText, lpCaption: PAnsiChar; uType: UINT): Integer; stdcall; var S: AnsiString; begin S := AnsiString('Function interepted. Original message: ' + lpText); Result := TOrigMessageBoxA(OrigAddr)(Wnd, PAnsiChar(S), lpCaption, uType); end; procedure TForm1.FormCreate(Sender: TObject); begin OrigAddr := GetProcAddress(GetModuleHandle(user32), 'MessageBoxA'); ReplaceIATEntry(OrigAddr, @InterceptedMessageBoxA); end; procedure TForm1.Button1Click(Sender: TObject); begin MessageBoxA(0, 'Test Message', nil, 0); end; 

ここでは、フォームコンストラクターがMessageBoxA関数のインターセプトを設定し、インターセプトの操作を示すためにボタンハンドラーでMessageBoxA呼び出しが行われます。

インターセプトハンドラー自体は非常に単純です。 インポートテーブルのエントリのみが変更され、元の関数の本体は修正されていないため、元の関数に制御を移すには、OrigAddr変数に格納されている関数の以前に格納されたアドレスを呼び出すだけで十分です。

小さなニュアンスがあります
静的関数は、遅延インポートセクションにあります。 この宣言は非常に簡単です。サンプルコードを次のように少し変更してみましょう。

  function DelayedMessageBoxA(hWnd: HWND; lpText, lpCaption: PAnsiChar; uType: UINT): Integer; stdcall; external user32 name 'MessageBoxA' delayed; procedure TForm1.Button1Click(Sender: TObject); begin DelayedMessageBoxA(0, 'Test Message', nil, 0); end; 

DelayedMessageBoxA関数は、実際には同じMessageBoxAです。
ただし、「 delayed 」パラメーターは、delayed importセクションでこの関数の呼び出しを記録します。

ニュアンス :「 遅延 」パラメータは、Delphiの以前のバージョンでは使用できないため、この記事のデモ例では、このコードは見つかりません。

上記で実装されたインターセプターはインポートテーブルでのみ変更を行うため、この方法で宣言された関数呼び出しは制御されません。 これを行うには、IMAGE DIRECTORY_ENTRY DELAYED IMPORTを編集する必要があります。

この記事では、遅延インポートのテーブルを編集するオプションを考慮していません。原則として、興味深いことは何もありません。原則は従来のインポートと同じで、わずかに異なる構造が使用されます。 (興味のある方は、このインターセプターの実装を宿題と見なします:)。

6.エクスポートテーブルの編集による傍受。


インポートテーブルと遅延インポートの編集は、静的に宣言された関数には適していますが、動的な宣言と関数呼び出しに対しては機能しません。

平易な言語の場合、前の例に別のボタンを追加して、ハンドラーに次のコードを記述してください。

 procedure TForm1.Button2Click(Sender: TObject); type TOrigMessageBoxA = function(Wnd: HWND; lpText, lpCaption: PAnsiChar; uType: UINT): Integer; stdcall; var OrigMessageBoxA: TOrigMessageBoxA; begin @OrigMessageBoxA := GetProcAddress(GetModuleHandle(user32), 'MessageBoxA'); OrigMessageBoxA(0, 'Test Message', nil, 0); end; 

ボタンを押すと、インターセプターが機能しなかったことがわかります。 実際のところ、GetProcAddress関数は、インポートテーブルをバイパスして関数のアドレスを受け取り、ライブラリエクスポートテーブルに焦点を当てることになっています。

上記のメソッドによって呼び出された関数にインターセプトハンドラーを強制的に応答させるには、必要なライブラリのエクスポートテーブルを直接変更する必要があります。

簡単に言えば、エクスポートテーブルには、PEファイルによってエクスポートされた関数、その名前、インデックス(序数)、および関数のアドレスに関する情報が含まれています。一部の関数はインデックスによってのみエクスポートされ、名前はありません。そのような場合、それらの関数へのアクセスはインデックスを介してのみ行われます。

エクスポートテーブルの作業の一般的な説明は、Matt Pitrekによる同じ記事にあります。
さらに情報が必要な場合は、次の記事をご覧ください:
PE。レッスン7. Iczelionに起因するテーブルのエクスポート

アプリケーション:このインターセプトオプションは、動的に呼び出されるAPI関数にのみ使用されます。

次の例は、インターセプターのアドレスを設定する関数のわずかな変更を除いて、実際には前の例と変わりません。実際にはコード自体:

 unit uEAT; interface uses Windows, Classes, Controls, Forms, StdCtrls; type TForm1 = class(TForm) Button1: TButton; procedure Button1Click(Sender: TObject); procedure FormCreate(Sender: TObject); end; var Form1: TForm1; implementation {$R *.dfm} uses DeclaredTypes; function ReplaceEATEntry(const DllName: string; OldProc, NewProc: FARPROC): Boolean; var ImageBase: Cardinal; DOSHeader: PImageDosHeader; NTHeader: PImageNtHeaders; ExportDirectory: PImageExportDirectory; pFuntionAddr: PDWORD; OrdinalCursor: PWORD; Ordinal, Protect: DWORD; FuntionAddr: FARPROC; I: Integer; begin Result := False; if OldProc = nil then Exit; if NewProc = nil then Exit; ImageBase := GetModuleHandle(PChar(DllName)); //     DOSHeader := PImageDosHeader(ImageBase); NTHeader := PImageNtHeaders(DWORD(DOSHeader) + DWORD(DOSHeader^._lfanew)); ExportDirectory := PImageExportDirectory(DWORD(ImageBase) + DWORD(NTHeader^.OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress)); I := 1; //     OrdinalCursor := Pointer(ImageBase + DWORD(ExportDirectory^.AddressOfNameOrdinals)); while I < Integer(ExportDirectory^.NumberOfNames) do begin //       Ordinal := OrdinalCursor^; //       FuntionAddr := Pointer(ImageBase + DWORD(ExportDirectory^.AddressOfFunctions)); FuntionAddr := Pointer(ImageBase + PDWORD(DWORD(FuntionAddr) + Ordinal * 4)^); // ,    ? if FuntionAddr = OldProc then begin //  ,  ,      pFuntionAddr := PDWORD(ImageBase + DWORD(ExportDirectory^.AddressOfFunctions) + Ordinal * 4); //        hInstance  NewProc := Pointer(DWORD(NewProc) - ImageBase); //    if VirtualProtect(pFuntionAddr, SizeOf(DWORD), PAGE_EXECUTE_READWRITE, Protect) then try //   ... //pFuntionAddr^ := Integer(NewProc); // ...   . InterlockedExchange(Integer(PImageThunkData(pFuntionAddr)^._function), Integer(NewProc)); Result := True; finally VirtualProtect(pFuntionAddr, SizeOf(DWORD), Protect, Protect); FlushInstructionCache(GetCurrentProcess, pFuntionAddr, SizeOf(DWORD)); end; Break; end; Inc(I); Inc(OrdinalCursor); end; end; var OrigAddr: Pointer = nil; function InterceptedMessageBoxA(Wnd: HWND; lpText, lpCaption: PAnsiChar; uType: UINT): Integer; stdcall; type TOrigMessageBoxA = function(Wnd: HWND; lpText, lpCaption: PAnsiChar; uType: UINT): Integer; stdcall; var S: AnsiString; begin S := AnsiString('Function interepted. Original message: ' + lpText); Result := TOrigMessageBoxA(OrigAddr)(Wnd, PAnsiChar(S), lpCaption, uType); end; procedure TForm1.FormCreate(Sender: TObject); begin OrigAddr := GetProcAddress(GetModuleHandle(user32), 'MessageBoxA'); ReplaceEATEntry(user32, OrigAddr, @InterceptedMessageBoxA); end; procedure TForm1.Button1Click(Sender: TObject); type TOrigMessageBoxA = function(Wnd: HWND; lpText, lpCaption: PAnsiChar; uType: UINT): Integer; stdcall; var OrigMessageBoxA: TOrigMessageBoxA; begin @OrigMessageBoxA := GetProcAddress(GetModuleHandle(user32), 'MessageBoxA'); OrigMessageBoxA(0, 'Test Message', nil, 0); end; end. 

インポートテーブルを編集する場合のように、関数本体は変更されません。したがって、元のコントロールを制御するには、古いアドレスを呼び出すだけで十分です。

7.関数エントリポイントのスプライシングインターセプト。


スプライシングは、インターセプトを設定する最も効率的な方法です。これは、インターセプトされた関数の開始時に、インターセプトハンドラーへの移行のasmコードが5〜6(またはそれ以上)バイトのサイズに設定されているという事実から成ります。以前のインターセプターのインストールオプションとは異なり、この方法はどの環境でも使用できます。静的、仮想、動的クラスメソッド、API関数、および基本的にはすべてをインターセプトできます。)

スプライシングを使用する場合、API関数の宣言方法、静的または動的に呼び出されることは関係ありません。関数本体の先頭で、インターセプトコードが呼び出しをキャッチします。

確かに、これらはすべてこの方法の利点であり、マイナスが始まります。

まず、関数の本体が変更され、インターセプションハンドラーから元の関数を呼び出すために、最初に詰まったバイトを復元する必要があります。実行が完了したら、インターセプトasmコードをその場所に返します。

次に、インターセプトコードが削除された時点で、制御された関数を別のスレッドから呼び出すことができ、この呼び出しをトレースできません。

第三に、さらに悪いことが起こる可能性があります-インターセプトコードを復元または削除して関数ヘッダーを変更した時点で、別のスレッドから制御された関数を呼び出すことができます。 5バイト以上を書き込むことはアトミック操作ではないため、ある時点で、通常のコードではなく、ゴミが関数の先頭にあります。その結果、この場合、呼び出し元のスレッドの崩壊が発生する可能性があります。通常のasmコードの代わりに、ガベージコードが実行されます。
場合によっては、この動作を回避できますが、それについては後で詳しく説明します。

インターセプトコード自体について少し説明します。つまり、どこから来たのでしょうか。
asmインターセプトコードを命令の先頭に記述しても、たとえば「JMP 100」という行をそこに記述することにはなりません。プロセッサーはアセンブラーについては何も知りませんが、マシンコードについては知っています。したがって、開発者は、使用するインターセプターの具体的な設計を決定した後、それをプロセッサーが理解できるマシンコードに変換する必要があります。これは、関数の先頭に配置されます。
実際、かなり複雑に聞こえますが、それはそれをするのと同じくらい簡単です。このために、インテルのマニュアルが役立ちます。このマニュアルから、インターセプターで必要なシーケンスを生成するために使用される命令のオペコードを見つけることができます。 asm insertに必要なコードを書くだけで、コンパイラーが私のために生成したマシンコードの種類を確認するだけで、さらに簡単になります。

完全に明確でない場合は、最も一般的なインターセプターオプションのいくつかを見てみましょう。

1. JMP NEAR OFFSET
5バイトの命令、最初のバイトはJMP NEAR rel32命令のオペコードである$ E9です。残りの4つはOFFSETパラメーターです。
OFFSETは、次の式に従って計算されます。OFFSET = DestinationAddr-CurrentAddr-命令マシンのコードサイズ
DestinationAddrはインターセプトハンドラのアドレスです
CurrentAddrは、JMP NEAR OFFSET命令が配置されるアドレス

です。コードの形式は次のようになります。

 procedure SliceNearJmp(OldProc, NewProc: FARPROC); var SpliceRec: packed record JmpOpcode: Byte; Offset: DWORD; end; Tmp: DWORD; begin SpliceRec.JmpOpcode := $E9; SpliceRec.Offset := DWORD(NewProc) - DWORD(OldProc) - SizeOf(SpliceRec); VirtualProtect(OldProc, SizeOf(SpliceRec), PAGE_EXECUTE_READWRITE, OldProtect); Move(SpliceRec, OldProc^, SizeOf(SpliceRec)); VirtualProtect(OldProc, SizeOf(SpliceRec), OldProtect, OldProtect); end; 

2. PUSH ADDR + RET
6バイト命令、最初のバイトは$ 68で、これはPUSH imm32命令のオペコードです。次の4バイトはADDRパラメーターです。
つまり、この一連の指示は簡単に機能します。最初のPUSH命令はADDRジャンプアドレスをスタックにプッシュし、2番目のRET命令はスタックの右側にある指定されたアドレスにジャンプします。

コードの形式では、次のようになります。

 procedure SlicePushRet(OldProc, NewProc: FARPROC); var SpliceRec: packed record PushOpcode: Byte; Offset: FARPROC; RetOpcode: Byte; end; begin SpliceRec.PushOpcode := $68; SpliceRec.Offset := NewProc; SpliceRec.RetOpcode := $C3; VirtualProtect(OldProc, SizeOf(SpliceRec), PAGE_EXECUTE_READWRITE, OldProtect); Move(SpliceRec, OldProc^, SizeOf(SpliceRec)); VirtualProtect(OldProc, SizeOf(SpliceRec), OldProtect, OldProtect); end; 

原則として、両方のオプションには生命権があり、よく使用されます。しかし、両方とも安全ではありません。

MOV EAX、ADDR + JMP EAXという2つの命令のインターセプターバリアントもあります。この7バイトの構造は、FASTCALL規則を使用して関数をインターセプトするために使用する場合、適切なソリューションではありません。事実、Delphiではこの合意がデフォルトで使用され、その特徴は、関数の最初の3つのパラメーターがレジスタEAX、ECX、およびEDXに配置されることです。このタイプのインターセプターを適用すると、EAXレジスターに入るパラメーターは上書きされます。したがって、このオプションは考慮しません。

次に、このようなインターセプターを使用することの安全性について説明します。インターセプトする関数がHotPatchメカニズムを使用しないことが確実な場合にのみ、インターセプトに上記の2つのオプションを使用することをお勧めします。

事実、レドモンドのスタッフは、エントリポイントの接続をより安全にするための小さな抜け穴を提供してくれました。そして、はい、「HotPatch」と呼ばれ

ます:) 要するに、逆アセンブラーでMSからライブラリを開くと、各API関数の本体の前に5つのNOP命令があり、関数本体は何もしないMOV EDI、EDI(ダブルバイトPUSH xxx)

画像

最初の5つの命令のサイズにより、代わりにJMP NEAR OFFSET命令コードを記述することができます。さらに、記録時には、関数自体は変更されません。この命令を記述した後、関数の最初の2バイトをアトミックに変更し、代わりにJMP SHORT -7命令の2バイトのマシンコード($ EBと$ F9バイト)を追加できます。

HotPachのサンプルコードは次のようになります。

 procedure SliceHotPath(OldProc, NewProc: FARPROC); var SpliceRec: packed record JmpOpcode: Byte; Offset: DWORD; end; NopAddr: Pointer; OldProtect: DWORD; begin SpliceRec.JmpOpcode := $E9; NopAddr := PAnsiChar(OldProc) - SizeOf(SpliceRec); SpliceRec.Offset := DWORD(NewProc) - DWORD(NopAddr) - SizeOf(SpliceRec); VirtualProtect(NopAddr, 7, PAGE_EXECUTE_READWRITE, OldProtect); Move(SpliceRec, NopAddr^, SizeOf(SpliceRec)); asm mov ax, $F9EB mov ecx, OldProc lock xchg word ptr [ecx], ax end; VirtualProtect(NopAddr, 7, OldProtect, OldProtect); end; 

このようなインターセプターを無効にするには、最初の2バイト命令を返すだけで十分であり、インターセプター本体へのジャンプ自体が行われる残りの5バイトを処理する必要はありません。

残念ながら、通常の関数では、HotPachメソッドを使用してインターセプトすることはできません。関数の前に必要な空きスペースがないためです。彼らにとっては、パッチの最初の2つの考慮されたオプションの1つが適切です。

ただし、ライブラリをHotPatch用に準備する必要がある場合は、次の指示に慣れることができます。Create Hotpatchable Image True、これはMS VC専用です。

3つのインターセプトの例はすべてサンプルコードです。インターセプトコードがインストールされた瞬間のみを表示します。インターセプターを完全に動作させるには、インターセプターをインストールする前にどこかに保存する必要がある元の関数の元のコードを復元する必要があります。

次に、最初の方法で傍受を示します。これを行うには、新しいプロジェクトを作成し、メインフォームにボタンを配置して、次のコードを記述する必要があります。

 unit uNearJmpSplice; interface uses Windows, Classes, Controls, Forms, StdCtrls; type TForm1 = class(TForm) Button1: TButton; procedure FormCreate(Sender: TObject); procedure Button1Click(Sender: TObject); end; var Form1: TForm1; implementation {$R *.dfm} type //      JMP NEAR OFFSET TNearJmpSpliceRec = packed record JmpOpcode: Byte; Offset: DWORD; end; //         JMP NEAR OFFSET TNearJmpSpliceData = packed record FuncAddr: FARPROC; OldData: TNearJmpSpliceRec; NewData: TNearJmpSpliceRec; end; var NearJmpSpliceRec: TNearJmpSpliceData; //         procedure SpliceNearJmp(FuncAddr: Pointer; NewData: TNearJmpSpliceRec); var OldProtect: DWORD; begin VirtualProtect(FuncAddr, SizeOf(TNearJmpSpliceRec), PAGE_EXECUTE_READWRITE, OldProtect); try //   !!! Move(NewData, FuncAddr^, SizeOf(TNearJmpSpliceRec)); finally VirtualProtect(FuncAddr, SizeOf(TNearJmpSpliceRec), OldProtect, OldProtect); FlushInstructionCache(GetCurrentProcess, FuncAddr, SizeOf(TNearJmpSpliceRec)); end; end; //    MessageBoxA function InterceptedMessageBoxA(Wnd: HWND; lpText, lpCaption: PAnsiChar; uType: UINT): Integer; stdcall; var S: AnsiString; begin //   SpliceNearJmp(NearJmpSpliceRec.FuncAddr, NearJmpSpliceRec.OldData); try //    S := AnsiString('Function interepted. Original message: ' + lpText); Result := MessageBoxA(Wnd, PAnsiChar(S), lpCaption, uType); finally //   SpliceNearJmp(NearJmpSpliceRec.FuncAddr, NearJmpSpliceRec.NewData); end; end; procedure InitNearJmpSpliceRec; begin //      NearJmpSpliceRec.FuncAddr := GetProcAddress(GetModuleHandle(user32), 'MessageBoxA'); //      ,     Move(NearJmpSpliceRec.FuncAddr^, NearJmpSpliceRec.OldData, SizeOf(TNearJmpSpliceRec)); //   JMP NEAR NearJmpSpliceRec.NewData.JmpOpcode := $E9; //    NearJmpSpliceRec.NewData.Offset := PAnsiChar(@InterceptedMessageBoxA) - PAnsiChar(NearJmpSpliceRec.FuncAddr) - SizeOf(TNearJmpSpliceRec); end; procedure TForm1.FormCreate(Sender: TObject); begin //     InitNearJmpSpliceRec; //  MessageBoxA SpliceNearJmp(NearJmpSpliceRec.FuncAddr, NearJmpSpliceRec.NewData); end; procedure TForm1.Button1Click(Sender: TObject); begin MessageBoxA(0, 'Test MessageBoxA Message', nil, 0); end; end. 

TNearJmpSpliceRec構造は、傍受コードによって妨害されたバイトに関する情報を格納するために使用されます。同じ構造がインターセプトコードマスコードの保存に使用されます。
確立されたインターセプトに関する一般情報はTNearJmpSpliceData構造に格納されます。この構造には、元の関数データとインターセプターに関する2つのフィールドに加えて、関数のアドレスも格納されます。

最初に、InitNearJmpSpliceRecプロシージャでこの構造体が初期化され、その後、SpliceNearJmpプロシージャを使用してインターセプションコードが設定されます。

ボタンが押されると、InterceptedMessageBoxAインターセプトハンドラーが呼び出され、元の関数の呼び出し後にインターセプターが削除され、復元されます。

PUSH ADDR + RETを使用して2番目の方法でインターセプトを行う場合、TNearJmpSpliceRec構造体の説明とInitNearJmpSpliceRec構造体の初期化手順をわずかに変更する必要があります。他のすべては同じままです。

さて、これがインターセプトコードが3番目の方法(HotPatch経由)でどのように見えるかです。
これを次の2つの章で使用します。繰り返しにならないように、別のモジュールに配置するからです。

 unit CommonHotPatch; interface uses Windows; const LOCK_JMP_OPKODE: Word = $F9EB; type //      JMP NEAR OFFSET TNearJmpSpliceRec = packed record JmpOpcode: Byte; Offset: DWORD; end; THotPachSpliceData = packed record FuncAddr: FARPROC; SpliceRec: TNearJmpSpliceRec; LockJmp: Word; end; var HotPathSpliceRec: THotPachSpliceData; procedure InitHotPatchSpliceRec; procedure SpliceNearJmp(FuncAddr: Pointer; NewData: TNearJmpSpliceRec); procedure SpliceLockJmp(FuncAddr: Pointer; NewData: Word); implementation //         procedure SpliceNearJmp(FuncAddr: Pointer; NewData: TNearJmpSpliceRec); var OldProtect: DWORD; begin VirtualProtect(FuncAddr, SizeOf(TNearJmpSpliceRec), PAGE_EXECUTE_READWRITE, OldProtect); try Move(NewData, FuncAddr^, SizeOf(TNearJmpSpliceRec)); finally VirtualProtect(FuncAddr, SizeOf(TNearJmpSpliceRec), OldProtect, OldProtect); FlushInstructionCache(GetCurrentProcess, FuncAddr, SizeOf(TNearJmpSpliceRec)); end; end; //         procedure SpliceLockJmp(FuncAddr: Pointer; NewData: Word); var OldProtect: DWORD; begin VirtualProtect(FuncAddr, 2, PAGE_EXECUTE_READWRITE, OldProtect); try asm mov ax, NewData mov ecx, FuncAddr lock xchg word ptr [ecx], ax end; finally VirtualProtect(FuncAddr, 2, OldProtect, OldProtect); FlushInstructionCache(GetCurrentProcess, FuncAddr, 2); end; end; function InterceptedMessageBoxA(Wnd: HWND; lpText, lpCaption: PAnsiChar; uType: UINT): Integer; stdcall; var S: AnsiString; begin //   SpliceLockJmp(HotPathSpliceRec.FuncAddr, HotPathSpliceRec.LockJmp); try //    S := AnsiString('Function interepted. Original message: ' + lpText); Result := MessageBoxA(Wnd, PAnsiChar(S), lpCaption, uType); finally //   SpliceLockJmp(HotPathSpliceRec.FuncAddr, LOCK_JMP_OPKODE); end; end; procedure InitHotPatchSpliceRec; begin //      HotPathSpliceRec.FuncAddr := GetProcAddress(GetModuleHandle(user32), 'MessageBoxA'); //      ,     Move(HotPathSpliceRec.FuncAddr^, HotPathSpliceRec.LockJmp, 2); //   JMP NEAR HotPathSpliceRec.SpliceRec.JmpOpcode := $E9; //    HotPathSpliceRec.SpliceRec.Offset := PAnsiChar(@InterceptedMessageBoxA) + 5 - PAnsiChar(HotPathSpliceRec.FuncAddr) - SizeOf(TNearJmpSpliceRec); end; end. 

アプリケーションでこのモジュールを使用するのは最も簡単です。

 unit uHotPachSplice; interface uses Windows, Classes, Controls, Forms, StdCtrls; type TForm1 = class(TForm) Button1: TButton; procedure FormCreate(Sender: TObject); procedure Button1Click(Sender: TObject); end; var Form1: TForm1; implementation uses CommonHotPatch; {$R *.dfm} procedure TForm1.FormCreate(Sender: TObject); begin //     InitHotPatchSpliceRec; //     NOP- SpliceNearJmp(PAnsiChar(HotPathSpliceRec.FuncAddr) - 5, HotPathSpliceRec.SpliceRec); //  MessageBoxW SpliceLockJmp(HotPathSpliceRec.FuncAddr, LOCK_JMP_OPKODE); end; procedure TForm1.Button1Click(Sender: TObject); const TestStr: AnsiString = 'Test MessageBoxA Message'; begin MessageBoxA(0, PAnsiChar(TestStr), nil, 0); end; end. 

2番目のコードは説明なしで残します。インターセプトの以前のバージョンとの違いはごくわずかです。

8.トラップのインストールによるライブラリの実装。


上記のすべては、ローカルアプリケーションでのインターセプトに関連していますが、開発者が他の誰かのプロセスでインターセプトする必要がある正確さを示しています。

インターセプターは、最も複雑なものから、いくつかの方法で別のプロセスの本体にインターセプターを配置できます-VirtualAllocExを介して必要なメモリブロックを選択し、インターセプターコードをその中に書き込む(私はそれを考慮しません)

インターセプターを実装する最も簡単な方法は、グローバルトラップによってエイリアンアドレス空間にロードされたライブラリーの本体にインターセプターコードを配置することです。

ライブラリローダーのコードは次のようになります。

 program hook_loader; {$APPTYPE CONSOLE} uses Windows; var hLib: THandle; HookProcAddr: Pointer; HookHandle: HHOOK; begin hLib := LoadLibrary('hook_splice_lib.dll'); try HookProcAddr := GetProcAddress(hLib, 'HookProc'); Writeln('MessageBoxA intercepted, press ENTER to resume...'); HookHandle := SetWindowsHookEx(WH_GETMESSAGE, HookProcAddr, hLib, 0); Readln; UnhookWindowsHookEx(HookHandle); finally FreeLibrary(hLib); end; end. 

SetWindowsHookEx関数が実行されるとすぐに、グローバルトラップがインストールされます。そのハンドラーはhook_splice_lib.dllライブラリにあります。このライブラリは、GetMessage関数またはPeekMessage関数を呼び出すことにより、メッセージキューで動作するすべてのプロセスに自動的にロードされます。

ライブラリコードは、上記の傍受の例のいずれかをほぼ繰り返します。

 library hook_splice_lib; uses Windows, CommonHotPatch in '..\common\CommonHotPatch.pas'; {$R *.res} procedure DLLEntryPoint(dwReason: DWORD); begin case dwReason of DLL_PROCESS_ATTACH: begin //     InitHotPatchSpliceRec; //     NOP- SpliceNearJmp(PAnsiChar(HotPathSpliceRec.FuncAddr) - 5, HotPathSpliceRec.SpliceRec); //  MessageBoxW SpliceLockJmp(HotPathSpliceRec.FuncAddr, LOCK_JMP_OPKODE); end; DLL_PROCESS_DETACH: begin //      SpliceLockJmp(HotPathSpliceRec.FuncAddr, HotPathSpliceRec.LockJmp); end; end; end; function HookProc(Code: Integer; WParam: WPARAM; LParam: LPARAM): LRESULT; stdcall; begin Result := CallNextHookEx(0, Code, WParam, LParam); end; exports HookProc; begin DLLProc := @DLLEntryPoint; DLLEntryPoint(DLL_PROCESS_ATTACH); end. 

違いは最小限です。ライブラリ本体には、インターセプトコード自体に加えて、フックフック関数HookProcが実装されており、キュー内の次のフックを呼び出すだけです。 WH_GETMESSAGEトラップが正しく機能するためにのみ必要です。

インターセプターは、DLL_PROCESS_ATTACHのロードの通知を受信するとDLLEntryPointにインストールされ、DLL_PROCESS_DETACHのアンロードの通知を受信するとインターセプターは削除されます。

DLLProcのオーバーラップコードについて少し説明します。実際、このプロシージャのオーバーラップは、DLL_PROCESS_DETACH通知を受信するための1つのみを対象としています。実際、コードが制御を受け取ったとき、DLL_PROCESS_ATTACH通知は既にライブラリに渡されており、DLLProc内で処理されています。DLLEntryPoint呼び出し(DLL_PROCESS_ATTACH)は、基本的に既に受信した呼び出しを複製するだけです。ただし、ライブラリがアンロードされると、DLLProcは既にDLLEntryPointハンドラーに変更されており、アンロードに関する通知が届きます。そこで、インターセプターを初期化解除するために必要なアクションを実行できます。

9.リモートプロセスでスレッドを作成してライブラリをデプロイします。


ライブラリをすべてのプロセスにグローバルに導入することは、しばしば正当化されません。特定のプロセスで特定のIPAを傍受したい場合、他の人に気を取られても意味がありません。さらに、場合によっては、トラップがインストールされているにもかかわらず、トラップをトリガーするために必要なアクションを実行しないため、ライブラリが必要なプロセスにロードされない場合があります。例として、このコンソールアプリケーションを見てみましょう。

 program test_console8; {$APPTYPE CONSOLE} uses Windows; begin Writeln('Press enter to show message...'); Readln; MessageBoxA(0, 'First message', '', 0); Writeln('Press enter to show message...'); Readln; MessageBoxA(0, 'Second message', '', 0); end. 

通常、コンソールアプリケーションはメッセージキューで動作せず、ルールとしてGetMessageまたはPeekMessage関数の呼び出しを使用しないため、最初のメッセージが表示されても、インターセプトされません。

しかし、これにもかかわらず、2番目のメッセージは確立されたトラップによってインターセプトされます。実際には、最初のメッセージボックス(および他のメッセージボックス)を表示するために、メッセージキューが引き続き使用され(実際にはモーダルウィンドウが表示されるため)、ライブラリが表示されるときにロードされるため、2番目のMessageBox呼び出しのインターセプトが説明されます。

この動作(コンソールで最初のMessageBox呼び出しをインターセプトできないこと)を回避するために、プロセス識別子(PID)を認識して、ライブラリを強制的にロードできます。

インターセプターを備えたライブラリーは、以前のものとほぼ同じままで、唯一の変更点は、HookProcトラップハンドラーの代わりに、次の形式のライブラリーアンロード関数を実装することです。

 procedure SelfUnload(lpParametr: Pointer); stdcall; begin FreeLibraryAndExitThread(HInstance, 0); end; exports SelfUnload; 

ライブラリの実装は、次のコードで実行されます。

 const DllName = 'thread_splice_lib.dll'; function InjectLib(ProcessID: Integer): Boolean; var Process: HWND; ThreadRtn: FARPROC; DllPath: AnsiString; RemoteDll: Pointer; BytesWriten: DWORD; Thread: DWORD; ThreadId: DWORD; begin Result := False; //   Process := OpenProcess(PROCESS_CREATE_THREAD or PROCESS_VM_OPERATION or PROCESS_VM_WRITE, True, ProcessID); if Process = 0 then Exit; try //       DllPath := AnsiString(ExtractFilePath(ParamStr(0)) + DLLName) + #0; RemoteDll := VirtualAllocEx(Process, nil, Length(DllPath), MEM_COMMIT or MEM_TOP_DOWN, PAGE_READWRITE); if RemoteDll = nil then Exit; try //         if not WriteProcessMemory(Process, RemoteDll, PChar(DllPath), Length(DllPath), BytesWriten) then Exit; if BytesWriten <> DWORD(Length(DllPath)) then Exit; //     Kernel32.dll ThreadRtn := GetProcAddress(GetModuleHandle('Kernel32.dll'), 'LoadLibraryA'); if ThreadRtn = nil then Exit; //    Thread := CreateRemoteThread(Process, nil, 0, ThreadRtn, RemoteDll, 0, ThreadId); if Thread = 0 then Exit; try //     ... Result := WaitForSingleObject(Thread, INFINITE) = WAIT_OBJECT_0; finally CloseHandle(Thread); end; finally VirtualFreeEx(Process, RemoteDll, 0, MEM_RELEASE); end; finally CloseHandle(Process); end; end; 

この方法の原理はリヒターによって説明されたので、私はそれにこだわらない。
また、アンロードするには、次のコードを実装する必要があります。

 function ResumeLib(ProcessID: Integer): Boolean; var hLibHandle: THandle; hModuleSnap: THandle; ModuleEntry: TModuleEntry32; OpCodeData: Word; Process: HWND; BytesWriten: DWORD; Thread: DWORD; ThreadId: DWORD; ExitCode: DWORD; PLibHandle: PDWORD; OpCode: PWORD; CurrUnloadAddrOffset: DWORD; UnloadAddrOffset: DWORD; begin Result := False; //          hLibHandle := LoadLibrary(PChar(DLLName)); try UnloadAddrOffset := DWORD(GetProcAddress(hLibHandle, 'SelfUnload')) - hLibHandle; if UnloadAddrOffset = -hLibHandle then Exit; finally FreeLibrary(hLibHandle); end; //        hModuleSnap := CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, ProcessID); if hModuleSnap <> INVALID_HANDLE_VALUE then try FillChar(ModuleEntry, SizeOf(TModuleEntry32), #0); ModuleEntry.dwSize := SizeOf(TModuleEntry32); if not Module32First(hModuleSnap, ModuleEntry) then Exit; repeat if AnsiUpperCase(ModuleEntry.szModule) = AnsiUpperCase(DLLName) then begin //     CurrUnloadAddrOffset := ModuleEntry.hModule + UnloadAddrOffset; Break; end; until not Module32Next(hModuleSnap, ModuleEntry); finally CloseHandle(hModuleSnap); end; //   Process := OpenProcess(PROCESS_CREATE_THREAD or PROCESS_VM_OPERATION or PROCESS_VM_WRITE, True, ProcessID); if Process = 0 then Exit; try //   jmp [ebx] OpCode := VirtualAllocEx(Process, nil, 2, MEM_COMMIT or MEM_TOP_DOWN, PAGE_READWRITE); if OpCode = nil then Exit; try OpCodeData := $23FF; if not WriteProcessMemory(Process, OpCode, @OpCodeData, 2, BytesWriten) then Exit; //     (    EBX   ) PLibHandle := VirtualAllocEx(Process, nil, 4, MEM_COMMIT or MEM_TOP_DOWN, PAGE_READWRITE); if PLibHandle = nil then Exit; try if not WriteProcessMemory(Process, PLibHandle, @CurrUnloadAddrOffset, 4, BytesWriten) then Exit; //   Thread := CreateRemoteThread(Process, nil, 0, OpCode, PLibHandle, 0, ThreadId); if Thread = 0 then Exit; try //     ... if (WaitForSingleObject(Thread, INFINITE) = WAIT_OBJECT_0) then if GetExitCodeThread(Thread, ExitCode) then Result := ExitCode = 0; finally CloseHandle(Thread); end; finally VirtualFreeEx(Process, PLibHandle, 0, MEM_RELEASE); end; finally VirtualFreeEx(Process, OpCode, 0, MEM_RELEASE); end; finally CloseHandle(Process); end; end; 

ここでは、Windows 2000からWindows 7までの行で機能する文書化されていないメカニズムを1つ使用します(8つではチェックしませんでしたが、ロジックは同じです)。実際、CreateRemoteThread関数のlpParameterパラメーターに渡された値は、リモートアプリケーションでスレッドが開始されると、常にEBXレジスタに配置されます。このコードのタスクは、スレッドがリモートプロセスで作業を開始するJMP [EBX]命令を配置することです。SelfUnloadプロシージャのアドレスがEBXにある場合、制御はそれに転送されますが、このプロシージャ内では、ライブラリをアンロードして現在のスレッドを閉じるためのコードが既に実装されています。

もちろん、SelfUnload関数から直接アンロードメカニズムを直接開始することもできますが、この瞬間を見せたかったので、あまりscられないでください:)

10.モニタリング


実用的な部分では、少し理論的に終わったとみなします。
インターセプトされた関数の呼び出しを正しくログに記録する方法については、一般的な答えはありませんが、一般的な推奨事項はあります。

まず、どのパラメーターをログに記録するかを決定し、次のグループに分類する必要があります。


理由はわかりませんが、私の「学生」はこの点にギャグを抱えていました。彼らは常にログの順序を混乱させ、ReadFileを呼び出す前にログを呼び出し、データバッファの不足に驚きました。

さて、最初の点でもう少し-関数を呼び出す前に常に着信データのロギングを行います。そうしないと、WriteFileに転送されたバッファがこの呼び出しを処理したドライバによってわずかに変更されるという事実に遭遇します。

11.要約


これはおそらく、傍受手法の説明で終了する価値があります。はい、実際には実際には残っていません。コアで傍受の方法を説明することには意味がありません。これはすでにDelphiコミュニティ向けの記事ではなく、オーバーフロー時の傍受に関するトリックも考慮しません。原理そのものを説明するのに時間がかかりすぎた理由と「それがどのように」機能したか。

考慮されていない唯一のニュアンスは、HotPatchを使用するときにインターセプトが残っていました。つまり、インターセプターを削除する必要は必ずしもなく、不要なジェスチャーなしで実行できます。
しかし、残念なことに、このニュアンス全体を説明するデモアプリケーションを準備する時間がありませんでした。また、不必要に資料の量を増やしたくなかったので、この点については記事の後半で説明します。

実際、これがこのトピックで伝えたかったことのすべてです。この知識をどのように適用するかはあなた次第です。はい、もちろん、この情報はさまざまな悪意のあるものの開発に適用できることを理解していますが、より有用なソフトウェアに使用することもできます。特に、私は通常、ブラウザを通過するトラフィックを分析するような些細なタスクにインターセプトを使用します。空き時間に自分で実装できるのに、同じ機能を提供するソフトウェアに何百ドルも支払うのはなぜですか?このリンク

で記事の例を見ることができます。成功した監視。アレクサンダー(ラウゼ_)ベーグル2013年1月




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


All Articles