ポータブル実行可能 (PE) 形式
まず始めるべきことはPE形式です。この形式の知識と理解は、Windowsプラットフォーム向けのアンチウイルスエンジンを開発するための前提条件です(歴史的に、世界中のウイルスの大多数はWindowsを対象としています)。
Portable Executable (PE) フォーマットは、Windows オペレーティングシステムが実行可能ファイル(.EXE や .DLL ファイルなど)を格納するために使用するファイルフォーマットです。1993年に Windows NT のリリースと共に導入され、以来 Windows システム上の実行ファイルの標準フォーマットとなっています。
PE形式が導入される前は、Windowsでは実行可能ファイルにさまざまな形式が使用されており、16ビットプログラム用のNew Executable (NE) 形式や32ビットプログラム用のCompact Executable (CE) 形式などがありました。これらの形式は独自のルールや慣習を持っており、オペレーティングシステムがプログラムを確実にロードして実行することを難しくしていました。
実行ファイルのレイアウトと構造を標準化するために、MicrosoftはWindows NTのリリースと共にPEフォーマットを導入しました。PEフォーマットは32ビットおよび64ビットのプログラムの両方に対応する共通のフォーマットとして設計されました。
PEフォーマットの主要な特徴の一つは、ファイルの始まりに位置し、実行可能ファイルについての重要な情報をオペレーティングシステムに提供する多くのフィールドを含んでいる標準化されたヘッダーの使用です。このヘッダーにはIMAGE_DOS_HEADERおよびIMAGE_NT_HEADER構造体が含まれており、これはIMAGE_FILE_HEADERとIMAGE_OPTIONAL_HEADERの二つの主要なセクションに分けられています。
PEフォーマットのヘッダーのほとんどはヘッダーファイルWinNT.hで宣言されています
IMAGE_DOS_HEADER
IMAGE_DOS_HEADER 構造体は、MS-DOSとの後方互換性をサポートするために使用されるレガシーヘッダーです。この構造体はMS-DOSに必要なファイル情報、例えばプログラムのコードやデータの位置、プログラムのエントリポイントなどを格納するために使用されます。これにより、PEファイルとしてコンパイルされた場合に、MS-DOS用に書かれたプログラムがWindows NT上で実行可能になります。
typedef struct _IMAGE_DOS_HEADER
{
WORD e_magic;
WORD e_cblp;
WORD e_cp;
WORD e_crlc;
WORD e_cparhdr;
WORD e_minalloc;
WORD e_maxalloc;
WORD e_ss;
WORD e_sp;
WORD e_csum;
WORD e_ip;
WORD e_cs;
WORD e_lfarlc;
WORD e_ovno;
WORD e_res[4];
WORD e_oemid;
WORD e_oeminfo;
WORD e_res2[10];
DWORD e_lfanew; // offset of IMAGE_NT_HEADER
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
次に興味深い分野があります:
-
e_magic フィールドは、ファイルが有効なPEファイルとして認識されるために使用されます。ご覧のとおり、e_magicフィールドはファイルの「マジックナンバー」を指定する16ビットの符号なし整数です。マジックナンバーは、ファイルが有効なPEファイルであることを識別する特別な値です。これは16進数の0x5A4Dに設定されており、これは文字「MZ」のASCII表現です(IMAGE_DOS_SIGNATURE)。
-
e_lfanew フィールドはIMAGE_NT_HEADERS 構造体の位置を指定するために使用されます。この構造体にはPEファイルのレイアウトと特性についての情報が含まれています。e_lfanewフィールドは、ファイル内のIMAGE_NT_HEADERS 構造体の位置を指定する32ビットの符号付き整数です。通常、ファイルの開始からの構造体までのオフセットに設定されています。
歴史
1980年代初頭、MicrosoftはMS-DOSと呼ばれる新しいオペレーティングシステムの開発に取り組んでいました。これは個人用コンピュータ向けに設計されたシンプルで軽量なオペレーティングシステムでした。MS-DOSの主要な特徴の一つは、実行可能ファイル(コンピュータで実行できるプログラム)を実行できる能力でした。
実行可能ファイルを簡単に識別するために、MS-DOSの開発者たちはすべての実行可能ファイルの始まりに特別な「マジックナンバー」を使用することに決めました。このマジックナンバーは、データファイルや設定ファイルなど他のタイプのファイルと実行可能ファイルを区別するために使われます。
MS-DOS チームの開発者であった Mark Zbikowski は、魔法の数字として「MZ」という文字を使用するアイデアを思いつきました。ASCIIコードで、「M」は16進数で0x4Dに、「Z」は16進数で0x5Aに表されます。これらの値を組み合わせると、魔法の数字0x5A4Dが得られ、これは「MZ」という文字のASCII表現です。
今日でも「MZ」署名は、Windowsオペレーティングシステムで使用される主な実行ファイル形式であるPEファイルを識別するために使用されています。これは、PEファイルの最初の構造であるIMAGE_DOS_HEADER 構造のe_magicフィールドに保存されています。
IMAGE_NT_HEADER
IMAGE_NT_HEADERは、1993年にリリースされたWindows NTオペレーティングシステムで導入されたデータ構造です。これは、実行可能ファイル(PEファイル)の内容を読み取り、解釈する標準的な方法をオペレーティングシステムに提供するために設計されました。
Windows NTのリリースに伴い、Microsoftは実行可能ファイルのレイアウトと構造を標準化する方法としてIMAGE_NT_HEADERを導入しました。これにより、オペレーティングシステムは単一のフォーマットのみをサポートすることで、プログラムのロードと実行が容易になりました。
https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-image_nt_headers32
https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-image_nt_headers64
typedef struct _IMAGE_NT_HEADERS32
{
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
typedef struct _IMAGE_NT_HEADERS64
{
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER64 OptionalHeader;
} IMAGE_NT_HEADERS64, *PIMAGE_NT_HEADERS64;
IMAGE_NT_HEADERは、Windowsオペレーティングシステムのすべてのポータブル実行可能(PE)ファイルの始まりに現れる構造です。この構造には、実行可能ファイルのサイズ、レイアウト、および意図された目的など、オペレーティングシステムに重要な情報を提供する多くのフィールドが含まれています。
IMAGE_NT_HEADER 構造体は、二つの主要なセクションに分けられています: IMAGE_FILE_HEADER とIMAGE_OPTIONAL_HEADER。
IMAGE_FILE_HEADER
IMAGE_FILE_HEADER は、マシンタイプ(例:x86、x64)、ファイルのセクション数、ファイル作成日時など、実行可能ファイル全体に関する情報を含んでいます。
https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-image_file_header
typedef struct _IMAGE_FILE_HEADER
{
WORD Machine;
WORD NumberOfSections;
DWORD TimeDateStamp;
DWORD PointerToSymbolTable;
DWORD NumberOfSymbols;
WORD SizeOfOptionalHeader;
WORD Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
この構造には以下のフィールドがあります:
-
Machine: このフィールドは、ファイルが構築されたターゲットアーキテクチャを指定します。このフィールドの値は、ファイルが構築されたときにコンパイラによって決定されます。いくつかの一般的な値は以下の通りです:
-
IMAGE_FILE_MACHINE_I386: このファイルはx86アーキテクチャ、または32ビット用に設計されています。
-
IMAGE_FILE_MACHINE_AMD64: このファイルはx64アーキテクチャ、または64ビット用に設計されています。
-
IMAGE_FILE_MACHINE_ARM: このファイルはARMアーキテクチャ用に設計されています。
-
-
NumberOfSections: このフィールドはPEファイルのセクション数を指定します。PEファイルは複数のセクションに分割され、各セクションにはコード、データ、リソースなどの異なるタイプの情報が含まれています。このフィールドは、オペレーティングシステムがファイル内のセクション数を決定するために使用されます。
-
TimeDateStamp: このフィールドにはファイルが構築された時のタイムスタンプが含まれます。タイムスタンプは、1970年1月1日 00:00:00 UTC 以来の秒数を表す4バイトの値として格納されます。このフィールドは、ファイルが最後に構築された時期を特定するのに役立ち、デバッグやバージョン管理に有用です。
-
PointerToSymbolTable: このフィールドは、存在する場合、COFF(Common Object File Format)シンボルテーブルのファイルオフセットを指定します。COFFシンボルテーブルには、関数名、変数名、行番号など、ファイルで使用されるシンボルに関する情報が含まれています。このフィールドはデバッグ目的でのみ使用され、リリースビルドでは通常存在しません。
-
NumberOfSymbols: このフィールドは、存在する場合、COFFシンボルテーブルのシンボル数を指定します。このフィールドはPointerToSymbolTableと連携して、ファイル内のCOFFシンボルテーブルを特定するために使用されます。
-
SizeOfOptionalHeader: このフィールドはオプショナルヘッダのサイズを指定します。オプショナルヘッダにはファイルについての追加情報が含まれており、通常はファイルのエントリポイント、インポートされたライブラリ、スタックとヒープのサイズ情報が含まれています。
-
Characteristics: このフィールドはファイルのさまざまな属性を指定します。一般的な値には以下のものがあります:
-
IMAGE_FILE_EXECUTABLE_IMAGE: このファイルは実行可能ファイルです。
-
IMAGE_FILE_DLL: このファイルは動的リンクライブラリ(DLL)です。
-
IMAGE_FILE_32BIT_MACHINE: このファイルは32ビットファイルです。
-
IMAGE_FILE_DEBUG_STRIPPED: このファイルはデバッグ情報が削除されています。
-
これらのフィールドは、オペレーティングシステムがファイルをメモリに読み込んで実行するときに使用されるファイルに関する重要な情報を提供します。IMAGE_FILE_HEADER構造のフィールドを理解することで、PEファイルの構造とオペレーティングシステムがそれをどのように使用するかについての理解が深まります。
各フィールドの可能な値のほとんどは、ヘッダーファイルWinNT.hで宣言されています。
IMAGE_OPTIONAL_HEADER
IMAGE_FILE_HEADER 構造体の後には、IMAGE_OPTIONAL_HEADER 構造体によって記述されるオプショナルヘッダが続きます。オプショナルヘッダには、エントリポイントのアドレス、イメージのサイズ、インポートディレクトリのアドレスなど、イメージに関する追加情報が含まれています。
https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-image_optional_header32
https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-image_optional_header64
typedef struct _IMAGE_OPTIONAL_HEADER32
{
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
DWORD BaseOfData;
DWORD ImageBase;
DWORD SectionAlignment;
DWORD FileAlignment;
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage;
DWORD SizeOfHeaders;
DWORD CheckSum;
WORD Subsystem;
WORD DllCharacteristics;
DWORD SizeOfStackReserve;
DWORD SizeOfStackCommit;
DWORD SizeOfHeapReserve;
DWORD SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
typedef struct _IMAGE_OPTIONAL_HEADER64
{
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
ULONGLONG ImageBase;
DWORD SectionAlignment;
DWORD FileAlignment;
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage;
DWORD SizeOfHeaders;
DWORD CheckSum;
WORD Subsystem;
WORD DllCharacteristics;
ULONGLONG SizeOfStackReserve;
ULONGLONG SizeOfStackCommit;
ULONGLONG SizeOfHeapReserve;
ULONGLONG SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER64, *PIMAGE_OPTIONAL_HEADER64;
以下はIMAGE_OPTIONAL_HEADER 構造の各フィールドの詳細な説明です:
-
Magic: このフィールドはPEファイルに存在するオプショナルヘッダーのタイプを指定します。最も一般的な値は32ビットファイルのIMAGE_NT_OPTIONAL_HDR32_MAGICまたは64ビットファイルのIMAGE_NT_OPTIONAL_HDR64_MAGICです。
-
MajorLinkerVersionとMinorLinkerVersion: これらのフィールドはファイルをビルドするために使用されたリンカーのバージョンを指定します。リンカーはオブジェクトファイルとライブラリを単一の実行ファイルに結合するために使用されるツールです。
-
SizeOfCode: このフィールドはファイル内のコードセクションのサイズを指定します。コードセクションには実行ファイルの機械語コードが含まれています。
-
SizeOfInitializedData: このフィールドはファイル内の初期化済みデータセクションのサイズを指定します。初期化済みデータセクションには、グローバル変数など、実行時に初期化されるデータが含まれています。
-
SizeOfUninitializedData: このフィールドはファイル内の未初期化データセクションのサイズを指定します。未初期化データセクションには、bssセクションなど、実行時に初期化されないデータが含まれています。
-
AddressOfEntryPoint: このフィールドはファイルのエントリポイントの仮想アドレスを指定します。エントリポイントはプログラムの開始アドレスであり、ファイルがメモリにロードされると最初に実行される命令です。
-
BaseOfCode: このフィールドはコードセクションの開始の仮想アドレスを指定します。
-
ImageBase: このフィールドはファイルがメモリにロードされるべき優先仮想アドレスを指定します。このアドレスはファイル内のすべての仮想アドレスのベースアドレスとして使用されます。
-
SectionAlignment: このフィールドはファイル内のセクションのアラインメントを指定します。ファイル内のセクションは通常、この値の倍数にアラインされ、パフォーマンスが向上します。
-
FileAlignment: このフィールドはディスク上のファイル内のセクションのアラインメントを指定します。ファイル内のセクションは通常、この値の倍数にアラインされ、ディスクのパフォーマンスが向上します。
-
MajorOperatingSystemVersionとMinorOperatingSystemVersion: これらのフィールドは、ファイルを実行するために必要なオペレーティングシステムの最小バージョンを指定します。
-
MajorImageVersionとMinorImageVersion: これらのフィールドはイメージのバージョンを指定します。イメージバージョンはバージョン管理目的でファイルのバージョンを識別するために使用されます。
-
MajorSubsystemVersionとMinorSubsystemVersion: これらのフィールド는ファイルを実行するために必要なサブシステムのバージョンを指定します。サブシステムは、ファイルが実行される環境であり、WindowsコンソールやWindows GUIなどです。
-
Win32VersionValue: このフィールドは予約されており、通常は0に設定されています。
-
SizeOfImage: このフィールドはメモリにロードされた際のイメージのサイズをバイト単位で指定します。
-
SizeOfHeaders: このフィールドはヘッダーのサイズをバイト単位で指定します。ヘッダーにはIMAGE_FILE_HEADERとIMAGE_OPTIONAL_HEADERが含まれます。
-
CheckSum: このフィールドはファイルの整合性をチェックするために使用されます。チェックサムはファイルの内容を合計してこのフィールドに格納することによって計算されます。チェックサムは改ざんや破損によるファイルの変更を検出するために使用されます。
-
Subsystem: このフィールドはファイルを実行するために必要なサブシステムを指定します。可能な値にはIMAGE_SUBSYSTEM_NATIVE、IMAGE_SUBSYSTEM_WINDOWS_GUI、IMAGE_SUBSYSTEM_WINDOWS_CUI、IMAGE_SUBSYSTEM_OS2_CUIなどがあります。
-
DllCharacteristics: このフィールドはファイルの特性を指定します。たとえば、それがダイナミックリンクライブラリ(DLL)であるか、ロード時に再配置が可能かどうかなどです。可能な値にはIMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE、IMAGE_DLLCHARACTERISTICS_NX_COMPATなどがあります。
-
SizeOfStackReserve: このフィールドはプログラム用に予約されているスタックのサイズをバイト単位で指定します。スタックは関数コール情報などの一時的なデータを格納するために使用されます。
-
SizeOfStackCommit: このフィールドはプログラム用にコミットされているスタックのサイズをバイト単位で指定します。コミットされたスタックはメモリで実際に予約されているスタックの部分です。
-
SizeOfHeapReserve: このフィールドはプログラム用に予約されているヒープのサイズをバイト単位で指定します。ヒープは実行時に動的にメモリを割り当てるために使用されます。
-
SizeOfHeapCommit: このフィールドはプログラム用にコミットされているヒープのサイズをバイト単位で指定します。コミットされたヒープはメモリで実際に予約されているヒープの部分です。
-
LoaderFlags: このフィールドは予約されており、通常は0に設定されています。
-
NumberOfRvaAndSizes: このフィールドはIMAGE_OPTIONAL_HEADER内のデータディレクトリエントリの数を指定します。データディレクトリにはファイルのインポート、エクスポート、リソースなどに関する情報が含まれています。
-
DataDirectory: このフィールドはファイル内のデータディレクトリの位置とサイズを指定するIMAGE_DATA_DIRECTORY構造の配列です
IMAGE_SECTION_HEADER
PE(ポータブル実行可能ファイル)の文脈においてセクションとは、特定のタイプのデータやコードを保持するファイル内の連続したメモリブロックです。PEファイルでは、セクションはコード、データ、リソースなど、ファイルの異なる部分を整理し保存するために使用されます。
PEファイルの各セクションは固有の名前を持ち、IMAGE_SECTION_HEADER 構造によって記述されます。これには、セクションのサイズ、位置、特性などの情報が含まれています。以下はIMAGE_SECTION_HEADERのフィールドです:
IMAGE_SECTION_HEADERは、Windowsオペレーティングシステムで使用されるPortable Executable (PE) ファイル形式で使用されるデータ構造です。PEファイル形式は、実行可能ファイル、DLL、およびWindowsオペレーティングシステムによってメモリにロードされる他のタイプのファイル用です。各セクションヘッダーは、ファイル内の連続するデータブロックを記述し、セクションの名前、セクションがロードされる仮想メモリアドレス、およびセクションのサイズなどの情報を含みます。セクションヘッダーは、コードやデータセクションなど、ファイルの特定の部分を特定し、アクセスするために使用できます。
IMAGE_SECTION_HEADER 構造体はWindows Platform SDKで定群されており、winnt.hヘッダーファイルで見つけることができます。以下はC++で定義されている構造体の例です:
#pragma pack(push, 1)
typedef struct _IMAGE_SECTION_HEADER
{
BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; // 8
union {
DWORD PhysicalAddress;
DWORD VirtualSize;
} Misc;
DWORD VirtualAddress;
DWORD SizeOfRawData;
DWORD PointerToRawData;
DWORD PointerToRelocations;
DWORD PointerToLinenumbers;
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
DWORD Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
#pragma pack(pop)
ご覧の通り、この構造体はC++のstructとして定義されており、セクションの名前、仮想サイズ、仮想アドレス、生データサイズ、生データへのポインター、再配置、行番号、再配置と行番号の数を含むフィールドがあります。さらに、Characteristicsフィールドには、そのセクションが実行可能か、読み取り可能か、書き込み可能かなど、セクションの特性を説明するフラグが含まれています。
-
Name:この8バイトの配列は、セクションの名前を指定するために使用されます。名前はヌル終端文字列であれば何でも良いですが、通常はファイルの異なる部分に意味のある名前を付けるために使用され、実行可能コードの場合は ".text"、初期化されたデータの場合は ".data"、読み取り専用データの場合は ".rdata"、初期化されていないデータの場合は ".bss" などがあります。セクションの名前は、オペレーティングシステムによってファイル内のセクションを特定するため、またデバッガーや他のツールによってセクションとその内容を識別するために使用されます。
-
VirtualSize:このフィールドは、メモリ内のセクションのサイズをバイト単位で指定します。この値は、ファイルがメモリにロードされたときにセクションが占めるメモリの量を表します。セクションの仮想サイズは、ファイルがメモリにロードされた際にセクションに割り当てる必要のあるメモリ量をオペレーティングシステムが決定するために使用されます。
-
VirtualAddress:このフィールドは、メモリ内のセクションの開始アドレスをバイト単位で指定します。この値は、セクションがメモリにロードされる開始アドレスであり、セクションがメモリ内のどの位置にロードされるかをオペレーティングシステムが決定するために使用されます。セクションの仮想アドレスは、ファイルがメモリにロードされるときにセクション内のアドレスを適切にメモリアドレスに変換するためにもオペレーティングシステムによって使用されます。
-
SizeOfRawData:このフィールドは、ファイル内のセクションのサイズをバイト単位で指定します。この値は、ファイル内にセクションが占めるスペースの量を表し、オペレーティングシステムがファイル内のセクションのサイズを決定するために使用されます。セクションの生データのサイズは、オペレーティングシステムによってファイル内のセクションを特定し、メモリにロードされる際のセクションのサイズを決定するために使用されます。
-
PointerToRawData:このフィールドは、ファイル内のセクションのオフセットをバイト単位で指定します。この値は、ファイル内のセクションの位置を表し、セクションのデータがどこにあるかを特定するために使用されます。セクションの生データへのポインターは、オペレーティングシステムによってファイル内のセクションを特定し、メモリにロードされる際のセクションの位置を決定するために使用されます。
-
PointerToRelocations:このフィールドは、セクションの再配置情報のオフセットをバイト単位で指定します。再配置情報は、ファイルがメモリにロードされるときにセクション内のアドレスを適切に解決するために修正するのに使用されます。セクションの再配置へのポインターは、オペレーティングシステムによってセクションの再配置情報を特定し、ファイルがメモリにロードされる際にセクション内のアドレスをどのように修正するかを決定するために使用されます。
-
PointerToLinenumbers:このフィールドは、セクションの行番号情報のオフセットをバイト単位で指定します。行番号情報はデバッグ目的で使用され、セクションを生成したソースコードに関する情報を提供します。セクションの行番号へのポインターは、デバッガーや他のツールによってセクションを生成したソースコードを識別し、セクションの内容に関する詳細情報を提供するために使用されます。
-
NumberOfRelocations:このフィールドは、セクションの再配置エントリの数を指定します。再配置エントリは、セクション内のアドレスを修正する方法を説明する記録であり、ファイルがメモリにロードされるときに適切に解決することができます。セクションの再配置数は、オペレーティングシステムによってセクションの再配置情報のサイズを決定し、ファイルがメモリにロードされる際に処理する必要のある再配置エントリの数を知るために使用されます。
-
NumberOfLinenumbers:このフィールドは、セクションの行番号エントリの数を指定します。行番号エントリは、セクションを生成したソースコードに関する情報を提供する記録であり、デバッグ目的で使用されます。セクションの行番号数は、デバッガーや他のツールによってセクションの行番号情報のサイズを決定し、ソースコードを生成したセクション에関する情報を得るために処理する必要のある行番号エントリの数を知るために使用されます。
-
Characteristics:このフィールドは、セクションの属性を指定するフラグのセットです。セクションに使用される一般的なフラグには、セクションに実行可能コードが含まれていることを示す IMAGE_SCN_CNT_CODE、初期化されたデータが含まれていることを示す IMAGE_SCN_CNT_INITIALIZED_DATA、初期化されていないデータが含まれていることを示す IMAGE_SCN_CNT_UNINITIALIZED_DATA、セクションが実行可能であることを示す IMAGE_SCN_MEM_EXECUTE、セクションが読み取り可能であることを示す IMAGE_SCN_MEM_READ、そしてセクションが書き込み可能であることを示す IMAGE_SCN_MEM_WRITE があります。これらのフラグは、オペレーティングシステムによってセクションのプロパティを決定し、ファイルがメモリにロードされるときにセクションをどのように扱うかを知るために使用されます。
これらのフィールドは、オペレーティングシステムや他のプログラムがファイルのメモリレイアウトを管理し、ファイルの特定の部分、たとえばコードセクションやデータセクションなどにアクセスして位置を特定するために使用されます。
重要:Portable Executable (PE) ファイル形式で使用されるIMAGE_NT_HEADER構造において、VirtualAddress と PhysicalAddress フィールドは異なるものを指します。
IMAGE_NT_HEADER 構造体が含まれるセクションが実行時にメモリ内でロードされる仮想アドレスを指定するために VirtualAddress フィールドが使用されます。このアドレスはプロセスのベースアドレスに対して相対的であり、プログラムがセクションのデータにアクセスするために使用されます。
PhysicalAddress フィールドは、PEファイル内の IMAGE_NT_HEADER 構造が含まれるセクションのファイルオフセットを指定するために使用されます。これは、オペレーティングシステムがメモリにロードされる際にファイル内のセクションのデータを検索するために使用されます。
すべてのヘッダーフィールドおよびIMAGE_NT_HEADER のオフセットは、メモリ用に定義されており、仮想アドレスで操作されます。ディスク上の任意のフィールドをオフセットする必要がある場合は、以下のコードのrva2offset 関数を使用して、仮想アドレスを物理アドレスに変換する必要があります。
要約すると、VirtualAddressはプログラムがメモリ内のセクションにアクセスするために使用され、PhysicalAddressはオペレーティングシステムがファイル内のセクションを特定するために使用されます。
インポート
プログラムがコンパイルされると、コンパイラはプログラムの関数のためのマシンコードを含むオブジェクトファイルを生成します。しかし、オブジェクトファイルにはプログラムを実行するために必要なすべての情報が含まれているとは限りません。例えば、オブジェクトファイルにはプログラム内で定義されていないが外部ライブラリによって提供される関数への呼び出しが含まれている可能性があります。
これがインポートテーブルの用途です。インポートテーブルは、プログラムの外部依存関係と、これらの依存関係からインポートする必要がある関数をリストアップします。動的リンカーは、実行時にこの情報を使用して、インポートされた関数のアドレスを解決し、それらをプログラムにリンクします。
例として、Windowsオペレーティングシステムからの関数を使用するプログラムを考えてみましょう。このプログラムは、画面上にメッセージボックスを表示するuser32.dllライブラリからMessageBox関数への呼び出しを含むかもしれません。MessageBox関数のアドレスを解決するために、プログラムはインポートテーブルにuser32.dllのインポートを含める必要があります。
同様に、プログラムがサードパーティのライブラリの関数を使用する必要がある場合、そのライブラリのインポートをインポートテーブルに含める必要があります。例えば、OpenSSLライブラリの関数を使用するプログラムは、そのインポートテーブルに libssl.dll ライブラリのインポートを含めるでしょう。
IMAGE_IMPORT_DIRECTORY
IMAGE_IMPORT_DIRECTORY は、Windows オペレーティングシステムがダイナミックリンクライブラリ (DLL) からポータブル実行ファイル (PE) へ関数やデータをインポートするために使用されるデータ構造です。これはIMAGE_DATA_DIRECTORYの一部であり、PEファイルのIMAGE_OPTIONAL_HEADER に保存されているデータ構造のテーブルです。
IMAGE_IMPORT_DIRECTORY は、Windows ローダーによって使用され、PEファイルが使用するインポートされた関数およびデータを解決します。これは、インポートされた関数とデータのアドレスをDLL内の対応する関数およびデータのアドレスにマッピングすることによって行います。これにより、PEファイルはDLLの関数およびデータをPEファイル自体の一部であるかのように使用することができます。
IMAGE_IMPORT_DIRECTORYは、PEファイルによってインポートされる単一のDLLを記述する一連のIMAGE_IMPORT_DESCRIPTOR構造体で構成されています。各IMAGE_IMPORT_DESCRIPTOR構造体は、以下のフィールドを含んでいます:
-
OriginalFirstThunk: インポートされた関数のテーブルへのポインター。
-
TimeDateStamp: DLLが最後に更新された日時。
-
ForwarderChain: 転送されたインポート関数のチェーン。
-
Name: null終端文字列としてのDLLの名前。
-
FirstThunk: DLLにバインドされているインポート関数のテーブルへのポインター。
typedef struct _IMAGE_IMPORT_DESCRIPTOR
{
union {
DWORD Characteristics; // 0 for terminating null import descriptor
DWORD OriginalFirstThunk; // RVA to original unbound IAT (PIMAGE_THUNK_DATA)
} DUMMYUNIONNAME;
DWORD TimeDateStamp; // 0 if not bound,
// -1 if bound, and real date ime stamp
// in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)
// O.W. date/time stamp of DLL bound to (Old BIND)
DWORD ForwarderChain; // -1 if no forwarders
DWORD Name;
DWORD FirstThunk; // RVA to IAT (if bound this IAT has actual addresses)
} IMAGE_IMPORT_DESCRIPTOR;
OriginalFirstThunk テーブル(またはOriginalFirstThunk が0の場合はFirstThunk )
オフセットのテーブルによって指される文字列 (OriginalFirstThunk テーブルまたはFirstThunk (OriginalFirstThunk が0の場合))
仕組みはどのようになっていますか?
Microsoftによって実装されたインポートメカニズムはコンパクトで美しいです!
アプリケーションが使用するサードパーティライブラリ(Windowsシステムのものを含む)のすべての関数のアドレスは、特別なテーブル - the import tableに保存されます。このテーブルはモジュールがロードされたときに満たされます(インポートを満たす他のメカニズムについては後で話し合います)。
さらに、サードパーティライブラリから関数が呼び出されるたびに、コンパイラは通常以下のコードを生成します:
call dword ptr [__cell_with_address_of_function] // for x86 architecture
call qword ptr [__cell_with_address_of_function] // for x64 architecture
したがって、ライブラリから関数を呼び出すためには、システムローダーはイメージの一か所にこの関数のアドレスを一度だけ書き込む必要があります。
C++ パーサー
そして今、実行可能ファイルのインポートテーブルの最もシンプルなパーサー(x86およびx64と互換性あり)を作成しましょう!
#include "stdafx.h"
/*
*
* Copyright (C) 2022, SToFU Systems S.L.
* All rights reserved.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
*/
namespace ntpe
{
static constexpr uint64_t g_kRvaError = -1;
// These types is defined in NTPEParser.h
// typedef std::map< std::string, std::set< std::string >> IMPORT_LIST;
// typedef std::vector< IMAGE_SECTION_HEADER > SECTIONS_LIST;
//**********************************************************************************
// FUNCTION: alignUp(DWORD value, DWORD align)
//
// ARGS:
// DWORD value - value to align.
// DWORD align - alignment.
//
// DESCRIPTION:
// Aligns argument value with the given alignment.
//
// Documentation links:
// Alignment: https://learn.microsoft.com/en-us/cpp/cpp/alignment-cpp-declarations?view=msvc-170
//
// RETURN VALUE:
// DWORD aligned value.
//
//**********************************************************************************
DWORD alignUp(DWORD value, DWORD align)
{
DWORD mod = value % align;
return value + (mod ? (align - mod) : 0);
};
//**********************************************************************************
// FUNCTION: rva2offset(IMAGE_NTPE_DATA& ntpe, DWORD rva)
//
// ARGS:
// IMAGE_NTPE_DATA& ntpe - data from PE file.
// DWORD rva - relative virtual address.
//
// DESCRIPTION:
// Parse RVA (relative virtual address) to offset.
//
// RETURN VALUE:
// int64_t offset.
// g_kRvaError (-1) in case of error.
//
//**********************************************************************************
int64_t rva2offset(IMAGE_NTPE_DATA& ntpe, DWORD rva)
{
/* retrieve first section */
try
{
/* if rva is inside MZ header */
PIMAGE_SECTION_HEADER sec = ntpe.sectionDirectories;
if (!ntpe.fileHeader->NumberOfSections || rva < sec->VirtualAddress)
return rva;
/* walk on sections */
for (uint32_t sectionIndex = 0; sectionIndex < ntpe.fileHeader->NumberOfSections; sectionIndex++, sec++)
{
/* count section end and allign it after each iteration */
DWORD secEnd = ntpe::alignUp(sec->Misc.VirtualSize, ntpe.SecAlign) + sec->VirtualAddress;
if (sec->VirtualAddress <= rva && secEnd > rva)
return rva - sec->VirtualAddress + sec->PointerToRawData;
};
}
catch (std::exception&)
{
}
return g_kRvaError;
};
//**********************************************************************************
// FUNCTION: getNTPEData(char* fileMapBase)
//
// ARGS:
// char* fileMapBase - the starting address of the mapped file.
//
// DESCRIPTION:
// Parses following data from mapped PE file.
//
// Documentation links:
// PE format structure: https://learn.microsoft.com/en-us/windows/win32/debug/pe-format
//
// RETURN VALUE:
// std::optional< IMAGE_NTPE_DATA >.
// std::nullopt in case of error.
//
//**********************************************************************************
#define initNTPE(HeaderType, cellSize) \
{ \
char* ntstdHeader = (char*)fileHeader + sizeof(IMAGE_FILE_HEADER); \
HeaderType* optHeader = (HeaderType*)ntstdHeader; \
data.sectionDirectories = (PIMAGE_SECTION_HEADER)(ntstdHeader + sizeof(HeaderType)); \
data.SecAlign = optHeader->SectionAlignment; \
data.dataDirectories = optHeader->DataDirectory; \
data.CellSize = cellSize; \
}
std::optional< IMAGE_NTPE_DATA > getNTPEData(char* fileMapBase, uint64_t fileSize)
{
try
{
/* PIMAGE_DOS_HEADER from starting address of the mapped view*/
PIMAGE_DOS_HEADER dosHeader = (IMAGE_DOS_HEADER*)fileMapBase;
/* return std::nullopt in case of no IMAGE_DOS_SIGNATUR signature */
if (dosHeader->e_magic != IMAGE_DOS_SIGNATURE)
return std::nullopt;
/* PE signature adress from base address + offset of the PE header relative to the beginning of the file */
PDWORD peSignature = (PDWORD)(fileMapBase + dosHeader->e_lfanew);
if ((char*)peSignature <= fileMapBase || (char*)peSignature - fileMapBase >= fileSize)
return std::nullopt;
/* return std::nullopt in case of no PE signature */
if (*peSignature != IMAGE_NT_SIGNATURE)
return std::nullopt;
/* file header address from PE signature address */
PIMAGE_FILE_HEADER fileHeader = (PIMAGE_FILE_HEADER)(peSignature + 1);
if (fileHeader->Machine != IMAGE_FILE_MACHINE_I386 &&
fileHeader->Machine != IMAGE_FILE_MACHINE_AMD64)
return std::nullopt;
/* result IMAGE_NTPE_DATA structure with info from PE file */
IMAGE_NTPE_DATA data = {};
/* base address and File header address assignment */
data.fileBase = fileMapBase;
data.fileHeader = fileHeader;
/* addresses of PIMAGE_SECTION_HEADER, PIMAGE_DATA_DIRECTORIES, SectionAlignment, CellSize depending on processor architecture */
switch (fileHeader->Machine)
{
case IMAGE_FILE_MACHINE_I386:
initNTPE(IMAGE_OPTIONAL_HEADER32, 4);
return data;
case IMAGE_FILE_MACHINE_AMD64:
initNTPE(IMAGE_OPTIONAL_HEADER64, 8);
return data;
}
}
catch (std::exception&)
{
}
return std::nullopt;
}
//**********************************************************************************
// FUNCTION: getImportList(IMAGE_NTPE_DATA& ntpe)
//
// ARGS:
// IMAGE_NTPE_DATA& ntpe - data from PE file.
//
// DESCRIPTION:
// Retrieves IMPORT_LIST(std::map< std::string, std::set< std::string >>) with all loaded into PE libraries names and imported functions.
// Map key: loaded dll's names.
// Map value: set of imported functions names.
//
// Documentation links:
// Import Directory Table: https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#import-directory-table
//
// RETURN VALUE:
// std::optional< IMPORT_LIST >.
// std::nullopt in case of error.
//
//**********************************************************************************
std::optional< IMPORT_LIST > getImportList(IMAGE_NTPE_DATA& ntpe)
{
try
{
/* if no imaage import directory in file returns std::nullopt */
if (ntpe.dataDirectories[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress == 0)
return std::nullopt;
IMPORT_LIST result;
/* import table offset */
DWORD impOffset = rva2offset(ntpe, ntpe.dataDirectories[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);
/* imoprt table descriptor from import table offset + file base adress */
PIMAGE_IMPORT_DESCRIPTOR impTable = (PIMAGE_IMPORT_DESCRIPTOR)(impOffset + ntpe.fileBase);
/* while names in import table */
while (impTable->Name != 0)
{
/* pointer to DLL name from offset of current section name + file base adress */
std::string modname = rva2offset(ntpe, impTable->Name) + ntpe.fileBase;
std::transform(modname.begin(), modname.end(), modname.begin(), ::toupper);
/* start adress of names in look up table from import table name RVA */
char* cell = ntpe.fileBase + ((impTable->OriginalFirstThunk) ? rva2offset(ntpe, impTable->OriginalFirstThunk) : rva2offset(ntpe, impTable->FirstThunk));
/* while names in look up table */
for (;; cell += ntpe.CellSize)
{
int64_t rva = 0;
/* break if rva = 0 */
memcpy(&rva, cell, ntpe.CellSize);
if (!rva)
break;
/* if rva > 0 function was imported by name. if rva < 0 function was imported by ordinall */
if (rva > 0)
result[modname].emplace(ntpe.fileBase + rva2offset(ntpe, rva) + 2);
else
result[modname].emplace(std::string("#ord: ") + std::to_string(rva & 0xFFFF));
};
impTable++;
};
return result;
}
catch (std::exception&)
{
return std::nullopt;
}
};
//**********************************************************************************
// FUNCTION: getImportList(IMAGE_NTPE_DATA& ntpe)
//
// ARGS:
// std::wstring_view filePath - path to file.
//
// DESCRIPTION:
// Retrieves IMPORT_LIST(std::map< std::string, std::set< std::string >>) with all loaded into PE libraries names and imported functions bu path.
// Map key: loaded dll's names.
// Map value: set of imported functions names.
//
// Documentation links:
// Import Directory Table: https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#import-directory-table
//
// RETURN VALUE:
// std::optional< IMPORT_LIST >.
// std::nullopt in case of error.
//
//**********************************************************************************
std::optional< IMPORT_LIST > getImportList(std::wstring_view filePath)
{
std::vector< char > buffer;
/* obtain base address of mapped file from tools::readFile function */
bool result = tools::readFile(filePath, buffer);
/* return nullopt if readFile failes or obtained buffer is empty */
if (!result || buffer.empty())
return std::nullopt;
/* get IMAGE_NTPE_DATA from base address of mapped file */
std::optional< IMAGE_NTPE_DATA > ntpe = getNTPEData(buffer.data(), buffer.size());
if (!ntpe)
return std::nullopt;
/* return result of overloaded getImportList function with IMAGE_NTPE_DATA as argument */
return getImportList(*ntpe);
}
//**********************************************************************************
// FUNCTION: getSectionsList(IMAGE_NTPE_DATA& ntpe)
//
// ARGS:
// IMAGE_NTPE_DATA& ntpe - data from PE file.
//
// DESCRIPTION:
// Retrieves SECTIONS_LIST from IMAGE_NTPE_DATA.
// SECTIONS_LIST - vector of sections headers from portable executable file.
// Sections names exmaple: .data, .code, .src
//
// Documentation links:
// IMAGE_SECTION_HEADER: https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-image_section_header
// Section Table (Section Headers): https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#section-table-section-headers
//
// RETURN VALUE:
// std::optional< SECTIONS_LIST >.
// std::nullopt in case of error.
//
//**********************************************************************************
std::optional< SECTIONS_LIST > getSectionsList(IMAGE_NTPE_DATA& ntpe)
{
try
{
/* result vector of section directories */
SECTIONS_LIST result;
/* iterations through all image section headers poiners in IMAGE_NTPE_DATA structure */
for (uint64_t sectionIndex = 0; sectionIndex < ntpe.fileHeader->NumberOfSections; sectionIndex++)
{
/* pushing IMAGE_SECTION_HEADER from iamge section headers */
result.push_back(ntpe.sectionDirectories[sectionIndex]);
}
return result;
}
catch (std::exception&)
{
}
/* returns nullopt in case of error */
return std::nullopt;
}
}
プロジェクトの全コードは私たちのgithubで見ることができます:
https://github.com/SToFU-Systems/DSAVE
使用されたツールのリスト
- PE Tools: https://github.com/petoolse/petools これはPEヘッダーフィールドを操作するためのオープンソースツールです。x86およびx64ファイルをサポートしています。
- WinDbg: https://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/debugger-download-tools マイクロソフトのシステムデバッガー。Windows OSのシステムプログラマーにとって欠かせないツールです。
- x64Dbg: https://x64dbg.com シンプルで軽量なオープンソースのx64/x86デバッガーです。
- WinHex: http://www.winhex.com/winhex/hex-editor.html WinHexは、コンピュータフォレンジック、データ回復、ローレベルデータ編集の分野で特に役立つユニバーサルヘックスエディタです。
次は何か?
皆様のサポートに感謝し、引き続きコミュニティへのご参加を楽しみにしています
次の記事では、皆さんと一緒にファジーハッシングモジュールを作成し、ブラックリストとホワイトリストについても触れながら、最も単純なインポートテーブルアナライザーについても取り上げます。
この記事の著者に関する質問は、以下のメールアドレスに送信できます:articles@stofu.io
ご注意いただきありがとうございます。良い一日をお過ごしください!