ハイブリッド DLL の構成

id:ladybug:20070113の続き。

前回の内容をふまえ、2つの PE ファイルを次の図ように加工する必要がある。

前回書いていないポイントとしては、

  • 太線で囲んだ部分のマージをうまく実施する
  • .text や rva に含まれるモジュール内相対アドレスを調整する
  • .idata のマージ時、シンボル名は序数順でソートしておかなければならない

というところであろうか。
自力で PE ファイルが書き出せるような人には、rva やらの説明は不要であろうから省略して、*1 いきなり Robert Simpson さんの合成手法のアプローチを紹介する。

.NET Assembly の作成

まず、アセンブリ側を最初に作成する。アセンブリ側を作成するにあたって、API DLL を参照するための P/Invoke を使用することができ、参照に使用する DLL やインポートライブラリは不要なので、API DLL 側の設計のみが出来てれば十分であるというのが1つのポイントだろう。
とりあえず、うまい題材が思いつかないので、単純なメッセージボックスを表示する API を考える。

__declspec(dllexport) extern "C" void GreetingW(LPCWSTR name);

アセンブリの実装は、

public class Greeting
{
  public static void Show(string name)
  {
    GreetingW(name);
  }

  [DllImport("Greeting.dll", CharSet=CharSet.Unicode, SetLastError=true)]
  private static extern void GreetingW(string name);
}

ここで、Greeting.dll という名前を指定した DllImport を使用しているが、この Greeting.dll は最終的に生成されるハイブリッドライブラリのファイル名で、このコードが格納される自分自身のことである。もちろん、自分自身の公開する API をインポートすることは必須ではないが、それをやらないでどうする、といったところだ。

IL 保存用セクションの作成

次に、API DLL 側を作成する。このとき、任意に PE セクションを生成できる開発環境を使用することが前提となる。Visual C++ は特定の変数の領域を異なる PE セクションに配置することができるので、この要件をクリアする。通常通りに API を実装した後、PE セクションを1つ作成する。PE セクションの生成については、Win32 グローバルフックを使用するとき、フックハンドルを共有セクションに配置する例などを良く見かけることができる。*2

// この pragma 以降の変数を ".clr" という名前のデータセクションに保存する
#pragma data_seg(".clr")  

// 848 バイト確保する
char managed_code_place_holder[848] = { 0 };

// 変数の保存先を通常のデータセクションへ戻す
#pragma data_seg()

// リンカにコマンドラインオプションを追加する。
// ".clr" セクションに E(executable), R(readonly) の属性を設定
#pragma comment(linker, "/SECTION:.clr,ER")

この例では、848 バイトの容量をもつ .clr というセクションを持った API DLL が作成されることになる。このセクションの用途は事前に説明したように、Greeting クラスをコンパイルした後の IL を格納するための領域となる。 コンパイルされた Greeting クラスは 744 バイトの容量、rva エントリ 100 バイトの 844 バイトなのだが、スタートアップルーチンのエントリポイントを保持するために4バイトが必要となるため、848 バイト以上を確保する必要がある。
このようにして PE セクションを作成することで、PE ファイルを0から出力するのではなく、この予約された PE セクションに IL を埋め込むことでハイブリッドライブラリを作成することができるようになる。また、API DLL のコンパイル時に CLR 用のセクションがすでに存在する状態にできるため、コンパイルされた .text セクションに記載される .idata のアドレスはハイブリッド化後のアドレスをさしていることになる。これは、.text セクションから JMP 命令などを検索してアドレスの調整をするような複雑な処理を行うことなく .idata のマージを完了させることができる、ということを意味する。

つづく

*1:たぶん理解できてるけど説明するの面倒だし!

*2:余談だが、現行の Windows OS では、グローバルフックハンドルはフックの解除に必要なだけであり、フックチェインのリレーには NULL を与えることができる。このためグローバルフックにおいてフックハンドルを共有セクションに保持する必要はない。