PE结构解析

本文详细介绍了PE文件的结构,包括DOS头、NT头、标准PE头、可选PE头、节表、导出表、导入表、重定位表和绑定导入表。探讨了各部分的字段含义和作用,如DOS头的64个字节、标准PE头的20个字节以及节表的40个字节等,并提及了地址转换和RVA的概念。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一. DOS头

原来为DOS系统使用,现在只需要记住如下两项。

1、DOC头:						
						
WORD   e_magic                *				"MZ标记" 用于判断是否为可执行文件.		
DWORD  e_lfanew;              *				PE头相对于文件的偏移,用于定位PE文件		
						

 

如上图所示,DOS头一共40H个字节,即64个字节。

结尾处4个字节指向了标准PE头的位置:D8。在这中间的文字是编译器加入的一些描述信息。这些字节是可以修改的。

 

二. NT头

NT头中包括了标准PE头和可选PE头。

struct _IMAGE_NT_HEADERS {
0x00 DWORD Signature;  //E8的位置,也就是DOS头中e_lfanew指向的位置。
0x04 _IMAGE_FILE_HEADER FileHeader;
0x18 _IMAGE_OPTIONAL_HEADER OptionalHeader;
};

 

三. 标准PE头

标准PE头一共20个字节。

						
2、标准PE头:						
						
WORD    Machine;              *        程序运行的CPU型号:0x0 任何处理器/0x14C 386及后续处理器		
WORD    NumberOfSections;     *        文件中存在的节的总数,如果要新增节或者合并节 就要修改这个值.		
DWORD   TimeDateStamp;        *        时间戳:文件的创建时间(和操作系统的创建时间无关),编译器填写的.		
DWORD   PointerToSymbolTable; 						
DWORD   NumberOfSymbols; 						
WORD    SizeOfOptionalHeader; *        可选PE头的大小,32位PE文件默认E0h 64位PE文件默认为F0h  大小可以自定义.		
WORD    Characteristics;      *        每个位有不同的含义,可执行文件值为10F 即0 1 2 3 8位置1 		
						

将可以修改的位置改为F

如果Machine清零会提示版本过低,无法在此电脑上运行。 

NumberOfSections:清零或者改为不正确的值仍然无法运行。

SizeOfOptionalHeader,Characteristics均不可清零。

Characteristics 每个位有不同的含义,可执行文件值为10F 即0 1 2 3 8位置1。

(此表只描述了15位的信息。不确定是否16位全部表示信息。)

 

用鼠标选中的部分就是标准PE头,一共20个字节。打开的这个软件的标准PE头的位置和前一个就不同了,DOS的末尾将指向了100H这个位置。

 

四. 可选PE头 

可选PE头的大小,32位PE文件默认E0h 64位PE文件默认为F0h  大小可以自定义.        

3、可选PE头:	

WORD    Magic;        *				说明文件类型:10B 32位下的PE文件     20B 64位下的PE文件
BYTE    MajorLinkerVersion;				
BYTE    MinorLinkerVersion;				
DWORD   SizeOfCode;*				所有代码节的和,必须是FileAlignment的整数倍 编译器填的  没用
DWORD   SizeOfInitializedData;*		        已初始化数据大小的和,必须是FileAlignment的整数倍 编译器填的  没用
DWORD   SizeOfUninitializedData;*               未初始化数据大小的和,必须是FileAlignment的整数倍 编译器填的  没用
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;*				内存中整个PE文件的映射的尺寸,可以比实际的值大,但必须是SectionAlignment的整数倍
DWORD   SizeOfHeaders;*				所有头+节表按照文件对齐后的大小,否则加载会出错
DWORD   CheckSum;*				校验和,一些系统文件有要求.用来判断文件是否被修改. 每两个字节作为一个数,循环相加
WORD    Subsystem;				
WORD    DllCharacteristics;				
DWORD   SizeOfStackReserve;*				初始化时保留的堆栈大小 
DWORD   SizeOfStackCommit;*				初始化时实际提交的大小 
DWORD   SizeOfHeapReserve;*				初始化时保留的堆大小 
DWORD   SizeOfHeapCommit;*				初始化时实践提交的大小 
DWORD   LoaderFlags;				
DWORD   NumberOfRvaAndSizes;*				目录项数目
_IMAGE_DATA_DIRECTORY DataDirectory[16];

最后一项为数据目录,一共16个,第16个未定义。

 

将校验和的最高位改成11 22这样就没事,改为FF AA这样程序将会无法启动。

 

五. 节表

每个节表有28H个字节,即40个字节。在所有节表的末尾编译器还会添加40个0,标志着整个结构的结束。

#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;	

1. 节表的名字, 8个字节。

2. Misc  双字 是该节在没有对齐前的真实尺寸,该值可以不准确。

3. VirtualAddress 节区在内存中的偏移地址。加上ImageBase才是在内存中的真正地址.

4. SizeOfRawData  节在文件中对齐后的尺寸.

5. PointerToRawData 节区在文件中的偏移.

6. PointerToRelocations 在obj文件中使用 对exe无意义

