PE文件头
说明
PE结构好文
windows下的各种类型定义
对pe文件头的解析
各种数据结构的定义都在 Winnt.h中
DOS头
typedef struct _IMAGE_DOS_HEADER {
WORD e_magic; // MZ头
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];
LONG e_lfanew; // PE头的开始位置
} IMAGE_DOS_HEADER,*PIMAGE_DOS_HEADER;
PE头
typedef struct _IMAGE_NT_HEADERS64 {
DWORD Signature; //PE头
IMAGE_FILE_HEADER FileHeader; //文件头
IMAGE_OPTIONAL_HEADER64 OptionalHeader; //可选头
} IMAGE_NT_HEADERS64,*PIMAGE_NT_HEADERS64;
文件头
IMAGE_FILE_HEADER
typedef struct _IMAGE_FILE_HEADER {
WORD Machine; //可执行文件的目标CPU类型
WORD NumberOfSections; //PE文件的节区个数
DWORD TimeDateStamp; //从1970.1.1到创建该文件时的秒数
DWORD PointerToSymbolTable;
DWORD NumberOfSymbols;
WORD SizeOfOptionalHeader; //可选头的大小
WORD Characteristics; //文件类型(dll...)
} IMAGE_FILE_HEADER,*PIMAGE_FILE_HEADER;
选项头
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; //PE(dos头,pe头,文件头...等所有头的)头部的大小
DWORD CheckSum; //校验和
WORD Subsystem; //
WORD DllCharacteristics;
ULONGLONG SizeOfStackReserve;
ULONGLONG SizeOfStackCommit;
ULONGLONG SizeOfHeapReserve;
ULONGLONG SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes; //固定值16,也就是下面的 IMAGE_NUMBEROF_DIRECTORY_ENTRIES
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
//数据目录表
} IMAGE_OPTIONAL_HEADER64,*PIMAGE_OPTIONAL_HEADER64;
选项头的最后就是下面这个结构体的数组
这个数组的每一项都描述了一个特定的空间(导入表,导出表...)
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
该数组每一项的意义
#define IMAGE_DIRECTORY_ENTRY_EXPORT 0
#define IMAGE_DIRECTORY_ENTRY_IMPORT 1
#define IMAGE_DIRECTORY_ENTRY_RESOURCE 2
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION 3
#define IMAGE_DIRECTORY_ENTRY_SECURITY 4
#define IMAGE_DIRECTORY_ENTRY_BASERELOC 5
#define IMAGE_DIRECTORY_ENTRY_DEBUG 6
#define IMAGE_DIRECTORY_ENTRY_COPYRIGHT 7
#define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE 7
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8
#define IMAGE_DIRECTORY_ENTRY_TLS 9
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 11
#define IMAGE_DIRECTORY_ENTRY_IAT 12
#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 13
#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14
15是预留位置
导出表
函数导出的方式有两种,一种是按名字导出,一种是按序号导出。这两种导出方式在导出表中的描述方式也不相同
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics; //保留
DWORD TimeDateStamp; //导出表的时间戳
WORD MajorVersion;
WORD MinorVersion;
DWORD Name; //模块的名字
DWORD Base; //如果是按序号导出,序号从base开始
DWORD NumberOfFunctions;
DWORD NumberOfNames; //按名字导出函数的数量
DWORD AddressOfFunctions; // 一个RVA,指向一个DWORD数组,数组中的每一项是一个导出函数的RVA,顺序与导出序号相同
DWORD AddressOfNames; // 一个RVA,依然指向一个DWORD数组,数组中的每一项仍然是一个RVA,指向一个表示函数名字
DWORD AddressOfNameOrdinals; // 一个RVA,还是指向一个WORD数组,数组中的每一项与AddressOfNames中的每一项对应,表示该名字的函数在AddressOfFunctions中的序号
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
比较绕
这里我们来实际操作一下,解析一下kernel32.dll的导出表
先解析可选头,把导出表的开始地址(virtual_address)和长度(size)解析出来
那么IMAGE_EXPORT_DIRECTORY的起始地址就是virtual_address
那么Name的偏移量处就是一个char*数组,这里就是KERNEL32.dll
注意计算出Name的结构体偏移后,要加上映射地址
这里其实比较简单,值得注意的有
1.AddressOfNameOrdinals指向的数组的每一项的需要加上Base,才是真正的序列号(这个base一般都是1)
2.通过函数名,在名字数组中找到对应的项,然后根据该项,在对应的序号数组中,拿到序号,最后通过序号拿到函数的地址
3.计算地址的时候比较绕,容易算错,多check
导入表
在PE文件加载时,会根据这个表里的内容加载依赖的DLL,并填充所需函数的地址
typedef struct _IMAGE_IMPORT_DESCRIPTOR{
union{
DWORD Characteristics;
DWORD OriginalFirstThunk;//导入名称表(INT)
};
DWORD TimeDateStamp; //时间戳
DWORD ForwarderChain; //在老版的绑定中,它引用API的第一个forwarder chain(传递器链表)。它可被设置为0xFFFFFFFF以代表没有forwarder。
DWORD Name; //dll名称
DWORD FirstThunk; //导入地址表(IAT)
}IMAGE_IMPORT_DESCRIPTOR;
在磁盘上时,INT和IAT的值一样(也有不一样的情况,此时IAT指向的内容已经被填充好),加载到内存时,IAT就会指向,由加载器填充好的真实的地址
这一块网上好像没有特别全的信息,可以在《解密黑客编程》IAT hook中寻找
先拿到导入表的起始地址和大小
然后在导入表起始位置有一个IMAGE_IMPORT_DESCRIPTOR数组,每个数组元素对应一个dll,以一个全0的结构体结束
INT和IAT都会指向一个IMAGE_THUNK_DATA32结构体数组,该数组以全0结构体结尾,
//OriginalFirstThunk和FirstThunk都指向的是_IMAGE_THUNK_DATA32结构体
//导入名称表最高位是0,就是名称导入,此时该值指向一个IMAGE_IMPORT_BY_NAME结构体,保存了名字
//最高位是1,就是序号导入
typedef struct _IMAGE_THUNK_DATA32 {
union {
DWORD ForwarderString; // PBYTE
DWORD Function; // PDWORD,导入函数的地址,在加载到内存后,这里才起作用
DWORD Ordinal; // 假如是序号导入的,会用到这里
DWORD AddressOfData; //PIMAGE_IMPORT_BY_NAME,假如是函数名导入的,用到这 里 ,它指向另外一个结 构体:PIMGE_IMPORT_BY_NAME
} u1;
} IMAGE_THUNK_DATA32;
typedef struct _IMAGE_IMPORT_BY_NAME{
WORD Hint; //序号
BYTE NAME[1]; //函数名,越界访问即可
}IMAGE_IMPORT_BY_NAME,*PIMAGE_IMPORT_BY_NAME;
节表
节的个数由文件头的NumberOfHeader给出
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;
地址转换
VA虚拟地址,也就是程序装载进内存的位置
RVA相对虚拟地址,地址 - 程序装载地址
FileOffset在文件中的地址
在磁盘上时,对齐 = 选项头.FileAlignment
在内存中时,对齐 = 选项头.SectionAlignment
装载入内存的起始地址 = 可选头.ImageBase
RVA到FileOffset的转换思路 : 查地址到当前节的偏移,然后通过查节头的地址,即可转换
实例分析
简单的c++代码,弹出一个MessageBox("hello")
DOS大小64字,末尾四个字就是PE头的开始位置:0x80 00 00 00
然后是264字的PE头:4字的PE标记符 + 20字的文件头 + 240字的可选头
把文件头提取出来
number of sections :0x 11 00 也就是17个
size of optional header: 0x f0 00 也就是240,选项头的大小要从节表读取
characteristics : 0x 27 00 ,也就是 0b00100111,标记该文件的类型(exe,dll,...)
然后就是选项头(一般的文件的选项头的结尾就在.text的前面)
这里要用64位的选项头分析
size of code 所有包含可执行属性的节的大小: 0x 00 1e 00 00
address of entry point 入口点 :0x 00 15 00 00
base of code 代码段的起始位置 : 0x 00 10 00 00
base of data 数据段的起始位置 : 0x 00 00 40 00
image base 文件被装入内存后的首选建议装载地址 :0x 00 00 00 00
section alignment 内存对齐 :0x 00 10 00 00
file alignment 磁盘对齐 : 0x 00 20 00 00
size of image 装入内存后的总大小 :
size of headers 整个PE(DOS头 + PE头 + 节表)头的大小 :
number og rva and sizes 数据目录项的个数 :
最后(4(DWORD 虚拟地址) + 4(DWROD size)) * 16字,也就是数据目录表 :IMAGE_DATA_DIRECTORY DataDirectory[16]
data directory 数据目录表 :0x 10 00 00 00 可以看到这里就是16个,似乎该值可以改动,如下
为什么这里是2
然后选项头后面就是节表,从前面可以知道这里有16个节表
地址计算
RVA 相对虚拟地址
VA 虚拟地址
offset 文件偏移
RVA = VA - image base
offset = 该数据所在的节偏移 + (RVA(该数据) - RVA(该数据所在的节))
PE查看器
解析的时候注意结构体的版本,在64位下和32位下有一些区间
前置知识
涉及各种指针操作,直接用宏封装,可以直接提取对应的成员,自动化寻址
/*
---- From XDU's mzb
*/
#include <bits/stdc++.h>
#include <Windows.h>
#include <Winnt.h>
using namespace std;
using ll = long long int;
#define offset(data,member) (size_t)(&(((typeof(data)*)0)->member))
#define point_shift(p,dis) reinterpret_cast<decltype(p)>(reinterpret_cast<BYTE*>(p) + dis)
#define get_addr(p,type,member) reinterpret_cast<decltype(type::member)*>(point_shift(p,offset(type,member)))
#define get_value(p,type,member) *(get_addr(p,type,member))
template<typename T>
void show_bytes(T* p)
{
auto show_16 = [](ll n) -> char
{
if (n <= 9)
return n + '0';
else
return n - 10 + 'A';
};
BYTE* base = reinterpret_cast<BYTE*>(p);
for (ll i = 0;i < ll(sizeof(T));i++)
{
ll val = ll(base[i]);
cout << setw(4) << show_16(val / 16)
<< show_16(val % 16);
if (i % 16 == 15 or i + 1 == sizeof(T))
cout << "\n";
}
}
struct PE
{
HANDLE h_file;
HANDLE h_map;
LPVOID base;
BOOL open_file(string file_name)
{
h_file = CreateFile(file_name.c_str(),
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL);
if (h_file == INVALID_HANDLE_VALUE)
{
return FALSE;
}
h_map = CreateFileMapping(h_file,
NULL,
PAGE_READWRITE | SEC_IMAGE,
0,0,0);
if (h_map == NULL)
{
CloseHandle(h_file);
return FALSE;
}
base = MapViewOfFile(h_map,
FILE_MAP_READ | FILE_SHARE_WRITE,
0,0,0);
if (base == NULL)
{
CloseHandle(h_map);
CloseHandle(h_file);
return FALSE;
}
return TRUE;
}
PE()
{
h_file = NULL;
h_map = NULL;
base = NULL;
}
~PE()
{
CloseHandle(h_map);
CloseHandle(h_file);
}
IMAGE_DOS_HEADER* get_dos_header();
IMAGE_NT_HEADERS64* get_pe_header();
IMAGE_FILE_HEADER* get_file_header();
IMAGE_OPTIONAL_HEADER64* get_optional_header();
vector<IMAGE_SECTION_HEADER> enum_section_header();
vector<tuple<WORD,string>> enum_export(); // 序号,函数名,函数地址
};
void show_hex(PE const& pe,ll n)
{
auto show_16 = [](ll n) -> char
{
if (n <= 9)
return n + '0';
else
return n - 10 + 'A';
};
BYTE* p = static_cast<BYTE*>(pe.base);
for (ll i = 0;i < 20;i++)
{
cout << setw(5) << i << " ";
for (ll k = 0;k < 16;k++)
{
ll val = ll(p[i * 16 + k]);
cout << setw(4) << show_16(val / 16)
<< show_16(val % 16);
}
cout << "\n";
}
}
IMAGE_DOS_HEADER* PE::get_dos_header()
{
IMAGE_DOS_HEADER* addr = static_cast<IMAGE_DOS_HEADER*>(base);
if (get_value(addr,IMAGE_DOS_HEADER,e_magic) != IMAGE_DOS_SIGNATURE)
{
return NULL;
}
else
{
return addr;
}
}
IMAGE_NT_HEADERS64* PE::get_pe_header()
{
auto dos_header = get_dos_header();
if (dos_header == NULL)
{
return nullptr;
}
IMAGE_NT_HEADERS64* ret = point_shift(reinterpret_cast<IMAGE_NT_HEADERS64*>(base),get_value(dos_header,IMAGE_DOS_HEADER,e_lfanew));
if (get_value(ret,IMAGE_NT_HEADERS64,Signature) != IMAGE_NT_SIGNATURE)
{
return nullptr;
}
else
{
return ret;
}
}
IMAGE_FILE_HEADER* PE::get_file_header()
{
auto pe_header = get_pe_header();
if (pe_header == nullptr)
{
return NULL;
}
else
{
return get_addr(pe_header,IMAGE_NT_HEADERS64,FileHeader);
}
}
IMAGE_OPTIONAL_HEADER64* PE::get_optional_header()
{
auto pe_header = get_pe_header();
if (pe_header == nullptr)
{
return NULL;
}
else
{
return get_addr(pe_header,IMAGE_NT_HEADERS64,OptionalHeader);
}
}
vector<IMAGE_SECTION_HEADER> PE::enum_section_header()
{
if (get_file_header() == nullptr)
{
return vector<IMAGE_SECTION_HEADER>();
}
auto num_of_sections = get_value(get_file_header(),IMAGE_FILE_HEADER,NumberOfSections);
auto size_of_optional = get_value(get_file_header(),IMAGE_FILE_HEADER,SizeOfOptionalHeader);
cout << "num of sections = " << num_of_sections << "\n";
cout << "size of optional = " << size_of_optional << "\n";
auto base = point_shift(reinterpret_cast<IMAGE_SECTION_HEADER*>(get_optional_header()),size_of_optional);
return vector<IMAGE_SECTION_HEADER>(base,base + num_of_sections);
}
vector<tuple<WORD,string>> PE::enum_export()
{
auto optional_header = get_optional_header();
if (optional_header == nullptr)
{
return vector<tuple<WORD,string>>();
}
auto data_directory = get_addr(optional_header,IMAGE_OPTIONAL_HEADER64,DataDirectory);
auto import = reinterpret_cast<IMAGE_DATA_DIRECTORY*>(data_directory);
auto virtual_address = get_value(import,IMAGE_DATA_DIRECTORY,VirtualAddress);
auto size = get_value(import,IMAGE_DATA_DIRECTORY,Size);
auto export_header = reinterpret_cast<IMAGE_EXPORT_DIRECTORY*>(point_shift(base,virtual_address));
auto name = get_value(export_header,IMAGE_EXPORT_DIRECTORY,Name);
auto number_of_names = get_value(export_header,IMAGE_EXPORT_DIRECTORY,NumberOfNames);
auto number_of_functions = get_value(export_header,IMAGE_EXPORT_DIRECTORY,AddressOfFunctions);
auto address_of_names = get_value(export_header,IMAGE_EXPORT_DIRECTORY,AddressOfNames);
auto address_of_name_ordinals = get_value(export_header,IMAGE_EXPORT_DIRECTORY,AddressOfNameOrdinals);
auto p_name = reinterpret_cast<DWORD*>(point_shift(base,address_of_names));
auto p_id = reinterpret_cast<DWORD*>(point_shift(base,address_of_name_ordinals));
vector<tuple<WORD,string>> ret;
for (ll i = 0;i < number_of_names;i++)
{
ret.emplace_back(get_value(export_header,IMAGE_EXPORT_DIRECTORY,Base) +
*(reinterpret_cast<WORD*>(point_shift(base,address_of_name_ordinals)) + i),
reinterpret_cast<char*>(point_shift(base,p_name[i])));
}
return ret;
}
int main()
{
PE pe;
string dll_name = "kernel32.dll";
if (pe.open_file(dll_name.c_str()) == 0)
{
return -1;
}
auto v = pe.enum_export();
for (auto it : v)
{
cout << "id = " << setw(10) << left << get<0>(it) << " "
<< "function " << setw(20) << left << get<1>(it) << "\n";
}
return 0;
}
本文来自博客园,作者:XDU18清欢,转载请注明原文链接:https://www.cnblogs.com/XDU-mzb/p/15415360.html