PE文件结构深入详解

一、PE结构基础

看了很多PE结构类的东东,要不上来就是整体结构,要不就是一大堆ASM代码,看的我等菜鸟有点难受!所以自己写个帖·学习PE我们先来弄懂几个问题!
1:几个地址的概念
VA:虚拟地址,也就是内存中的地址!
RVA:相对虚拟地址,等于VA-ImageBase
Offset:物理地址,磁盘上文件的地址,等于RVA-ImageBase-节偏移!
PE装入器:程序需要装入内存后才可以运行,PE装入器就是为了装入PE文件
装入:装入是将磁盘上程序的指令数据转的地址载入内存并进行地址转换的过程!
连接:连接是将多个OBJ文件制作成可供PE装载器装入模块的过程!
对于装入和连接的理解我们在下面进行!
2:OD中的地址为什么是虚拟地址
很多朋友都知道内存中的地址为VA,那么为什么是VA呢?这是有装入概念引起的,上面说明了装入的概念,在DOS年代,内存中任何时刻只有一个任务,
所以将程序直接载入内存就可以了,这称为静态装入,当多任务操作系统出现后,所谓的多任务是人类不可感知的CPU时间片下的单任务,多个任务需要同时装
入,这时候每一个任务的必须在前一个任务的后面,所以磁盘上的程序装入内存进行地址转换的时候必须知道前一个任务的结束的内存地址,这就是可重定位装入方
式,但是伴着X86保护模式的产生,动态可重定位方式产生,在WINDOWS中是一种运行时动态可重定位方式,也就是说当程序真正运行的时候才会去进行地
址转换,程序载入内存的时候只是一种内存映射,执行程序的时候系统会通过内存中的页表去查找数据和指令的真实地址!这里在说一下分页机制中的两个基本概
念,分页是分的进程,而内存是分块的,页表要解决的就是进程的分页的号码与内存分块号码的对应,一定要学会用集合的概念去理解东西!
3:DLL文件是怎么来的
朋友都知道DLL文件是动态连接库,不过为了说明的更清楚,我们还是看一下它的起源,世界是一个完全不相同又存在众多交集的矛盾,每个程序虽然不同,
但是它们总有交集——功能相同的地址,从统计学上来说,我们如果将很多程序的交集提取出来,当程序需要的时候我们就不用手动的去输入了,直接引入它就可以
了,这就是库概念的引入的原因!最早使用库的方法就是STATIC
LINKING,静态连接方式,这与动态的连接的根本区别在于前者每个程序都有一个库COPY,而动态连接只会有一个库的引用说明,并没有将库连接的时候
写进每一个程序,所以WINDOWS缺少DLL文件的时候,某些程序会不能执行,这时候PE装入器会自动告诉我们,而且是运行程序时候的才报错,显然这说明Windows的DLL文件是一种运行时动态连接的工作方式!
那么它到底有什么好处呢?库是一个程序相同功能的交集,相同功能到底有多少,我们是不知道的,相同功能的代码进行升级的时候我们也不知道,采用静态连
接如果库作了更新显然程序就得重写了,静态连接库的缺点,恰恰就是动态连接的优点,这也是引入的根本动机,整个过程就是一个出现问题解决问题的过程!运行
时动态连接这种方式的优点解决了将库都载入内存的麻烦,一个程序所需要的库也许并不是一个必然事件,所以单纯的动态连接只会让程序在宏观上不效率!所以
WINDOWS使用了运行时动态连接的方法!

二、PE结构
PE结构的一切元素,只有一个目的就是为了让程序载入内存!这是一个根本解决也是伴随为什么产生PE这种结构的原因!说白了它要解决的问题就是一个地
址转换的问题,怎么将磁盘上的地址转换为内存中的地址并利于程序的执行!为了更好的学习,我们先来把握重要东西,然后说一下一般有什么用!
1:整体结构
IMAGE_DOS_HEADER
DOS_STUB
IMAGE_NT_HEADERS
IMAGE_SECTION_HEADER
SECTION 1
SECTION 2
SECTION ……
SECTION N

