
ExportHider: 実行時にエクスポートテーブルを生成することで、DLLファイルからエクスポートされた関数を隠す。
ExportHiderは、ファイルシステム上のDLLのエクスポートディレクトリからエクスポート関数を隠すためのコードスタブを含むC++ DLLテンプレートを生成します。関数定義を記述してファイルをコンパイルした後、CFF ExplorerのようなPEファイルビューアでは隠されたエクスポート関数は表示されません。ただし、テンプレート内のコードスタブは実行時にエクスポートディレクトリを再作成するため、正当なGetProcAddress呼び出しは正常に実行されます。この方法は、動的なDLLロードまたはカスタムDLLローダーの場合にのみ機能します。
通常、DLLファイル(CまたはC++)でエクスポート関数を定義する場合、関数名の前に**__declspec(dllexport)**キーワードを付けるか、.defファイルを作成します。コンパイル後、コンパイラはエクスポート関数に関する情報を保持するための特定のテーブルであるエクスポートディレクトリを作成します。エクスポートディレクトリの構造は以下の通りです:
プロセスがDLLファイルから関数を使用しようとするとき、Windowsローダーはこの構造を解析し、AddressOfFunctions、AddressOfNames、AddressOfNameOrdinals配列を使用して要求された関数をインポートします。
関数をインポートする具体的な手順はferreirascのブログ記事で詳しく説明されていますが、簡単に言うと、名前でインポートされる関数の場合、ローダーはAddressOfNames配列(この配列の値は単なるRVA値)を反復処理し、指定された名前を検索します。ローダーが"i"番目の位置で一致を見つけると、AddressOfNameOrdinals配列のi番目のインデックスを参照し、この関数に関連付けられた序数を取得します。序数を取得したローダーは、序数値の位置にあるAddressOfFunctionsを参照し、インポートされた関数に関連付けられたRVAを最終的に取得します。
ここで重要な点は、LoadLibraryが呼び出されたときのWindowsローダーによるこれらすべての検索およびアクセス操作は、DLLがプロセスアドレス空間にマッピングされた後に行われることです。DLLマッピング中、PEヘッダーを含むDLLファイル全体がメモリに書き込まれ、ローダーはメモリ内のヘッダーを解析してエクスポートディレクトリに到達します。これは、DLL自身がプロセスにアタッチされた後、メモリ内のPEヘッダーを上書きしてエクスポートディレクトリアドレス(単にDataDirectoryの0番目のインデックス)を変更できれば、ローダーはコンパイラが追加したエクスポートディレクトリではなく、任意のエクスポートディレクトリ内でインポートする関数を検索できることを意味します。
__ _ _ _
/__\_ ___ __ ___ _ __| |_ /\ /(_) __| | ___ _ __
/_\ \ \/ / '_ \ / _ \| '__| __|/ /_/ / |/ _` |/ _ \ '__|
//__ > <| |_) | (_) | | | |_/ __ /| | (_| | __/ |
\__/ /_/\_\ .__/ \___/|_| \__\/ /_/ |_|\__,_|\___|_|
|_|
by @R0h1rr1m
Usage of C:\Users\Public\DLLDemo\ExportHider.exe:
-h | --help ヘルプメッセージを表示します。
-i | --input <入力パス> 隠す関数名のリストを含む入力ファイルのパス。(必須)
-o | --output <出力パス> DLLテンプレートの出力パス。(必須)
-n | --name <DLL名> エクスポートディレクトリ用のDLLの名前。(必須)
-c | --count <その他の関数の数> 隠されないその他のエクスポート関数の数。
-i | --input <入力パス>パラメータについては、隠す関数名を行ごとに格納した入力ファイルのパスを指定する必要があります。入力ファイルの内容例は次のようになります:
TestFunction1
TestFunction2
TestFunction3
-c | --countパラメータについては、すべてのエクスポート関数を隠したくない場合(つまり、__declspec(dllexport)または.defファイルでエクスポートされ、PEファイルビューアでファイルシステム上のDLLファイルのエクスポートディレクトリに表示される関数がいくつかある場合)、このパラメータを使用してその数を指定してください。ツールはメモリ計算時にこの情報を必要とします。
この手法を試したい場合、プロジェクトの開発中に遭遇した興味深い点が2つあります。プロジェクトを変更する前に知っておく必要があるかもしれません:
GetProcAddress関数を名前で呼び出す場合、二分探索のようなアルゴリズムを使用してエクスポート関数を検索します。そのため、隠された関数を含むすべてのエクスポート関数の名前は、AddressOfNames配列内でソートされている必要があります。そうでない場合、GetProcAddress関数はNULLを返します。そのため、この配列のメンバーをソートするためにバブルソートアルゴリズムを使用しました。AddressOfNames配列、AddressOfFunctions配列、DataDirectory配列、およびその他のいくつかのフィールドは、相対仮想アドレス(RVA)の値を必要とします。さらに、これらのRVA値はDWORDサイズのフィールドに保持されます。VirtualAllocやHeapAllocなどの動的メモリ割り当て関数を使用して新しい任意のエクスポートディレクトリ用のメモリ領域を割り当てると、割り当てられたアドレスはDLLマッピング領域から遠く離れ、RVA値がDWORDサイズのフィールドに収まらず、整数オーバーフローが発生します。そのため、DLLテンプレートではメモリ要件のためにバイト配列型のグローバル変数を使用しました。このプロジェクトを作成する際の最初の目標は、エクスポートが欠落している(またはエクスポートテーブルが完全に存在しない)にもかかわらず、新しく作成されたプロセスによって正常にロードされるDLLを生成することでした。その動作がDLLサイドローディングのペイロードに新たな遊び場をもたらすかもしれないと考えました。しかし、エクスポートディレクトリ修正スタブがWindowsローダーによるDLLのエクスポート関数のチェックよりも前に実行されるような関数や方法を見つけることはできませんでした。
より技術的には、NTDLL内のWindowsローダーに関連するコードセクションで、以下のフローと各DLLに対する関数呼び出しに気付きました(古いメモから収集;私はリバースエンジニアの専門家ではないため、いくつか間違いがあるかもしれません):
1. LdrpMapDll - DLLがプロセスアドレス空間にマッピングされる場所。DLL名条件を満たすすべてのDLLは直接メモリに配置され、ファイルシステムバージョンの事前チェックは行われません。
2. LdrpSnapModule - Windowsローダーがインポートの解決を開始する場所。各インポート記述子について、PE構造を解析し、エクスポートテーブルをチェックし、インポートされた関数を二分探索し、そのRVAを計算し、対応する呼び出し元プロセスのインポートアドレステーブルエントリにそのアドレスを書き込みます。
3. LdrpDoPostSnapWork - ステップ2が各インポート関数で成功した場合、メモリ保護、TLS初期化、CFG有効化がこの関数で行われます。
4. LdrpInitializeNode - ステップ3が成功した場合、このステップにはモジュールリンク関数があります。
5. LdrpCallTlsInitializers - DllMain関数の前にTLSコールバックが呼び出される場所。
6. LdrpCallInitRoutine - インポートされたDLLに対してDLLMain自体が初めて呼び出される場所。元のソリューションでは、この関数はエクスポートテーブルを修正するには遅すぎます。
DLLをインポートする実行可能ファイルを実行するとき、WindowsローダーがDLLのエクスポートテーブル内で要求された関数名を見つけられない場合、実行を停止し、LdrpSnapModuleの後に実行される関数は実行されません。
DLLサイドローディングの場合、呼び出し元のプロセスを変更することはできません。したがって、エクスポートテーブルを動的に修正する唯一のチャンスは、ローダーがLdrpSnapModule関数のチェック中にすぐに停止するため、LdrpMapDllとLdrpSnapModule関数の間でコードを実行する機会を見つけることです。TLSコールバック、セカンダリDLLロード、転送エクスポート、その他の回避策を試しましたが、いずれもそのような場所を見つけるのに役立ちませんでした。そのため、残念ながらこの方法はDLLサイドローディングや静的にインポートされたDLLには直接機能しません。この問題に対する解決策や実行可能な回避策を発見された場合、アイデアについて話し合ったり、一緒にアプローチを検討したり、実装したりするなど、非常に喜んでさらに探求します。この方向での貢献は大いに歓迎します。
DLLサイドローディングの場合にこの方法を使用する1つの可能な方法は、作業全体を2つのDLL(プロキシDLLとペイロードDLL)に分割することです。プロキシDLLはEXEが期待する名前を持ち、ローダーの静的インポートチェックを満たす可視のエクスポートを持ちます。一方、ペイロードDLLは実際の隠された機能を含み、エクスポートが欠落しており、DllMainでエクスポートテーブルを再構築します。これはこの問題に対する良い回避策とは思えないため、実装しませんでした。
許可されたセキュリティテスト専用です。明示的な許可なくシステムに対してこのツールを悪用することは違法です。