読者です 読者をやめる 読者になる 読者になる

【お知らせ】プログラミング記事の投稿はQiitaに移行しました。

PEの.idataをアセンブラで考える

PEの.idataをアセンブリ言語で記述しながら説明します。

PEからDLLを参照するには.idataセクションにDLL名とシンボル名を記述します。実行時にWindowsのローダが.idataを見てLoadLibraryとGetProcAddressに相当する処理を行い、取得したアドレスを.idataに書き込みます。アドレスが書き込まれる領域はサンクと呼ばれており、あらかじめ.idataに用意しておきます。プログラムはサンクに書き込まれたアドレスをcallすることでDLL内の関数を呼べます。

例:

DLLシンボルサンク
a.dllfoo
bar
b.dllhoge
fuga

これがどのようにバイナリに落とされているかを見ていきます。

構造

.idataの大まかな構造は以下の通りです。

名称 対応
IDT (Import Directory Table) IMAGE_IMPORT_DESCRIPTOR DLL
ILT (Import Lookup Table) DWORD シンボル
IAT (Import Address Table) DWORD サンク
Hint/Name Table IMAGE_IMPORT_BY_NAME 序数・文字列(シンボル)
DLL Name char 文字列

文字列データは後ろの方に並んでおり、IDTやILTにはオフセットが格納されています。

IDT

参照されるDLLの数だけIMAGE_IMPORT_DESCRIPTORがあり、その後に終端としてゼロで埋められたIMAGE_IMPORT_DESCRIPTORが来ます。

フィールドにはILT/DLL名/IATへのポインタが格納されます。それ以外のフィールドは必須ではないため説明を省略します(ゼロのままでも動きます)。

ポインタの値はモジュールの先頭からの相対アドレスです。仕様書ではRVA(相対仮想アドレス)と呼ばれます。例えばEXEが0x00400000に配置され、.idataが0x00402000に配置された場合、.idataのRVAは0x2000となります。

例: (nasm形式)

org 0x2000
IDT_A_DLL: dd ILT_A_DLL, 0, 0, A_DLL, IAT_A_DLL
IDT_B_DLL: dd ILT_B_DLL, 0, 0, B_DLL, IAT_B_DLL
IDT_LAST : dd         0, 0, 0,     0,         0

ILT

DLLごとに、参照されるシンボルの数のH/N Tableへのポインタと終端のヌルポインタがあります。

例:

ILT_A_DLL: dd HN_foo , HN_bar , 0
ILT_B_DLL: dd HN_hoge, HN_fuga, 0

ILTは必須ではなく、なくても動きます。ILTがない場合、実行時にメモリ上のILTを参照してH/N Tableを追うことができなくなります。 → PEの.idataを図解(ILTなし)

IAT

ILTとまったく同じです。サンクとして扱われ、実行時にWindowsによって値が書き換えられます。

例:

IAT_A_DLL:
  imp_foo:  dd HN_foo
  imp_bar:  dd HN_bar
            dd 0
IAT_B_DLL:
  imp_hoge: dd HN_hoge
  imp_fuga: dd HN_fuga
            dd 0
呼び出し方

プログラムから以下のように呼び出します。

call [imp_foo]
call [imp_bar]
call [imp_hoge]
call [imp_fuga]

C言語の場合、ヘッダで__declspec(dllimport)として明示されていなければ、このようなコードは生成できません。対策として、リンク時にトランポリンを生成する方法があります。

foo:  jmp [imp_foo]
bar:  jmp [imp_bar]
hoge: jmp [imp_hoge]
fuga: jmp [imp_fuga]

トランポリンがあれば、通常の関数と同様に呼び出すことができます。

call foo
call bar
call hoge
call fuga

Hint/Name Table

シンボルの序数と名前が入っています。両方を指定する必要はなく、どちらか片方だけでインポートできます。文字列のルックアップ時間を節約するため、Windows CEでは序数のみでインポートすることが多いようです。今回の例では名前だけを指定します。

2バイトでalignする必要があります。

例:

HN_foo:
  dw 0
  db "foo", 0
  align 2

HN_bar:
  dw 0
  db "bar", 0
  align 2

HN_hoge:
  dw 0
  db "hoge", 0
  align 2

HN_fuga:
  dw 0
  db "fuga", 0
  align 2

DLL Name

ヌル終端の文字列を並べるだけです。

例:

A_DLL: db "a.dll", 0
B_DLL: db "b.dll", 0

図解

例をつなげてアセンブルすると以下のようなバイナリとなります。これが.idataです。

生成

.idataの各ブロックを別々に作りながらRVAを埋め込んで、最後に連結してアドレスを解決すると、ほぼ仕様のまま生成できます。

http://ideone.com/c85psd

Buffer create() {
  Buffer idt, ilt, iat, hn, name;
  uint32_t zero = 0;
  for (auto dll = begin(); dll != end(); ++dll) {
    idt << ilt.rva() << zero << zero << name.rva() << iat.rva();
    name << dll->first;
    for (auto sym = dll->second.begin(); sym != dll->second.end(); ++sym) {
      iat.put(sym->second);
      ilt << hn.rva();
      iat << hn.rva();
      hn << uint16_t(0) << sym->first;
      if (hn.size() & 1) hn << uint8_t(0);
    }
    ilt << zero;
    iat << zero;
  }
  idt.expand(20);
  return Buffer() << idt << ilt << iat << hn << name;
}

仕様書

以上の説明は私が分かりやすいと思う形で再構成したものです。

正式な仕様はマイクロソフトで公開されている仕様書を参照してください。