HotPatch用に準備された関数をインターセプトするときの適切なスプライシング

前の記事で、関数をインターセプトするための5つのオプションとそのバリエーションを調べました。



確かに、その中に考慮されていない2つの不快な状況を残しました。

1.トラップが削除された時点で、インターセプトされた関数を呼び出します。

2. 2つの異なるスレッドからのインターセプトされた関数の同時呼び出し。



最初のケースでは、インターセプターをインストールしたプログラマーは全体像を見ることができません。 一部のデータは彼を通過します。

2番目のケースは、インターセプターがインストールされているアプリケーションが落ちるまで、より深刻な結果をもたらす恐れがあります。



これらの状況は両方とも、スプライシングが使用されている場合にのみ発生します。 インポート/エクスポートテーブルなどを通じてインターセプトされた場合 インターセプトされた関数のボディの変更は発生しないため、インターセプトのこれらのオプションは過度のボディの動きを必要としません。



この記事では、HopPatch用に準備された関数のエントリポイントのスプライシングについて、さらに詳しく調べます。 これらの関数は、上記のエラーを回避する方法を提供します。



JMP NEAR OFFSETまたはPUSH ADDR + RET(これらのエラーに対して最も脆弱な)によるスプライシングは考慮されません。 良い方法では、長さの逆アセンブラーを実装せずに、このオプションを必要に応じてインターセプトすることはできません。







1. CreateWindowExWへの呼び出しをインターセプトするアプリケーションを実装します



まず、インターセプトが解除された時点でインターセプトされた関数の呼び出しが発生する可能性があるため、APIをインターセプトするときにデータの損失を明確に示すアプリケーションを準備します。



新しいプロジェクトを作成し、TMemo、TOpenDialog、TButtonの3つの要素をメインフォームに配置します。



アプリケーションの本質:ボタンがクリックされると、インターセプトがCreateWindowExW関数に設定され、ダイアログが表示されます。 ダイアログを閉じた後、TMemoはダイアログによって作成されたすべてのウィンドウに関する情報を表示します。



これを行うには、 前の記事のコードの一部、つまり次のものが必要です。



1.傍受のための型と定数の宣言:



const LOCK_JMP_OPKODE: Word = $F9EB; JMP_OPKODE: Word = $E9; type //      JMP NEAR OFFSET TNearJmpSpliceRec = packed record JmpOpcode: Byte; Offset: DWORD; end; THotPachSpliceData = packed record FuncAddr: FARPROC; SpliceRec: TNearJmpSpliceRec; LockJmp: Word; end; const NearJmpSpliceRecSize = SizeOf(TNearJmpSpliceRec); LockJmpOpcodeSize = SizeOf(Word);
      
      







2. NEAR JMPおよびアトミックレコーディングSHORT JMPを記録する手順



 //         procedure SpliceNearJmp(FuncAddr: Pointer; NewData: TNearJmpSpliceRec); var OldProtect: DWORD; begin VirtualProtect(FuncAddr, NearJmpSpliceRecSize, PAGE_EXECUTE_READWRITE, OldProtect); try Move(NewData, FuncAddr^, NearJmpSpliceRecSize); finally VirtualProtect(FuncAddr, NearJmpSpliceRecSize, OldProtect, OldProtect); end; end; //         procedure SpliceLockJmp(FuncAddr: Pointer; NewData: Word); var OldProtect: DWORD; begin VirtualProtect(FuncAddr, LockJmpOpcodeSize, PAGE_EXECUTE_READWRITE, OldProtect); try asm mov ax, NewData mov ecx, FuncAddr lock xchg word ptr [ecx], ax end; finally VirtualProtect(FuncAddr, LockJmpOpcodeSize, OldProtect, OldProtect); end; end;
      
      







