カットの下のソース、詳細、説明...
だから、私はいわゆるの実装を完了するのに時間がかかりました 永続的な「元の関数を呼び出すときにコードを書き換える必要のないトラップ。 ソースコードはここにあり、コンパイルされたバージョンはここにあります 。
必要に応じて、ワームの缶やパンドラの箱を開けました。 そして、完璧なインターセプターを作成する義務から自分を解放する時が来ました。なぜなら、 ブランチの数と実装のチェックにより、このタスクは非決定的なドメインにスムーズに変換されます。
この点で、このコードはWS2_32関数に対してのみ正しく機能し、この例のフレームワーク内でのみ機能することを直ちに警告する必要があります。 他の関数をインターセプトするには、エントリポイントでの命令のより広範な分析(特定の関数またはリアルタイム分析)の方向でコードを終了する必要があります。 そして事はこれです。
上の図からわかるように、インターセプターは、エントリポイントの最初の5バイトを無条件に独自のハンドラーに切り替えることでトラップを設定します。 上書きされるこれらのバイトは、構造に保存されます。 前の例では、元の関数が呼び出されるたびにエントリポイントを元の状態に復元したため、この点で問題はありませんでした。 しかし、今では条件が変更されています-バイトを書き換えることはもうありません。
したがって、コピーした場所から直接、エントリポイントから元のコードの最初の命令を実行し、最後にワイプしなかった命令に戻ります。 そしてここで、x86プロセッサのマシンコードの機能に近づきます。
プロセッサの命令にはさまざまなサイズがあり、1バイトから15バイトまでさまざまです。 したがって、事前に、5バイトのパッチ方式でトラップをインストールした後、どのように、どのように正確に破損するかはわかりません。 原則として、エントリポイントには何でも構いません。そのため、一般的な方法から特定の方法に切り替えるか、ロジックを複雑にするかの2つのオプションがあります。
「一般から特定へ」とは、すべての機能を調査し、それぞれに最適な方法を見つけた後、特定の機能にトラップを設定することを意味します。 たとえば、同じWinSockには、XP、XPSP1、XPSP2、XPSP3、Vista、VistaSP1、Vista PU、Windows 7などの多くのバージョンがあるため、これはあまり便利ではありません。 また、システムライブラリのバージョンもインストールできるあらゆる種類のセキュリティ更新プログラムがあります。 したがって、プライベートメソッドによるトラップの正しい操作のためには、同じライブラリのすべての可能なバージョンを調べる必要があります。 幸いなことに、ほとんどの場合、ライブラリのバージョンが異なっていても、エントリポイントで互いにそれほど違いはありません。
「複雑なロジック」とは、命令パーサーを記述することを意味します
a)プロセッサのオペコードのサイズを計算して、制御を常に「全体」命令に転送し、途中ではありません。
b)問題のある領域を検出し、それらを正しく処理できるようになる
問題のある場所の最も明白な例は、「エイリアン」トラップです。 プロセスに侵入し、関数をインターセプトする最初の人ではないと仮定します。 この場合、最初の命令はほとんどの場合JMP XX XX XX XX XX、場合によってはPUSH XX XX XX XX XX RETN、そして理論的には何でも表示されます。 したがって、JMPの場合、遷移は相対的であり、その実行のアドレスに対して相対的であると見なされるため、コピーしたコードを別の場所から正しく実行することはできません。 したがって、この移行はカウントする必要があります。 ただし、PUSH-RETNの場合、アドレスを再カウントする必要はありません。 そして、「何でも」の場合、問題のある場所のそのような例があるかもしれません:
00004EE1: 3C01 cmp al,1 00004EE3: 7404 je 000004EE9 00004EE5: E916010000 jmp 000005000
この場合、複雑な
- 5バイトで上書きされる相対条件分岐があり、その相対関係のためにハンドラーで正しく実行することはできません。 次の2つの方法があります。元のアドレスへの移行を再計算し、その結果、命令の長さを2バイトから6バイトに変更する可能性が非常に高くなります。 または、条件付きジャンプがおそらくジャンプするコードも自分にコピーします。 最初のケースでは、命令のサイズを変更するため、後続のすべてのコードを新しいアドレスに再カウントする必要があります。 2番目のケースでは、遷移が発生するときと発生しないときの状況をシミュレートし、制御を受け取ることが保証されている場所への戻りを絞る必要があることは明らかです。 繰り返しますが、条件付き遷移が指すコードにも条件付きジャンプと無条件ジャンプがある可能性があると考えると、非常に興味深い問題に直面します
- 無条件ジャンプの一部、またはJMP命令自体も、5バイトで消去されます。 次の損傷のない命令に戻ると、無条件ジャンプをスキップするため、元のコードが正しく機能しないことは明らかです。 別のケースでは、命令全体をコピーすると、無条件の遷移は相対的であるため、ハイパースペースへのどこかで行われます。 したがって、新しい場所から正しい住所を計算する必要があり、前の段落と一緒に退屈な生活を約束します。
あなたがいじめのすべての微妙さに染み込んでいるなら、私は一つの良いアドバイスを与えることができます。 ユニバーサルアナライザーを作成することは可能ですが、そのためには、同様のハックごとに多くのソリューションテンプレートを準備する必要があります。 たとえば、「無条件の遷移が最初にある場合」、「条件付きの遷移がある場合」、「関数呼び出しがある場合」、「中間にある場合...」など 次に、指示の段階的な分析に従って、各テンプレートが適用されます。 しかし、私を信じて、これにはコード作成への多大な投資が必要であり、今のところ準備ができていません。
上記の理由により、この例では、WS2_32の特定のソリューションに焦点を当てました。 したがって、インターセプトするすべての関数は標準として始まります。
8BFF mov edi,edi 55 push ebp 8BEC mov ebp,esp 83ECXX sub esp,0000000XX
マイクロソフトがMicrosoft Detoursで同様の手法を使用するためにこれを特に行ったのではないかと思います。 しかし、私は個人的に彼を見たり感じたりしなかったので、この主題について空想を続けることはしません。 したがって、WinSockに必要なすべての関数の開始は、関数の開始時の無条件ジャンプのサイズに理想的に適合します。
a)最初の3つの命令は正確に5バイトを占有します
b)最初の3つの命令は特定のアドレスに関連付けられておらず、ハンドラー内で実行できます
このために、Interceptor.cppコードの変更に進みます...まず
/************************************************************************/ /* */ /************************************************************************/ BOOL hookInstall(PAPIHOOK thisHook) { ... #ifdef PERSISTENT_HOOKS // thisHook->oldCodeSize = 0; // 5 // . // (5 ) for (; thisHook->oldCodeSize < HOOK_CODE_SIZE; ) { // int opSize = getX86InstructionLength((PBYTE) thisHook->oldAddr + thisHook->oldCodeSize); // thisHook->oldCodeSize += opSize; } // + thisHook->oldCode = (UCHAR *) xmalloc(thisHook->oldCodeSize + HOOK_CODE_SIZE); if (NULL == thisHook->oldCode) { SetLastError(ERROR_NOT_ENOUGH_MEMORY); return FALSE; } DWORD fl; // EXECUTE DEP VirtualProtect(thisHook->oldCode, thisHook->oldCodeSize + HOOK_CODE_SIZE, PAGE_EXECUTE_READWRITE, &fl); // memcpy(thisHook->oldCode, thisHook->oldAddr, thisHook->oldCodeSize); // ... thisHook->oldCode[thisHook->oldCodeSize] = asmJMP; // ... , // JMP DWORD *d = (DWORD*) ((PBYTE) (thisHook->oldCode + thisHook->oldCodeSize + 1)); *d = (DWORD) ((PBYTE) thisHook->oldAddr + thisHook->oldCodeSize) - (DWORD) d - sizeof(DWORD); #else // // thisHook->oldCodeSize = HOOK_CODE_SIZE; memcpy(thisHook->oldCode, thisHook->oldAddr, HOOK_CODE_SIZE); #endif // thisHook->isInstalled = TRUE; // hookEnable(thisHook); #ifdef PERSISTENT_HOOKS // VirtualProtect(thisHook->oldAddr, HOOK_CODE_SIZE, oldFlags, &oldFlags); #endif return TRUE; }
ここでは、マジック関数getX86InstructionLength()の呼び出しを除き、すべてが明確になっているはずです。 質問に悩まされないために、私は答えます。 最初は、この関数は常に5を返しました。特殊なケースを実装することに同意したため、あまり需要はありません。エントリポイントでの最初の3つの命令の長さは5バイトです。 しかし、私はもう少し進んで、ソースには実際のx86命令長アナライザーがあります。 これは誰かの将来のスマートインターセプターへの私の貢献です。いわば...この投稿では、問題の広大さのためにその方向に逸脱したくありません。
そのため、上記のコードは、エントリポイントから最初の5バイトをそれ自体にコピーし、シリアル番号4の命令で元の関数に無条件遷移を戻した後、そのまま残ります。 したがって、エントリポイントでコードを書き換えるトリックを使用せずに、元の関数への静的呼び出しを「転送」します。
同様に、トラップの削除を追加し、関数の初期状態を復元します。
/************************************************************************/ /* */ /************************************************************************/ BOOL hookRemove(PAPIHOOK thisHook) { // , if (!thisHook->isInstalled) return FALSE; #ifdef PERSISTENT_HOOKS DWORD oldFlags; if (!VirtualProtect(thisHook->oldAddr, HOOK_CODE_SIZE, PAGE_EXECUTE_READWRITE, &oldFlags) || IsBadWritePtr(thisHook->oldAddr, HOOK_CODE_SIZE)) { SetLastError(ERROR_WRITE_PROTECT); return FALSE; // } #endif // hookDisable(thisHook); #ifdef PERSISTENT_HOOKS VirtualProtect(thisHook->oldAddr, HOOK_CODE_SIZE, oldFlags, &oldFlags); if (thisHook->oldCode != NULL) xfree(thisHook->oldCode); #endif // thisHook->isInstalled = FALSE; // thisHook->newAddr = (LPVOID) NULL; thisHook->oldAddr = (LPVOID) NULL; return TRUE; }
DEP(データ実行防止)を正しく機能させるには、元のコードのコピーが保存されるメモリにEXECUTE権限を設定する必要があることに注意してください。 そうでない場合、システムは単にこのコードを実行させません。 今は何も書き換える必要がないため、インターセプトされた関数を宣言するマクロを追加する価値があります。
#ifdef PERSISTENT_HOOKS /************************************************************************/ /* */ /* : typedef int (WSAAPI *PF_send) (SOCKET s, char *buf, int len, int flags); int WSAAPI my_send(SOCKET s, char *buf, int len, int flags) { PAPIHOOK thisHook = hookFind(my_send); PF_send p_send = (PF_send) thisHook->oldProc; if (NULL == thisHook || NULL == p_send) return (int) 0; int rv; ... /************************************************************************/ #define DEFINE_HOOK(RTYPE, CTYPE, NAME, ARGS)\ typedef RTYPE(CTYPE *PF_##NAME) ##ARGS; \ RTYPE CTYPE my_##NAME ##ARGS \ { \ PAPIHOOK thisHook = hookFind(my_##NAME); \ PF_##NAME p_##NAME = (PF_##NAME) thisHook->oldProc; \ if (NULL == thisHook || NULL == p_##NAME) \ return (RTYPE) 0; \ RTYPE rv; #define LEAVE_HOOK() } \ return rv;
コードからわかるように、元のインポートされた関数の代わりに、thisHook-> oldProcでプロトタイプを呼び出します。 これはまさに、元の5バイトを保存し、それらをトランジションバックで補足した場所です。
概して、これですべてです。 ソースでは、PERSISTENT_HOOKSディレクティブが古いインターセプトメソッドと新しいインターセプトメソッドの切り替えを担当し、デフォルトでは新しいメソッドが上書きなしで使用されます。
ところで、傍受を元のコードの奥深くで実行する別の方法については、意図的に言及しませんでした。 これはさらに複雑な作業ですが、さらに興味深い作業です。 誰かが「なぜこれが必要なの?」と私に尋ねました。 ヒントで答えます。エントリポイントでコードフラグメントのチェックサムを考慮し、その変更に敏感なプログラムがあります。 しかし幸いなことに、ブラウザはまだその中にはありません。
私はこの分野の先駆者ではなく、独創的なふりをしていないことを忘れないでください。 ですから、この方向への私の努力よりも、より洗練された解決策を持っている人々がいます。
よろしく
// st