PEの.idataをアセンブリ言語で記述しながら説明します。
PEからDLLを参照するには.idataセクションにDLL名とシンボル名を記述します。実行時にWindowsのローダが.idataを見てLoadLibraryとGetProcAddressに相当する処理を行い、取得したアドレスを.idataに書き込みます。アドレスが書き込まれる領域はサンクと呼ばれており、あらかじめ.idataに用意しておきます。プログラムはサンクに書き込まれたアドレスをcallすることでDLL内の関数を呼べます。
例:
DLL | シンボル | サンク |
---|---|---|
a.dll | foo | |
bar | ||
b.dll | hoge | |
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の各ブロックを別々に作りながらRVAを埋め込んで、最後に連結してアドレスを解決すると、ほぼ仕様のまま生成できます。
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; }
アドレスの決定を遅延させるのにラムダ式を使おうとして試行錯誤したけど、結局アドレスをスマートポインタで共有する方法に落ち着いた。idataの構築が簡単に記述できた。(IData::create) ideone.com/c85psd
— 七誌さん (@7shi) 11月 23, 2012