3. THotPachSpliceData構造体の初期化をわずかに変更



 //       procedure InitHotPatchSpliceRec(const LibraryName, FunctionName: string; InterceptHandler: Pointer; out HotPathSpliceRec: THotPachSpliceData); begin //      HotPathSpliceRec.FuncAddr := GetProcAddress(GetModuleHandle(PChar(LibraryName)), PChar(FunctionName)); //      ,     Move(HotPathSpliceRec.FuncAddr^, HotPathSpliceRec.LockJmp, LockJmpOpcodeSize); //   JMP NEAR HotPathSpliceRec.SpliceRec.JmpOpcode := JMP_OPKODE; //    (  NearJmpSpliceRecSize  , // ..     ) HotPathSpliceRec.SpliceRec.Offset := PAnsiChar(InterceptHandler) - PAnsiChar(HotPathSpliceRec.FuncAddr); end;
      
      







このコードはすべて別個のSpliceHelperモジュールに配置されます。次の章で必要になります。



メインフォームに移りましょう。2つのグローバル変数が必要です。



 var HotPathSpliceRec: THotPachSpliceData; WindowList: TStringList;
      
      







HotPathSpliceRec変数には、インターセプターに関する情報が含まれます。 2番目には、作成されたウィンドウのリストが含まれます。



フォームコンストラクターで、THotPachSpliceData構造体を初期化します。



 procedure TForm1.FormCreate(Sender: TObject); begin //     InitHotPatchSpliceRec(user32, 'CreateWindowExW', @InterceptedCreateWindowExW, HotPathSpliceRec); //     NOP- SpliceNearJmp(PAnsiChar(HotPathSpliceRec.FuncAddr) - NearJmpSpliceRecSize, HotPathSpliceRec.SpliceRec); end;
      
      







元の関数の代わりに呼び出されるインターセプター関数を作成します。



 function InterceptedCreateWindowExW(dwExStyle: DWORD; lpClassName: PWideChar; lpWindowName: PWideChar; dwStyle: DWORD; X, Y, nWidth, nHeight: Integer; hWndParent: HWND; hMenu: HMENU; hInstance: HINST; lpParam: Pointer): HWND; stdcall; var S: string; Index: Integer; begin //   SpliceLockJmp(HotPathSpliceRec.FuncAddr, HotPathSpliceRec.LockJmp); try //      Index := -1; if not IsBadReadPtr(lpClassName, 1) then begin S := 'ClassName: ' + string(lpClassName); S := IntToStr(WindowList.Count + 1) + ': ' + S; Index := WindowList.Add(S); end; //    Result := CreateWindowExW(dwExStyle, lpClassName, lpWindowName, dwStyle, X, Y, nWidth, nHeight, hWndParent, hMenu, hInstance, lpParam); //       if Index >= 0 then begin S := S + ', handle: ' + IntToStr(Result); WindowList[Index] := S; end; finally //   SpliceLockJmp(HotPathSpliceRec.FuncAddr, LOCK_JMP_OPKODE); end; end;
      
      







そして、ボタンハンドラを実装するために最後に残ります。



 procedure TForm1.Button1Click(Sender: TObject); begin //  CreateWindowExW SpliceLockJmp(HotPathSpliceRec.FuncAddr, LOCK_JMP_OPKODE); try //           WindowList := TStringList.Create; try //   OpenDialog1.Execute; //      Memo1.Lines.Text := WindowList.Text; finally WindowList.Free; end; finally //   SpliceLockJmp(HotPathSpliceRec.FuncAddr, HotPathSpliceRec.LockJmp); end; end;
      
      







すべての準備が整ったら、プログラムを実行することができます。



この章で実装されているコードについては詳しく説明しません。 前の記事で詳しく説明した以上のものであり、もう一度ペイントしても意味がありません。



プログラムを実行し、ボタンをクリックして、「キャンセル」ボタンをクリックしてダイアログを閉じます。次のようになります。



画像



したがって、通常のTOpenDialogを開くと、さまざまなクラスの14のウィンドウが作成されることがわかりました。



それでは、実際にそうであるかどうかを確認しましょう。



2.アプリケーションウィンドウのツリーを表示するための補助ユーティリティを作成します。



インターセプターの動作を確認するには、アプリケーションのウィンドウの現在のリストを表示できるサードパーティのユーティリティを使用して、インターセプターで受信したすべての情報を確認する必要があります。



もちろん、Spy ++などのサードパーティプログラムを使用できますが、私たちはプログラマなので、特に実装する時間が安いので、自分で実装する必要があります。



