2007年02月02日
Windowsに土足で乱入?! 〜 API フックのための予備知識
■ はじめに
前回の記事では Windows のイベントをフックする方法についてお話しましたが、特定の Windows API 呼び出しををフックするにはどうすればよいでしょう?
それを考えるためには、まずどのような仕組みでプログラムが API を呼び出だしているのかを調べておく必要がありそうです。
ご存知の通り、Windows API は、所定の Windows プログラムから呼び出すことの可能なエクスポート関数として OS 環境の DLL(Dynamic Link Library) 群に格納されています。そして、あるプログラムの実行に必要な API を含む DLL は、実行ファイル起動時の初期化時にプロセスへリンクされ、その後は所定の API を透過的に呼び出すことができるようになります。
DLL をプロセスへリンクするには、実行形式をビルドする際に所定のインポートライブラリをリンクしておく方法(暗黙のリンク)と、コード上ででLoadLibrary系の API を使用する方法(明示的なリンク)がありますが、前者の場合、必要な DLL + API の情報は「インポート情報」として実行ファイル内に記述されています。
実行ファイル内のインポート情報は Microsoft VisualStudio に付属するdumpbin.exeユーティリティ等で参照することができますが、この情報の実体はどういう形をしているのでしょう?また、どうすればそこへアクセスすることができるでしょう?
今回はこの点について考えてみることにしましょう。
前回の記事では Windows のイベントをフックする方法についてお話しましたが、特定の Windows API 呼び出しををフックするにはどうすればよいでしょう?
それを考えるためには、まずどのような仕組みでプログラムが API を呼び出だしているのかを調べておく必要がありそうです。
ご存知の通り、Windows API は、所定の Windows プログラムから呼び出すことの可能なエクスポート関数として OS 環境の DLL(Dynamic Link Library) 群に格納されています。そして、あるプログラムの実行に必要な API を含む DLL は、実行ファイル起動時の初期化時にプロセスへリンクされ、その後は所定の API を透過的に呼び出すことができるようになります。
DLL をプロセスへリンクするには、実行形式をビルドする際に所定のインポートライブラリをリンクしておく方法(暗黙のリンク)と、コード上ででLoadLibrary系の API を使用する方法(明示的なリンク)がありますが、前者の場合、必要な DLL + API の情報は「インポート情報」として実行ファイル内に記述されています。
実行ファイル内のインポート情報は Microsoft VisualStudio に付属するdumpbin.exeユーティリティ等で参照することができますが、この情報の実体はどういう形をしているのでしょう?また、どうすればそこへアクセスすることができるでしょう?
今回はこの点について考えてみることにしましょう。
■ Windows 実行ファイルについて
EXE, DLL 等の Windows 実行形式(PE: Portable Executable)の先頭部分は以下の構成を持ちます。
"IMAGE_xxx" はいずれも WINNT.h ヘッダファイルに定義のある構造体です。この内、API のインポート情報は IMAGE_OPTIONAL_HEADER 構造体の DataDirectory メンバの保持するアドレス情報から牽引することができます。
■ システムローダの役割
物理ストレージ上のバイナリファイルである実行ファイルがひとたび「起動」されると、Windows のシステムローダは次の仕事を行います。
1. 新しいプロセスのために仮想アドレス空間を作成する。
2. 実行ファイルを仮想アドレス空間へモジュールとしてマッピングする。
(実行形式のバイナリをそのままの形でマッピングするのではなくどのセクションをどのオフセットへアサインするかも併せて判断)
3. モジュールに含まれるインポート情報の分析を行い、必要な DLL をプロセスのアドレス空間へモジュールとしてマッピングする。 さらに、マッピングされた DLL モジュールに含まれるインポート情報についても同様の手順を踏む。
4. モジュールの必要とするインポート API が所定の DLL から実際にエクスポートされているか順次チェックを行い、モジュールのインポート情報セクションに含まれるインポートアドレステーブルへ API のアドレスを上書きする(※1)。
(※1) bind.exe ユーティリティ等による「バインド」操作を経てあらかじめインポートアドレステーブルの正規化された実行形式が、実行形式自身の期待する条件を満たす状態でロードされた場合にはこの手順は省略されます
一連の処理の中でエラーを検知するとローダはエラーメッセージを表示します。
すべてが成功すればプログラムは正常に開始され、解決済みのインポート情報を参照することにより所定の API へアクセスできるようになります。
■ RVA (Relative Virtual Addresses) について
EXE,DLL等の実行形式ファイルがプロセスのアドレス空間へマッピングされる際には「好ましいアドレス」が存在します。しかし、実際にそこへロードされるとは限りません。そのため、実行形式ファイル内での所定のセクションの指定には絶対アドレスではなく仮想アドレスが使用されます。このアドレス表記のことを RVA (Relative Virtual Addresses) といいます。
これは単純にロードアドレス先頭からのオフセット情報であり、RVA 値にモジュールのロードアドレスを加算することで所定の絶対アドレスを求めることができます。
ちなみに、Windows プログラミングでの「HMODULE」は、ロードされた所定のモジュールの先頭アドレスを指すものです。
■ インポート API 情報へのアクセス
ロードずみモジュール内でインポート API 情報を参照する方法を以下に示します。
構造体ブロックの単純なリンクをだどればよいことがわかります。
構造体メンバの内特に重要なものは図中に記述していますが、詳細は WINNT.h を参照して下さい。
■ インポートアドレステーブル(IAT)とインポートネームテーブル(INT)
上図の通り、IMAGE_IMPORT_DESCRIPTOR の OriginalFirstThunk メンバと FirstThunkメンバはそれぞれ INT と IAT を示します。
これらはいずれも Name メンバで示されるモジュールに含まれる所定の API ごとに一要素を構成する配列情報であり、同一の要素番号を持つエントリは同一のインポートAPI についての情報を保持しています。
リンクずみの実行形式ファイルの上では、FirstThunk は OriginalFirstThunk と同一の情報を保持しています(※2)が、「システムローダの役割」項で触れた通り、実行時にはローダにより API アドレス情報に書き換えられます。
(※2) bind.exe ユーティリティ等でバインド操作ずみのバイナリでは異なります
なお、INT の IMAGE_THUNK_DATA エントリの最上位ビットが立っていればそのエントリは API 名情報を持つ IMAGE_IMPORT_BY_NAME への RVA ではなく、当該インポート DLL上の序数情報であることを示しており、ここから最上位ビットを落とした値が実際の序数です。
名前・序数のいずれも、ローダによる API アドレス解決の際のシンボルとして有効です。
■ コードを書いてみる
最後に、ここまでの知識を元に、指定された実行ファイルのインポート API 情報の一覧を出力するサンプルコードを C 言語で書いてみることにします。
なお、最も重要な IMAGE_IMPORT_DESCRIPTOR テーブルのアドレスの取得は、ImageDirectoryEntryToData() API を使うことで簡単に実現できます。
<ImageDirectoryEntryToData>
http://msdn.microsoft.com/library/ja/jpdllpro/html/_win32_ImageDirectoryEntryToData.asp
実行時の出力例
次回は、所定の API を参照するコードが実行形式イメージにおいてどのように表現されているかを調べてみることにします。
■ 参考文献
・An In-Depth Look into the Win32 Portable Executable File Format
by Matt Pietrek
・Advanced Windows 改訂第4版 (Microsoft Press)
by Jeffrey Richter, 長尾 高弘 訳
(株)アスキー刊 ISBN-13: 978-4756138057
EXE, DLL 等の Windows 実行形式(PE: Portable Executable)の先頭部分は以下の構成を持ちます。
┌────────────────┐
│ MS-DOS互換用ブロック │
│ ┏━━━━━━━━━━━━┓ │
│ ┃IMAGE_DOS_HEADER────┨ │
│ ┃ │e_lfanew┃e_lfanew が
│ ┣━━━━━━━┷━━━━┫ │ IMAGE_NT_HEADERS先頭アドレスを指す
│ ┃DOS用スタブコード ┃ │
│ ┗━━━━━━━━━━━━┛ │
│ イメージヘッダ (IMAGE_NT_HEADERS)
│ http://msdn2.microsoft.com/en-us/library/ms680336.aspx
│ ┏━━━━━━━━━━━━┓ │
│ ┃シグニチャ(DWORD) ┃"PE" 0x00 0x00
│ ┣━━━━━━━━━━━━┫
│ ┃IMAGE_FILE_HEADER ┃モジュールの基本情報
│ ┣━━━━━━━━━━━━┫
│ ┃IMAGE_OPTIONAL_HEADER ┃各種アドレス・サイズ・バージョン情報
│ ┃ ┌──────────┨
│ ┃ │IMAGE_DATA_DIRECTORY┃データディレクトリ
│ ┗━┷━━━━━━━━━━┛ │
│ セクションテーブル │
│ ┏━━━━━━━━━━━━┓ │
│ ┃IMAGE_SECTION_HEADER ┃ │
│ ┣━━━━━━━━━━━━┫ │
│ ┃ : ┃ │
: :
"IMAGE_xxx" はいずれも WINNT.h ヘッダファイルに定義のある構造体です。この内、API のインポート情報は IMAGE_OPTIONAL_HEADER 構造体の DataDirectory メンバの保持するアドレス情報から牽引することができます。
■ システムローダの役割
物理ストレージ上のバイナリファイルである実行ファイルがひとたび「起動」されると、Windows のシステムローダは次の仕事を行います。
1. 新しいプロセスのために仮想アドレス空間を作成する。
2. 実行ファイルを仮想アドレス空間へモジュールとしてマッピングする。
(実行形式のバイナリをそのままの形でマッピングするのではなくどのセクションをどのオフセットへアサインするかも併せて判断)
3. モジュールに含まれるインポート情報の分析を行い、必要な DLL をプロセスのアドレス空間へモジュールとしてマッピングする。 さらに、マッピングされた DLL モジュールに含まれるインポート情報についても同様の手順を踏む。
4. モジュールの必要とするインポート API が所定の DLL から実際にエクスポートされているか順次チェックを行い、モジュールのインポート情報セクションに含まれるインポートアドレステーブルへ API のアドレスを上書きする(※1)。
(※1) bind.exe ユーティリティ等による「バインド」操作を経てあらかじめインポートアドレステーブルの正規化された実行形式が、実行形式自身の期待する条件を満たす状態でロードされた場合にはこの手順は省略されます
一連の処理の中でエラーを検知するとローダはエラーメッセージを表示します。
すべてが成功すればプログラムは正常に開始され、解決済みのインポート情報を参照することにより所定の API へアクセスできるようになります。
■ RVA (Relative Virtual Addresses) について
EXE,DLL等の実行形式ファイルがプロセスのアドレス空間へマッピングされる際には「好ましいアドレス」が存在します。しかし、実際にそこへロードされるとは限りません。そのため、実行形式ファイル内での所定のセクションの指定には絶対アドレスではなく仮想アドレスが使用されます。このアドレス表記のことを RVA (Relative Virtual Addresses) といいます。
これは単純にロードアドレス先頭からのオフセット情報であり、RVA 値にモジュールのロードアドレスを加算することで所定の絶対アドレスを求めることができます。
ちなみに、Windows プログラミングでの「HMODULE」は、ロードされた所定のモジュールの先頭アドレスを指すものです。
■ インポート API 情報へのアクセス
ロードずみモジュール内でインポート API 情報を参照する方法を以下に示します。
構造体ブロックの単純なリンクをだどればよいことがわかります。
構造体メンバの内特に重要なものは図中に記述していますが、詳細は WINNT.h を参照して下さい。
┏━━━━━━━━━━━━┓
┃IMAGE_OPTIONAL_HEADER ┃
┃ ┌──────────┨
┃ │IMAGE_DATA_DIRECTORY╂─┐
┗━┷━━━━━━━━━━┛ │RVA
┌─────────────┘
↓データディレクトリ配列
┏━━━━━━━━━━━━┓
┃IMAGE_DATA_DIRECTORY[0] ┃
┣━━━━━━━━━━━━┫(←#define IMAGE_DIRECTORY_ENTRY_IMPORT 1)
┃IMAGE_DATA_DIRECTORY[1] ╂─┐
┣━━━━━━━━━━━━┫ │RVA
: │
┣━━━━━━━━━━━━┫ │
┃IMAGE_DATA_DIRECTORY[15]┃ │
┗━━━━━━━━━━━━┛ │
┌────────────┘
↓インポートデスクリプタ:インポートモジュール単位の配列
┏━━━━━━━━━━━━━┓
┃IMAGE_IMPORT_DESCRIPTOR[0]┃
┃ ┃
┃ ・OriginalFirstThunk╂───────────┐RVA
┃ ・Name╂─→ "KERNEL32.dll\0" │
┃ ・FirstThunk╂┐RVA │
┣━━━━━━━━━━━━━┫│ │
┃IMAGE_IMPORT_DESCRIPTOR[1]┃RVA │
┣━━━━━━━━━━━━━┫│ │
: │ │
┃IMAGE_IMPORT_DESCRIPTOR[N]┃│ │
┗━━━━━━━━━━━━━┛│ │
│ │
┌────────────┘ ┌───────┘
↓インポートアドレステーブル ↓インポートネームテーブル
┏━━━━━━━━━━┓(IAT) ┏━━━━━━━━━━┓(INT)
┃IMAGE_THUNK_DATA[0] ┃ ┃IMAGE_THUNK_DATA[0] ┃
┃ ┃ ┃ ┃
┃ ・u1.Function╂┐ ┌╂・u1.AddressOfData ┃
┣━━━━━━━━━━┫│ │┣━━━━━━━━━━┫
┃IMAGE_THUNK_DATA[1] ┃│ RVA┃IMAGE_THUNK_DATA[1] ┃
┣━━━━━━━━━━┫│ │┣━━━━━━━━━━┫
: │ │ :
┃IMAGE_THUNK_DATA[n] ┃│ │┃IMAGE_THUNK_DATA[n] ┃
┗━━━━━━━━━━┛│ │┗━━━━━━━━━━┛
│ ↓
↓ ┏━━━━━━━━━━━┓
┏━━━━━┓ ┃IMAGE_IMPORT_BY_NAME ┃
┃0xXXXXXXXX┃=┃ ┃
┗━━━━━┛ ┃・Hint=49 ┃
(CloseHandle API のアドレス↑) ┃・Name="CloseHandle\0"┃
┗━━━━━━━━━━━┛
・IMAGE_IMPORT_DESCRIPTOR の Name メンバの指すモジュール名および
IMAGE_IMPORT_BY_NAME の Name メンバの指す API 名は常に ANSI で表記される
・IMAGE_IMPORT_DESCRIPTOR 配列、および IAT, INT の IMAGE_THUNK_DATA
配列は可変長であり、配列終端には構造体の全メンバが 0 にセットされた
エントリが配置される
■ インポートアドレステーブル(IAT)とインポートネームテーブル(INT)
上図の通り、IMAGE_IMPORT_DESCRIPTOR の OriginalFirstThunk メンバと FirstThunkメンバはそれぞれ INT と IAT を示します。
これらはいずれも Name メンバで示されるモジュールに含まれる所定の API ごとに一要素を構成する配列情報であり、同一の要素番号を持つエントリは同一のインポートAPI についての情報を保持しています。
リンクずみの実行形式ファイルの上では、FirstThunk は OriginalFirstThunk と同一の情報を保持しています(※2)が、「システムローダの役割」項で触れた通り、実行時にはローダにより API アドレス情報に書き換えられます。
(※2) bind.exe ユーティリティ等でバインド操作ずみのバイナリでは異なります
なお、INT の IMAGE_THUNK_DATA エントリの最上位ビットが立っていればそのエントリは API 名情報を持つ IMAGE_IMPORT_BY_NAME への RVA ではなく、当該インポート DLL上の序数情報であることを示しており、ここから最上位ビットを落とした値が実際の序数です。
名前・序数のいずれも、ローダによる API アドレス解決の際のシンボルとして有効です。
■ コードを書いてみる
最後に、ここまでの知識を元に、指定された実行ファイルのインポート API 情報の一覧を出力するサンプルコードを C 言語で書いてみることにします。
なお、最も重要な IMAGE_IMPORT_DESCRIPTOR テーブルのアドレスの取得は、ImageDirectoryEntryToData() API を使うことで簡単に実現できます。
<ImageDirectoryEntryToData>
http://msdn.microsoft.com/library/ja/jpdllpro/html/_win32_ImageDirectoryEntryToData.asp
#include <windows.h>
#include <stdio.h>
#include <imagehlp.h>
#pragma comment(lib, "Imagehlp.lib")
int main(int ac, char *av[])
{
HMODULE hMod, hLoad = NULL;
ULONG Size;
IMAGE_IMPORT_DESCRIPTOR *pImpDesc;
IMAGE_THUNK_DATA *pThunkINT, *pThunkIAT;
IMAGE_IMPORT_BY_NAME *pImpByName;
if (ac < 2) { // 引数がなければ自プロセスを対象に
hMod = GetModuleHandle(NULL);
} else { // 指定された実行形式をローダにかける
hMod = hLoad = LoadLibrary(av[1]);
}
if (!hMod) {
printf("HMODULE err\n");
return -1;
}
printf("Base Address = 0x%p\n", hMod);
// ベースアドレスからインポートデスクリプタのアドレスを得る
pImpDesc = (IMAGE_IMPORT_DESCRIPTOR*)
ImageDirectoryEntryToData(hMod,
TRUE,
IMAGE_DIRECTORY_ENTRY_IMPORT,
&Size);
printf("Import Desc = 0x%p, Size = %d bytes\n", pImpDesc, Size);
// インポートモジュール情報のループ
while (pImpDesc->Name) {
// モジュール名
LPSTR pszModName = (LPSTR)((BYTE*)hMod + pImpDesc->Name);
printf("Module = [%s]\n", pszModName);
// インポートネームテーブルのアドレスを得る
pThunkINT = (IMAGE_THUNK_DATA*)((BYTE*)hMod + pImpDesc->OriginalFirstThunk);
// インポートアドレステーブルのアドレスを得る
pThunkIAT = (IMAGE_THUNK_DATA*)((BYTE*)hMod + pImpDesc->FirstThunk);
// インポートAPIの情報
while (pThunkINT->u1.Function) {
// API アドレスを出力
printf("\tAddress = 0x%p ", (PROC)pThunkIAT->u1.Function);
if (pThunkINT->u1.AddressOfData & 0x80000000) {
// シンボルが序数情報の場合
DWORD dwOrd =pThunkINT->u1.AddressOfData ^ 0x80000000;
printf("Ordinal %d (0x%x)\n", dwOrd, dwOrd);
} else {
// シンボルが名前情報の場合
pImpByName = (IMAGE_IMPORT_BY_NAME*)
((BYTE*)hMod+ pThunkINT->u1.AddressOfData);
printf("[%s]\n", (LPSTR)(pImpByName->Name));
}
pThunkINT++;
pThunkIAT++;
}
pImpDesc++;
}
if (hLoad) FreeLibrary(hLoad);
return 0;
}
実行時の出力例
Base Address = 0x00400000
Import Desc = 0x00407D98
Module = [imagehlp.dll]
Address = 0x76C441B1 [ImageDirectoryEntryToData]
Module = [KERNEL32.dll]
Address = 0x7C9505D4 [HeapAlloc]
Address = 0x7C812AC6 [GetSystemInfo]
Address = 0x7C80AA66 [FreeLibrary]
Address = 0x7C801D77 [LoadLibraryA]
Address = 0x7C80B529 [GetModuleHandleA]
:
(後略)
次回は、所定の API を参照するコードが実行形式イメージにおいてどのように表現されているかを調べてみることにします。
■ 参考文献
・An In-Depth Look into the Win32 Portable Executable File Format
by Matt Pietrek
・Advanced Windows 改訂第4版 (Microsoft Press)
by Jeffrey Richter, 長尾 高弘 訳
(株)アスキー刊 ISBN-13: 978-4756138057