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

Log.i53

Themidaのアンパックを目指すブログ改め使い物になるえんじにゃを目指すブログ

IAT:インポートアドレステーブルについて

 そもそもIATってなんぞ
f:id:i53:20150702224157j:plain

 現状アンパック作業におけるIATの再構築はOllyDumpやImpRecなどのツールアシストで行っていましたがツールが利用できなかったとしたら分析者はマニュアルでこれを再構築する必要が生じます。IATのマニュアル再構築のためにまずIATについて理解しておく必要があります。またIATフックなどのテクニックを利用する場合にもIATの構造を理解しておく必要があります。自身の理解のためにIATについて簡単にまとめました。

# 簡単化のためにRVA(Relative Virtual Address)という単語はすべて(イメージベースからの/仮想アドレスにおける)オフセットという言葉に置き換えています

IAT(Import Address Table)

 Import Address Table(インポート・アドレス・テーブル)は、アプリケーションのファイル構造体(PEフォーマット)に配置されるコールテーブルです。IATには特定のDLLによってインポートされたルーチンの開始アドレスが格納されています。アプリケーションにリンクされた各DLLは、そのロード時に独自のIATを持っています。これを理解することすなわちPEフォーマットを理解しなければならないということが言えます。

PE(Portable Executable)フォーマット

 PEフォーマットは、32ビット及び64ビット版のMicrosoft Windows上で使用される実行ファイルのファイルフォーマットです。このフォーマットは単に幾重にもネストされた構造体の集合として見ることができます。PEフォーマットの各構造体や定数について、Microsoft Platform SDKに含まれるヘッダファイル、winnt.h内に定義されているので詳細についてはそちらを参照するか、以下のリンクの概要図が非常に参考になります:
PE Format Poster.graffle - OpenRCE(PDF)
 

IATへのアクセス

 PEフォーマットを理解している前提で実行ファイルからIATを参照するための概要図を以下に示します:
f:id:i53:20150717191507p:plain

実際に確認してみる

 notepad.exe(SHA-1:110A5DA8905B0B0C261411C7455A22F5E6BEF8F6)のバイナリを実際に確認してみます。

 DOSヘッダーの内容は以下のとおりです。e_lfanew(offset:0x3C)の値を参照するとNTヘッダーのファイル先頭からのオフセットは0xE0であることが分かります:
f:id:i53:20150717190747p:plain

 NTヘッダーの内容は以下のとおりです。Signature(4バイト)、FileHeader(20バイト)、OptionalHeader(224バイト)で構成されています:
f:id:i53:20150717193644p:plain 

 オプショナルヘッダーのデータディレクトリ(8バイトで構成される)の配列はオプショナルヘッダーのオフセット0x60から始まります。また、IMAGE_DIRECTORY_ENTRY_IMPORTは配列の2番目の要素を指すインデクスです。従って、以下の赤枠部分がインポートディレクトリの内容となります:
f:id:i53:20150717195356p:plain

 この例であればインポートディスクリプタの仮想アドレスが0x7604であり、そのサイズは0xC8バイトであることが分かります。では、ファイル先頭からオフセット0x00007604に移動してみると…
f:id:i53:20150717202358p:plain

 WTF?! 明らかにインポートディスクリプタではない別のところ(関数の名前テーブルの途中)を参照しています。

 この理由ですが、実はこのオフセットはファイルベースからのオフセットを指すのではなく、PEローダーがPEファイルをメモリマップした後のイメージベースからのオフセット(=仮想アドレス)を指しているからです。PEファイルにおけるインポートディスクリプタのオフセットを把握するためにはセクションヘッダ構造体のパラメータを利用して簡単な計算を行う必要があります。

 では、各セクションのセクションヘッダを参照してみましょう。セクションヘッダは40バイトで構成されており、PEヘッダの後にファイルヘッダのnumberOfSectionの数だけ続いています。この場合セクション数は3つで、.textセクション、.dataセクション、.rsrcセクションの順に並んでいることが分かります:
f:id:i53:20150717220330p:plain

 PEファイルにおけるIATのオフセットを知るために重要になるのがセクションヘッダにおけるVirtualSize, VirtualAddress, PointerToRawDataフィールドです。

 VirtualAddressフィールドはセクションの仮想アドレスであり、PEローダーがセクションをメモリにマッピングする際にこのフィールドの値を使用します。したがって、この値が0x1000でPEファイルが0x400000にロードされている場合、セクションは0x401000からロードされることになります。

 VirtualSizeフィールドはセクションデータの実際のサイズです。このサイズはディスク上のセクションデータのサイズ(PointerToRawData)より小さくなる可能性があり、これはPEローダーがメモリを割り当てる際のサイズとして利用されます。

 PointerToRawDataフィールドはPEローダーがセクション内のデータを読み出すために使用されるものです。すなわちセクションデータのファイルベースからのオフセットとなります。

 各セクションのVirtualSizeVirtualAddressPointerToRawDataをハイライトしたものを以下の図に示します:
f:id:i53:20150718020625p:plain

 .textセクションはVirtualSize:0x7748, VirtualAddress:0x1000, PointerToRowData:0x0400、.dataセクションはVirtualSize:0x1BA8, VirtualAddress:0x7C00, PointerToRowData:0x0900、.rsrcセクションはVirtualSize:0x8368, VirtualAddress:0xB000, PointerToRawData:0x8400です。

 まずは、各DLLのインポートディスクリプタがどのセクションに含まれているのかVirtualAddressとVirtualSizeから計算します。この例では.textセクションがメモリにロードされる場合、ロードベースからオフセット0x1000の位置よりVirtualSize:0x7748バイト分(0x1000 ~ 0x8748まで)メモリが割り当てられることになります。1つ目のインポートディスクリプタの仮想アドレスは0x00007604だったので、このインポートディスクリプタは.textセクションに含まれているということが分かります。

 では、PEファイルにおける.textセクションのデータとメモリマップ後の.textセクションのデータの対応を以下の図に示します:
f:id:i53:20150718233052p:plain

 この対応から元のPEファイルにおけるインポートディスクリプタのオフセットを逆算します。まず、メモリマップ後のセクション内におけるインポートディスクリプタのオフセットは仮想アドレスの差分(ImportDirectory.VirtualAddress - VirtualAddress)から0x6604となります。そして元のファイルにおいてセクションが開始されるオフセットはPointerToRawDataから参照できます。つまり、このファイル内に置けるインポートディスクリプタのオフセットはPointerToRowDataに先ほどの差分結果を加算したものになります。よって、インポートディスクリプタのファイルにおけるオフセットは0x400 + 0x6604 = 0x6A04となります。

 では、0x6A04からインポートディスクリプタ(1つ目のDLL)の内容を参照していきます。IATのオフセットにアクセスするにはFirstThunkを参照します。インポートディスクリプタのFirstChunkはオフセット0x10から始まるは最後のDWORD値であり、この場合は0x12C4となります:
f:id:i53:20150718042458p:plain

 こちらも仮想アドレスですのでまたファイルオフセットを計算する必要があります。仮想アドレス0x12C4なのでこれは.textセクションに含まれていることが分かります。仮想アドレスの差分が0x12C4 - 0x1000 = 0x2C4であり、PointerToRawDataが0x400なので、ファイルにおけるオフセットは0x400 + 0x2C4 = 0x6C4となります。

 オフセット0x6C4に移動してみるとcomdlg32.dllからインポートしている関数のコールテーブルを確認することができます:
f:id:i53:20150718231459p:plain

 IATとは別にインポート関数の名前を格納するILTという構造も存在します。こちらはインポートディスクリプタのOriginalFirstThunkフィールドから参照されるTHUNK_DATA構造体のAddressOfDataフィールドから、IMAGE_IMPORT_BY_NAME構造体のNameフィールドを参照することによりDLLのルーチン名を取得することができます。

 PEフォーマットの概要やその他の詳細については以下のリンクを参考にして下さい。