首页 > 代码库 > PE文件结构深入详解

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:过程理解
理解一个过程后再去理解它的不足或者说具体内容,这是一种整体把握思想

PE文件结构深入详解