新しいプロジェクトを作成し、TTreeViewをメインフォームに配置してから、次のコードを実装します。



 type TdlgWindowTree = class(TForm) WindowTreeView: TTreeView; procedure FormCreate(Sender: TObject); private procedure Sys_Windows_Tree(Node: TTreeNode; AHandle: HWND; ALevel: Integer); end; ... procedure TdlgWindowTree.FormCreate(Sender: TObject); begin Sys_Windows_Tree(nil, GetDesktopWindow, 0); end; procedure TdlgWindowTree.Sys_Windows_Tree(Node: TTreeNode; AHandle: HWND; ALevel: Integer); type TRootNodeData = record Node: TTreeNode; PID: Cardinal; end; var szClassName, szCaption, szLayoutName: array[0..MAXCHAR - 1] of Char; szFileName : array[0..MAX_PATH - 1] of Char; Result: String; PID, TID: Cardinal; I: Integer; RootItems: array of TRootNodeData; IsNew: Boolean; begin //      while AHandle <> 0 do begin //    GetClassName(AHandle, szClassName, MAXCHAR); //  ( Caption)  GetWindowText(AHandle, szCaption, MAXCHAR); //    if GetWindowModuleFilename(AHandle, szFileName, SizeOf(szFileName)) = 0 then FillChar(szFileName, 256, #0); TID := GetWindowThreadProcessId(AHandle, PID); //   AttachThreadInput(GetCurrentThreadId, TID, True); VerLanguageName(GetKeyboardLayout(TID) and $FFFF, szLayoutName, MAXCHAR); AttachThreadInput(GetCurrentThreadId, TID, False); //  Result := Format('%s [%s] Caption = %s, Handle = %d, Layout = %s', [String(szClassName), String(szFileName), String(szCaption), AHandle, String(szLayoutName)]); //       if ALevel in [0..1] then begin IsNew := True; for I := 0 to Length(RootItems) - 1 do if RootItems[I].PID = PID then begin Node := RootItems[I].Node; IsNew := False; Break; end; if IsNew then begin SetLength(RootItems, Length(RootItems) + 1); RootItems[Length(RootItems) - 1].PID := PID; RootItems[Length(RootItems) - 1].Node := WindowTreeView.Items.AddChild(nil, 'PID: ' + IntToStr(PID)); Node := RootItems[Length(RootItems) - 1].Node; end; end; //   Sys_Windows_Tree(WindowTreeView.Items.AddChild(Node, Result), GetWindow(AHandle, GW_CHILD), ALevel + 1); //   ( )  AHandle := GetNextWindow(AHandle, GW_HWNDNEXT); end; end;
      
      







実際には、実行のためにすべてを実行できます。



画像



3.結果を分析する



次に、両方のプログラムの結果を比較します。 次のようにします。

1.インターセプターでプログラムを実行し、ダイアログを表示するボタンをクリックします。

2. 2番目の章からユーティリティを実行します

3.最初のプログラムのダイアログを閉じて、インターセプトされたウィンドウに関する結果を取得します。



私たちは見ます:



画像



Auto-Suggest DropDownクラスのウィンドウは赤で強調表示されています。それが何であるかを見てみましょう。



画像



しかし、さらに4つのウィンドウ、2つのスクロールバー、ListViewが含まれており、SysHeader32の子も保持しています。 しかし、これはすでに興味深いです。 両方のアプリケーションのウィンドウハンドルは同じですが、ListViewもSysHeader32も、最初のアプリケーションの2つのスクロールも同じではありません。



しかし、最初のリストにそれらが表示されないという事実は何の意味もありません。 これらのウィンドウは、インターセプターが削除されたときに作成されました。これは、1つの理由でのみ発生する可能性があります。CreateWindowExWへの呼び出しは、それ自体への再帰呼び出しにつながる可能性があるためです。



そのため、インターセプターの削除と復元が不要な方法でインターセプターコードを実装する必要があります。



4.傍受コードを削除せずに、傍受した関数を呼び出します。



最後の記事のこの写真を見てみましょう。



画像



これがMessageBoxW関数の始まりです。 最初の命令は、5つのNOP命令が先行する何もしない命令MOV EDI、EDIです。



