PE文件解析
什么是PE文件
Portable Executable文件 是一种可移植可执行文件格式
是windows中 exe(应用程序) dll(动态链接库) sys(驱动)等二进制文件的标准文件格式
PE Struct
通过010editor打开一个exe文件 使用exe模板
可以看见PE文件主要由 Dos头 Dos存根 Nt头 数据目录 节表数据
组成
Dos头
MZ头部
这是为了兼容DOS系统留下来的东西 其中比较重要的有e_magic
e_lfanew
标准结构体如下
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
文件开始的前两个字节 系统通过这个字段识别文件格式 固定为5A4D
这是我们判断文件是否是PE文件的一个参考
e_lfanew
指向nt头
00 00 01 00
对应到nt头
Dos Stub
不重要
紧跟在MZ头后面
在Dos环境下运行的程序
运行后输出This program cannot be run in DOS mode.
NT头
由签名 文件头 可选头组成
签名 固定的00004550
文件头
文件头结构如下
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;
数据类型 | 变量名 | 解释 |
---|---|---|
WORD | Machine | 目标平台 只能为几个固定的值 表明运行在什么架构 |
WORD | NumberOfSections | 节的数量 |
DWORD | TimeDateStamp | 时间戳 |
DWORD | PointerToSymbolTable | 符号表指针 通常为0 |
DWORD | NumberOfSymbols | 符号数量 通常为0 |
WORD | SizeOfOptionalHeader | 可选头大小 |
WORD | Characteristics | 文件特性 只能为几个固定的值 表明具体是什么PE文件 |
可选头
#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES 16
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
指定是32位还是64位 还是ROM
AddressOfEntryPoint
到程序入口点的RVA
ImageBase
加载到内存中的首选地址 也就是PE文件会被加载到进程的那个地址
SizeOfImage
在内存中加载的总大小 会进行内存对齐
SectionAlignment
内存对齐的大小 默认是0x1000
FileAlignment
文件对齐 默认是0x200
DataDirectory
IMAGE_DATA_DIRECTORY
结构体组成的数组
包含如导入表 导出表等的 RVA 和 大小
数据目录
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
保存了 RVA 和 大小
比较重要的有
导入表
列出依赖的dll及其中的函数
导出表
列出向外部提供的函数 一般是dll有
资源表
包含程序的资源数据 如 icon version
节表
实际存放数据的地方
比如常见的 .text
代码段 .data
数据段 .rsrc
资源段 .reloc
重定位表段
节表的数量由文件头中NumberOfSections
决定
结构如下
#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;
Name
节的名称
VirtualAddress
节的RVA 节在加载到内存后的位置
Characteristics
描述节的属性 包括是否可读写执行 如何对齐
PointerToRawData
指向节的文件位置
SizeOfRawData
对齐后大小
代码实现
这里不关心FOA 直接读到内存中用VA
1、 加载文件到内存
DWORD FileReadSize;
char path[] = "C:\\Windows\\System32\\calc.exe";
HANDLE hFile = CreateFileA(path, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
DWORD dwFileSize = GetFileSize(hFile, NULL);
LPVOID FileImage = VirtualAlloc(NULL, dwFileSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
ReadFile(hFile, FileImage, dwFileSize, &FileReadSize, NULL);
CloseHandle(hFile);
2、 获取nt头
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)FileImage;
PIMAGE_NT_HEADERS pNtHeader = (PIMAGE_NT_HEADERS)((DWORD64)FileImage+pDosHeader->e_lfanew);
3、 获取数据目录 要用什么就用对应的结构体 这里以导入表为例
void PrintImportTable(PIMAGE_IMPORT_DESCRIPTOR importTable, LPVOID fileImage) {
while (importTable->Name) {
const char* dllName = (const char*)((DWORD64)fileImage + importTable->Name);
std::cout << "DLL Name: " << dllName << std::endl;
// 遍历导入的函数
PIMAGE_THUNK_DATA thunk = (PIMAGE_THUNK_DATA)((DWORD64)fileImage + importTable->OriginalFirstThunk);
if (!thunk) {
thunk = (PIMAGE_THUNK_DATA)((DWORD64)fileImage + importTable->FirstThunk);
}
while (thunk->u1.AddressOfData) {
if (IMAGE_SNAP_BY_ORDINAL(thunk->u1.Ordinal)) {
std::cout << " Ordinal: " << IMAGE_ORDINAL(thunk->u1.Ordinal) << std::endl;
}
else {
PIMAGE_IMPORT_BY_NAME importByName = (PIMAGE_IMPORT_BY_NAME)((DWORD64)fileImage + thunk->u1.AddressOfData);
std::cout << " Function: " << importByName->Name << std::endl;
}
thunk++;
}
importTable++;
}
}
IMAGE_DATA_DIRECTORY dataDir = (IMAGE_DATA_DIRECTORY)pNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT];
PIMAGE_IMPORT_DESCRIPTOR importTable = (PIMAGE_IMPORT_DESCRIPTOR)((DWORD64)FileImage + dataDir.VirtualAddress);
PrintImportTable(importTable, FileImage);
4、 获取节表 遍历输出
WORD sectionNum = pNtHeader->FileHeader.NumberOfSections;
std::cout << "-------------------------------------------------------"<< std::endl;
PIMAGE_SECTION_HEADER pSecHeader = (PIMAGE_SECTION_HEADER)((DWORD64)pNtHeader+ sizeof(IMAGE_NT_HEADERS));
for (size_t i = 0; i < sectionNum; i++) {
PIMAGE_SECTION_HEADER currentPSecHeader = pSecHeader + i;
std::cout << "Section Name: " << currentPSecHeader->Name << std::endl;
}