logo

PEファイルリソースへの紹介

こんにちは、親愛なる読者の皆さん!

この記事では、PEファイルの重要で興味深い部分の一つであるPE IMAGE RESOURCES(コンパイル後にプログラムにリンクされたデータ)について説明します。リソースツリーの内部構造に深く入り込み、WinAPIを使用せずに独自のネイティブリソースパーサーを書くことにします。

PE(ポータブル実行可能)ファイルのリソースは、アプリケーションの不可欠な部分を形成する埋め込まれたデータです。これらには画像、アイコン、テキスト文字列、ダイアログボックス、フォントなどさまざまな要素が含まれています。これらのリソースは実行可能ファイルに直接統合され、実行中のアプリケーションでの利用可能性が保証されます。画像、テキスト、および音声などのリソースは、しばしばプログラムがコンパイルされた後に追加されることが多いという点が重要です。このアプローチは、開発プロセスをより柔軟で効率的にするいくつかの重要な利点を持っています。

想像してみてください。あなたが新しいアプリケーションを開発しているチームの一員であると。チームはプログラマー、デザイナー、コンテンツマネージャーで構成されています。それぞれのメンバーが独自かつ有用なものを作り出すために貢献しています。

プロジェクトの初めには、プログラマーはコードの作成とテストに集中します。彼らはアプリケーションのフレームワークを作り、すべての機能が正しく動作することを確認します。同時に、デザイナーやコンテンツマネージャーは、インターフェース用のリソース - 画像、音声、テキストを作成します。この並行作業により、チームは迅速かつ効率的に前進することができます。

プログラミングの主要部分が完了すると、リソースをアプリケーションに統合する時です。これは、再コンパイルする必要なく、すでにコンパイルされたアプリケーションにリソースを追加することを可能にする特別なツールを使用して行われます。これは非常に便利です、特にリソースを変更するか更新する必要がある場合 - プロジェクト全体を再コンパイルする必要はありません。

このプロセスの重要な側面の一つはローカリゼーションです。リソースをメインコードから分離することで、アプリケーションのローカリゼーションがずっと簡単になります。インターフェースのテキストやエラーメッセージは簡単に翻訳して置き換えることができ、メインコードに干渉することなく行えます。これにより、アプリケーションは世界中のさまざまな言語市場に適応させることができ、多くのユーザーにとってアクセスしやすく理解しやすいものとなります。
 

リソース使用の例

  • アプリケーションアイコン: Windowsエクスプローラーやタスクバーに表示されるアイコンは、アプリケーションの視覚的表現を提供します。
  • ダイアログボックス: アプリケーションがユーザーの対話のために表示するダイアログボックスの定義、例えば設定や警告ウィンドウなど。
  • メニュー: ユーザーインターフェースで使用されるメニュー構造は、ナビゲーションと機能性を提供します。
  • 文字列: アプリケーション内でテキスト表示に使用されるローカライズされた文字列、エラーメッセージやツールチップ、その他のユーザーインターフェースを含む。
  • サウンド: 特定の状況でアプリケーションによって再生可能なオーディオファイル、例えばサウンド通知など。
  • カーソル: ユーザーインターフェースとの対話に使用されるグラフィックカーソル、例えば矢印、ポインター、またはアニメーションカーソル。
  • バージョン情報 (Version Info): アプリケーションのバージョン情報、著作権情報、製品名、その他のバージョン関連データを含む。
  • マニフェスト: Windowsのバージョン要件やセキュリティ設定を含む、アプリケーションの構成情報を含むXMLファイル。
  • RC_DATA: 開発者によって定義された任意のデータで、バイナリデータ、設定ファイル、その他のアプリケーション固有のリソースを含むことがあります。

ポータブル実行ファイル内のリソースを表示および編集する方法は?

コンパイルされたアプリケーション内のリソースを閲覧、編集できますか? もちろんです!必要なのは適切なツールだけです。コンパイルされた実行可能ファイル内のリソースを扱うための幅広い機能を提供するツールがあります。それには、リソースの閲覧、編集、追加、削除が含まれます。

