PE结构-1

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_HEADERSizeOfOptionalHeader字段存储了它的大小,从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_HEADERIMAGE_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_HEADER64SectionAlignmentFileAlignment对齐。

节实际上是相同属性数据的组合。节被装入内存时,同一个节所对应的内存也被赋予相同的属性。

节的尺寸有两方面:

  • 上面所说的对齐方式
  • 为没有初始化数据的节留下空间

有些节不需要映射到内存,比如.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非常重要。

多数区块名习惯以.开头,这不是必须的。名字前带有$的区块在载入时会按照美元后面字母顺序被合并。

病毒会新开一个区块

.textVirtualAddress通常为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文件在内存中的基地址是0x00400000DLL文件是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)。

猜你喜欢

转载自blog.csdn.net/Ga4ra/article/details/94594490