8

(私たち) modder に API を提供するゲームの拡張機能をプログラミングしています。この API はさまざまなものを提供しますが、1 つの制限があります。API は「エンジン」専用です。つまり、エンジンに基づいてリリースされたすべてのモディフィケーション (モッド) は、いかなる種類の (モッド固有の) API も提供または持っていません。「署名スキャナー」を作成しました (注: 私のプラグインは共有ライブラリとしてロードされ、-share と -fPIC でコンパイルされます) 対象の関数を見つけます (これは Linux を使用しているので簡単です)。説明するために、特定のケースを取り上げます。目的の関数へのアドレスを見つけました。その関数ヘッダーは非常に単純です。int * InstallRules(void);. 何も (void) を取り、(関心のあるオブジェクトへの) 整数ポインターを返します。今、私がやりたいことは、迂回を作成することです (関数の開始アドレスがあることを覚えておいてください)、自分の関数に、次のように動作させたいと思います:

void MyInstallRules(void)
{
    if(PreHook() == block) // <-- First a 'pre' hook which can block the function
        return;
    int * val = InstallRules(); // <-- Call original function
    PostHook(val); // <-- Call post hook, if interest of original functions return value
}

これが取引です。私は関数フックについてまったく経験がなく、インライン アセンブリについての知識はほとんどありません (AT&T のみ)。インターネット上の事前に作成された迂回パッケージは、Windows専用であるか、まったく別の方法を使用しています (つまり、dll をプリロードして元のパッケージを上書きします)。だから基本的に; 軌道に乗るにはどうすればいいですか?呼び出し規則 (この場合は cdecl) について読んで、インライン アセンブリについて学ぶべきですか、それとも何をすべきですか? おそらく、Linux 迂回路用の既に機能するラッパー クラスが最適でしょう。最後に、次のような単純なものが欲しいです。

void * addressToFunction = SigScanner.FindBySig("Signature_ASfs&43"); // I've already done this part
void * original = PatchFunc(addressToFunction, addressToNewFunction); // This replaces the original function with a hook to mine, but returns a pointer to the original function (relocated ofcourse)
// I might wait for my hook to be called or whatever
// ....

// And then unpatch the patched function (optional)
UnpatchFunc(addressToFunction, addressToNewFunction);

ここで完全に満足のいく答えを得ることができないことは理解していますが、私はここで薄い氷の上にいるので、道順についていくつかの助けをいただければ幸いです...迂回路について読みましたが、ほとんどありませんドキュメント (特に Linux の場合) はまったくありません。「トランポリン」として知られているものを実装したいと思いますが、この知識を習得する方法を見つけることができないようです。

注: _thiscall にも興味がありますが、私が読んだ限りでは、GNU 呼び出し規約 (?) で呼び出すのはそれほど難しくありません。

4

1 に答える 1

12

このプロジェクトは、他の人が異なるバイナリで異なる機能をフックできるようにする「フレームワーク」を開発するためのものですか? それとも、あなたが持っているこの特定のプログラムをフックする必要があるだけですか?

まず、2 番目のことが必要であるとしましょう。プログラムで確実にフックしたい関数がバイナリにあるだけです。これを普遍的に行うことの主な問題は、これを確実に行うのは非常に難しいことですが、ある程度の妥協を厭わない場合は、間違いなく実行可能です。また、これが x86 のものであると仮定しましょう。

関数をフックしたい場合、それを行う方法がいくつかあります。Detoursが行うのはインライン パッチです。彼らは、 Research PDF 文書でそれがどのように機能するかについての素晴らしい概要を持っています。基本的な考え方は、関数があるということです。

00E32BCE  /$ 8BFF           MOV EDI,EDI
00E32BD0  |. 55             PUSH EBP
00E32BD1  |. 8BEC           MOV EBP,ESP
00E32BD3  |. 83EC 10        SUB ESP,10
00E32BD6  |. A1 9849E300    MOV EAX,DWORD PTR DS:[E34998]
...
...

関数の先頭を関数の CALL または JMP に置き換え、パッチで上書きした元のバイトをどこかに保存します。

00E32BCE  /$ E9 XXXXXXXX    JMP MyHook
00E32BD3  |. 83EC 10        SUB ESP,10
00E32BD6  |. A1 9849E300    MOV EAX,DWORD PTR DS:[E34998]

(5 バイトを上書きしたことに注意してください。)これで、元の関数と同じパラメーターと同じ呼び出し規則で関数が呼び出されます。関数が元の関数を呼び出したい場合 (ただし、そうする必要はありません)、「トランポリン」を作成します。これは、1) 上書きされた元の命令を実行し、2) 元の関数の残りの部分にジャンプします。