こちらは、すでにコンパイル済みの実行可能ファイル内のリソースを表示または編集するために使用できるリソースエディタのリストです:

  • Resource Hacker:これは32ビットおよび64ビットのWindowsアプリケーション用リソースエディタです。実行可能ファイル(.exe, .dll, .scr等)およびコンパイルされたリソースライブラリ(.res, .mui)のリソースを表示および編集することができます。 Resource Hacker
  • ResEdit:Win32プログラム用の無料リソースエディタ。ダイアログ、アイコン、バージョン情報、その他のタイプのリソースの作業に適しています。 ResEdit
  • Resource Tuner:問題のある実行可能ファイルを開き、他のエディタでは見えない隠されたデータを編集可能にするリソースエディタです。 Resource Tuner
  • Resource Builder:Windows用のパワフルでフル機能を持つリソースエディタ。リソースファイル(.RC, .RES など)の作成、編集、コンパイル、またはコンパイルされた実行可能ファイル内のリソースの編集を可能にします。 Resource Builder
  • Visual Studio:リソースを追加、削除、変更することができるリソースエディタを提供します。 Visual Studio
  • Resource Tuner Console:リソースの編集に適した強力なコマンドラインツールで、バッチ(.bat)ファイルでの使用に理想的です。 Resource Tuner Console
  • Qt Centre:Qtを使用してコンパイルされた実行可能ファイルのリソースファイルを編集できます。 Qt Centre

リストの最初のツールである Resource Hacker についてもう少し詳しく見てみましょう。

このツールは、実行可能ファイルからリソースを表示および抽出するだけでなく、それらを編集することも可能です!

これらのリソースには、アイコン、メニュー、ダイアログボックス、その他の種類のデータが含まれており、通常、PEファイルの特定のセクションである .rsrc(リソースセクション)に位置しています。ただし、これは厳格なルールではなく、例外が発生することがあることに注意してください。

これらのリソースをナビゲートし、アクセスするための重要な側面は、IMAGE_DATA_DIRECTORY[IMAGE_DIRECTORY_ENTRY_RESOURCES]です。このディレクトリエントリーは、PEファイルのオプショナルヘッダーの一部であり、データディレクトリ配列にあります。これはイメージ内のリソースへのポインターや参照として機能します。IMAGE_DIRECTORY_ENTRY_RESOURCESは、リソースデータの位置(相対仮想アドレスなど)やサイズに関する情報を提供します。

 

ポータブル実行可能ファイルのリソース構造

概要

PE(Portable Executable)ファイルのリソースセクションで使用されている構造について詳しく見ていきましょう。Windows PEファイルのリソースセクションは、独特な三層階層ツリー構造を持っています。このツリーは、アイコン、カーソル、文字列、ダイアログなどのリソースを整理し、アクセスするために使用されます。以下のように構造化されています:

レベル 1: リソースの種類

ツリーの最上位にはリソースタイプがあります。各リソースタイプは、数値識別子(ID)または文字列名によって識別できます。

レベル 2: リソース名

二つ目のレベルでは、各リソースタイプにはそれぞれの名前または識別子があります。これにより、複数のアイコンや行など、同じタイプのリソースを複数持つことができます。

レベル3:リソース言語

3つ目のレベルでは、各リソースは異なる言語ローカリゼーションのためのバリアントを持っています。これにより、ダイアログなどの同じリソースを異なる言語でローカライズすることができます。

データ構造

次のデータ構造はこの階層を表すために使用されています:

  • IMAGE_RESOURCE_DIRECTORY:この構造体は、ツリーの各レベルのヘッダーを表し、そのレベルのエントリに関する一般的な情報を含んでいます。
  • IMAGE_RESOURCE_DIRECTORY_ENTRY:これらは、別のIMAGE_RESOURCE_DIRECTORYを指すサブディレクトリであるか、またはツリーの最終葉であり、実際のリソースデータを指す要素です。
  • IMAGE_RESOURCE_DATA_ENTRY:この構造体はリソースデータ自体を指し、そのサイズとオフセットを含んでいます。

リソースツリーの可視化は、次のようになるかもしれません:

Root (IMAGE_DATA_DIRECTORY[IMAGE_DIRECTORY_ENTRY_RESOURCES].VirtualAddress)
|
+-- Type (RT_ICON, RT_STRING, ...)
    |
    +-- Name (ID or String)
        |
        +-- Language (Locale ID)
            |
            +-- Data (Actual resource data)
IMAGE_RESOURCE_DIRECTORY
|
|-- IMAGE_RESOURCE_DIRECTORY_ENTRY (Resource Types)
|   |-- IMAGE_RESOURCE_DIRECTORY (Resource names)
|   |   |-- IMAGE_RESOURCE_DIRECTORY_ENTRY (Names)
|   |   |   |-- IMAGE_RESOURCE_DIRECTORY (Languages)
|   |   |   |   |-- IMAGE_RESOURCE_DIRECTORY_ENTRY (Languages)
|   |   |   |   |   |-- IMAGE_RESOURCE_DATA_ENTRY (Resource data)

