SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

特集記事

DbgHelpを利用してDLLがエクスポートしている関数を列挙する

DLLやEXEなどのPE形式のファイルから情報を取得する

  • X ポスト
  • このエントリーをはてなブックマークに追加

プログラムの説明

メモリーマップトファイルの利用

 DbgHelpを利用してイメージを操作するには、いくつかの方法があります。一番簡単なのは、DbgHelpのMapAndLoad関数を利用することですが、ここでは通常のファイルとしてオープンし、マップする方法を利用します。

 メモリーマップトファイルを利用すると、ファイルからバッファにシーケンシャルにデータを読み込むのと異なり、見かけ上連続したメモリー上でファイル内の移動ができます。読み込み専用で利用する分には、通常のファイルより簡単に扱えるため、ぜひとも呼び出し方法を覚えておきましょう。

ソースファイルの説明

 ソースファイルは、ヘッダファイルのプリコンパイル用の「stdafx.cpp」、利用するヘッダを定義した「stdafx.h」、そしてプログラム本体である「showexports.cpp」の3つのファイルから構成されています。

 「showexports.cpp」は、2つの部分に分かれています。

  • ファイルの前半は、DllFileというクラスの定義です。
  • DllFileは、オープン済みファイルハンドルをコンストラクタで受け取り、エクスポートしている関数を列挙するメソッドを提供します。
  • ファイルの後半は、main関数とエラー出力とDllFileを呼び出す関数の実装です。

main関数

int _tmain(int argc, _TCHAR* argv[])
{
    if (argc != 2)
    {
        fprintf(stderr, "usage: proxygen dll\n");
        return 1;
    }
    HANDLE hFile = CreateFile(argv[1], GENERIC_READ,
             FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
    if (hFile != INVALID_HANDLE_VALUE)
    {
        try
        {
            DllFile file(hFile);
            file.EnumExports(printExport);
        }
        catch (LPTSTR ploc)
        {
            print_error(ploc);
        }
        CloseHandle(hFile);
    }
    else
    {
        print_error(_T("Open"));
    }
    return 0;
}

 mainでは、与えられた引数を利用してDLLをオープンします。成功した場合、DllFileクラスのインスタンスを作成し、EnumExportsメソッドを呼び出して関数名を表示します。

printExport関数

static BOOL printExport(WORD ordinal, LPSTR pfun)
{
    printf("%d fname=%s\n", ordinal, pfun);
    return TRUE;
}

 printExport関数は、DllFile#EnumExportsメソッドへの引数としてmain関数で指定している関数です。ここでは、単に序数と関数名を標準出力へ出力します。

 なお、DllFile#EnumExportsはメソッドの引数として与えられた関数を、エクスポートしている関数の序数と関数名を引数にして呼び出すメソッドです。

print_error関数

static void print_error(LPTSTR loc)
{
    DWORD error = GetLastError();
    if (!error)
    {
        _ftprintf(stderr, _T("%s error\n"), loc);
    }
    else
    {
        LPTSTR lpMsgBuf = NULL;
        FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER
                    | FORMAT_MESSAGE_FROM_SYSTEM
                    | FORMAT_MESSAGE_IGNORE_INSERTS,
            NULL, error, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
            (LPTSTR)&lpMsgBuf, 32, NULL);
        _ftprintf(stderr, _T("%s error: %s\n"), loc, lpMsgBuf);
        LocalFree(lpMsgBuf);
    }
}

 DllFileクラスが処理中にスローした例外などによって呼び出されるエラー表示関数です。

 GetLastErrorの結果が0の場合は、DllFileがDLL以外のファイルをコンストラクタへ与えたなどの理由でスローしたエラーなので、単にそのメッセージを表示します。

 GetLastErrorの結果が0以外の場合は、API呼び出しでエラーになったことを意味するため、FormatMessage関数でエラーメッセージを取り出して表示します。

DllFile::DllFile

