排行榜 统计
  • 文章总数:1688 篇
  • 评论总数:5 条
  • 分类总数:8 个
  • 最后更新:昨天 21:09

快速学习PE结构-1

本文阅读 17 分钟
首页 程序人生 正文

PE文件结构

简介:

PE(PortableExecutable),即可移植的执行体。所有的Windows(包括Win9x、WinNT、Win CE ····)下的可执行文件(包括EXE、DLL、SYS、OCX、COM··--·)均使用PE文件结构,这些使用PE文件结构的可执行文件也成为PE文件。

作为一个普通的程序员也许没有必要掌握 PE文件结构,因为普通的程序员大多都是开发服务性、决策性、的软件。但是对于学习黑客编程,学习安全编程的程序员来说,那么掌握PE 文件结构的知识就非常重要了。因此,我们要进行对PE文件结构地学习。

PE结构有一个整体上的认识,要知道PE结构包含的结构体有DOS头、PE头(PE标识、文件头、可选头)、节表、节表数据等。


常用结构体介绍:

DOS头部其实是一个DOS头部,也可以叫MZ头,该部分用于DOS下加载可执行程序,是用IMAGE_DOS_HEADER来定义的

DOS存根是一段简单的程序,主要用于输出"This program cannot be run in DOS mode"类似的字符串。

PE头部是保存Windows系统加载可执行文件的重要信息。

节表:是PE文件对后续节的描述。

节表的数据:每个节实际上是一个容器,可以包含代码、数据等等,每个节可以有独立的内存权限,比如代码节默认有读/执行权限,节的名字和数量可以自己定义。

DOS头部详解IMAGE_DOS_HEADER:

DOS部分主要是为了兼容以前的DOS系统,DOS部分可以分为DOS MZ文件头(IMAGE_DOS_HEADER)和DOS块(DOS Stub)组成,PE文件的第一个字节位于一个传统的MS-DOS头部,称作IMAGE_DOS_HEADER,占用64个字节 ,其结构如下:

typedef struct _IMAGE_DOS_HEADER {      // DOS .EXE header
    WORD   e_magic;        // 一个WORD类型,值是一个常数0x4D5A,用文本编辑器查看该值位‘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;                    // 指向PE文件头的地址
  } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

DOS部分我们主要的是e_magic成员和e_lfanew成员,前者是标识PE指纹的一部分,后者则是寻找PE文件头的部分,除了这两个成员,其他成员全部用0填充都不会影响程序正常运行,所以我们不需要过多的对其他部分深究,DOS部分在16进制编辑器中看就是下图的部分:

1679032990_6414029e725d5275c594b.jpg!small

DOS存根:我们可以看到e_lfanew指向PE文件头,我们可以通过它来寻找PE文件头,而DOS块的部分自然就是PE文件头和DOS MZ文件头中间的部分,这部分是由链接器所写入的,可以随意进行修改,并不影响程序的运行:1679033011_641402b30b79e8ab68ecd.jpg!small

PE头部详解IMAGE_NT_HEADERS:

PE文件头由PE文件头标志,标准PE头,扩展PE头三部分组成。PE头部是真正用来装载Win32程序的头部,PE文件头标志是50 45 00 00,也就是PE,我们从结构体的角度看一下PE文件头的详细信息:

64位和32位结构体不同是IMAGE_OPTIONAL_HEADER的字节不同

