PE结构-1
本部分的目的是搞懂下图。
1. Dos头
n每个PE文件是以一个DOS程序开始的,有了它,一旦程序在DOS下执行,DOS才能识别出这是有效的执行体。
winnt.h
中的DOS头部结构体:
typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header
WORD e_magic; // Magic number, [4D 5A]==MZ
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; // at 0x3c offset, File address of new exe header
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
开头总是4D 5A
。
最后一个e_lfanew
很重要,它指向PE文件头。
将一个pe文件放进hexeditor,查看0x3c处的LONG
型数据,即PE Header起始偏移量,这个偏移处以PE..(50 45 00 00)
开头,标志PE文件的开始。
PE装载器将从IMAGE_DOS_HEADER
结构中的e_lfanew
字段里找到PE Header
的起始偏移量,加上基地址就得到PE文件头的指针。
PNTHeader = ImageBase + dosHeader->e_lfanew
2. NT头
PE Header 是PE相关结构NT映像头IMAGE_NT_HEADER
的简称,里边包含着许多PE装载器用到的重要字段。
winnt.h
中NT头部结构体:
typedef struct _IMAGE_NT_HEADERS64 {
DWORD Signature; //50 45 00 00 : P E . .
IMAGE_FILE_HEADER FileHeader; //+04h
IMAGE_OPTIONAL_HEADER64 OptionalHeader; //+18h
} IMAGE_NT_HEADERS64, *PIMAGE_NT_HEADERS64;
typedef struct _IMAGE_FILE_HEADER {
WORD Machine; //+04h
WORD NumberOfSections; //+06h store the number of struct IMAGE_SECTION_HEADER in section table
DWORD TimeDateStamp; //+08h
DWORD PointerToSymbolTable; //+0ch
DWORD NumberOfSymbols; //+10h,
WORD SizeOfOptionalHeader; //+14h,!!!!
WORD Characteristics; //+16h
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
官方文档:https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-_image_file_header
第二个字段所说的区块表(section)是紧跟在NT头部后面的(回忆PE文件结构)。
最后一个字段是文件属性。
IMAGE_OPTIONAL_HEADER64
IMAGE_FILE_HEADER
还不足以定义PE文件的属性,还需要IMAGE_OPTIONAL_HEADER64
这个可选结构。
_IMAGE_FILE_HEADER
的SizeOfOptionalHeader
字段存储了它的大小,从PE头偏移18h
加上该字段的值(到.text
),就是IMAGE_OPTIONAL_HEADER64
部分。
来看下winnt.h
如何定义这个结构。
官方文档:https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-_image_optional_header64
typedef struct _IMAGE_OPTIONAL_HEADER64 {
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint; //RVA address!!!!
DWORD BaseOfCode;
ULONGLONG ImageBase;
DWORD SectionAlignment; //内存中区块的对齐大小 0x1000==4kB
DWORD FileAlignment; //文件中区块的对齐大小 0x0200==512B
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage;
DWORD SizeOfHeaders;
DWORD CheckSum;
WORD Subsystem; //how to build the initial gui
WORD DllCharacteristics;
ULONGLONG SizeOfStackReserve;
ULONGLONG SizeOfStackCommit;
ULONGLONG SizeOfHeapReserve;
ULONGLONG SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER64, *PIMAGE_OPTIONAL_HEADER64;
病毒一般会改变其中个别字段。
ImageBase
是优先装入地址,exe总是使用独立的虚拟地址空间,总能按照这个地址装入,所以exe不需要重定位信息,dll的宿主对应地址则可能被占用,所以,_IMAGE_FILE_HEADER
的IMAGE_FILE_RELOCS_STRIPPED
字段,exe总为1,dll总为0.
最后一个字段很重要,数组元素个数为16,即#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES 16
,它就是数据目录表。
这16个结构用来定义各个节多种不同用途的数据块,如导出表、导入表、资源和重定位表。
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress; //start RVA
DWORD Size; //length
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
// Directory Entries
#define IMAGE_DIRECTORY_ENTRY_EXPORT 0 // Export Directory
#define IMAGE_DIRECTORY_ENTRY_IMPORT 1 // Import Directory
#define IMAGE_DIRECTORY_ENTRY_RESOURCE 2 // Resource Directory
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION 3 // Exception Directory
#define IMAGE_DIRECTORY_ENTRY_SECURITY 4 // Security Directory
#define IMAGE_DIRECTORY_ENTRY_BASERELOC 5 // Base Relocation Table
#define IMAGE_DIRECTORY_ENTRY_DEBUG 6 // Debug Directory
// IMAGE_DIRECTORY_ENTRY_COPYRIGHT 7 // (X86 usage)
#define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE 7 // Architecture Specific Data
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8 // RVA of GP
#define IMAGE_DIRECTORY_ENTRY_TLS 9 // TLS Directory
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10 // Load Configuration Directory
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 11 // Bound Import Directory in headers
#define IMAGE_DIRECTORY_ENTRY_IAT 12 // Import Address Table
#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 13 // Delay Load Import Descriptors
#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14 // COM Runtime descriptor
3. 区块表
section table,也叫节表。
在执行一个PE文件的时候,windows 并不在一开始就将特别大的整个文件读入内存的,而是采用与内存映射文件类似的机制。
windows 装载器在装载的时候仅仅建立好虚拟地址和PE文件之间的映射关系。
当且仅当真正执行到某个内存页中的指令或者访问某一页中的数据时,这个页面才会被从磁盘提交到物理内存,这种机制使文件装入的速度和文件大小没有太大的关系。
Windows 装载器在装载DOS部分、PE文件头部分和节表(区块表)部分是不进行任何特殊处理的,而在装载节(区块)的时候则会自动按节(区块)的属性做不同的处理。
一般情况下,它会处理以下几个方面的内容:
- 内存页的属性;
- 节的偏移地址;
- 节的尺寸;
- 不进行映射的节。
windows对内存属性的设置是以页为单位进行的,所以节在内存中的对齐单位必须至少是一个页的大小,32位系统一般是4kB,64位系统则一般是8Kb == 2000H。
节的起始地址在内存中和文件中分别按照_IMAGE_OPTIONAL_HEADER64
的SectionAlignment
和FileAlignment
对齐。
节实际上是相同属性数据的组合。节被装入内存时,同一个节所对应的内存也被赋予相同的属性。
节的尺寸有两方面:
- 上面所说的对齐方式
- 为没有初始化数据的节留下空间
有些节不需要映射到内存,比如.reloc
,它是为装载器准备的。
PE文件所有节的属性都定义在节表中,节表紧挨PE文件头,由一系列_IMAGE_SECTION_HEADER
组成,这个结构用来描述一个节,排列顺序与节一致。
节表以一个空的_IMAGE_SECTION_HEADER
结束,所以节表中该结构的数量等于节的数量加一。
官方文档:https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-_image_section_header
//winnt.h
#define IMAGE_SIZEOF_SHORT_NAME 8
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME];
union {
DWORD PhysicalAddress;
DWORD VirtualSize; //size before assigned,usually used
} Misc;
DWORD VirtualAddress; //RVA actually,integral multiple of SectionAlignment
DWORD SizeOfRawData; //size after assigned by FileAlignment
DWORD PointerToRawData; // used in .obj file,point to the section in file.
//meaningless in exe
DWORD PointerToRelocations;
DWORD PointerToLinenumbers;
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
DWORD Characteristics; //rwx,by bit
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
#define IMAGE_SIZEOF_SECTION_HEADER 40
VirtualAddress、PointerToRawData
非常重要。
多数区块名习惯以.
开头,这不是必须的。名字前带有$
的区块在载入时会按照美元后面字母顺序被合并。
病毒会新开一个区块
.text
的VirtualAddress
通常为0x1000。
4. 区块
section,也叫节。
section | description |
---|---|
.text | Borland c++编译器下叫做code |
.data | 可读写,存放全局或静态变量 |
.rdata | 只读。两种情况:exe中存放调试目录;存放说明字符串。 |
.idata | 导入表,含dll信息,会合并到另外一个区块。 |
.edata | 导出表,关联exp文件,也会被合并。 |
.rsrc | 图标、菜单、位图等,不能合并。 |
.bss | 未初始化数据,已被.data扩展替代 |
.crt | c++ run time lib data |
.tls | thread local storage,包含用于支持通过__declspec(thread) 声明的tls变量的数据 |
.reloc | 可执行文件的基址重定位,一般dll才需要。release模式下链接器不会给exe附加基址重定位。 |
.sdata | 可通过全局指针相对寻址的短可读写数据 |
.srdata | 可通过全局指针相对寻址的短只读数据 |
.pdata | 异常表,含基于cpu的IMAGE_RUNTIME_FUNCTION_ENTRY 类型的数组 |
.debug$S | obj文件中codeview格式符号 |
.debug$T | oobj文件中codeview格式类型记录 |
.debug$P | (使用预编译头时)包含预编译头的信息 |
.drectve | 只用于obj文件,包含连接器指令, |
.didat | 非release模式下延迟加载的导入数据,release模式下这些数据被合并到其它节 |
vc++用以下代码控制名称:#pragma data_msg("NAME");
它告诉编译器把数据放进叫做NAME
区块中,而不是默认的.data区块。
区块一般是从obj文件开始,被编译器放置的。链接器将obj和库中的块根据属性合并,成为一个最终的区块。一个好处是节省空间。
注意,.rsrc,.reloc,.pdata
不能合并到其它区块。
对齐
映射后,dos文件头、PE文件头和区块表偏移位置与大小均没有变化。
默认情况下,EXE
文件在内存中的基地址是0x00400000
,DLL
文件是0x10000000
。
文件/内存中,一个区块不足0x200/0x1000字节时,余下空间用00填充。
换算RVA和文件偏移
RVA = VA - Image Base
文件偏移地址是相对于文件开始处0字节的偏移;RVA是相对与0x400000处的偏移。
计算出RVA后,用RVA减去区段起始虚拟偏移,最后加上此区段的文件偏移就是文件偏移地址。
section | 起始虚拟偏移 | 起始文件偏移 |
---|---|---|
.text | 0x1000 | 0x0400 |
.data | 0xF000 | 0x0600 |
.rsrc | 0x018000 | 0x9400 |
起始文件偏移可查看_IMAGE_SECTION_HEADER.PointerToRawData
RVA = 虚拟内存地址(VA)−装载基址(Image Base)
文件偏移地址= RVA −段起始偏移 + 段起始文件偏移
例如,已知VA为0x411210,则RVA=0x011210,位于.data段,则FileOffset = 0x0600 + (0x011210 - 0xF000)。