7. PointerToLinenumbers 行号表的位置 调试的时候使用

8. NumberOfRelocations 在obj文件中使用  对exe无意义

9. NumberOfLinenumbers 行号表中行号的数量 调试的时候使用

10、Characteristics 节的属性

 

六. 导出表

typedef struct _IMAGE_EXPORT_DIRECTORY {				
    DWORD   Characteristics;				// 未使用
    DWORD   TimeDateStamp;				// 时间戳
    WORD    MajorVersion;				// 未使用
    WORD    MinorVersion;				// 未使用
    DWORD   Name;				// 指向该导出表文件名字符串
    DWORD   Base;				// 导出函数起始序号
    DWORD   NumberOfFunctions;				// 所有导出函数的个数
    DWORD   NumberOfNames;				// 以函数名字导出的函数个数
    DWORD   AddressOfFunctions;     // 导出函数地址表RVA				
    DWORD   AddressOfNames;         // 导出函数名称表RVA				
    DWORD   AddressOfNameOrdinals;  // 导出函数序号表RVA				
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;				

NumberOfFunctions:大于等于导出函数的个数。因为这个值 = .def末尾序号 - 首个序号,如果自定义的序号不是连续的,那么这个值自然会大于导出函数的个数。

末尾三个值都是RVA地址,可以写一个RVA ---> FOA的转换函数。AddressOfNames表里的地址也是RVA的,同样需要转换。

Base:为导出函数的其实序号,即给出的序号中第一个、最小那一个。

 

找(导出)函数的两种方式:

1. 通过名字导出 AddressOfNames表中获得序号,找对应位置的AddressOfOrdinals位置的值,这个值就指向了AddressOfFunctions表的下标,这个位置存储的就是名字对应的函数地址。

2. 通过序号导出 这里所说的序号也是就.def文件中定义的序号。序号 - 第一个序号 = AddressOfFunctions表的下标。

注意:所有的地址都是RVA都需要转换为FOA。

 

导入表

typedef struct _IMAGE_IMPORT_DESCRIPTOR {										
    union {										
        DWORD   Characteristics;           										
        DWORD   OriginalFirstThunk;         						//RVA 指向IMAGE_THUNK_DATA结构数组				
    };										
    DWORD   TimeDateStamp;               						//时间戳				
    DWORD   ForwarderChain;              										
    DWORD   Name;						//RVA,指向dll名字,该名字已0结尾				
    DWORD   FirstThunk;                 						//RVA,指向IMAGE_THUNK_DATA结构数组				
} IMAGE_IMPORT_DESCRIPTOR;										
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;										

加载前: 

加载后: 

 

加载前后不同,要用INT表查IAT表。

typedef struct _IMAGE_THUNK_DATA32 {					
    union {					
        PBYTE  ForwarderString;					
        PDWORD Function;					
        DWORD Ordinal;					
        PIMAGE_IMPORT_BY_NAME  AddressOfData;					
    } u1;					
} IMAGE_THUNK_DATA32;					
typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32;					
					
					
typedef struct _IMAGE_IMPORT_BY_NAME {					
    WORD    Hint;		函数地址表索引,不是导出序号			
    BYTE    Name[1];					
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;					

 

重定位表

1、通过IMAGE_DATA_DIRECTORY结构的VirtualAddress                
属性 找到第一个IMAGE_BASE_RELOCATION                
                
2、判断一共有几块数据:                
                
最后一个结构的VirtualAddress与SizeOfBlock都为0                
                
3、具体项 宽度:2字节                
                
 也就是这个数据                
                
 内存中的页大小是1000H 也就是说2的12次方 就可以表示                
 一个页内所有的偏移地址 具体项的宽度是16字节 高四位                
 代表类型:值为3 代表的是需要修改的数据 值为0代表的是                
 用于数据对齐的数据,可以不用修改.也就是说 我们只关注                
 高4位的值为3的就可以了.                
                
4、VirtualAddress 宽度:4字节                
                
当前这一个块的数据,每一个低12位的值+VirtualAddress 才是                
真正需要修复的数据的RVA                
                
真正的RVA = VirtualAddress + 具体项的低12位                
                
5、SizeOfBlock 宽度:4字节                
                
当前块的总大小                
                
具体项的数量 = (SizeOfBlock - 8)/2                 
 

 

绑定导入表

就是提前写好导入表,不用加载dll的时候现修改。用绑定导入表验证是否有绑定。

typedef struct _IMAGE_BOUND_IMPORT_DESCRIPTOR {						
    DWORD   TimeDateStamp;						
    WORD    OffsetModuleName;						
    WORD    NumberOfModuleForwarderRefs;						
// Array of zero or more IMAGE_BOUND_FORWARDER_REF follows						
} IMAGE_BOUND_IMPORT_DESCRIPTOR,  *PIMAGE_BOUND_IMPORT_DESCRIPTOR;						

OffsetModuleName地址不是RVA,而是第一个DESCRIPTOR的值+OffsetModuleName。这么设计也许是为了每个结构节省2字节的内存吧。

 

之后看情况/心情更新

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值