64 ビット Windows での関数フックを示す、ますます複雑になる一連のプログラム。
これらのプログラムは、関数フックの仕組みを独学しながら書きました。これらは、同じことをしようとしている他の人 (または、これがどのように機能するかを忘れてしまった後の将来の私) にとって役立つかもしれません。これらは順番に見ることを目的としています。関数がプログラムで初めて使用されるとき、その関数は、名前の前にアンダースコアが付けられた状態で、その特定のサンプル プログラムの .cpp ファイルに組み込まれます。同じ関数を使用する後続のサンプルでは、hooking_common.h に含まれるその関数のコピーを使用して、コードの重複を最小限に抑え、後のサンプル プログラムを読みやすい大きさに保ちます。
それらを整理するために少し作業をしましたが、後の例はまだ少し乱雑です。このリポジトリの最終結果は、完全な機能を備えたフック ライブラリを作成するには十分ではありませんが、その道を歩み始めるには十分であることに注意することが重要です。
実行時に、一部のプロジェクトは、ビルドされている他のプロジェクト (DLL を挿入するには DLL がビルドされている必要がある)、または同時に実行されている (ターゲット プログラムをフックするには、すでに実行されている必要がある) ことに依存する場合があります。
すべての例は、Windows SDK 10.0.17763.0 を備えた Visual Studio 2019 (v142) を使用して構築されました。 VS または SDK のバージョンに依存するものは何もないと思いますが、念のためここに記載します。ほぼ確実に、MSVC 固有のものがいくつかあります。
最後に、最後のトランポリンの例では、mspaint にフックをインストールします。将来のある時点で、mspaint が更新されると、この例は壊れると思います。この記事の執筆時点では、mspaint の現在のバージョンは 1909 (OS ビルド 18363.1016) でした。
例は、トランポリンを使用する例と使用しない例の 2 つのカテゴリに分類されます。トランポリン以外の例は、さまざまな状況でのプログラム フローを 1 つの関数から別の関数にリダイレクトすることを示すためにのみ存在します。トランポリンの構築は複雑で、関数フックがどのように機能するかを理解しようとしていたとき、最初に非トランポリンのサンプルを構築することから始めることが非常に役に立ちました。さらに、さまざまな (すでに実行されている) プロセスにフックをインストールする方法を示す例で使用される 4 つの「ターゲット プログラム」があります。
これらの例のほとんどは、フックに関連したメモリ リークを引き起こします。これらの例は単にフックの概念を説明するためのものであり、これらの「リークされた」割り当てはプログラムが終了するまで存在する必要があるため、私はあまり気にしません。
関数フック手法に関する標準用語はあまりないようですが、このリポジトリのコード (および Readme) では次の用語が使用されています。
これらの例では、フックをインストールするときにトランポリンを作成しないため、これらの関数は、フックされた後は元の関数が完全に使用できなくなるという点で、「破壊的な」フックを示していると考えられます。
プログラム フローを同じプログラム内の別の関数にリダイレクトするジャンプ命令で関数の開始バイトを上書きする小さな例。トランポリンが構築されていないため、この操作は破壊的であり、元の関数は呼び出すことができなくなります。これは、リポジトリ内の唯一の 32 ビットの例です。
前の例の 64 ビット バージョン。 64 ビット アプリケーションでは、関数がメモリ内でかなり遠くに配置され、32 ビット相対ジャンプ命令では到達できない場合があります。 64 ビットの相対ジャンプ命令がないため、このプログラムは最初に「リレー」関数を作成します。この関数には、メモリ内のどこにでも到達できる (そしてペイロード関数にジャンプする) 絶対 jmp 命令のバイトが含まれています。ターゲット関数にインストールされる 32 ビット ジャンプは、ペイロードにすぐにジャンプするのではなく、このリレー関数にジャンプします。
前のプロジェクトの手法を使用して、無料関数ではなくメンバー関数をフックする例を示します。
前の例とは少し異なり、このプログラムは、オブジェクトの vtable を通じて関数のアドレスを取得することによって、仮想メンバー関数にフックをインストールする方法を示しています。仮想関数を扱った例は他にありませんが、ここに含めるのに十分興味深いと思いました。
実行中の別のプロセスにフックをインストールする最も単純な例。この例では、DbgHelp ライブラリを使用して、ターゲット プロセス内の関数 (A - 無料関数を持つターゲット) を文字列名で検索します。これは、ターゲット プログラムがデバッグ シンボルを有効にしてビルドされている場合にのみ可能です。この例は単純ではありますが、(リモート プロセスの検索と操作のための) 多数の新機能が導入されているため、以前のプログラムよりも少し長くなります。
この例では、別のプロセスが DLL からインポートした関数をフックする方法を示します。ここで説明する ASLR の仕組みにより、リモート プロセスで DLL 関数のアドレスを取得する方法には微妙な違いがあります。それ以外の点では、この例は前の例とほぼ同じです。
この例では、DLL によってインポートされず、シンボル テーブルにも存在しない関数にフックをインストールする方法を示します (リモート プロセスにデバッグ シンボルがないためと考えられます)。これは、文字列名でターゲット関数を見つける (簡単な) 方法がないことを意味します。代わりに、この例では、x64dbg などの逆アセンブラを使用して、フックする関数の相対仮想アドレス (RVA) を取得したことを前提としています。このプログラムは、その RVA を使用してフックをインストールします。
上記と同様ですが、この例では、生のマシン コード バイトを書き込むのではなく、DLL インジェクションを使用してペイロード関数をインストールします。ペイロードを再度 C++ で作成できるため、作業がはるかに簡単になります。この例のペイロードは、プロジェクト 08B-DLL-Payload に含まれています。
次の例では、フック時にトランポリンをインストールします。これは、プログラムがフックのインストール後もターゲット関数のロジックを実行できることを意味します。フックをインストールするとターゲット関数の少なくとも最初の 5 バイトが上書きされるため、これらの 5 バイトに含まれる命令はトランポリン関数に移動されます。したがって、トランポリン関数を呼び出すと、ターゲット関数の元のロジックが効果的に実行されます。
例 2 と同等のトランポリン設置。逆アセンブリ エンジンを使用せずにトランポリンを作成する方法を示したかったので、この例は少し奇妙です。この場合、ターゲット関数は最初に既知の 5 バイトの命令を持つように作成されているため、その関数の最初の 5 バイトをトランポリン関数にコピーするだけで済みます。これは、トランポリンの正確なサイズがわかっており、修正が必要な相対アドレス指定を使用しないため、トランポリンの作成が非常に簡単であることを意味します。本当に特定のユースケース用にトランポリンを作成している場合は、おそらくこれのバリエーションを実行するだけで済むでしょう。
この例は、前の例と同様のシナリオを示していますが、今回はターゲット関数から盗む必要があるバイトを取得するために逆アセンブラ (キャップストーン) を使用している点が異なります。これにより、簡単に実行できることがわかっている関数だけでなく、あらゆる関数でフック コードを使用できるようになります。この例では、ターゲットを絞ったフック (前の例と同様) から汎用フック関数の構築に移っているため、実際には多くの処理が行われています。トランポリンは相対呼び出し/ジャンプを絶対アドレスを使用する命令に変換する必要があるため、事態はさらに複雑になります。これも汎用フックの 100% 洗練された例ではなく、ループ命令や 5 バイト未満の命令で関数をフックしようとすると失敗します。
基本的に上記と同じですが、この例には、フックのインストール中に実行中のすべてのスレッドを一時停止するコードが含まれています。すべての場合においてスレッドセーフであることが保証されているわけではありませんが、何もしないよりははるかに安全であることは間違いありません。
これは、前の 2 つの例で使用したフック/トランポリン コードを拡張して、複数の関数が同じペイロードにリダイレクトすることをサポートし、ペイロード関数がフックがインストールされている他の関数を呼び出せるようにします。
これは、別のプロセス (この場合、ターゲット アプリ B - DLL からの無料関数を備えたターゲット) にフックをインストールする最初のトランポリンの例です。すべてのフッキング ロジックは、DLL ペイロード 13B - Trampoline インポートされた Func DLL ペイロードに含まれています。ここにはそれほど新しい点はありません。この例では、すでに行われているトランポリンのフック処理と、DLL からインポートされた関数をフックするための前に示したテクニックを組み合わせただけです。
レポの最高傑作。この例では、dll ペイロード (14B - トランポリン フック MSPaint ペイロード) を mspaint の実行インスタンスに挿入します (これを実行する前に、自分で mspaint を起動する必要があります)。インストールされたフックにより、MSPaint で実際に選択した色に関係なく、ブラシが赤で描画されます。正直なところ、前の例で示されていないものは何もありません。これが不自然なプログラムで動作するのを見るのは、ただただ素晴らしいことです。
ループ内でフリー関数を呼び出す単純なターゲット アプリケーション。デバッグ情報を含めてコンパイルされています。
DLL (B2 - GetNum-DLL) からインポートされたフリー関数をループで呼び出すターゲット アプリケーション。
ループ内で非仮想メンバー関数を呼び出すターゲット アプリケーション。
ループ内で仮想メンバー関数を呼び出すターゲット アプリケーション。