このツリーの各ノードはIMAGE_RESOURCE_DIRECTORYを表しており、葉はIMAGE_RESOURCE_DATA_ENTRIESで、これはリソースデータを直接指しています。リソースを手動で解析する場合、開発者はこのツリーのルートから始めて、必要なデータを見つけるためにすべてのレベルを順にナビゲートする必要があります。

 

IMAGE_RESOURCE_DIRECTORY

この構造はリソースツリーの各レベルのヘッダーとして機能し、そのレベルにあるエントリの情報を含んでいます。

typedef struct _IMAGE_RESOURCE_DIRECTORY {
    DWORD   Characteristics;
    DWORD   TimeDateStamp;
    WORD    MajorVersion;
    WORD    MinorVersion;
    WORD    NumberOfNamedEntries;
    WORD    NumberOfIdEntries;
    //  IMAGE_RESOURCE_DIRECTORY_ENTRY DirectoryEntries[];
} IMAGE_RESOURCE_DIRECTORY, *PIMAGE_RESOURCE_DIRECTORY;
  • Characteristics:通常は使用されず、0に設定されます。
  • TimeDateStamp:リソースの作成時のタイムスタンプ。
  • MajorVersion and MinorVersion:リソースディレクトリのバージョン。
  • NumberOfNamedEntries:名前を持つリソースエントリの数。
  • NumberOfIdEntries:数値識別子を持つリソースエントリの数。

 

IMAGE_RESOURCE_DIRECTORY_ENTRY

サブディレクトリまたはツリーの最終リーフのどちらかになる要素。

typedef struct _IMAGE_RESOURCE_DIRECTORY_ENTRY {
    union {
        struct {
            DWORD NameOffset:31;
            DWORD NameIsString:1;
        };
        DWORD   Name;
        WORD    Id;
    };
    union {
        DWORD   OffsetToData;
        struct {
            DWORD   OffsetToDirectory:31;
            DWORD   DataIsDirectory:1;
        };
    };
} IMAGE_RESOURCE_DIRECTORY_ENTRY, *PIMAGE_RESOURCE_DIRECTORY_ENTRY;
  • 名前:NameIsStringが1に設定されている場合、このフィールドにはリソースの名前を表すUNICODE文字列を指すオフセットが含まれます。NameIsStringが0に設定されている場合、Idフィールドは数値識別子を使用してリソースを識別するために使用されます。
  • OffsetToData:DataIsDirectoryが1に設定されている場合、このフィールドには別のIMAGE_RESOURCE_DIRECTORY(すなわち、サブディレクトリ)を指すオフセットが含まれます。DataIsDirectoryが0に設定されている場合、このオフセットはIMAGE_RESOURCE_DATA_ENTRYを指します。

 

IMAGE_RESOURCE_DATA_ENTRY

この構造はリソースの実際のデータを指しています。

typedef struct _IMAGE_RESOURCE_DATA_ENTRY {
    DWORD   OffsetToData;
    DWORD   Size;
    DWORD   CodePage;
    DWORD   Reserved;
} IMAGE_RESOURCE_DATA_ENTRY, *PIMAGE_RESOURCE_DATA_ENTRY;
  • OffsetToData:リソースセクションの開始からリソースデータまでのオフセット。
  • Size:リソースデータのサイズ(バイト単位)。
  • CodePage:リソースデータのエンコーディングに使用されるコードページ。
  • Reserved:予約済み; 通常は0に設定されます。

重要! IMAGE_RESOURCE_DIRECTORY およびIMAGE_RESOURCE_DIRECTORY_ENTRY のオフセットは、リソースの開始地点(IMAGE_DATA_DIRECTORY[IMAGE_DIRECTORY_ENTRY_RESOURCES].VirtualAddress)から計算されますが、IMAGE_RESOURCE_DATA_ENTRY のオフセットのみがベースイメージの開始地点から計算されます!

 

ネイティブリソースパーサーを書こう!

