マニュアルアンパックのIAT再構築を手動でやった話


この記事は、Aizu Advent Calendar 2016の11日目の記事です。
前日の人は@RomTinさんで、翌日の人は@je_je_laさんです。

最近マニュアルアンパックに挑戦したので、その話をします。
IAT(Import Address Table)再構築に着目した記事なので、パックが何かとかそういう話は省きます(というか色々省きます)。
使用するパッカーはupx(ver 3.03)で、x86でビルドしたPEファイルを使用します。
使っているデバッガはOllyDbg1.10OllyDumpというプラグインを適用したものです

マニュアルアンパックのフロー

upxはざっくり以下の流れでアンパックします。

今回は赤枠の話です。
それ以前の部分に関しては、ググるなり参考サイトを見るなりしてください。

ダンプまで

とりあえずhello worldプログラムを用意してパックします。
ollyDbgでOEP(Original Entry Point)以降をollyDumpでメモリダンプします。


自分でIATを再構築するので、Rebuild Importのチェックは外します。
この例では、OEPのオフセットは0x1580なのでmodifyを1580にします。

IATについて

ダンプしたPEファイルをそのまま実行しようとすると、以下のようなエラーが出ると思います。

これはIATが不正な値のため起こるエラーです。

IATとは、PEファイルがインポートする関数の情報が含まれたテーブルです。
実行される前には、PEファイルが持っている関数名へのポインタを持っていて、イメージがロードされたあとは関数の実アドレスが解決され、IATは実アドレスで上書きされます。この関数名へのポインタはIMAGE_IMPORT_BY_NAMEと言うものです。
OEPまで進めてメモリをダンプするとIATが実アドレスを持ったままになっています。本来は対応するIMAGE_IMPORT_BY_NAMEを持っていなければいけないので、そのまま実行しようとするとエラーになってしまいます。なので、IATを再構築しないとアンパックが完了しないのです。

IATの再構築のやり方

IATの再構築は、ImpRECスペシャルねこまんま57号などのツールで簡単に行うことができます。

今回はツールに頼らずデバッガとバイナリエディタでのIATの再構築をします。

まずは、ダンプしたPEファイルのIATを探し出します。
そのためにまず、インポートディスクリプタのRVA(Relative Virtual Address)の値を取得します。
この値はNTヘッダが持つオプショナルヘッダが持っています。NTヘッダは0x50 0x40 0x00 0x00(asciiで"PE")から始まる248バイトのバイナリ列です。オプショナルヘッダはNTヘッダの25バイト目からです。インポートディスクリプタのRVAはオプショナルヘッダの105バイト目からの4バイトです。
以下の図の赤で囲った部分がオプショナルヘッダで、青で囲った部分がインポートディスクリプタのRVAです。

つまり、この例だと0x91dcです。

次に、各セクションのVirtualAddressとPointerToRawDataを参照します。これはセクションヘッダと呼ばれる40バイトの情報に含まれます。upxでパックした場合UPX0、UPX1、.rsrcの3つのセクションができますので、その各セクションから情報を取得します。VirtualAddressはセクションヘッダの13バイト目からの4バイトで、PointerToRawDataは最後の4バイトです。
以下の図はUPX0のVirtualAddress(青)とPointerToRawData(緑)を示しています。赤はセクションヘッダ全体です。

つまり、UPX0のVirtualAddressは0x1000で、PointerToRwoDataも0x1000ということになります。

この要領3つのセクションの値を取得します。

セクション/値 VirtualAddress PointerToRawData
UPX0 0x1000 0x1000
UPX1 0x7000 0x7000
.rsrc 0x9000 0x9000

インポートディスクリプタのRVAは0x91dcなので.rsrcの中に属しているはずです。
インポートディスクリプタのオフセットは以下の式で計算できます。
セクションのPointerToRawData + (インポートディスクリプタのRVA - セクションのVirtualAddress)
今回だと、0x9000 + (0x91dc - 0x9000) = 0x91dcとなります。
ファイル先頭からオフセット分移動した場所にインポートディスクリプタがあるわけですが、この中の先頭から17バイト目から始まる4バイトのFirstThunkという値が、IATのオフセットになります。
以下の図の青で囲った部分つまり0x9290がIATのオフセットです。赤はインポートディスクリプタを囲っています。

以下の赤で囲った部分がIATです。

こういったPEファイルの情報はPEiDPEViewなどを使用すれば簡単に取得できます。

さて、4バイトずつ関数の情報が入っているのですが、現状だと実アドレスが入ってしまっていて実行ができない状態です。なのでIMAGE_IMPORT_BY_NAMEに書き換えてあげましょう。
すぐ下に、IMAGE_IMPORT_BY_NAMEが固まっている場所があるので、その中から必要なものを取得します。
ですがその前に、どこをどの名前にしてやればいいのかわからないので、デバッガでどのアドレスがどの関数に対応してるかを見に行く必要があります。
では、最初の4バイトの0x76d38500を例に書き換えを行います。
ollydbgでOEPまで実行した状態にし、ViewからExecutable modulesを選択します。
すると各モジュールの情報が出るかと思います。


この中から、関係ありそうなBase addressを持つモジュール(今回だと0x76d10000のKERNEL32)のところでCtrl+Nもしくは右クリックのメニューからView namesを選択。
するとエクスポート(もしくはインポート)している関数一覧が出てくるのでその中から合致するアドレスを探します。
Addressと書いてあるところをクリックしてアドレスでソートしてやると見やすくなります。

この図の灰色でハイライトしているところが、0x76d38500という実アドレスにロードされたLoadLibraryAという関数を指しています。

さてIATのアドレスをLoadLibraryAのIMAGE_IMPORT_BY_NAMEへ変更してあげます。
ここで気をつけるべきは、IMAGE_IMPORT_BY_NAMEは以下の形をした構造体のため、直接文字列の先頭アドレスを指してしまうとうまく行きません。

typedef struct {
    WORD Hint;
    BYTE Name[1];
} IMAGE_IMPORT_BY_NAME;

WORD型の分、つまり2バイト前のアドレスを指して上げる必要があります。
今回の例だと、以下の赤で囲った部分がLoadLibraryAという文字列 + 2バイトなので、先頭アドレスは0x93b0になります。

この値に書き換えてやれば良いわけです。(以下の図の緑)

この調子で全部書き換えます。

これで実行できるようになったかと思います。

IATの手動再構築は以上です。

おわりに

今回は、手動でIATの再構築を行う記事でした。
upxを例に出していますが、他のパッカーでも同様にできるのではないかなと思っています(やっていない)。パッカーの種類で変わってくるのは、主にアンパックのルーチンで、OEPにたどり着けばあとは同じはずなので。

自分もやってみて間もないので、間違っているところもあるかと思います。その場合は容赦なく刺してください。
また、この記事は色々省いています。特に、PEファイルフォーマット周りの話はろくに触れず、雑にバイト数だけ示しています。
PEファイルフォーマットはかなり複雑で、1つの記事で理解できるものではないし僕もわかっているわけではないので。

IATの再構築は人の手でやるべき作業ではないので、よっぽどのことがない限りアンパックはツールに頼りまくりましょう。

参考文献

アンパッキング(IAT再構築を手動で)
UPXマニュアルアンパック 2/3
UPXにおけるIATのマニュアル再構築 【分析編】
IAT:インポートアドレステーブルについて
PEFormat.pdf
インポートアドレステーブルと API フック
PEファイルの構造 - インポートアドレステーブル - IAT
PE(Portable Executable)ファイルフォーマット その2 ~ インポート情報とインポート関数の列挙 ~
PE(Portable Executable)ファイルフォーマットの概要