リバースエンジニアリングへの道
出田 守です。
最近、情報セキュリティに興味を持ち、『リバースエンジニアリング-Pythonによるバイナリ解析技法』という本(以降、「教科書」と呼びます)を読みました。
「こんな世界があるのか!かっこいい!」と感動し、私も触れてみたいということでド素人からリバースエンジニアリングができるまでを書いていきたいと思います。
ちなみに、教科書ではPython言語が使用されているので私もPython言語を使用しています。
ここを見ていただいた諸先輩方からの意見をお待ちしております。
軌跡
環境
OS: Windows10 64bit Home (日本語)
CPU: Intel® Core™ i3-6006U CPU @ 2.00GHz × 1
メモリ: 2048MB
Python: 3.6.5
私の環境は、普段Ubuntu16.04を使っていますが、ここではWindows10 64bitを仮想マシン上で立ち上げております。
ちなみに教科書では、Windowsの32bitで紹介されています。
さらっとPEフォーマット
前回はさらっとELFを学びました。ということはPEフォーマットについてもさらっと学ぼうということになります(私の本能がそう言っています笑)。なので今回はPEフォーマットをさらっと学びます。
では公式ドキュメント?とヘッダファイル、他のサイトページなどを参考にして進めていきます。
PEフォーマットとは
PE(Portable Executable)フォーマットは、現在Windowsで主流の実行(イメージ)ファイルフォーマットです。Windowsでは、実行ファイルをイメージファイルということが多いようです。
ちなみにドキュメントには他にもCOFF(Common Object File Format)という記載がありました。これについて調べてみました。ざっくりですがつまり、COFFが昔Unixで流行ってたけど色々問題があって、今では一部の派生バージョンとPEフォーマットが普及しているという状況らしいです。ということはPEフォーマットの中にCOFFが受け継がれているというイメージでしょうか。それともCOFFはオブジェクトファイルの事でしょうか。どちらにしても調べていくうちに分かってくるでしょう。
実際のPEフォーマットを確認するにはWindows SDKのwinnt.hを参照すると良いようです。私もこのヘッダファイルを参照していきます。
用語
ドキュメントでは最初用語解説をしているので、その一部を示します。
Name | Description |
---|---|
date/time stamp | スタンプはPEファイルあるいはCOFFファイルの様々な場所でそれぞれ違った用途で使用されます。多くは、各スタンプのフォーマットはCランタイムライブラリのtime関数と同じです。 |
file pointer | (オブジェクトファイルの場合)リンカと(イメージファイルの場合)ローダのよる処理の前の、ファイル自体のアイテムの位置。それ以外は、ディスクのファイルの位置を指します。 |
linker | Microsoft Visual Studioが提供するリンカを指します。 |
object file | リンカに入力として与えるファイルを指します。リンカはローダに入力するために使われるイメージファイルを生成します。 |
RVA(Relative virtual address | 相対仮想アドレスを指します。イメージファイルでは、アイテムのアドレスはイメージファイルのベースアドレスからのオフセットです。アイテムのRVAはほとんどの場合常にファイルポインタの位置がとは異なります。オブジェクトファイルではメモリに割り当てられないため意味がありません。この場合、RVAはリンク中に再配置が後で適用されるセクション内のアドレスを指します。簡単にするためには、コンパイラは各セクションのRVAをゼロに設定するだけです。 |
section | PEファイルあるいはCOFFファイル内の基本コードあるいはデータを指します。例えば、オブジェクトファイルのすべてのコードは単一のセクションにまとめることができ、あるいは(コンパイラの動作に応じて)各関数は独自のセクションを占有することができます。 |
VA | 仮想アドレスを指します。イメージファイルのベースアドレスからのオフセットではないということ以外はRVAと同じです。このアドレスは物理メモリとは独立した各プロセスに明確なVAスペースをWindowsが作成しているためVAと呼ばれます。ほとんどの場合で、VAをアドレスとみなすべきです。VAはローダが希望の場所にイメージをロードしない可能性があるため、RVAほど位置を予測できません。 |
PEフォーマット
ここからはPEフォーマットの中身を見ていきます。
ただ、めちゃくちゃヘッダとメンバの数が多そうなんですよ。これを一個一個調べていくのは面倒ですし、必要ないと思うのです。
なので必要そうなのを選択して調べていきます。それ以外のものは今後必要になったら学んでいくことにします。
めちゃめちゃ参考になりましたサイトを載せておきます。
http://www.atmarkit.co.jp/ait/articles/1202/17/news129.html
http://www.openrce.org/reference_library/files/reference/PE%20Format.pdf
https://qiita.com/cha1aza/items/f64dc4351517a2477ef1#ms-dos-headerms-dos-real-mode-stub-progrum
http://home.a00.itscom.net/hatada/mcc/doc/pe.html
http://hp.vector.co.jp/authors/VA050396/index.html
http://hp.vector.co.jp/authors/VA050396/tech_11.html
上記PDFも全体像が見られて便利ですね。もしかしたら古いかもしれませんが。
IMAGE_DOS_HEADER
typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header
WORD e_magic; // Magic number
WORD e_cblp; // Bytes on last page of file
WORD e_cp; // Pages in file
WORD e_crlc; // Relocations
WORD e_cparhdr; // Size of header in paragraphs
WORD e_minalloc; // Minimum extra paragraphs needed
WORD e_maxalloc; // Maximum extra paragraphs needed
WORD e_ss; // Initial (relative) SS value
WORD e_sp; // Initial SP value
WORD e_csum; // Checksum
WORD e_ip; // Initial IP value
WORD e_cs; // Initial (relative) CS value
WORD e_lfarlc; // File address of relocation table
WORD e_ovno; // Overlay number
WORD e_res[4]; // Reserved words
WORD e_oemid; // OEM identifier (for e_oeminfo)
WORD e_oeminfo; // OEM information; e_oemid specific
WORD e_res2[10]; // Reserved words
LONG e_lfanew; // File address of new exe header
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
- e_magic
マジックナンバー。0x5A4D(MZ)という値が格納される。 - e_lfanew
参考サイトより新しい形式のヘッダへのファイル内オフセットを示している。新しい形式のヘッダとはPE ファイルフォーマットなら NT ヘッダのことであり、e_lfanew は NT ヘッダへのオフセットということになる。そして、そのNTヘッダこそがPEファイルの真のヘッダということになる。MS-DOS Real-Mode Stub Programのサイズは可変長なので、このオフセットの値から新しい形式のヘッダをたどらなければいけない。
NT_HEADERS
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32
- Signature
4Byteのシグネチャで、"PE\0\0"の値を持ちます。 - FileHeader
IMAGE_FILE_HEADER構造体を指します。 - OptionalHeader
IMAGE_OPTIONAL_HEADER構造体を指します。
IMAGE_OPTIONAL_HEADER
typedef struct _IMAGE_OPTIONAL_HEADER {
//
// Standard fields.
//
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
DWORD BaseOfData;
//
// NT additional fields.
//
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;
- Magic
イメージファイルの状態を表します。各状態値を以下に示します。- IMAGE_NT_OPTIONAL_HDR_MAGIC32(0x10b)
32bitの実行可能イメージファイル。 - IMAGE_NT_OPTIONAL_HDR_MAGIC64(0x20b)
64bitの実行可能イメージファイル。 - IMAGE_ROM_OPTIONAL_HDR_MAGIC(0x107)
ROMイメージファイル。
- IMAGE_NT_OPTIONAL_HDR_MAGIC32(0x10b)
- AddressOfEntryPoint
ImageBaseアドレスを基準にしたエントリポイントのアドレス。
実行ファイルでは、これは開始アドレスを指します。デバイスドライバでは、これは初期化関数のアドレスを指します。DLLではエントリポイントはオプションです。これが必要ない時は0を指定します。
参考サイトによると実際のエントリポイントのアドレスはImageBaseにAddressOfEntryPointを加算することで得られるとのことです。 - ImageBase
メモリにロードされた時のイメージの最初のByteの希望するアドレス。つまりイメージファイルがメモリにロードされたアドレスです。この値は64KByteの倍数です。
デフォルトアドレスはDLLでは0x10000000に、アプリケーション(実行ファイル)では0x00400000に、Windows CE(組込み向けのOS)では0x00010000に配置されます。
イメージファイルのロードがImageBaseのアドレスで失敗した場合、再配置情報を持っていればそれを用いて別のアドレスにロードしてくれます。 - SectionAlignment
セクションがメモリにロードされる時の境界を指します。この値はFileAlignment値以上でなければなりません。デフォルト値はシステムのページサイズです。
参考サイトより例えば、セクションのサイズそのものが10byteでも、この値が0x1000だと次のセクションは0x1000に配置される。また、サイズが0x1010の場合には次のセクションは0x2000となる。
- FileAlignment
イメージファイルのセクションデータの境界を指します。値は、512〜64Kの間の2の累乗でなければなりません。デフォルトは512Byteです。
もしSectionAlignmentがシステムのページサイズより少なければ、FileAlignmentの値もSectionAlignmentに合わせる必要があります。 - SizeOfImage
すべてのヘッダを含むイメージのByteサイズ。SectionAlignmentの倍数である必要があります。
参考サイトよりまず最初にセクションが必要とするバイトを求め、それからページ境界に揃え、最終的にSectionAlignment境界に揃えたサイズの合計から算出される。
- SizeOfHeaders
FileAlignmentで指定された値の倍数に丸められた次のアイテムの合計サイズです。- IMAGE_DOS_HEADERのe_lfanew
- 4Byte signature
- IMAGE_FILE_HEADERのサイズ
- optional headerのサイズ
- すべてのセクションヘッダのサイズ
- CheckSum
イメージファイルのチェックサムです。IMAGEHELP.DLLに計算アルゴリズムがあるようです。 - SizeOfStackReserve
スタックとして予約されたByteサイズ。 - SizeOfStackCommit
スタックとしてコミットするByteサイズ。 - SizeOfHeapReserve
ヒープとして予約されたByteサイズ。 - SizeOfHeapCommit
ヒープとしてコミットするByteサイズ。 - DataDirectory
IMAGE_DATA_DIRECTORY構造体の最初のポインタ。
IMAGE_SECTION_HEADER
IMAGE_OPTIONAL_HEADERの直後に、IMAGE_SECTION_HEADERが続くようです。
#define IMAGE_SIZEOF_SHORT_NAME 8
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME];
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;
#define IMAGE_SIZEOF_SECTION_HEADER 40
- Name
8ByteのnullパディングUTF-8文字列のセクション名。 - Misc.PhysicalAddress
物理アドレス。 - Misc.VirtualSize
メモリへロードされた時のセクションの合計Byteサイズ。SizeOfRawDataよりもこの値が大きければ、セクションはゼロで埋められます。
このフィールドは実行可能イメージでのみ有効で、オブジェクトファイルでは0がセットされます。 - VirtualAddress
メモリへロードされた時のセクションの最初のByteのImageBaseからの相対アドレス。オブジェクトファイルでは、再配置が適用される前の最初のByteのアドレスを指します。 - SizeOfRawData
ディスクの初期化されたデータのByteサイズ。これはIMAGE_OPTIONAL_HEADERのFileAlignmentの倍数の値である必要があります。
この値がMisc.VirtualSizeよりも小さい場合、セクションの残りの部分はゼロで埋められます。 - PointerToRawData
COFFファイル内の最初のページのファイルポインタ。これはIMAGE_OPTIONAL_HEADERのFileAlignmentの倍数の値である必要があります。
未初期化データだけがセクションに格納されている場合、値にはゼロがセットされます。 - PointerToRelocations
セクションの再配置エントリの始点ファイルポインタ。再配置情報がない場合、値にはゼロがセットされます。 - PointerToLinenumbers
セクションの行番号エントリの始点ファイルポインタ。COFF行番号がない場合、値にはゼロがセットされます。 - NumberOfRelocations
セクションの再配置エントリの数。実行可能ファイルではこの値はゼロです。 - NumberOfLinenumbers
セクションの行番号エントリの数。 - Characteristics
参考サイトよりセクションの特性を表すフラグの組み合わせ。各フラグはIMAGE_SCN_XXXXとしてWinNT.hに定義されています。たとえば、コードが配置されるセクションなら、コードを含むIMAGE_SCN_CNT_CODEフラグ、読取専用を示すIMAGE_SCN_MEM_READフラグ、実行可能であることを示すIMAGE_SCN_MEM_EXECUTEフラグがセットされます。
IMAGE_DATA_DIRECTORY
IMAGE_OPTIONAL_HEADERのDataDirectoryのインデックスでIMAGE_DATA_DIRECTORYの意味が変わるようです。
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES 16
- VirtualAddress
テーブルの相対仮想アドレス。 - Size
テーブルのByteサイズ
以下にオフセットと意味を載せておきます。
[https://docs.microsoft.com/ja-jp/windows/desktop/api/winnt/ns-winnt-_image_data_directory より引用]
PEフォーマットについてはまだまだ深いことを学ばなければいけません。
今後も色々やっていくうちにPEフォーマット参照することも多くなってくると思うので、その都度学んでいきたいと思います。
今回はここまでにします。
まとめ
- PEフォーマットはWindowsで現在主流の実行ファイルフォーマット
- PEフォーマットではRVAの考え方が重要になってきそう
- IMAGE_OPTIONAL_HEADERのDataDirectoryのインデックスでIMAGE_DATA_DIRECTORYの意味が変わる