DllFile(HANDLE hFile) : pFile(NULL), hMap(NULL)
{
    HANDLE h = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, 0, NULL);
    if (!h)
    {
        throw _T("CreateMapping");
    }
    LPBYTE pfbase = (LPBYTE)MapViewOfFile(h, FILE_MAP_READ, 0, 0, 0);
    if (!pfbase)
    {
        CloseHandle(h);
        throw _T("Mapping View");
    }
    check_coff(pfbase);
    hMap = h;
    pFile = pfbase;
}

 CreateFileMapping関数を呼び出してメモリーマップトファイルを作成します。次に、MapViewOfFile関数を呼び出して実際にマッピングし、先頭のポインタを取得します。最後に、check_coffメソッドを呼び出して、与えられたファイルがDLLのイメージかどうかを検証します。

 COFF(Common Object File Format)とは、PEファイルより前にマイクロソフト社が利用していたイメージ/オブジェクトファイルのフォーマットです。PEファイルは、COFFのファイルヘッダ構造体をcheck_coffメソッドでの検証対象であるIMAGE_FILE_HEADER構造体として利用しているため、ここでは「check_coff」という名称を利用しています。

 すべてが完了したら、ハンドルなどをインスタンス変数に保持します。ここで保持したハンドルなどはデストラクタで解放します。

DllFile::~DllFile

virtual ~DllFile()
{
    if (pFile)
    {
        UnmapViewOfFile(pFile);
    }
    if (hMap)
    {
        CloseHandle(hMap);
    }
}

 デストラクタでは、コンストラクタで設定したメモリーマップトファイルのハンドルなどのリソースを解放します。解放が必須なリソースの処理を行っているため、派生クラスで横取りされないようにvirtualとして定義します。

 なお、UnmapViewOfFile関数の引数の値は、MapViewOfFileの結果と一致している必要がある点には気をつけてください。例えば、処理の都合でpFile++というように直接ポインタを操作する場合には、元のポインタ値を別に保存しておきUnmapViewOfFile関数に与えるようにします。

DllFile::check_coffメソッド

void check_coff(LPBYTE pfbase)
{
    PIMAGE_NT_HEADERS pnt = ImageNtHeader(pfbase);
    if (pnt->Signature != *(DWORD*)PEMARK)
    {
        throw _T("bad signature");
    }
    if (pnt->FileHeader.Machine != IMAGE_FILE_MACHINE_I386
        || pnt->FileHeader.SizeOfOptionalHeader == 0
        || (pnt->FileHeader.Characteristics & DLL_CHARACTERISTIC)
               != DLL_CHARACTERISTIC
        || pnt->OptionalHeader.Magic != PE32)
    {
        throw _T("bad module");
    }

}

 このメソッド内で呼び出しているImageNtHeader関数は、ファイルの先頭ポインタを与えるとDOSスタブなどをスキップしてIMAGE_NT_HEADERS構造体をポイントして返すDbgHelpのAPIです。

 ここではシグネチャのPE\0\0 4バイトおよび、マシン種別、イメージかどうか、DLLかどうかといった条件を検証します。

DllFile::EnumExportsメソッド

