[C#]PE檔案格式簡易介紹與PE檔案的檢測

PE檔案是Portable Executable的簡稱,是windows系統中任何可執行模組或者DLL的檔案格式。這邊要介紹一下如何透過程式檢測檔案是否為合法的PE檔,因此必須針對PE檔的格式做些初步的介紹,就讓我們先來看一下PE檔的格式圖吧:

image

檔案最前端為MS-DOS Header,這邊可能會有人奇怪為什麼是叫MS-DOS Header,這是因為它並不是一個新的header,早在MS-DOS第二版時就有這個Header了,之所以會在一般Window下的PE檔保留有這個Header,是因為希望該PE檔在DOS下運行也能做些動作,也就是上面格式圖中MS-DOS Real-Mode Stub Program的部分,所以Window下的PE檔在DOS下運行時才可秀出“This program cannot be run in DOS mode.”訊息。

MS-DOS Header對應的是C++的_IMAGE_DOS_HEADER Structure,所以我們可以透過該Structure來進一步的了解MS-DOS Header的格式:

typedef struct _IMAGE_DOS_HEADER {  // DOS .EXE header
USHORT e_magic; // Magic number
USHORT e_cblp; // Bytes on last page of file
USHORT e_cp; // Pages in file
USHORT e_crlc; // Relocations
USHORT e_cparhdr; // Size of header in paragraphs
USHORT e_minalloc; // Minimum extra paragraphs needed
USHORT e_maxalloc; // Maximum extra paragraphs needed
USHORT e_ss; // Initial (relative) SS value
USHORT e_sp; // Initial SP value
USHORT e_csum; // Checksum
USHORT e_ip; // Initial IP value
USHORT e_cs; // Initial (relative) CS value
USHORT e_lfarlc; // File address of relocation table
USHORT e_ovno; // Overlay number
USHORT e_res[4]; // Reserved words
USHORT e_oemid; // OEM identifier (for e_oeminfo)
USHORT e_oeminfo; // OEM information; e_oemid specific
USHORT e_res2[10]; // Reserved words
LONG e_lfanew; // File address of new exe header
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;


裡面最重要的就是e_magic與e_lfanew這兩個成員。e_magic是主要是用來存放MS-DOS Header的簽章,所有兼容於DOS格式的PE檔其值皆會被設為0x5A4D,也就是ASCII的MZ,這邊之所以會是MZ而不是DOS之類比較好識別的字樣也是有其原因的,它其實是當初主導MS-DOS開發的Mark Zbikowski縮寫。

#define IMAGE_DOS_SIGNATURE             0x5A4D      // MZ



這也就是我們用十六進制編輯軟體來看執行檔時常會瞄到最前面有個MZ的關係。

image



至於e_lfanew則是一個偏移量,該偏移量表示PE Header的偏移位置,它位於PE檔0x3C的位置,所以我們可以透過這個偏移值來找到PE檔內的PE Header。

找到PE檔的PE Header位置後,我們可檢查前4Byte是否是PE00,若上面的MS-DOS Header那邊的檢查過了,這邊也是PE00的話,則代表它是合法的PE檔。

#define IMAGE_NT_SIGNATURE              0x00004550  // PE00

typedef struct _IMAGE_NT_HEADERS {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER OptionalHeader;
} IMAGE_NT_HEADERS, *PIMAGE_NT_HEADERS;

所以我們透過十六進位編輯器來看PE檔的話,先找到0x3C位置找到偏移值,依照偏移值找到的地方就會是PE00。

image

到這邊應該對PE檔的格式有個初步的了解了,依照這樣的概念可以知道要檢查一個PE檔是否合法,我們可以去檢查檔案中是否是MZ開頭與PE Header是否是PE00開頭。所以檢測的PE檔的程式寫起來會像下面這樣:

        public Boolean IsValidPEFile(string file)
{
using (FileStream s = new FileStream(file, FileMode.Open, FileAccess.Read))
{
using (BinaryReader r = new BinaryReader(s))
{
byte[] bytes = r.ReadBytes(2);

// Verify file starts with “MZ” signature.
if ((bytes[0] != 0x4d) || (bytes[1] != 0x5a))
{
// Not a PE file.
return false;
}

// OFFSET_TO_PE_HEADER_OFFSET = 0x3c
s.Seek(0x3c, SeekOrigin.Begin);

// read the offset to the PE Header
uint offset = r.ReadUInt32();

// go to the beginning of the PE Header
s.Seek(offset, SeekOrigin.Begin);

bytes = r.ReadBytes(4);

// Verify PE header starts with ‘PE\0\0’.
if ((bytes[0] != 0x50) || (bytes[1] != 0x45) || (bytes[2] != 0) || (bytes[3] != 0))
{
// Not a PE file.
return false;
}
return true;
}
}
}

這邊可以實際撰寫個較為完整的範例:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace PEFileCheckDemo
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}

private void button1_Click(object sender, EventArgs e)
{
if (openFileDialog1.ShowDialog() == System.Windows.Forms.DialogResult.OK)
{
string file = openFileDialog1.FileName;
MessageBox.Show((IsValidPEFile(file) ? “Valid” : “InValid”) + “ PE File!!”);
}
}

public Boolean IsValidPEFile(string file)
{
using (FileStream s = new FileStream(file, FileMode.Open, FileAccess.Read))
{
using (BinaryReader r = new BinaryReader(s))
{
byte[] bytes = r.ReadBytes(2);

// Verify file starts with “MZ” signature.
if ((bytes[0] != 0x4d) || (bytes[1] != 0x5a))
{
// Not a PE file.
return false;
}

// OFFSET_TO_PE_HEADER_OFFSET = 0x3c
s.Seek(0x3c, SeekOrigin.Begin);

// read the offset to the PE Header
uint offset = r.ReadUInt32();

// go to the beginning of the PE Header
s.Seek(offset, SeekOrigin.Begin);

bytes = r.ReadBytes(4);

// Verify PE header starts with ‘PE\0\0’.
if ((bytes[0] != 0x50) || (bytes[1] != 0x45) || (bytes[2] != 0) || (bytes[3] != 0))
{
// Not a PE file.
return false;
}
return true;
}
}
}
}
}

界面簡單的放個按鈕,按下後可選取要檢測的檔案

image

然後會彈出檢測的結果,像是若是Window下的執行檔應該會彈出”Valid PE File!!”,若是選取的是執行檔外的檔,像是文字檔之類的,應該會彈出”InValid PE File!!”。

image image

Link


  • Portable Executable


  • Peering Inside the PE: A Tour of the Win32 Portable Executable File Format


  • An In-Depth Look into the Win32 Portable Executable File Format


  • Exploring pe file headers using managed code


  • The Portable Executable File Format


  • x86 Disassembly/Windows Executable Files


  • [PDF]the PE format


  • [PPT]解析Windows 執行檔


  • [PDF]使用Windbg调试程序的一些基础知识


  • IMAGE_NT_HEADERS structure