WinAPIを使用せずに独自のネイティブリソースパーサーを作成する時です!EnumResourceTypesEnumResourceNames などのWindows API関数を使用するのではなく、リソースを手動でパースすることには、特にセキュリティ分析やアンチウイルススキャンの文脈でいくつかの利点があります:

  • セキュリティ:API関数のEnumResourceTypesやEnumResourceNamesは、実行可能ファイルをプロセスのアドレス空間にロードする必要があり、ファイルにウイルスやトロイの木馬が含まれている場合、悪意のあるコードが実行される可能性があります。リソースの手動解析により、このリスクを避けることができます。
  • プラットフォーム独立性:手動リソース解析は、オペレーティングシステムのバージョンやそのWinAPIに依存しないため、より普遍的な解決策となります。
  • ヒューリスティック分析:手動解析により、新規または未知の脅威を特定するために必要となる複雑なヒューリスティックや検出アルゴリズムを適用することができます。
  • パフォーマンス:多数のファイルをスキャンする場合、特にWinAPIを使用するよりも最適化されたパフォーマンスで解析することができます。
  • コントロール:手動解析では、分析者がプロセスを完全にコントロールし、特定の分析ニーズに合わせて微調整することができますが、API関数は限定的なコントロールを提供し、リソースのすべての側面を明らかにすることはありません。
  • 保護:マルウェアは、標準APIによって検出されない方法でリソースを操作するなど、様々な方法を使用して検出を回避することがあります。手動解析は、そのような操作を検出することができます。
  • 完全なアクセス:API関数は、特にリソースが破損している場合や意図的に変更されている場合、すべてのリソースへのアクセスを提供しないことがあります。手動解析により、APIの制限を受けることなくすべてのデータを分析することができます。
  • エラーハンドリング:API関数を使用する場合、エラーハンドリングは限定的かもしれませんが、手動解析ではファイル構造の非標準的な状況や異常に対してより柔軟な対応が可能です。
struct ResourceInfo
{
    DWORD Size; 	// Size of the resource data
    PBYTE data; 	// Offset of the resource data from the beginning of the file

    union {
        WORD TypeID;                        // Resource type ID or
        PIMAGE_RESOURCE_DIR_STRING_U Type;  // resource type
    };

    union {
        WORD NameID;                        // Resource name ID or
        PIMAGE_RESOURCE_DIR_STRING_U Name;  // resource name
    };

    WORD  Language; // Language of the resource
};