typedef struct _IMAGE_NT_HEADERS64 {
    DWORD Signature;
    IMAGE_FILE_HEADER FileHeader;
    IMAGE_OPTIONAL_HEADER64 OptionalHeader;
} IMAGE_NT_HEADERS64, *PIMAGE_NT_HEADERS64;
typedef struct _IMAGE_NT_HEADERS {
    DWORD Signature; 						//PE文件头标志 => 4字节
    IMAGE_FILE_HEADER FileHeader; 			//文件头标准PE头 => 20字节
    IMAGE_OPTIONAL_HEADER32 OptionalHeader; //扩展PE头 => 32位下224字节(0xE0) 64位下240字节(0xF0)
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
PE标识Signature:

PE标识占4个字节,winhex打开如下所示4550,ANSI码为PE

1679033086_641402fe3eb7d8a89a743.jpg!small

标准PE头IMAGE_FILE_HEADER:

标准PE头结构如下,有20个字节,主要存储运行(平台、节的数量、时间、拓展头大小、文件类型)我们可以从PE文件头标志后20个字节找到它,如下图:

typedef struct _IMAGE_FILE_HEADER {
    WORD    Machine; 				//WORD类型占用2个字节,可以运行在什么平台上 任意:0 ,Intel 386以及后续:14C x64:8664
    WORD    NumberOfSections; 		//WORD类型占用2个字节,节的数量
    DWORD   TimeDateStamp; 			//DWORD类型占用4个字节,表示文件何时被创建的编译器填写的时间戳
    DWORD   PointerToSymbolTable;   //调试相关
    DWORD   NumberOfSymbols; 		//调试相关
    WORD    SizeOfOptionalHeader;   //WORD类型占用2个字节,标识扩展PE头大小
    WORD    Characteristics;        //WORD类型占用2个字节,文件的类型,该字段取值如下图
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

1679033104_641403108f23b3178c060.jpg!small

1679033123_641403231b41e450b1de5.jpg!small

扩展PE头IMAGE_OPTIONAL_HEADER:

IMAGE_OPTIONAL_HEADER被称为"可选头"。虽然被称为可选头,但是必须存在的一个头,之所以称作为"可选头"认为是在该头的数据目录数组中,有的数据目录项(后面会讲到)是可有可无的,这部分内容是可选的,因此成为可选头。

可选头紧挨着文件头,例如:文件头的结束位置为0x0000011F,那么可选头起始位置为0x00000120,

在32位和64位系统上大小是不同的,在32位系统上有224个字节(64位系统上有240个字节 0xF0)

1679033154_6414034258257e5869bee.jpg!small

32位结构如下:

typedef struct _IMAGE_OPTIONAL_HEADER {
    //
    // Standard fields.
    //

    WORD    Magic;						//指定文件状态的类型 PE32: 10B PE64: 20B
    BYTE    MajorLinkerVersion;			//主链接版本号
    BYTE    MinorLinkerVersion;			//次链接版本号
    DWORD   SizeOfCode;					//所有含有代码的区块的大小 编译器填入 没用(可改)
    DWORD   SizeOfInitializedData;		//所有初始化数据区块的大小 编译器填入 没用(可改)
    DWORD   SizeOfUninitializedData;	//所有含未初始化数据区块的大小 编译器填入 没用(可改)
    DWORD   AddressOfEntryPoint;		//程序入口RVA
    DWORD   BaseOfCode;					//代码区块起始RVA
    DWORD   BaseOfData;					//数据区块起始RVA

    //
    // 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;					//内存中整个PE文件的映射的尺寸,可比实际值大,必须是SectionAlignment的整数倍
    DWORD   SizeOfHeaders; 					//所有的头加上节表文件对齐之后的值
    DWORD   CheckSum;						//映像校验和,一些系统.dll文件有要求,判断是否被修改
    WORD    Subsystem;						
    WORD    DllCharacteristics;				//文件特性,不是针对DLL文件的,16进制转换2进制可以根据属性对应的表格得到相应的属性
    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;

64位结构如下:

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;
    DWORD       CheckSum;
    WORD        Subsystem;
    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;

这里介绍32位的结构,因为32位结构覆盖完了64位所有属性:

(1)Magic:该成员变量指定了文件的状态类型

(2)MajorLinkerVersion:主链接版本号

(3)MinorLinkerVersion:次链接版本号

(4)SizeOfCode:代码节的大小,如果有多个代码节的话,就是它们的总和。该处是指所有包含可执行属性的节的大小。

(5)SizeOflnitializedData: 已初始化数据块的大小。

(6)SizeOfUninitializedData:未初始化数据块的大小。

(7)AddressOfEntryPoint:程序执行的入口地址,该地址是一个相对虚拟地址,该地址简称EP。如果加壳后,找到了该地址,就被称作了OEP。该地址指向的不是main(),也不是WinMain()的地址该地址指向了运行库代码的地址。对于DLL这个值的意义不大,因为DLL甚至可以没有DIIMain()函数,没有DIIMain()是无法捕获DLL的4个消息的

(8)BaseOfCode:代码段的起始相对虚拟地址。

(9)BaseOfData:数据段的起始相对虚拟地址。

(10)ImageBase:文件被装入内存后的首选建议装载地址。对于EXE文件来说,通常情况下该地址就是装载地址:对于DLL 来说,可能就不是其装入内存后的地址了。程序加载到内存的起始地址

(11)SectionAlignment:节被装入内存后的对齐值。节被映射到内存中需要对齐的单位。通常情况下0x1000,也就是4KB大小。Windows操作系统的内存分页一般为4KB

(12)FileAlignment:节在文件中的对齐值。节在磁盘上是对齐单位。

(13)MajorOperatingSystemVersion:要求最低操作系统的主版本号。

(14)MinorOperatingSystemVersion:要求最低操作系统的次版本号。

(15)MajorImageVersion:可执行文件的主版本号。

(16)MinorImageVersion:可执行文件的次版本号

(I7)MajorSussystemVersion:要求最低子系统的主版本号。

(18)MinorSubsystemVersion:要求最低子系统的次版本号。

(19)Win32VersionValue:该成员变量是被保留的。

(20)SizeOflmage:可执行文件装入内存后的总大小。该大小按内存对齐方式对齐

(21)SizeOfHeaders:PE头的大小,这个PE 头泛指DOS头、PE头、节表的总和大小。

(22)CheckSum:校验和对于EXE文件通常为0对SYS文件则必须有一个校验和

(23)Subsystem:可执行文件的子系统类型。

(24)DIICharacteristics:指定DLL文件的属性。该值大部分时候为0。

(25)SizefStackReserve:为线程保留的栈大小

(26)SizeOfStackCommit:为线程已经提交的找大小

(27)SizeOfHeapReserve:为线程保留的堆大小

(28)SizeOfHeapCommit:为线程已经提交的堆大小

(29)LoaderFlags:被废弃的成员值。MSDN上的原话为“This member is obsolete”。但是该值在某些情况下还是会被用到的,比如针对旧版OD时,修改该值会起到反调试的作用

(30)NumberOfRvaAndSizes:数据目录项的个数。

(31)DataDirectory:数据目录表,由NumberOfRvaAndSize个IMAGE_DATA_DIRECTORY结构体组成。该数组中包含了输入表、输出表、资源等数据的RVA。IMAGE_DATA_DIRECTORY 的定义如下

typedef struct _IMAGE_DATA_DIRECTORY (
	DWORD VirtualAddress;
	DWORD Size;
) IMAGE_DATA_DIRECTORY,*PIMAGE_DATA_DIRECTORY;

该结构体的第一个变量为该目录的相对虚拟地址的起始值,第二个是该目录的长度。数据目录中的部分成员在数组中的索引如图所示。

节表详解IMAGE_SECTION_HEADER:

节表的位置在IMAGE_OPTIONAL_HEADER的后面,节表中的每个IMAGE_SECTION_HEADER中都存放着可执行文件被映射到内存中所在位置的信息。其中节的个数由IMAGE_FILE_HEADER中的NumberOfSections给出。节表的结构如下,40个字节: DWORD 4个字节 WORD占2个字节

typedef struct _IMAGE_SECTION_HEADER {
    BYTE    Name[IMAGE_SIZEOF_SHORT_NAME]; //ASCII字符串 可自定义 只截取8个字节
    union {								   //该节在没有对齐之前的真实尺寸,该值可以不准确
            DWORD   PhysicalAddress;  //文件地址
            DWORD   VirtualSize;  //加载到内存中的节的总大小   
    } Misc;
    DWORD   VirtualAddress; 			   //加载到内存中的部分的第一个字节的地址,相对于映像基地址RVA
    DWORD   SizeOfRawData;				   //节在文件中对齐的大小
    DWORD   PointerToRawData;			   //节区在文件中的偏移
    DWORD   PointerToRelocations;
    DWORD   PointerToLinenumbers;
    WORD    NumberOfRelocations;
    WORD    NumberOfLinenumbers;
    DWORD   Characteristics;			   //节的属性
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

(1)Name: 该成员变量保存节的名称,节的名称用ASCII编码来保存。节名的长度IMAGE_SIZEOF_SHORT_NAME,这是一个宏,该宏的定义如下:

#define IMAGE_SIZEOF_SHORT_NAME 8

节名的长度为8个字节,多余的字节会被自动截断。通常情况下节名以“.”为开始,当然这是编译器的习惯。我们看一下图的前8个字节的内容为“2E74657874000000”其对应ASCII字符为“text

(2)VirtualSize: 区段装载到内存中的RVA,该值表示节区的大小,是在对齐处理前的区块的大小

(3)VirtualAddress: 该节区载入到内存后的相对虚拟地址。这个地址是按内存进行对齐的,是sectionAlignment的整数倍

(4)SizeOfRawData:该节区在磁盘上所占用的空间大小,该值通常是对齐后的值该字段包括经过FileAlignment调整后块的长度。例如FileAlignment的大小为200h,如果VirtualSize中的块长度为19Ah个字节,这一块保存的长度为200h个字节,用0进行填充。

(5)PointerToRawData:该节区在磁盘文件上的偏移值。

(6)PointerToRelocations:在exe文件中没用意义,在obj文件中,表示本块重定位信息的偏移量。

(7)PointerToLinenumbers:行号表在文件中的偏移量。这是文件的调试信息。

(8)NumberOfRelocations:在exe中无意义。

(9)NumberOfLinenumbers:该块在行号表中的行号数目

(10)Characteristics:节区属性。该属性的值如图所示。

1679033242_6414039a094cad50cb8cd.jpg!small1679033261_641403ad650356bef585b.jpg!small

地址转换器:

RVA和FOA的转换:

1679033286_641403c6ab5eb2bc96bde.jpg!small

RVA :相对虚拟地址(Relative Virtual Address),PE 文件中的各种数据结构中涉及地址的字段大部分都是以 RVA 表示的。

VA是当PE 文件被装载到内存中后,某个数据位置相对于文件头的偏移量。实际的内存地址

FOA (文件偏移地址):PE文件储存在磁盘上时, 某个数据的位置相对于文件头的偏移量,称为文件偏移地址(File Offset)或物理地址(RAW Offset)文件偏移地址从PE文件的第一个字节开始计 数,起始值为0。用十六进制工具 (例如WinHex、C32等)打开文件 所显示的地址就是文件偏移地址。

RVA = 内存地址(VA) - ImageBase

RVA 转换为 FOA:

某数据的文件偏移 = 该数据所在节的起始文件偏移 + (某数据的RVA –该数据所在节的起始RVA)

PointerToRawData: 该节区在磁盘文件上的偏移值
VitualAddress: 该节区载入到内存后的相对虚拟地址
Misc.VirtualSize: 表示节的大小,是在对齐处理前的区块的大小
SizeOfHeaders:  PE头的大小,这个PE 头泛指DOS头、PE头、节表的总和大小
SectionAlignment:加载到内存中的节的对齐方式,以字节为单位。此值必须大于或等于 FileAlignment成员。默认值是系统的页面大小。
FileAlignment:图像文件中各个部分的原始数据的对齐方式,以字节为单位。该值应该是 2 的幂,介于 512 和 64K(含)之间。默认值 512。如果SectionAlignment成员小于系统页面大小,则该成员必须与 SectionAlignment相同。
第一种情况:文件对齐跟内存对齐一样的情况,那么这样就可以直接去找 RVA的地址了 这个地址也就是FOA
第二种情况:文件对齐和内存对齐不一样的情况

1.如果RVA属于文件头部(DOS头 + PE头 + 节表),头部大小是文件对齐大小的整数倍!
那么不需要进行计算了,因为DOS头和PE头和节表在文件中和在内存中展开都是一样的,直接从开始位置寻
找到RVA个字节即可,就是找RVA,也就是FOA(文件偏移地址)pOptionalHeader->SizeOfHeaders

2.判断 RVA 位于哪个节
RVA >= 节.VirtualAddress
RVA <= 节.VirtualAddress + 当前内存对齐后的大小(节.Misc.VirtualSize)
差值 = RVA - 节.VirtualAddress

3.FOA = 节.PointerToRawData + 差值
RVA转换FOA例子:

创建一个a64.exe变量:

#include <iostream>
#include <stdlib.h>
int Test = 0x12345678;
int main(int argc, char* argv[])
{
    printf("全局变量地址 = %X \r\n", &Test);
    printf("全局变量值 = %X \r\n", Test);

    getchar();
}

输出的值是VA(内存地址)

1679033310_641403debe7ad6a3dcc1f.jpg!small

VA 4001C000

Imagebase 40000000

RVA = VA -ImageBase = 1C000

1.判断RVA是否在PE文件头中

001FF <1C000 不在PE文件头中

1679033328_641403f091d2f572535ec.jpg!small

2.判断属于哪个节:

.data.VirtualAddress >=1C000 且 1C000<1C200(.data.VirtualAddress+.data.SizeOfRawData) 所以属于这个data节区中

3.FOA = 节.PointerToRawData + 差值(RVA - 节.VirtualAddress)

=B200+1C000-1C000=B200

下图就找到了对应的值:

1679033348_64140404198276bde2992.jpg!small

在winhex中修改为78 56 44 12运行a64.exe成功

1679033372_6414041c90a0afc502c23.jpg!small

代码:

VA->Offset

void CPEmfcDlg::OnBnClickedButtoncalc() { //获取输入的VA DWORD dwRva; CString strRVA; GetDlgItem(IDC_EDIT_VAshow)->GetWindowText(strRVA); //字符串转换16进制 _stscanf_s(strRVA.GetBuffer(), _T("%x"), &dwRva); CString str; DWORD offset =VA2Offset(dwRva, m_pNtHdr, m_pSecHdr); str.Format("%08X", offset); SetDlgItemText(IDC_EDIT_offsetshow, str); //SetDlgItemText(IDC_EDIT_quduan, (LPCTSTR)m_pSecHdr[nInNum].Name); } DWORD VA2Offset(DWORD dwVa, PIMAGE_NT_HEADERS m_pNtHdr, PIMAGE_SECTION_HEADER m_pSecHdr) { //imagebase文件基地址 DWORD dwImageBase = m_pNtHdr->OptionalHeader.ImageBase; //文件对齐和内存对齐相等且不在文件头中 if (m_pNtHdr->OptionalHeader.FileAlignment == m_pNtHdr->OptionalHeader.SectionAlignment || dwVa <= m_pNtHdr->OptionalHeader.SizeOfHeaders) { return dwVa; } else { for (int nInNum = 0; nInNum < m_pNtHdr->FileHeader.NumberOfSections; nInNum++) { if (dwVa >= dwImageBase + m_pSecHdr[nInNum].VirtualAddress && dwVa <= dwImageBase + m_pSecHdr[nInNum].VirtualAddress + m_pSecHdr[nInNum].Misc.VirtualSize) { return m_pSecHdr[nInNum].PointerToRawData + dwVa - m_pSecHdr[nInNum].VirtualAddress - m_pNtHdr->OptionalHeader.ImageBase; } } } }

saaaa

小结:

本小节主要介绍了pe文件的文件结构(除了数据目录表,后篇会讲解数据目录表的作用),为主要是学习pe文件的笔记,比较基础。




本文作者:xiaoshuaishuai, 转载请注明来自FreeBuf.COM

本文来自投稿,不代表本站立场,如若转载,请注明出处:https://typecho.firshare.cn/archives/1754.html
免责声明:文章内容不代表本站立场,本站不对其内容的真实性、完整性、准确性给予任何担保、暗示和承诺,仅供读者参考,文章版权归原作者所有。避免网络欺诈,本站不倡导任何交易行为。如您私自与本站转载自公开互联网中的资讯内容中提及到的个人或平台产生交易,则需自行承担后果。本站在注明来源的前提下推荐原文至此,仅作为优良公众、公开信息分享阅读,不进行商业发布、发表及从事营利性活动。如本文内容影响到您的合法权益(内容、图片等),请及时联系本站,我们会及时删除处理。
-- 展开阅读全文 --
ThreatHound:一款功能强大的事件响应与威胁搜索辅助工具 
« 上一篇 04-03
APK加固方式及步骤
下一篇 » 04-04