找回密码
 立即注册
注册 登录
×
热搜: 活动 交友 discuz
查看: 121|回复: 0

加密解密之认识PE文件结构初体验

[复制链接]

1

主题

3

帖子

5

积分

新手上路

Rank: 1

积分
5
发表于 2023-1-19 08:02:53 | 显示全部楼层 |阅读模式
之前参加某培训班总结的一些C\C++和加密解密的电子书籍和视频资料,有需要可以文末自取哦!!!
7000字的总结来之不易,请大家抬抬小手来个三连,万分感谢!!!
PE格式是我们真正迈入WIN32编程的奠基石,是高手所必备的基本功(又是内功心法)。
PE 的意思就是 Portable Executable (可移植的执行体)。它是 Windows 环境自身所带的执行体文件格式。
它的一些特性继承自 Unix 的 Coff (common object file format) 文件格式。
- Windows平台:PE(Portable Executable)文件结构
- Linux平台:ELF(Executable and Linking Format)文件结构
"portable executable" (可移植的执行体)意味着此文件格式是跨 win32 平台的 : 也就是说即使 Windows 运行在非 Intel 的 CPU 上,任何 win32 平台的 PE 装载器都能识别和使用该文件格式。


识别一个文件是不是PE文件不应该只看文件后缀名,还应该通过PE指纹。
使用UE打开一个exe文件,发现文件的头两个字节都是MZ,0x3C位置保存着一个地址,查该地址处发现保存着“PE”,这样基本可以认定改文件是一个PE文件


基本上所有 win32 执行体 ( 除了 VxD 和 16 位的 Dll) 都使用 PE 文件格式,包括 NT 的内核模式驱动程序( kernel mode drivers )。因而研究 PE 文件格式给了我们洞悉 Windows 结构的良机。
在绝大多数病毒爱好者的眼中,真正的病毒技术在PE病毒中才会得到真正的体现,因为另病毒极度疯狂的DOS时代已经过去了。
PE文件使用的是一个平面地址空间,所有代码和数据都合并在一起,组成一个很大的结构。
文件的内容被分割为不同的区块(Section,又称为区段,节等),块中包含代码或数据。
“节”或“块”或”区块“都是一个意思,后文会穿插使用。
每个区块都有自己在内存中的属性,即可读/写,只读等。
每个区块都有不同的名字,这用名字主要用来表示区块的功能。
一般我们遇到的以下区块的含义是:  
.text是在编译或汇编结束时产生的一种块,它的内容全是指令代码   
.rdata是运行期只读数据   
.data是初始化的数据块   
.idata包含其它外来DLL的函数及数据信息,即输入表   
.rsrc包含模块的全部资源:如图标、菜单、位图等
PE文件非常好的一个地方就是在磁盘上的数据结构与在内存中的结构是一致的。
当系统装载一个可执行文件到内存中,主要就是将一个PE文件的某一部分映射到地址空间中。这样,PE文件的数据结构在磁盘和内存中就是一样的了。