std::optional> getAllResources(BYTE* pBase, uint64_t fileSize)
{
    IMAGE_RESOURCE_DIRECTORY* pTypesDirectory = nullptr;
    std::vector resources;

    try
    {
        //********************************************************
        //  parse PE header
        //********************************************************
        IMAGE_DOS_HEADER* pDosHeader = reinterpret_cast(pBase);
        IMAGE_NT_HEADERS* pNtHeaders = reinterpret_cast(pBase + pDosHeader->e_lfanew);

        // Verify that the PE signature is valid, indicating a valid PE file.
        if (pNtHeaders->Signature != IMAGE_NT_SIGNATURE)
            return std::nullopt;

        // Depending on the machine type (32-bit or 64-bit), obtain the resource directory data.
        IMAGE_DATA_DIRECTORY resourceDirectory;
        switch (pNtHeaders->FileHeader.Machine)
        {
        case IMAGE_FILE_MACHINE_I386:
            resourceDirectory = reinterpret_cast(pNtHeaders)->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_RESOURCE];
            break;
        case IMAGE_FILE_MACHINE_AMD64:
            resourceDirectory = reinterpret_cast(pNtHeaders)->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_RESOURCE];
            break;
        default:
            return std::nullopt;
        };

        // If the resource directory is empty, exit as there are no resources.
        if (resourceDirectory.Size == 0)
            return std::nullopt;

        // Convert the RVA of the resources to a RAW offset
        uint64_t resourceBase = ntpe::RvaToOffset(pBase, resourceDirectory.VirtualAddress);
        IMAGE_RESOURCE_DIRECTORY* pResourceDir = reinterpret_cast(pBase + resourceBase);

        //********************************************************
        // Start parsing the resource directory
        //********************************************************
        // Iterate through type entries in the resource directory.
        // parse types
        pTypesDirectory = pResourceDir;
        IMAGE_RESOURCE_DIRECTORY_ENTRY* pTypeEntries = (IMAGE_RESOURCE_DIRECTORY_ENTRY*)(pTypesDirectory + 1);

        for (uint64_t ti = 0; ti < pTypesDirectory->NumberOfNamedEntries + pTypesDirectory->NumberOfIdEntries; ti++)
        {
            // parse names
            IMAGE_RESOURCE_DIRECTORY_ENTRY* pTypeEntry = &pTypeEntries[ti];
            IMAGE_RESOURCE_DIRECTORY* pNamesDirectory = (IMAGE_RESOURCE_DIRECTORY*)(pBase + (pTypeEntry->OffsetToDirectory & 0x7FFFFFFF) + resourceBase);
            for (uint64_t ni = 0; ni < pNamesDirectory->NumberOfNamedEntries + pNamesDirectory->NumberOfIdEntries; ni++)
            {
                //  parse langs
                IMAGE_RESOURCE_DIRECTORY_ENTRY* pNamesEntries = (IMAGE_RESOURCE_DIRECTORY_ENTRY*)(pNamesDirectory + 1);
                IMAGE_RESOURCE_DIRECTORY_ENTRY* pNameEntry = &pNamesEntries[ni];
                IMAGE_RESOURCE_DIRECTORY* pLangsDirectory = (IMAGE_RESOURCE_DIRECTORY*)(pBase + (pNameEntry->OffsetToDirectory & 0x7FFFFFFF) + resourceBase);
                for (uint64_t li = 0; li < pLangsDirectory->NumberOfNamedEntries + pLangsDirectory->NumberOfIdEntries; li++)
                {
                    //  parse data
                    IMAGE_RESOURCE_DIRECTORY_ENTRY* pLangsEntries = (IMAGE_RESOURCE_DIRECTORY_ENTRY*)(pLangsDirectory + 1);
                    IMAGE_RESOURCE_DIRECTORY_ENTRY* pLangEntry = &pLangsEntries[li];
                    IMAGE_RESOURCE_DATA_ENTRY* pDataEntry = (IMAGE_RESOURCE_DATA_ENTRY*)(pBase + resourceBase + pLangEntry->OffsetToData);

                    // Save the resource information in a structured format.
                    ResourceInfo entry = {};
                    entry.Language = pLangsEntries->Id;
                    entry.Size = pDataEntry->Size;
                    entry.Type = (PIMAGE_RESOURCE_DIR_STRING_U)(pTypeEntry->NameIsString) ? (PIMAGE_RESOURCE_DIR_STRING_U)(pBase + pTypeEntry->NameOffset + resourceBase) : (PIMAGE_RESOURCE_DIR_STRING_U)(pTypeEntry->Id);
                    entry.Name = (PIMAGE_RESOURCE_DIR_STRING_U)(pNameEntry->NameIsString) ? (PIMAGE_RESOURCE_DIR_STRING_U)(pBase + pNameEntry->NameOffset + resourceBase) : (PIMAGE_RESOURCE_DIR_STRING_U)(pNameEntry->Id);
                    entry.data = ntpe::RvaToRaw(pBase, pDataEntry->OffsetToData);
                    resources.push_back(entry);
                }
            }
        }

        return resources;
    }
    catch (std::exception&)
    {
        return std::nullopt;
    };
}

プロジェクトの全コードは私たちのgithubで見つけることができます:

https://github.com/SToFU-Systems/DSAVE

リソースパーサーは、リソースタイプ、その名前、および各リソースの言語識別子を指すポインタを含む構造体のベクターを返します。各構造体にはリソースデータへのポインタがあります。この構造体は、読み込んで解析した実行ファイルがメモリ内にある限り、有効です。これは、独自のアンチウイルスを作成し、ファイルをスキャンする際に非常に便利です。ファイルが解放されると、ポインタは無効になります。

 

使用されたツールのリスト

  1. PE Tools: https://github.com/petoolse/petools これはPEヘッダーフィールドを操作するためのオープンソースツールです。x86およびx64ファイルに対応しています。
  2. Resource Hacker: https://www.angusj.com/resourcehacker. これは32ビットおよび64ビットのWindowsアプリケーション用のリソースエディタです。実行可能ファイル(.exe, .dll, .scrなど)およびコンパイル済みリソースライブラリ(.res, .mui)のリソースを表示および編集することができます。

 

結論

それで全部です、友達!

私たちはWindowsオペレーティングシステムのPORTABLE_EXECUTABLEファイルのリソースを探索し、独自のシンプルだがかなり効果的なネイティブリソースパーサーを書きました!

皆様のサポートに感謝し、引き続き私たちのコミュニティでのご参加を楽しみにしています!

この記事の著者に関する質問は、次のメールアドレスまでお送りください: articles@stofu.io

ありがとうございます!