侵入的なAVやEDRが、強化された可視性要件のために実行中のプロセスにホットパッチを導入する時代において、現代の攻撃者はこれらの監視をすり抜けるための堅牢なツールを必要としています。使用中の関数をその場でフック解除できる動的インポートリゾルバの提案実装は、攻撃者のレジリエンス強化に向けたさらなる一歩です。
ここで提案するソリューションは、リンカ解決型のWinAPIインポート(コンパイルされた実行可能ファイルのPEヘッダ、特にImport Address Tableに可視なまま)から、完全に動的なアプローチに切り替え、インポートを動的にのみ解決することに重点を置くことです。このような動的リゾルバには、オペレータ側からの指示なしにバックグラウンドで実行されるフック解除ロジックを組み込むことができます。
次のようにして、MessageBoxW をフックされず、監視されずに呼び出すことができます:
RESOLVE(user32, MessageBoxW);
_MessageBoxW(0, L"Look Ma! I'm unhooked!", L"Third - Unhooked", 0);
すべての魔法は RESOLVE マクロ定義内で発生し、_MessageBoxW という名前の ImportResolver<T> オブジェクトを構築します。

UnhookMe の例の仕組みは次の通りです:
MessageBoxW を表示します。これはフックの対象ではありません。MessageBoxW のプロローグを自分たちでフックし、メッセージを表示せずに常に0を返すようにします。UnhookingImportResolver リゾルバを使用して MessageBoxW を動的に解決します。これにより、適用されたプロローグパッチを検出し、元のバイトを復元して、MessageBoxW の機能を効果的にフック解除します。メッセージボックスが表示されている間に、次のログ行がコンソールのstdoutに出力されます:
[~] Resolved symbol kernel32.dll!CreateFileA
[~] Resolved symbol kernel32.dll!ReadProcessMemory
[~] Resolved symbol kernel32.dll!MapViewOfFile
[~] Resolved symbol kernel32.dll!VirtualProtectEx
[#] Found trampoline hook in symbol: MessageBoxW . Restored original bytes from file.
[~] Resolved symbol user32.dll!MessageBoxW
合計5つのC++ソースコード/ヘッダファイルをソリューションに含める必要があります。ただし、メインプログラムファイルは、以下に詳述する2つの必要なヘッダのみを含めればよいです。
resolver.h - UnhookingImportResolver の実装の大部分と便利なマクロ定義を含むヘッダresolver.cpp - グローバルオプションが定義されたソースコードusings.h - 一般的に使用されるWinAPIのための数十の using 型定義を含む、大きくて厄介なヘッダファイルPE.cpp - カスタムPEパーサのソースコードファイルPE.h - カスタムPEパーサのヘッダファイルプログラムは次の2つのヘッダのみを含める必要があります:
#include "usings.h"
#include "resolver.h"
リゾルバの動作やアクティビティの報告方法に影響を与える、変更可能なグローバルオプションがいくつかあります。これらは resolver.cpp ファイルの先頭で定義されています:
リゾルバのグローバルオプション:
globalQuietOption - 出力を一切行いたくない場合は true に設定globalVerboseOption - 詳細な冗長出力を行いたい場合は true に設定globalAntiSplicingOption - 解決された関数がフックされている場合にフックを解除するglobalLogFilePath - 出力ログ行のリダイレクト先。空の場合はstdoutを使用bool globalQuietOption = false;
bool globalVerboseOption = true;
bool globalAntiSplicingOption = true;
wchar_t globalLogFilePath[MAX_PATH] = L"";
リゾルバを使用するには、まず関数ポインタ型を厳密な形式の using 文で宣言する必要があります:
using fn_FunctionName = ReturnType WINAPI (
ParamType1 paramName1,
...,
ParamTypeN paramNameN,
);
このリポジトリには、数十の一般的なWindows API用の事前定義されたusing型を含む usings.h ヘッダファイルが付属しています。
FunctionName は、ImportResolver に解決させたい WinAPI に対応し、その関数ポインタは WINAPI 呼び出し規約(x86 では __stdcall、x64 では __fastcall)を持つものとしてマークされなければなりません。ReturnType は WINAPI 型修飾子の前に置く必要があります。
上記のように関数ポインタ型を定義すると、次の方法で使用できるようになります:
RESOLVE(libraryName, FunctionName);
ReturnType output = _FunctionName(param1, ..., paramN);
マクロ RESOLVE は ImportResolver テンプレートオブジェクトのインスタンス化と指定されたライブラリ名の調整を行います。
リゾルバはさらにいくつかのマクロ定義を導入し、さまざまな状況でコンストラクタ呼び出しを簡単に使用できるようにします:
#define RESOLVE(mod, func) RESOLVE_PARAMETERIZED(mod, func, ::globalVerboseOption, ::globalAntiSplicingOption)
#define RESOLVE_NO_UNHOOK(mod, func) RESOLVE_PARAMETERIZED(mod, func, ::globalVerboseOption, false)
#define RESOLVE_VERBOSE_UNHOOK(mod, func) RESOLVE_PARAMETERIZED(mod, func, true, true)
#define RESOLVE_VERBOSE_NOUNHOOK(mod, func) RESOLVE_PARAMETERIZED(mod, func, true, false)
#define RESOLVE_NOVERBOSE_UNHOOK(mod, func) RESOLVE_PARAMETERIZED(mod, func, false, true)
#define RESOLVE_NOVERBOSE_NOUNHOOK(mod, func) RESOLVE_PARAMETERIZED(mod, func, false, false)
リゾルバのコンストラクタ:
template<typename Ret, typename ...Args>
ImportResolver<Ret WINAPI(Args...)>(
std::string dllName,
std::string funcName,
bool _verbose = false,
bool _unhook = false,
bool *_wasItHooked = nullptr
)
このリゾルバは、カスタムPEヘッダパーサを活用し、参照されたすべてのDLLモジュールを処理してエクスポートをマッピングし、モジュールのPEヘッダの整合性と参照された関数のスタブバイトの整合性を検証します。
考え方は次の通りです:
まず、GetModuleHandle でアクセスできない場合、ユーザーが指定したライブラリ(RESOLVE マクロの最初のパラメータとして指定されたもの)を LoadLibrary でロードします。
次に、ロード/参照されたライブラリのPEヘッダを処理し、エクスポートをマッピングし、エクスポートアドレスの配列を取得するとともに、クロス検証のためにこれらのアドレスを自分たちで計算します。
DLLのExport Address Tableで定義されたルーチンのアドレスが期待と一致しない場合、そのエクスポートはEATフックされていると見なされます。同様に、その関数の実行可能ファイルのImport Address Table (IAT) エントリが変更され、DLLのコードセクション内の正しい場所を指していない場合、その関数はIATフックされていると見なされます。
これまでのところフックが見つからなかった場合、関数のプロローグの最初のNバイトを取得し、ディスクに保存されているDLLファイル内のものと比較します。メモリから取得したバイトとファイルから取得したバイトの間に不一致がある場合、関数がインラインパッチ(ホットパッチ)されたと見なします。
関数がフックされていると見なされた場合、元のエクスポートアドレス(自分たちで計算したもの)を返し、エントリのフックを解除します。パッチバイトが存在する場合は、それらを復元します。
最後に、リゾルバのパフォーマンスへの影響を最適化するために、ロードされたすべてのモジュールのイメージベースと解決された関数アドレスをキャッシュし、以降のアクセス時にキャッシュ(std::map)から返します。
このような動的フック解除リゾルバが直面する問題の中には、転送されたAPIのトラバースに関するものがあります(DLLには、この関数はこのモジュールには実装されておらず、別のモジュールにあることを示すExport thunkが含まれている場合があります)。この実装はサポートしていますが、トラバースロジックが時々壊れることがあります。
このプロジェクトや他のプロジェクトは、眠れない夜と たくさんの努力 の成果です。もし私の活動を気に入ってくださり、常にコミュニティに還元していることを評価していただけるなら、コーヒーを一杯ご馳走する ことを検討してください(できればビールの方がいいですが)感謝の気持ちとして! 💪
Mariusz Banach / mgeeky, 21
<mb [at] binary-offensive.com>
(https://github.com/mgeeky)