Windowsの実行ファイル形式(6)

ちょっとネタに詰まってきました。ゲーム系で私がわかるネタならかけるんですが・・・。

ネタ募集中?にしておきます。

今回はIMAGE_DIRECTORY_ENTRY_IMPORT(DLLのインポート情報)に関するネタです。

この部分はEXEファイルやDLLファイルでも改造(というより乗っ取り)によく利用される部分なので知っておくと便利かもしれません。

型名とメンバ名で記述しますが、使用するはじめの情報は

IMAGE_NT_HEADERS.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]

の部分です。このデータで示されたメモリ(あるいはファイルデータ)には外部のDLLを実行ファイルがどのように使用しているかを示すデータがあります。

この部分で関連している構造体はこんなものです。定義は一部省略していたりする(unionを削ったりしている)ので完全に同じものではありません。

typedef struct _IMAGE_IMPORT_DESCRIPTOR {
	DWORD OriginalFirstThunk;
	DWORD TimeDateStamp;
	DWORD ForwarderChain;
	DWORD Name;
	DWORD FirstThunk;
} IMAGE_IMPORT_DESCRIPTOR;
typedef struct _IMAGE_IMPORT_BY_NAME {
	WORD Hint;
	BYTE Name[1];
} IMAGE_IMPORT_BY_NAME;
typedef struct _IMAGE_THUNK_DATA64 {
	union {
		ULONGLONG Ordinal;
		ULONGLONG AddressOfData;
	} u1;
} IMAGE_THUNK_DATA64;
typedef struct _IMAGE_THUNK_DATA32 {
	union {
		DWORD Ordinal;
		DWORD AddressOfData;
	} u1;
} IMAGE_THUNK_DATA32;
#ifdef _WIN64
typedef IMAGE_THUNK_DATA64 IMAGE_THUNK_DATA;
#else
typedef IMAGE_THUNK_DATA32 IMAGE_THUNK_DATA;
#endif

IMAGE_IMPORT_DESCRIPTORの中で使用するメンバは次の3つくらいです。

  • Name

このインポート情報で読み込まれる外部DLLのDLL名へのアドレスがImageBase相対位置で示されます。

ただし、このDLL名は必ずCHAR型(UNICODEではなくASCIIコード)で示されているのでLoadLibraryに渡すときは必要に応じて変換する必要があります。

  • OriginalFirstThunk

このインポート情報で読み込まれる対象の外部DLLに存在すると思われるモジュール名へのアドレス(の先頭アドレス)がImageBase相対位置で示されるか序数そのものが示されます。

この情報は下に記述するFirstThunkの情報と一対となっています。このデータが0であるとき、インポート情報はこのデータで終端であることを示します。

このアドレスの先のデータはIMAGE_THUNK_DATAの配列の状態と同じです。

  • FirstThunk

OriginalFirstThunkが示しているモジュールの呼び出し先アドレスを保存するためのメモリ領域へのアドレス(の先頭アドレス)がImageBase相対位置で示されます。

実行ファイルが外部DLLのモジュールを呼び出すときの踏み台となるアドレスの内部仮想メモリの保存場所になります。

このアドレスの先のデータはDWORD_PTR(FARPROC *)の配列の状態と同じです。

そのほかの構造体の説明はちょっと省きます。とんでもなくわかりづらい説明ですが、実コードをみた方が早いでしょう。

kernel32のLoadLibrary関数などでDLLを呼び出すときのアドレスの解決手順の内部動作(例)です。

void *lpDLLImage; IMAGE_NT_HEADERS *lpImage; IMAGE_IMPORT_DESCRIPTOR *iid;
IMAGE_THUNK_DATA *lpThunkData; IMAGE_IMPORT_BY_NAME *lpImportByName;
FARPROC *lpfnThunk; LPSTR lpName; HMODULE hDLLModule; DWORD dwAdr;
・・・・
//lpDLLImageにはDLLが展開されたメモリの先頭アドレス、lpImageにはそのDLLのIMAGE_NT_HEADERSのアドレスが入っているものとする
dwAdr = lpImage->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress;
iid = (IMAGE_IMPORT_DESCRIPTOR *)((BYTE *)lpDLLImage + (DWORD_PTR)dwAdr);
for(;iid->OriginalFirstThunk != 0;iid++){
	lpName = (LPSTR)((BYTE *)lpDLLImage + (DWORD_PTR)iid->Name);
	hDLLModule = LoadLibraryA(lpName);
	if(hDLLModule != NULL){
		lpfnThunk = (FARPROC *)((BYTE *)lpDLLImage + (DWORD_PTR)iid->FirstThunk);
		lpThunkData = (IMAGE_THUNK_DATA *)((BYTE *)lpDLLImage + (DWORD_PTR)iid->OriginalFirstThunk);
		for(;lpThunkData->AddressOfData != 0;lpThunkData++,lpfnThunk++){
			if(IMAGE_SNAP_BY_ORDINAL(lpThunkData->Ordinal)) lpName = (LPSTR)IMAGE_ORDINAL(lpThunkData->Ordinal);
			else{
				lpImportByName = (IMAGE_IMPORT_BY_NAME *)((BYTE *)lpDLLImage + lpThunkData->AddressOfData);
				lpName = (LPSTR)(&(lpImportByName->Name[0]));
			}
			*lpfnThunk = GetProcAddress(hDLLModule,lpName);
			if(*lpfnThunk == NULL) return FALSE;
		}
	}
}

即席で作ったのでコードミスがあるかもしれませんが・・・。

ここで鍵となるのは

lpName = (LPSTR)((BYTE *)lpDLLImage + (DWORD_PTR)iid->Name); (1)
*lpfnThunk = GetProcAddress(hDLLModule,lpName); (2)

の動作です。(1)の部分は読み込むべきDLL名がどこにあるのかを示しているので、ファイル上のこの位置の情報を書き換えるとDLLの読み取りを自分が指定したDLLで乗っ取れるようになります。

また、(2)の部分はlpNameで示されたモジュールに対して呼び出すアドレスをこの動作で確定させている訳なので、

この部分で別のアドレスを代入してやれば自分の思ったモジュールを呼び出すことができる(つまりモジュールを乗っ取れる)訳です。

もちろん、この辺の構造体の説明はあまりない(VisualStudioについてくるヘルプにはありませんでした)ので注意を。

この記事を書いているときにちょっとwinnt.hをのぞきながら書いたわけですが、なんか便利な定義がうろうろしていますね・・・。

書いている途中に定義を更新したくらいです・・・。気がつかなかった自分が悪いですが。

次回は続きになるのか新しい記事のために充電するのかはよくわかりませんが。とりあえずここまでということで。


コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

この記事のトラックバック用URL