Trampoline:
    MOV EDI,EDI
    PUSH EBP
    MOV EBP,ESP
    JMP 00E32BD3

以上で、実行時にプロセッサ命令を発行してトランポリン関数を構築するだけで済みます。このプロセスの難しい部分は、あらゆる関数、あらゆる呼び出し規則、およびさまざまな OS/プラットフォームに対して、確実に機能させることです。問題の 1 つは、上書きする 5 バイトが命令の途中で終了する場合です。「命令の終わり」を検出するには、基本的に逆アセンブラーを含める必要があります。これは、関数の先頭に任意の命令が存在する可能性があるためです。または、関数自体が 5 バイトより短い場合 (常に 0 を返す関数XOR EAX,EAX; RETNは、わずか 3 バイトとして記述できます)。

現在のほとんどのコンパイラ/アセンブラは、5 バイトの長さの関数プロローグを生成します。まさにこの目的のために、フックします。わかりMOV EDI, EDIますか?「一体なぜ彼らは edi を edi に移行するのですか? それは何もしません!?」あなたは完全に正しいですが、これはプロローグの目的であり、正確に5バイトの長さです(命令の途中で終了しないでください)。逆アセンブリの例は私が作ったものではなく、Windows Vista の calc.exe であることに注意してください。

フックの実装の残りの部分は単なる技術的な詳細ですが、それが最も難しい部分であるため、何時間もの苦痛をもたらす可能性があります。また、質問で説明した動作:

void MyInstallRules(void)
{
    if(PreHook() == block) // <-- First a 'pre' hook which can block the function
        return;
    int * val = InstallRules(); // <-- Call original function
    PostHook(val); // <-- Call post hook, if interest of original functions return value
}

たとえば、「オリジナルを呼び出さないで」別の値を返したい場合があります。または、元の関数を 2 回呼び出します。代わりに、フック ハンドラーが元の関数を呼び出すかどうか、およびどこで呼び出すかを決定します。また、フックに 2 つのハンドラー関数は必要ありません。

これに必要な技術 (主に組み立て) について十分な知識がない場合、またはフッキングの方法がわからない場合は、Detours の機能を研究することをお勧めします。独自のバイナリをフックし、デバッガー (OllyDbg など) を使用して、アセンブリ レベルで正確に何をしたか、どの命令がどこに配置されたかを確認します。また、このチュートリアルが役立つ場合があります。

いずれにせよ、あなたのタスクが特定のプログラムでいくつかの関数をフックすることである場合、これは実行可能です。何か問題がある場合は、ここでもう一度質問してください. 基本的に、タスクをはるかに簡単にする多くの仮定 (関数のプロローグや使用される規則など) を行うことができます。

信頼性の高いフック フレームワークを作成したい場合は、まったく別の話であり、最初にいくつかの単純なアプリ用の単純なフックを作成することから始める必要があります。

また、この手法は OS 固有のものではなく、すべての x86 プラットフォームで同じであり、Linux と Windows の両方で機能することに注意してください。OS固有のもの、おそらくコードのメモリ保護を変更する必要があるということです(「ロックを解除」して、書き込みできるようにします)。これはmprotect、LinuxおよびVirtualProtectWindowsで行われます。また、呼び出し規約も異なります。それは、コンパイラで正しい構文を使用することで解決できることです。

もう 1 つの問題は「DLL インジェクション」です (Linux ではおそらく「共有ライブラリ インジェクション」と呼ばれますが、DLL インジェクションという用語は広く知られています)。(フックを実行する) コードをプログラムに入れる必要があります。私の提案は、可能であれば、プログラムが実行される直前にプログラムにロードされるライブラリを指定できる LD_PRELOAD 環境変数を使用することです。これは、ここのように何度も説明されています: LD_PRELOAD トリックとは何ですか? . 実行時にこれを行う必要がある場合は、gdb または ptrace を使用する必要があるのではないかと思いますが、これは (少なくとも ptrace に関しては) 非常に難しいと思います。

また、いくつかの優れたリソースも見つけました。

もう 1 つのポイント: この「インライン パッチ」は、これを行う唯一の方法ではありません。さらに簡単な方法があります。たとえば、関数が仮想関数である場合、またはライブラリ エクスポートされた関数である場合、すべてのアセンブリ/逆アセンブリ/JMP 処理をスキップして、単純にその関数へのポインタを置き換えることができます (仮想関数のテーブルまたはエクスポートされたシンボル テーブル)。

于 2012-05-02T19:25:14.760 に答える