これは、HotPatchによるインターセプトのために準備された関数のほとんどが、私たちがインターセプトしたCreateWindowExWを含めてほとんど同じように見えます。



関数がインターセプトされると、何もしない命令によって占有された割り当てられた7バイトの代わりに、次のコードが配置されます。



画像



実際、これはインストールしたインターセプターです。

MOV EDI、EDI命令の代わりに、JMP -7コードが配置され、前の命令に制御が移されます。

5つのNOP命令の代わりに、インターセプター関数の先頭にジャンプします。



CreateWindowExW関数の開始アドレスからではなく、その最初の有用なPUSH EBP命令のアドレスから実行を開始する場合、インストールしたインターセプターに影響を与えません。そうであれば、それを削除しても意味がありません。



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



 type TCreateWindowExW = function(dwExStyle: DWORD; lpClassName: PWideChar; lpWindowName: PWideChar; dwStyle: DWORD; X, Y, nWidth, nHeight: Integer; hWndParent: HWND; AMenu: HMENU; hInstance: HINST; lpParam: Pointer): HWND; stdcall; function InterceptedCreateWindowExW(dwExStyle: DWORD; lpClassName: PWideChar; lpWindowName: PWideChar; dwStyle: DWORD; X, Y, nWidth, nHeight: Integer; hWndParent: HWND; hMenu: HMENU; hInstance: HINST; lpParam: Pointer): HWND; stdcall; var S: string; Index: Integer; ACreateWindowExW: TCreateWindowExW; begin //      Index := -1; if not IsBadReadPtr(lpClassName, 1) then begin S := 'ClassName: ' + string(lpClassName); S := IntToStr(WindowList.Count + 1) + ': ' + S; Index := WindowList.Add(S); end; //    @ACreateWindowExW := PAnsiChar(HotPathSpliceRec.FuncAddr) + LockJmpOpcodeSize; Result := ACreateWindowExW(dwExStyle, lpClassName, lpWindowName, dwStyle, X, Y, nWidth, nHeight, hWndParent, hMenu, hInstance, lpParam); //       if Index >= 0 then begin S := S + ', handle: ' + IntToStr(Result); WindowList[Index] := S; end; end;
      
      







関数の先頭から2バイトのオフセットに等しい最初の有用な命令のアドレスを計算した後、一時変数ACreateWindowExWに格納し、その後、通常の方法で関数を呼び出します。



この場合に何が起こるか見てみましょう、これは私たちが期待するものです:



画像



そして、これは私たちに与えられたリストで見つけるものです。



画像



さて、「損失」を見つけました。14個ではなく、TOpenDialogが呼び出されると、同じ26個のウィンドウがすべて作成されます。



それはすべて悪名高い再帰呼び出しの問題であり、InterceptedCreateWindowExW関数の先頭にブレークポイントを設定すると、プロシージャコールスタックに表示されます。



画像



5.異なるスレッドからフック関数を呼び出しているときにエラーが発生しました。



このエラーでは、同じことが簡単です。 関数インターセプターを絶えず削除して復元すると、ある時点で、「lock xchg word ptr [ecx]、ax」という命令のSpliceLockJmp関数でエラーが発生します。 事実、この時点で別のスレッドからインターセプターのアドレスにページ属性を返す操作を完了することができ、スレッドでこのアドレスへの書き込みを許可したという事実にもかかわらず、実際のページ属性は完全に異なります。



これはまさに、このブランチの作成者が遭遇した動作です: recvをインターセプトします。



このエラーは、上記と同じ方法で解決する必要があります。

確かに、インターセプトハンドラーを忘れてはなりません。スレッドセーフでもありますが、ハンドラーの実装はユーザー次第です。



6.インターセプトされる関数の最初の2バイトをスキップすることは常に可能ですか?



興味深い質問とそれに対する答えは、必ずしもそうとは限りません。

HotPatchメソッドを使用したインターセプトの機能が準備されている場合、Microsoftは、その前に常に5つのNOP命令があり、そのような各機能は2バイト命令で始まることのみを保証します。 これ以上の保証はありません。