IMAGE_DOS_HEADER  64Byte的大小和DOS_STUB
这是DOS产物,因为PE结构产生的时候比较早,第一个WINDOWS是运行在DOS环境中的,所以为了和DOS兼容PE结构引入了这个东东!
IMAGE_DOS_HEAER只有两个比较有效,一个是大家熟知的MZ,它在编程的时候称为e_magic,还有一个就是e_lfanew,也就是用
C32ASM打开PE文件时候对应的3c处,它指向的是IMAGE_NT_HEADER的offset,是一个地址!好了,对于
IMAGE_DOS_HEADER的东东就说这些,
DOS_STUB是一个DOS下标准的EXE文件,类似于用MASM写的DOS APP!

这两个东东因为用处不大,所以常被我们用于修改PE头,目的是为了防调试、免杀、保存输入表等等! PE变形技术是一种有意思的东东,而IMAGE_DOS_HEADER与IMAGE_NT_HEADER的重叠又是最常见的!大家可以搜索一篇打造微型PE的文章看看!

IMAGE_NT_HEADERS 248 Byte
通过它的名字我们就知道这里面还有HEADER,因为它是一个HEADERS,那么有什么?
它包括三部分,IMAGE_NT_SIGNATURE IMAGE_FILE_HEADER IMAGE_OPTINAL_HEADER
IMAGE_FILE_HEADER 这个是定位物理信息
IMAGE_OPTIONAL_HEADER 这个是定位内存信息,所以这里一般都是一些RVA地址!

上面提到了,PE结构的根本问题就是解决地址转换!要实现这个根本问题它有几个步骤,第一个问题就是必须知道PE是不是有效,作为一个有效的PE一般
来说是验证IMAGE_DOS_HEADER的e_magic是不不为IMAGE_DOS_SIGNATURE(4BYTE),然后验证
IMAGE_NT_SIGNATRUE,不过我在做实验的时候发现有时候并不是这样的,在PE变形的时候有时候会出错,看来真是人们说的,近信书,不如无
书!这时我们先简单的理解IMAGE_NT_SIGNATURE和IMAGE_DOS_SIGNAUTRE的作用就是为了验证PE文件是否有效,这是我们
说的PE结构第一个解决的问题!

IMAGE_FILE_HEADER 20字节
它的重要结构一般有两个一个是SizeOfOptionalSection,它指是可选头的大小,PE结构实现的是自动装载过程,那么第一个结构与下
一个结构必须有一定的联系,PE编程就是利用这些联系进行一些简单的算术运算!另一个是NumberOfSections,这个指明了节表的数目,因为这
里存在一种套子思想,我一直很喜欢这种思想。先来描述一下我的套子思想,此思想来源于阴阳太极图,如果你也喜欢太极图,你会看到阴中或者阳中都有一个圈,
李小龙传奇中说那是眼睛,那竖直是操蛋,这是阴阳太极图的精妙所在,
它描述的是每一个阴中还有阴阳,每一个阳中还有阴阴。就是一种套子思想,大家慢慢提会,这里用它来说明结构数组,比如节有很多,每一个节都是一个节表结
构,它们合起来就是一种结构数组,这显然是一种最简单的数组思想,数组思想是什么,就是上面说的套子思想,所以这个NumberOfSecions其实就
是在告诉PE装入器数组的大小!