PE相关名词解释
(1)入口点(Entry Point)
PE文件执行时的入口点(Entry Point)。也就是说,程序在执行时的第一行代码地址应该就是这个值。有点像8086汇编语言中end start中start指向的入口地址。
(2)文件偏移地址(File Offset)
当PE文件储存在磁盘上的时候,各数据的地址称作文件的偏移地址。文件偏移地址从PE文件的第一个字节开始计数,起始值为0。
虚拟地址(Virtual Address, VA)
由于Windows程序运行在保护模式下,所以应用程序访问存储器所使用的逻辑地址称为虚拟地址(因为他不是真正的物理地址,真正的物理地址被windows老大妈的保护机制保护起来),又称为内存偏移地址(Memory Offset)。
这里要重复说一下的是,与实地址模式下的“段地址:偏移地址”索引方式类似,虚拟地址也写成“段:偏移量”的形式。但是……
不同之处在于这里的段不再是段地址,而是指段选择子。
例如:“0123:00401000”
0123:表示段选择子,其数据存储在CS段选择器里边,同一程序在不同系统环境下,此值可能不同,因此我们不需要关心;
00401000:此处表示内存中的虚拟地址,一般来说,同一个程序的同一条指令在不同系统环境下,此值相同(PE映射原理)。
基地址(ImageBase)
文件执行时将被映射到指定内存地址中,这个初始内存地址称为基地址。这个值是由PE文件本身设定的。
按照默认设置,用Visual C++建立的EXE文件基地址是00400000h,DLL文件基地址是10000000h。但是,这个值可以自己在编译器设定的。
DOS部分
DOS MZ文件头实际是一个结构体(IMAGE_DOS_HEADER),占64字节
typedef struct _IMAGE_DOS_HEADER {   // DOS .EXE header
WORD  e_magic;           // Magic number
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;          // File address of new exe header
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
DOS头用于16位系统中,在32位系统中DOS头成为冗余数据,但还存在两个重要成员e_magic字段(偏移 0x0)和 e_lfanew字段(偏移 0x3C)
e_magic保存“MZ”字符,e_lfanew保存PE文件头地址,通过这个地址找到PE文件头,得到PE文件标识“PE”。
e_magic和e_lfanew是验证PE指纹的重要字段,其他字段现基本不使用(可填充任意数据)
PE文件头(PE Header)
PE文件头是一个结构体(IMAGE_NT_HEADERS32),里面还包含两个其它结构体,占用4B + 20B + 224B
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature;             // PE文件标识 4Bytes
IMAGE_FILE_HEADER FileHeader;      // 40 Bytes
IMAGE_OPTIONAL_HEADER32 OptionalHeader; // 224 Bytes  PE32可执行文件,不讨论PE32+的情况
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
Signature字段设置为0x00004550,ANCII码字符是“PE00”,标识PE文件头的开始,PE标识不能破坏。
1、IMAGE_FILE_HEADER结构体
IMAGE_FILE_HEADER(映像文件头或标准PE头)结构包含PE文件的一些基本信息,该结构在微软的官方文档中被称为标准通用对象文件格式(Common Object File Format,COFF)头
typedef struct _IMAGE_FILE_HEADER {
WORD  Machine;        // 可运行在什么样的CPU上。0代表任意,Intel 386及后续:0x014C, x64: 0x8664
WORD  NumberOfSections;   // 文件的区块(节)数
DWORD  TimeDateStamp;     // 文件的创建时间。1970年1月1日以GMT计算的秒数,编译器填充的,不重要的值
DWORD  PointerToSymbolTable; // 指向符号表(用于调试)
DWORD  NumberOfSymbols;    // 符号表中符号的个数(用于调试)
WORD  SizeOfOptionalHeader; // IMAGE_OPTIONAL_HEADER32结构的大小,可改变,32位为E0,64位为F0
WORD  Characteristics;    // 文件属性
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
重要字段:NumberOfSections,SizeOfOptionalHeader
对应结构为下图绿线线部分


0x014C说明运行于x86 CPU;0x0003说明当前exe有3个节;
0x00E0说明IMAGE_OPTIONAL_HEADER32为224字节;
0x0103(0000 0001 0000 0011)代表文件属性 ,由下列对应位为1的组合


2、IMAGE_OPTIONAL_HEADER结构体
IMAGE_OPTIONAL_HEADER(可选映像头或扩展PE头)是一个可选的结构,是IMAGE_FILE_HEADER结构的扩展
大小由IMAGE_FILE_HEADER结构的SizeOfOptionalHeader字段记录(可能不准确)
typedef struct _IMAGE_OPTIONAL_HEADER {
WORD  Magic;         //说明文件的类型 PE32:10BH PE32+:20BH  Rom映像文件:107H
BYTE  MajorLinkerVersion;   //链接器主版本号
BYTE  MinorLinkerVersion;   //链接器次版本号
DWORD  SizeOfCode;       //所有代码节的总和(基于文件对齐) 编译器填的 没用
DWORD  SizeOfInitializedData; //包含所有已经初始化数据的节的总大小 编译器填的 没用
DWORD  SizeOfUninitializedData;//包含未初始化数据的节的总大小 编译器填的 没用
DWORD  AddressOfEntryPoint;  //程序入口RVA  在大多数可执行文件中,这个地址不直接指向Main、WinMain或DIMain函数,而指向运行时的库代码并由它来调用上述函数
DWORD  BaseOfCode;       //代码起始RVA,编译器填的  没用
DWORD  BaseOfData;       //数据段起始RVA,编译器填的  没用
DWORD  ImageBase;       //内存镜像基址 ,可链接时自己设置
DWORD  SectionAlignment;    //内存对齐   一般一页大小4k
DWORD  FileAlignment;     //文件对齐   一般一扇区大小512字节,现在也多4k
WORD  MajorOperatingSystemVersion; //标识操作系统版本号 主版本号
WORD  MinorOperatingSystemVersion; //标识操作系统版本号 次版本号
WORD  MajorImageVersion;   //PE文件自身的主版本号
WORD  MinorImageVersion;   //PE文件自身的次版本号
WORD  MajorSubsystemVersion; //运行所需子系统主版本号
WORD  MinorSubsystemVersion; //运行所需子系统次版本号
DWORD  Win32VersionValue;   //子系统版本的值,必须为0
DWORD  SizeOfImage; //内存中整个PE文件的映射的尺寸,可比实际的值大,必须是SectionAlignment的整数倍  
DWORD  SizeOfHeaders;     //所有头+节表按照文件对齐后的大小,否则加载会出错
DWORD  CheckSum;        //校验和,一些系统文件有要求.用来判断文件是否被修改
WORD  Subsystem;       //子系统  驱动程序(1) 图形界面(2) 控制台、DLL(3)
WORD  DllCharacteristics;   //文件特性 不是针对DLL文件的
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;
重要字段:
AddressOfEntryPoint:程序入口地址(RVA),下图为32C40H
ImageBase:内存镜像基地址,下图为400000H
FileAlignment:文件对齐,下图为200H
SectionAlignment:内存对齐,下图为1000H
DataDirectory[16]:数据目录表,由数个相同的IMAGE_DATA_DIRECTORY结构组成,
指向输出表、输入表、资源块,重定位表等(后面详解这里先跳过)
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD  VirtualAddress;  //对应表的起始RVA
DWORD  Size;       //对应表长度
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;


ImageBase + AddressOfEntryPoint = 程序实际运行入口地址(实际加载地址等于ImageBase)
0x400000 + 0x026BC0 = 0x426BC0 (使用OD运行程序发现就是从这个地址开始运行)
应用:在PE文件空白区添加代码,让程序执行先执行添加的代码再跳转程序入口
思路:
① 在PE的空白区构造一段代码(call -> E8)
② 修改入口地址为新增代码(IMAGE_OPTIONAL_HEADER.AddressOfEntryPoint)
③ 新增代码执行后,跳回入口地址(jmp -> E9)
RVA与FOA的转换
RVA:相对虚拟地址,FOA:文件偏移地址。
计算步骤:
① 计算RVA = 虚拟内存地址 - ImageBase
② 若RVA是否位于PE头:FOA == RVA
③ 判断RVA位于哪个节:
RVA >= 节.VirtualAddress (节在内存对齐后RVA )
RVA <= 节.VirtualAddress + 当前节内存对齐后的大小
偏移量 = RVA - 节.VirtualAddress;
④ FOA = 节.PointerToRawData + 偏移量;
应用举例:
有初始值的全局变量初始值会存储在PE文件中,想要修改文件中全局变量的数据值即
需要找到文件中存储全局变量值的地方,然后修改即可
后续有其他相关PE格式解析等更新,感谢官方平台给的支持和粉粉们的陪伴。
我是 @逃逸的卡路里 ,一个热爱生活,热爱分享,热爱交流的斜杠青年。



C开发电子书




WindowsPE权威指南和C+密解密系列资料整理
喜欢的小伙伴们可以自取,为努力学习的你加油鼓劲,愿前程似锦,未来可期!!!
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

Archiver|手机版|小黑屋| 黑客通

GMT+8, 2025-10-12 03:42 , Processed in 0.370886 second(s), 23 queries .

Powered by Discuz! X3.4

Copyright © 2020, LianLian.

快速回复 返回顶部 返回列表