MessageBoxWまたはCreateWindowExWコードを見ると、最初の有用なPUSH EBP命令が1バイトを占めることがわかります。 したがって、条件を満たさないため、この関数の本体の前には空の呼び出しMOV EDI、EDIがあります。 同じことは、長さが3バイト以上の命令で始まる関数にも当てはまります。 ただし、関数が2バイトの命令で始まる場合、HotPatchのすべての条件(5つのNOPと2バイト)が満たされるため、空のスタブで本体を膨らませることは意味がありません。



この場合、上記の方法を適用すると、エラー以外は表示されません。



このような関数の例は、RtlCreateUnicodeStringです。

役立つPUSH $ 0C命令で始まります。



画像



最も簡単な解決策は、元の関数を呼び出す前に元の命令を復元することですが、最初から言ったように、これにはエラーがたくさんあります。



したがって、私たちは仕事に直面しました-妨害された命令への呼び出しを提供し、インターセプトコードセットがあっても関数の機能を保証する:



画像



原則として、詰まった命令のマシンコードがあり、それはHotPathSpliceRec.LockJmp構造に格納されていますが、いくつかの理由で直接呼び出すことはできません。



まず、最初に、この構造はヒープ上にあります(より正確には、ヒープ上ではなく、割り当てられたメモリ内にあります。これは、Delphiがヒープメカニズムと直接連携しないためです)。 アドレスHotPathSpliceRec.LockJmpでCALLを何らかの方法で実行すると、エラーが発生します。



もちろん、正しいページ属性を設定できますが、これは面倒です。しかし、実行可能コードをデータ領域と混同しないでください。



第二に、実行をこの命令に転送する場合でも、呼び出された命令のオフセットを考慮して、JMP命令をその後の正しいアドレスに強制する必要があります(この場合、$ 77B062FBになります。前の図を参照)。



第三に、呼び出しに加えて、呼び出された関数に渡されるパラメーターを正しい順序でスタックに配置する必要があります。これにより、少なくともasm挿入を使用する必要が生じます。



すべてを順番に解決してみましょう。



ASM挿入からのパラメーターの受け渡しに関与しないように、このタスクをコンパイラーに割り当てることにより、何らかの種類のスプリングボード機能を実装できます。



つまり おおよそ次のようなインターセプターを作成します。



 function TrampolineRtlCreateUnicodeString(DestinationString: PUNICODE_STRING; SourceString: PWideChar): Integer; stdcall; begin asm db $90, $90, $90, $90, $90, $90, $90 end; end; function InterceptedRtlCreateUnicodeString(DestinationString: PUNICODE_STRING; SourceString: PWideChar): Integer; stdcall; begin Result := TrampolineRtlCreateUnicodeString(DestinationString, SourceString); ShowMessage(DestinationString^.Buffer); end;
      
      







この場合、インターセプターはスプリングボード呼び出しとロギングを処理します。



スプリングボード関数内では、7バイトが予約されています。これは、2バイトの詰まった命令と5バイトのNEAR JMPを書くのに十分な量です。

関数自体はコード領域にあり、その呼び出しで問題を引き起こすことはありません。



そして今、重要なニュアンス。

予約済みブロックの代わりにこれらの7バイトを書き込むと、Delphiの1つの不快な機能に遭遇します。 実際、Delphiコンパイラはほとんどの場合、関数のプロローグとエピローグを生成します。



たとえば、パッチの後、関数のコードが次のようになったとします。



 function TrampolineRtlCreateUnicodeString(DestinationString: PUNICODE_STRING; SourceString: PWideChar): Integer; stdcall; begin asm push $0C //    jmp $77B062FB //      end; end;
      
      







実際、次のようになります。



画像



つまり スタックには、2つのパラメーターDestinationStringおよびSourceStringの代わりに、EBPおよびECXレジスターの値が配置されます。これにより、まったく予測できない結果が生じます。



これは絶対に必要ではないので、簡単に行います。つまり、スプリングボードコードはこの関数の最初から直接記述され、関数プロローグの指示を上書きします。



しかし、実際には、これらの指示は絶対に必要ありません。 インターセプトされた関数の本体とその実行にジャンプした後、コントロールは、アクションによって歪められたスプリングボード関数ではなく、呼び出された場所に直接戻ります。 関数の傍受はハンドラーです。