int EnumExports(BOOL (*func)(WORD, LPSTR))
{
    ULONG size;
    PIMAGE_SECTION_HEADER psc;
    PIMAGE_NT_HEADERS pnt = ImageNtHeader(pFile);
    PIMAGE_EXPORT_DIRECTORY ped = (PIMAGE_EXPORT_DIRECTORY)
        ImageDirectoryEntryToDataEx(pFile, FALSE,
                    IMAGE_DIRECTORY_ENTRY_EXPORT, &size, &psc);
    if (!ped)
    {
        throw _T("no exported functions");
    }
    LPCSTR pname = (LPCSTR)ImageRvaToVa(pnt, pFile, ped->Name, &psc);
    ULONG* ppfuname = (ULONG*)ImageRvaToVa(pnt, pFile,
                                        ped->AddressOfNames, &psc);
    WORD* pordinal = (WORD*)Ima  geRvaToVa(pnt, pFile,
                                        ped->AddressOfNameOrdinals, &psc);
    for (DWORD i = 0; i < ped->NumberOfNames; i++, ppfuname++, pordinal++)
    {
        LPSTR pfun = (LPSTR)ImageRvaToVa(pnt, pFile, *ppfuname, &psc);
        if (!func(*pordinal + (WORD)ped->Base, pfun))
        {
            return -((int)i + 1);
        }
    }
    return ped->NumberOfNames;
}

 エクスポートされた序数と関数名を列挙して、引数で与えられた関数を呼び出します。

 ImageDirectoryEntryToDataEx関数は、IMAGE_OPTIONAL_HEADER構造体から指定されたインデックスのディレクトリ情報(PEファイル内の特定データの位置を示す情報)を返すDbgHelpのAPIです。

 ここでは、エクスポート情報のディレクトリ情報であるIMAGE_DIRECTORY_ENTRY_EXPORTを指定して呼び出します。最後の引数のpscは、関連するIMAGE_SECTION_HEADER構造体をポイントさせるために指定します。ここで返されたIMAGE_SECTION_HEADERを次に呼び出しているImageRvaToVa関数に与えると、RVA変換処理が多少高速化されます。

 以降、関数名の配列(IMAGE_EXPORT_DIRECTORY構造体のAddressOfNamesフィールド)、序数の配列(IMAGE_EXPORT_DIRECTORY配列のAddressOfNameOrdinalsフィールド)のRVAから実際の仮想アドレス(VA)をImageRvaToVa関数を呼び出して求めます。

 最後に、関数名を同様にImageRvaToVa関数で取り出して、序数と共に引数で与えられたユーザー関数を呼び出します。なお、序数のみが定義されていて関数名が定義されていないDLLでは、IMAGE_EXPORT_DIRECTORY構造体のNumberOfNamesフィールドの値よりNumberOfFunctionsフィールドの値の方が大きくなります。

ImageRvaToVa関数呼び出しについての補足
 実際には、ここで示したように、RVAからVAへのすべての変換にImageRvaToVa関数を呼び出す必要はありません。ここでの例のように、すべてのRVAが同一のIMAGE_SECTION_HEADER構造体によってポイントされているセクション内に含まれている場合は、バイアスを1個見つければ他のRVAに対してもそれを適用することができるからです。
 RVAからVAへの変換方法については、参考資料のJay Krell氏の投稿を参照してください。

まとめ

 PEファイルはプログラムですが、その内容をプログラムから直接データとして扱えます。これらの情報を適切に利用することで、C#のようにリッチなメタデータを持たないC/C++であっても、それなりの情報を得てメタな操作を行うことが可能となります。

 イメージからどのような処理を作ることができるかは、プログラムの課題として興味深いものがあります。しかもWindowsでは、ここで紹介したDbgHelpの他にもImageHlpなどのライブラリが提供されているため、イメージを直接扱うことはそれほど困難ではありません。

 イメージを扱うプログラムを作成する時に、本記事が少しでも参考になれば幸いです。

参考資料

この記事は参考になりましたか?

  • X ポスト
  • このエントリーをはてなブックマークに追加
特集記事連載記事一覧

もっと読む

この記事の著者

arton(アートン)

専門は業界特化型のミドルウェアやフレームワークとそれを利用するアプリケーションの開発。需要に応じてメインフレームクラスから携帯端末までダウンサイジングしたりアップサイジングしたりしながらオブジェクトを連携させていくという変化に富んだ開発者人生を歩んでいる。著書に『Ruby③ オブジェクト指向とはじめての設計...

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

  • X ポスト
  • このエントリーをはてなブックマークに追加
CodeZine(コードジン)
https://codezine.jp/article/detail/416 2006/06/19 00:00

おすすめ

アクセスランキング

アクセスランキング

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー

アクセスランキング

アクセスランキング