IMAGE_OPTIONAL_HEADER  224字节
IMAGE_OPTIONAL_HEADER,这个常用的我先列出来然后告诉大家怎么记住它们!
AddressOfEntryPoint:程序的入口点,这个大家比较熟悉,免杀的最后一步
ImageBase :基址,上面我在基本概念中作了说明
SectionAligment :内存对齐粒度(这个用GetSystemInfo()就可以找到
FileAlignment:文件对齐粒度(碰到陌生名字,就把它们当成美女的名字,多记几次就成了熟人了)
SizeOfImage:内存中的镜象大小
SizeOfHeaders:所有的头大小,这个可以通过IMAGE_BASE+SIZEOF_HEADERS来定位IMAGE_SECTION_HEADER的位置
DataDirectory:目录,这里面保存的是需要操作系统提供的东东比如DLL文件有128个字节
  
好了,上面列出这些比较重要的东东,那么它们的作用是什么呢?茫然的时候请回归根本,我们的问题是解决如何载入内存,那么要载入内存,我们首先要找到
第一个要载入的指令或者数据吧,这个就是入口点,找到了载入谁,我们要解决的问题就是载入到哪里?
载入到哪里呢?这就是有基址来说明,程序很大,怎么将它们分配的更加有规律呢》我们必须知道内存中最小的规律单位大小,这个就是内存对齐的粒度,我们知道
了载入内存的基本单位,要载入还得有一个前提,就是找到文件中的粒度,这有什么用,这可以算地址的!在上面我还说了一个现象,就是每一个结构和下一个结构
都是有联系的到这里我们来总结一下这些联系!
IMAGE_DOS_HAEDER的e_lfanew定位到了IMAGE_NT_HEADER的物理偏移,IMAGE_FILE_HEADER的NumberOfSecion指出节表结构数组的大小,同时指出为Opional_Header的大小!
OPTIONAL_HEADER指明了程序第一个要载入的地址,指明了它载入哪,指明了其它指令以多大单位来载入,上面只是初步工作,它与
IMAGE_SECTION_HEADER的联系,再于如何找定位IMAGE_SECTION_HEADER的位置!有e_lfanew找到了
IMAGE_NT_HEADERS,有IMAGE_NT_HEADERS的最后一个头也就是IMAGE_OPTIONAL_HEADER指明了
IMAGE_SECTION_HEADER的地址!

好了,下面让大家记住这些位置!要记住这些位置只要记住两个数字,16和32
IMAGE_NT_SIGNATURE也就是PE,这两个字符是PE头开始的标志!找到它你就找到了IMAGE_NT_HEADER的起始!

后面20个字节就是IMAGE_FILE_HEADER的内容,下面说重点,从IMAGE_FILE_HEADER起
加16个字节,就是AddressOfEntryPoint
加32个字节  左边是ImageBase右边依次是SecionAlignmen FileAlignment
从FileAlignment开始,大家注意这里不是从IMGA_FILE_HEADER的结尾算了,从FileAlignment开始加16个字节就是SizeOfImage,后面接的就是SizeOfHeaders!

相信记住16和32你就记住了大部分的内容!
对于DataDirectory它是128个字节,当你看到.text或者是.code节的时候,回推128个字节就是DataDirectory了!

IMAGE_DATA_DIRECTORY
这也是一个结构数组,它的定位方式也是通过宏来进行的,我这里只说输入表和它的关系!IMAGE_DATA_DIRECTORY只有两个重要的元素,
第一个是所指向元素的RVA,第二个指向元素的大小,RVA就可确定出所指向的元素的地址,大小队以元素的大小就是指向的结构数组的大小!
输入表是一个IMAGE_IMPORT_DESCRIPOR的结构!IMAGE_DATA_DIRECTORY的RVA值指向的就是它的RVA,SIZE/它的结构大小就是指向的IMAGE_IMPORT_DESCRIPTOR的数组大小!

IMAGE_IMPORT_DESCRIPOR的主要内容如下
OrignalThunk HNT的RVA
FirstThunk IAT的RVA
Name DLL文件名的RVA

大家明白这里面的值都是RVA就可以了,这个RVA指向的并是DLL中导出函数的RVA,它们指向一个
IMAGE_DATA_THUNK的结构,这个结构保存了导入函数的RVA,也就是说要定位一个DLL文件中的函数,必须经历三次RVA才可找到!

IMAGE_SECTION_HEADER
这个结构的主要内容有两个
VirtualAddress:这个是LORDPE中的Roffset的地址
PointToRawData:这个是LORDPE中的Voffset的地址
VirtualSize:是内存中的大小,除以上面说的粒度就知道需要几个基本功能单位了
SizeOfRawData:这个文件中的大小,除以上面说的文件粒度就知道有几个基本功能单位了!
它的作用是计算节偏移!  它的结构大小为20个字节,在PE文件中它是一个结构数组,数组大小有IMAGE_FILE_HEADER的NumberOfSection来决定,
这个大家打开一个PE文件自己找一次就可以了!下面直接给出一个上面所述内容的C++版的编程实现代码!

#include <windows.h>
#include <iostream>
using namespace std;
int main(int argc,char *argv[])
{
        //定义变量
        IMAGE_DOS_HEADER DosHeader;
        IMAGE_NT_HEADERS NtHeader;
        IMAGE_SECTION_HEADER SecHeader;

HANDLE Hfile;
        char FileName[256];
        DWORD Dwsize;
        int OffsetSection=0,NumSection=0;
        int i=0,j=0;
        int Offset=0,int Num=0;

//以系统自带的CMD程序为例进行说明
        GetSystemDirectory(FileName,256);
        strcat(FileName,"\\cmd.exe");
       
if((Hfile=CreateFile(FileName,GENERIC_WRITE|GENERIC_READ,FILE_SHARE_READ|FILE_SHARE_WRITE,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL))==INVALID_HANDLE_VALUE)
        {
                cout<<"INVALID_HANDLE_VALUE";
                return 0;
        }

//SetFilePointer()
        SetFilePointer(Hfile,0,0,FILE_BEGIN);

//ReadFile()
        ReadFile(Hfile,&DosHeader,sizeof(DosHeader),&Dwsize,NULL);
       
        if(DosHeader.e_magic!=IMAGE_DOS_SIGNATURE)
        {
                cout<<"没有DOS头"<<endl;
                CloseHandle(Hfile);
                return 0;
        }
        else
        {
                cout<<"有DOS头"<<endl;
        }

SetFilePointer(Hfile,DosHeader.e_lfanew,0,FILE_BEGIN);

ReadFile(Hfile,&NtHeader,sizeof(NtHeader),&Dwsize,NULL);

if(NtHeader.Signature!=IMAGE_NT_SIGNATURE)
        {
                cout<<"没有PE头"<<endl;
                CloseHandle(Hfile);
        }

else
        {
                cout<<"PE有效"<<endl;
                cout<<"######## IMAGE_FILE_HEADER的信息############"<<endl;
                cout<<"Machine:"<<NtHeader.FileHeader.Machine<<endl;
                cout<<"NumberOfSections:"<<NtHeader.FileHeader.NumberOfSections<<endl;
                cout<<"SizeOfOptionalHeader:"<<NtHeader.FileHeader.SizeOfOptionalHeader<<endl;
                cout<<endl;
                cout<<"######## IMAGE_OPTIONAL_HEADER的信息########"<<endl;
                cout<<"AddresssOfEntryPoint:"<<NtHeader.OptionalHeader.AddressOfEntryPoint<<endl;
                cout<<"ImageBase:"<<NtHeader.OptionalHeader.ImageBase<<endl;
                cout<<"SectionAlignment:"<<NtHeader.OptionalHeader.SectionAlignment<<endl;
                cout<<"FileAlignment:"<<NtHeader.OptionalHeader.FileAlignment<<endl;
                cout<<"SizeOfImage:"<<NtHeader.OptionalHeader.SizeOfImage<<endl;
                cout<<"NumberOfHeaders:"<<NtHeader.OptionalHeader.SizeOfHeaders<<endl;
                cout<<endl;
                cout<<"######## IMAGE_DESCRITOR结构数组的RVA地址####"<<endl;
               
cout<<"IMAGE_IMPORT_DESCRIPTOR的
RVA:"<<hex<<NtHeader.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress<<endl;
               
        }

//采用IMAGE_DOS_HEADER.e_lfanew+sizeof(IMAGE_NT_SIGNATURE)+sizeof(IMAGE_FILE_HEADER)+sizeof(IMAGE_OPTIONAL_HEADER)的算法
        NumSection=NtHeader.FileHeader.NumberOfSections;
        OffsetSection=DosHeader.e_lfanew+0x18+sizeof(IMAGE_OPTIONAL_HEADER);
        for(i=0;i<NumSection;i++)
        {
                SetFilePointer(Hfile,OffsetSection+sizeof(IMAGE_SECTION_HEADER)*i,0,NULL);
                ReadFile(Hfile,&SecHeader,sizeof(IMAGE_SECTION_HEADER),&Dwsize,NULL);
                for(j=0;j<8;j++)
                {
                        //输出每一个节头
                        cout<<SecHeader.Name[j];
                }
                cout<<endl;
                //输出每一个节的信息
                cout<<"PointToRawOfData:"<<hex<<SecHeader.PointerToRawData<<endl;
                cout<<"VirtualAddress:"<<hex<<SecHeader.VirtualAddress<<endl;
                //输出第一个节的节偏移并计算IMAGE_IMPORT_DESCRIPTOR结构数组的物理偏移
                if(i==0)
                {
                        //offset=va-ImageBase-节偏移
                       
cout<<".text段的节偏移:"<<hex<<SecHeader.VirtualAddress-
SecHeader.PointerToRawData<<endl;
                       
cout<<"IMAGE_IMPORT_DESCRIPTOR的物理偏
移:"<<NtHeader.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress-(SecHeader.VirtualAddress-SecHeader.PointerToRawData)<<endl;
                }
        }
       
       
        CloseHandle(Hfile);
        return 0;

}

三、思想总结
对比一下代码我想你就理解了,下一张看雪的图仔细看,一定要仔细,好了帖就到这吧!有了这些内容,基本上改个变态的PE就有了基础!
总结一下我们用了一些什么思想!
1:集合的思想
将不同的内容规到一个集合中然后让它们产生对应这就是一种函数的思想
2:结构数组套子思想
套子思想从太极图中得出的一个结论
3:回归思想
不容易理解时候回归根本,从根本问题中去理解为什么
4:存在就有道理
我个人觉得这个思想比较重要,多问个为什么,本文开始我就作了说明,它产述的就是这个思想,为什么是虚拟地址,一个简单的现象,却隐藏一个事物发展的过程!
5:过程理解
理解一个过程后再去理解它的不足或者说具体内容,这是一种整体把握思想

时间: 2024-10-06 17:10:50

PE文件结构深入详解的相关文章

pe文件头详解

原文地址:https://www.cnblogs.com/Chary/p/10050070.html

PE文件结构与函数导出表——详解与实例

PE文件结构与函数导出表--详解与实例 随着windows系统从Xp升级到Win7.Win8, 从32位升级到64位,PE文件结构在整体未变的情况下发生了一些小的变动,一方面是推荐的程序装载地址未采用,另一方面,导出函数序号不再是简单的升序,而是一定程度上的进行了乱序.本文首先对PE文件结构进行了详尽的解说,接着介绍了如何得出函数导出表,整个过程采用SysWoW64目录下的wininet.dll实例进行说明.在介绍过程中,明确指出了Win7.Win8等新系统相对Xp带来的区别. 文章链接:htt

PE文件结构详解(四)PE导入表

PE文件结构详解(二)可执行文件头的最后展示了一个数组,PE文件结构详解(三)PE导出表中解释了其中第一项的格式,本篇文章来揭示这个数组中的第二项:IMAGE_DIRECTORY_ENTRY_IMPORT,即导入表. 也许大家注意到过,在IMAGE_DATA_DIRECTORY中,有几项的名字都和导入表有关系,其中包括:IMAGE_DIRECTORY_ENTRY_IMPORT,IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT,IMAGE_DIRECTORY_ENTRY_IAT

PE文件结构详解(二)可执行文件头

在PE文件结构详解(一)基本概念里,解释了一些PE文件的一些基本概念,从这篇开始,将详细讲解PE文件中的重要结构. 了解一个文件的格式,最应该首先了解的就是这个文件的文件头的含义,因为几乎所有的文件格式,重要的信息都包含在头部,顺着头部的信息,可以引导系统解析整个文件.所以,我们先来认识一下PE文件的头部格式.还记得上篇里的那个图吗? DOS头和NT头就是PE文件中两个重要的文件头. 一.DOS头 DOS头的作用是兼容MS-DOS 操作系统中的可执行文件,对于32位PE文件来说,DOS所起的作用

PE文件结构详解(三)PE导出表

上篇文章 PE文件结构详解(二)可执行文件头 的结尾出现了一个大数组,这个数组中的每一项都是一个特定的结构,通过函数获取数组中的项可以用RtlImageDirectoryEntryToData函数,DataDirectory中的每一项都可以用这个函数获取,函数原型如下: PVOID NTAPI RtlImageDirectoryEntryToData(PVOID Base, BOOLEAN MappedAsImage, USHORT Directory, PULONG Size); Base:模

PE文件结构详解(五)延迟导入表

PE文件结构详解(四)PE导入表讲 了一般的PE导入表,这次我们来看一下另外一种导入表:延迟导入(Delay Import).看名字就知道,这种导入机制导入其他DLL的时机比较“迟”,为什么要迟呢?因为有些导入函数可能使用的频率比较低,或者在某些特定的场 合才会用到,而有些函数可能要在程序运行一段时间后才会用到,这些函数可以等到他实际使用的时候再去加载对应的DLL,而没必要再程序一装载就初始化好. 这个机制听起来很诱人,因为他可以加快启动速度,我们应该如何利用这项机制呢?VC有一个选项,可以让我

PE文件结构详解(六)重定位

前面两篇 PE文件结构详解(四)PE导入表 和 PE文件结构详解(五)延迟导入表 介绍了PE文件中比较常用的两种导入方式,不知道大家有没有注意到,在调用导入函数时系统生成的代码是像下面这样的: 在这里,IE的iexplorer.exe导入了Kernel32.dll的GetCommandLineA函数,可以看到这是个间接call,00401004这个地址的内存里保存了目的地址, 根据图中显示的符号信息可知,00401004这个地址是存在于iexplorer.exe模块中的,实际上也就是一项IAT的

PE文件结构详解

1.定位标准PE头 DOS Stub长度不固定,所以DOS头不是一个固定大小的数据结构.DOS头位于PE的起始位置,通过DOS头去定位后面标准PE头的位置就是通过字段e_lfanew. e_lfanew字段的值是一个相对偏移量,绝对定位时需要加上DOS MZ头的基地址. 也就是PE头的绝对位置是: PE_start = DOS MZ 基地址+IMAGE_DOS_HEADER.e_lfanew 2.PE文件结构 在32位系统下,最重要的部分是PE头和PE数据区. 32位系统下的PE文件被划分为:D

PE文件结构详解(一)基本概念

PE(Portable Execute) 文件是Windows下可执行文件的总称,常见的有DLL,EXE,OCX,SYS等,事实上,一个文件是否是PE文件与其扩展名无关,PE文件可以是任 何扩展名.那Windows是怎么区分可执行文件和非可执行文件的呢?我们调用LoadLibrary传递了一个文件名,系统是如何判断这个文件是一个合 法的动态库呢?这就涉及到PE文件结构了. PE文件的结构一般来说如下图所示:从起始位置开始依次是DOS头,NT头,节表以及具体的节. DOS头是用来兼容MS-DOS操