したがって、インターセプターの初期化は次の方法で実装します。



 //            procedure InitHotPatchSpliceRecEx(const LibraryName, FunctionName: string; InterceptHandler, Trampoline: Pointer; out HotPathSpliceRec: THotPachSpliceData); var OldProtect: DWORD; TrampolineSplice: TNearJmpSpliceRec; begin //      HotPathSpliceRec.FuncAddr := GetProcAddress(GetModuleHandle(PChar(LibraryName)), PChar(FunctionName)); //      ,     Move(HotPathSpliceRec.FuncAddr^, HotPathSpliceRec.LockJmp, LockJmpOpcodeSize); //   VirtualProtect(Trampoline, LockJmpOpcodeSize + NearJmpSpliceRecSize, PAGE_EXECUTE_READWRITE, OldProtect); try Move(HotPathSpliceRec.LockJmp, Trampoline^, LockJmpOpcodeSize); TrampolineSplice.JmpOpcode := JMP_OPKODE; TrampolineSplice.Offset := PAnsiChar(HotPathSpliceRec.FuncAddr) - PAnsiChar(Trampoline) - NearJmpSpliceRecSize; Trampoline := PAnsiChar(Trampoline) + LockJmpOpcodeSize; Move(TrampolineSplice, Trampoline^, SizeOf(TNearJmpSpliceRec)); finally VirtualProtect(Trampoline, LockJmpOpcodeSize + NearJmpSpliceRecSize, OldProtect, OldProtect); end; //   JMP NEAR HotPathSpliceRec.SpliceRec.JmpOpcode := JMP_OPKODE; //    (  NearJmpSpliceRecSize  , // ..     ) HotPathSpliceRec.SpliceRec.Offset := PAnsiChar(InterceptHandler) - PAnsiChar(HotPathSpliceRec.FuncAddr); end;
      
      







インターセプトされた関数自体の初期化と呼び出しは次のとおりです。



 type UNICODE_STRING = record Length: WORD; MaximumLength: WORD; Buffer: PWideChar; end; PUNICODE_STRING = ^UNICODE_STRING; function RtlCreateUnicodeString(DestinationString: PUNICODE_STRING; SourceString: PWideChar): BOOLEAN; stdcall; external 'ntdll.dll'; ... procedure TForm2.FormCreate(Sender: TObject); begin //       InitHotPatchSpliceRecEx('ntdll.dll', 'RtlCreateUnicodeString', @InterceptedRtlCreateUnicodeString, @TrampolineRtlCreateUnicodeString, HotPathSpliceRec); //     NOP- SpliceNearJmp(PAnsiChar(HotPathSpliceRec.FuncAddr) - NearJmpSpliceRecSize, HotPathSpliceRec.SpliceRec); end; procedure TForm2.Button1Click(Sender: TObject); var US: UNICODE_STRING; begin //  RtlCreateUnicodeString SpliceLockJmp(HotPathSpliceRec.FuncAddr, LOCK_JMP_OPKODE); try RtlCreateUnicodeString(@US, 'Test UNICODE String'); finally //   SpliceLockJmp(HotPathSpliceRec.FuncAddr, HotPathSpliceRec.LockJmp); end; end;
      
      







これで、ボタンをクリックして、傍受の結果をメッセージの形で見ることができます。



結論として



その結果、第6章に示されているスプライシング実装オプションは、HotPatch用に準備された関数のインターセプトの場合に最も一般的です。 これは、MOV EDI、EDIスタブの場合、およびインターセプトされる関数の先頭にある有用な命令の場合に正しく機能します。 記事の冒頭で説明したエラーの影響は受けませんが、このアルゴリズムを使用して通常の機能を傍受することはできません 、これについては以前書きまし



情報を断片に分割して一度にすべてを提供する必要がないことをおaびしますが、1年前にアドバイスされたように、内容を消化する時間があるように資料を少しずつ提供することをお勧めします:)



一方、すべての素材をヒープに入れると、まず時間がかかりますが、それは私にはありません。次に、大量のために判読できません(前例がありました)。

したがって、その方が良いです。



このリンクからサンプルのソースコードを取得できます。



©Alexander(Rouse_)ベーグル

2013年5月



All Articles