ククログ

株式会社クリアコード > ククログ > PEから公開されている関数名を抜き出す

PEから公開されている関数名を抜き出す

ELFから公開されている関数名を抜き出すMach-Oから公開されている関数名を抜き出すのPE(Portable Executable)版です。PEはWindowsの.exeや.dllなどで利用されているファイルフォーマットです。

artonさんがCodeZineでDbgHelpを利用してDLLがエクスポートしている関数を列挙するという同様の内容の記事を書いています。PEのフォーマットについても説明しているので、まず、この記事を読んでおくとよいでしょう。

ここでは、DbgHelpなどライブラリを一切使わずに自力でPEをパースし、関数名を抜き出します。そのため、MinGWでクロスコンパイルすることも簡単です。実際、CutterはMinGWを用いたクロスコンパイルに対応しています。

下準備

簡略化のためファイルの内容をすべてメモリに読み込んでから処理します。コツコツ資源を利用したい場合は少しづつ読み込みながら処理することになります。

ファイルの内容を読み込むために、便利なGLibのg_file_get_contents()を使いたいところですが、Windows環境ではGLibがインストールされていないことが多いので、ここでは自力で読み込むことにします。

char *content = NULL;
FILE *file;
char buffer[4096];
size_t bytes, total_bytes = 0;

file = fopen(argv[1], "rb");
if (!file) {
    perror("failed to fopen()");
    return -1;
}

while (!feof(file)) {
    char *original_content;

    bytes = fread(buffer, 1, sizeof(buffer), file);
    total_bytes += bytes;
    original_content = content;
    content = realloc(content, total_bytes);
    if (!content) {
        free(original_content);
        fclose(file);
        perror("failed to realloc()");
        return -1;
    }
    memcpy(content + total_bytes - bytes, buffer, bytes);
}
fclose(file);

これで、contentの中にファイルの内容が格納されました。これを使って公開されている関数名を抜き出します。

PEのフォーマットに関する情報はwinnt.hで定義されています。winnt.hwindows.hをincludeすると暗黙のうちにincludeされるので、windows.hだけincludeします。

#include <windows.h>

PEかどうかを判断

まず、ファイルがPEかどうかを判断します。

PEであればNTヘッダに"PE\0\0"という署名が入っているので、これを確認します。"PE\0\0"という署名はIMAGE_NT_SIGNATUREというマクロとして定義されているので、これを利用します。

IMAGE_DOS_HEADER *dos_header;
IMAGE_NT_HEADERS *nt_headers;

/* ファイルの先頭はDOSヘッダ */
dos_header = (IMAGE_DOS_HEADER *)content;
/* NTヘッダを見つける */
nt_headers = (IMAGE_NT_HEADERS *)(content + dos_header->e_lfanew);
/* 署名が"PE\0\0"かどうか確認 */
if (nt_headers->Signature == IMAGE_NT_SIGNATURE) {
    /* PEファイル */
}

DLLかどうかを判断

PEであることが確認できたら、DLLかどうかを確認します。

if (nt_headers->FileHeader.Characteristics & IMAGE_FILE_DLL) {
    /* DLL */
}

公開されているシンボルを探索し出力

公開されているシンボルはエクスポートデータセクションを見るとわかります。また、シンボルが関数かどうかは、実体がテキストセクションにあるかどうかで判断します。この方法が関数かどうかを判断する標準的な方法かはわかりませんが、実用上はこれで問題なさそうです。

よって、まず、エクスポートデータセクションヘッダとテキストセクションヘッダを見つけます。それぞれ、ヘッダの名前は以下のようになっているので、それを目印に見つけます。

エクスポートデータセクションヘッダ名

.edata

テキストセクションヘッダ名

.text

以下がソースコードです。

WORD i;
IMAGE_SECTION_HEADER *first_section;
IMAGE_SECTION_HEADER *edata_section;
IMAGE_SECTION_HEADER *text_section;

/* 最初のセクションヘッダ */
first_section = IMAGE_FIRST_SECTION(nt_headers);
for (i = 0; i < nt_headers->FileHeader.NumberOfSections; i++) {
    const char *section_name;

    section_name = (const char *)((first_section + i)->Name);
    /* 各セクションの名前を確認 */
    if (strcmp(".edata", section_name) == 0) {
        /* エクスポートデータセクションを発見 */
        edata_section = first_section + i;
    } else if (strcmp(".text", section_name) == 0) {
        /* テキストセクションを発見 */
        text_section = first_section + i;
    }
}

ヘッダが見つかったら、セクションの内容を見て、関数であるシンボル名を出力します。1

IMAGE_EXPORT_DIRECTORY *export_directory;
const char *base_address;
ULONG *name_addresses;
ULONG *function_addresses;
DWORD min_text_section_address, max_text_section_address;

/* エクスポートデータセクションの内容 */
export_directory =
    (IMAGE_EXPORT_DIRECTORY *)(content +
                               edata_section->PointerToRawData);

/* エクスポートデータセクション内のデータがあるアドレスを解決するための
   基準になるアドレス */
base_address =
    content +
    edata_section->PointerToRawData -
    edata_section->VirtualAddress;
/* シンボル名があるアドレス */
name_addresses =
    (ULONG *)(base_address + export_directory->AddressOfNames);
/* シンボルの実体への相対的なアドレス(RVA)があるアドレス */
function_addresses =
    (ULONG *)(base_address + export_directory->AddressOfFunctions);
/* テキストセクションのデータの相対的なアドレスの下限 */
min_text_section_address = text_section->VirtualAddress;
/* テキストセクションのデータの相対的なアドレスの上限 */
max_text_section_address =
    min_text_section_address + text_section->SizeOfRawData;

/* シンボル名毎に関数かどうか判断 */
for (i = 0; i < export_directory->NumberOfNames; i++) {
    const char *name;
    DWORD function_address;

    /* シンボル名 */
    name = base_address + name_addresses[i];
    /* シンボルの実体の相対的なアドレス */
    function_address = function_addresses[i];
    if (min_text_section_address < function_address &&
        function_address < max_text_section_address) {
        /* シンボルの実体がテキストセクションにあるなら関数 */
        printf("found: %s\n", name);
    }
}

参考

まとめ

winnt.hを使って、DbgHelpなどに依存せずに、PEから公開されている関数名を抜き出す方法を紹介しました。

サンプルプログラムはDebian GNU/Linux上でMinGWを使ってクロスコンパイルし、Wineで動作を確認しました。

今のところ、Cutterがサポートしている共有ライブラリのフォーマットはELF/Mach-O/PEです。このPE編で、公開されている関数名を抜き出す方法を紹介するシリーズは最後です。もし、今後Cutterが対応するフォーマットが増えれば、そのフォーマットから関数名を抜き出す方法を紹介するかもしれません。

  1. コメント中のRVAの説明はざっくりなので、詳しくは冒頭のartonさんの記事を参照してください。