.NET的PE文件结构篇(转)

一、开篇
  开篇我要讲述一个关于PE文件结构的文章,这篇文章动手能力比较强,希望大家能够动手进行操作,这边文章篇幅有可能会长一些,为了方便大家阅读我可以将其分为几个部分进行讲解,主要分为以下几个部分:
  ①  PE文件头
  ②  导入表
  ③  导出表
  ④  资源表
  下面我来讲解下为什么要学PE文件结构,因为了解PE文件结构就会了解到数据字典中第十五存放的就是元数据通过这个可以进一步研究元数据结构,至于.NET的 PE文件结构下一次进行分析
二、.NET的特殊之处
    这里我们不讲普通程序的PE文件结构,我们只针对当前.NET程序进行分析,了解普通的PE文件结构后,我们会知道.NET的PE结构不同之处在于在PE头中的IMAGE_OPTIONAL_HEARDER这个结构中的数据目录DataDirectory这个包括了映像文件中的CLR头的RVA和大小。这就使我们能够很快的进行扩展.NET的PE文件结构,下面我们就对文件进行分析,随便找一个.NET的程序,我这里有一个程序,我们用16进制编辑器打开,找到数据目录的第十五个,这个对应的2个字节的CLR头RVA和2个字节的大小。

  现在我们来记录下这个记录:
  CLR头:RVA:0x2008   size:0x48
  既然我们知道了CLR头的RVA和大小那么我们计算他在磁盘中的RVA也就是定位在磁盘中的位置,这里我们还需要其他几个区段的RVA和文件中的大小,这里我们就不在16进制编辑器中进行查找了我们直接打开,CFF Explorer将程序载入后我们查看区块表信息:

  那么我们就开始进行定位,定位该区段在文件中的地址,这里我们来看这个CLR头的RVA落在了那个区段上,CLR的RVA为0x2008,我们首先看的是第一个区段.text,该区段装在在内存的地址是0x2000,而这个区段的大小事0x12A00,所以这个区段的范围是0x2000-0x14A00,刚好0x2008落在这个区段上,那么我们来算出他在文件中的偏移,2008-2000=8,200+8=208;也就是0x208的位置是CLR在文件的RVA。下面实例图将表述算出来的过程:

  这张图已经很清楚的说明了这个RVA的换算公式,也是我们在这里标出来的?号处就是我们要的东西,这里内存中的CLR头是内存里面的地址0x2008而区段的开始RVA是0x2000这样就是S=0x2000,R=.0x2008那么差值=8,P=0x200这样的话?=0x208这样就算出了文件的RVA;
这样我们找到了CLR头的RVA 我们就来16进制编辑器中进行查看CLR头,下面是CLR头的结构:

typedef struct IMAGE_COR20_HEADER
{
    ULONG         cb;
    USHORT        MajorRuntimeVersion;
    USHORT        MinorRuntimeVersion;
    //符号表和开始信息
    IMAGE_DATA_DIRECTORY    MetaData;
    ULONG        Flags;
    union{
        DWORD    EntryPointToken;
        DWORD    EntryPointRVA;
               };
     //绑定信息
      IMAGE_DATA_DIRECTORY     Resource;
      IMAGE_DATA_DIRECTORY    StrongNameSignature;
      //常规的定位和绑定信息
      IMAGE_DATA_DIRECTORY    CodeMagagerTable;
      IMAGE_DATA_DIRECTORY    VTableFixups;
      IMAGE_DATA_DIRECTORY    ExprotAddressTableJumps;

IMAGE_DATA_DIRECTORY    MagageNativeHeader;
}IMAGE_COR20_HEADER
   下面是对应字段的描述和对应的大小偏移量等等信息:


偏移量

大小

字段名

描述

0

4

Cb

头的字节大小。

4

2

MajorRuntimeVersion

CLR需要运行程序的最小版本的主版本号。

6

2

MinorRuntimeVersion

CLR需要运行程序的最小版本的次版本号。

8

8

MetaData

RVA和元数据的大小。

16

4

Flags

二进制标记,在接下来的章节讨论。在ILAsm中,你可以通过显示地使用指令.corflags <integer value>和/或命令行选项/FLAGS=<integer value>详细指明这个值。这个命令行选项优先于指令。

20

4

EntryPointToken/EntryPointRVA

这个映像文件的入口点的元数据识别符(符号);对于DLL映像而言可以是0。这个字段识别了属于这个模块的一个方法或包括这个入口点方法的一个模块。在2.0或更新的版本中,这个字段可能包括内嵌的本地入口点方法的RVA

24

8

Resources

RVA和托管资源的大小。

32

8

StrongNameSignature

RVA和用于这个PE文件的哈希数据的大小,由加载器在绑定和版本控制中使用。

40

8

CodeManagerTable

RVA和代码管理表的大小。在现有的CLR发布版本中,这个字段是保留的,并被设置为0。

48

8

VTableFixups

RVA和一个由虚拟表(v-表)修正组成的数组的字节大小。在当前托管的编译器中,只有VC++连接器和IL编译器能够生成这个数组。

56

8

ExportAddressTableJumps

RVA和由jump thunk的地址组成的数组的大小。在托管的编译器中,只有8.0之前版本的VC++能够生成这种表,这将允许导出内嵌在托管PE文件中的非托管本地方法。在CLR的2.0版本中,这个入口是废弃的并且必须被设置为0。

64

8

ManagedNativeHeader

为预编译映像而保留的,被设置为0。

  既然我们已经知道了整个CLR头的结构,那么我们就来对.NET的这个文件进行十六进制查找下:CTRL+G查找0x208

  对应这一块就是CLR头的数据,我们可以一步一步进行分析,比如cb占2个字节那么他就是00000048这个数据,以此进行分析可以将所有数据进行分析出来。注意是这里面是以小端的形式存放,也就是他要从后面的是高位,前面的是地位。
  那么我可以注意到这个字段StrongNameSignature这个字段就是强命名的字段,如果程序加了强命名我们的一种手段就是将这个RVA和大小全部设置为0就去除了强命名。还有就是Flags标志位,标志里面去除COMIMAGE_FLAGEX_STRONGNAMESIGNED=0x00000008//此程序有强命名。
    这里我们要强调的是根据表中最重要的MetaData项,来查看元数据在PE文件中的存储格式,我们可以在上图中寻找到:

<ignore_js_op>

  其中元数据(MetaData)的RVA:0000B2D8,元数据的大小为:00009534,通过这个RVA我们可以将其换算成文件地址,那么这个RVA落在了第一个区段上也就是.text段上,这样的话我们就可以换算出文件中的RVA:0x94D8,那么我们就可以在16进制编辑器中查看元数据头的结构。首先我们先看一下整体结构是什么:

类型

字段

描述

DWORD

lSignature

424A5342h,就是4个固定的ASCII码,代表.NET四个创始人的首位字母缩写

WORD

iMajorVersion

元数据的主版本,一般为1

WORD

iMinorVersion

元数据的副版本,一般为1

DWORD

iExtraData

保留,为0

DWORD

iLength

接下来版本字符串的长度,包含尾部0,且按4字节对其

BYTE[ ]

iVersionString

UTF8格式的编译环境版本号

BYTE

fFlags

保留为0

BYTE

[padding]

此字节无意义,对齐用

WORD

iStreams

NStream的个数(流的个数)

  既然我们已经了解了元数据头的结构之后我们就对应的RVA看一下16进制编辑器里面的内容:

  这里我们就不将所有的字段的值取出来我们直接用CFF来看一下我们查找的数据是不是正确的;

  其实这里面最重要的就是我们要看一下流到底有多少个,这里面最后一个字段就是iStreams这里面显示的是5,那么就说明有5个流数据,接下来就开始分析几个流数据,紧接着元数据头便是几个流数据的头,流按存储结构的不同分为堆(heap)和表(Table),在元数据中堆是用来存储字符串和二进制对象。堆分为以下三种:
  #Strings:UTF8格式的字符串堆,包含各种元数据的名称(比如类名,方法名,成员名,参数等),以0开始以0结尾。
  #Blob:二进制数据堆,存储程序中非字符串信息,比如常量值,方法的signature、pubicKey等。每个数据的长度由该数据的前1-3为决定:0表示长度1字节,10表示长度2字节,110表示长度4字节。
  #GUID:存储所有的全局唯一标识
  #US:用户自定义字符串
  #~:元数据表流,重要的流,几乎所有元数据的信息都以表的形式存在
  上面我们已经提及到了,MetaData Root紧接着就是流数据,那么我们先看一下流数据的结构,方便我们对其进行分析:

   大小      字段      描述  
DWORD iOffset 该流的存储位置相对于MetaData   Root的偏移
DWORD iSize 该流占多少字节
char[] rcName 流的名称,与4字节对齐

既然我们看到流数据头的结构我们可以发现iOffset这个字段是关于流存储的位置,也就是流数据头里面存放的是真正流数据的位置,那么我们上面找到的元数据头的地址是RVA:0x94D8这样的话我们就可以找到真正的对应的流数据了!那么我们先看一下整体的流数据,我们已经知道一共有5个流数据。

<ignore_js_op>

  其中的红色“|”标示着下一个流数据结构的开始,相应对应的结果我用CFF更直观的展现给大家看,这样我们就可以进行一个详细的对比;

经过我们上下数据的比较数据完全符合那么,就说明我们流数据头找的是正确的。
既然我们将流数据头找出来,我们就对这5个流数据进行分析,这里我们就单纯的讲一下#~流,因为这个是.NET都要存在的!上面我们可以看到#~流相对于MetaData的偏移量是0x6C,0x94D8+0x6C就是真正该流数据的存储位置:0x9544,好的,既然已经寻找到了这个地址那么先来了解下#~内部存储结构是什么样的?请看下表:

大小

字段

描述

4 bytes

Reserved

保留,为0

1 byte

Major

元数据表的主版本号,于.NET主版本号一致

1 byte

Minor

元数据的副版本号,一般为0

1 byte

Heaps

Heap中定义数据时的索引的大小,为0表示16位索引值,若堆中数据超出16位数据表示范围,则使用32位索引值。01代表strings堆,02h代表GUID堆04h代表blob堆

1 byte

Rid

所有元数据表中记录最大索引值,在运行时有.NET计算,文件中通常为1

8 bytes

MaskValid

8字节长度的掩码,每个为代表一个表,为1表示该表有效,为0表示该表无效

8 bytes

Sorted

8字节长度的掩码,每个为代表一个表,为1表示该表已排序,反之为0

  下面我们来看一下该程序的#~元数据表流的存储内容,将程序载入到16进制编辑器中,CTRL+G进行搜索0x9544,这个地址就是元数据表流的开始位置:如下所示:

  红色地方代表的是Vaild,其中的数据是0XF0929B69D57,那么将其换算成二进制,看一下哪一些表是有效的,二进制数据如下图所示:

<ignore_js_op>

  其中红色部分表示表数据是有效的一共有24个表,元数据中所有的表:

00-Module 01-TypeRef 02-TypeDef
03-FiledPtr 04-Filed 05-MethodPtr
06-MethodDef 07-ParamPtr 08-Param
09-MethodImpl 10-MemberRef 11-Constant
12-CustomAttribute 13-FieldMarshal 14-DeclSecurity
15-ClassLayout 16-FieldLayout 17-StandAloneSig
18-EventMap 19-EventPtr 20-Event
21-PropertyMap 22-PropertyPtr 23-Property
24-MethodSemantics 25-MethodImpl 26-ModuleRef
27-TypeSpec 28-ImplMap 29-FiledRVA
30-ENCLog 31-ENCMap 32-AssemblyRef
33-AssemblyProcessor 34-AssemblyOS 35-Assembly
36- AssemblyRefProcessor 37- AssemblyRefOS 38- File
39-ExportedType 40-ManifestResource 41- NestedClass
42-GenericParam 43-MethodSpec 44-GenericParamConstraint

  紧接着元数据表头的是一串4字节数组,每个双字节代表该表中有多少项纪录(record),本程序中存在24个表那么就是,24*4=144个字节。那么我们就从元数据头结尾处进行查找:

  我们来验证一下正确性使用CFF来看一下:

  经过我们的验证确实是Module里面只有一条纪录。点开就可以看到内部结构是什么!这里我们不去讲所有表的结构。
这样我们已经知道了元数据是描述数据的数据,那么这句话要怎么理解呢?那么就来用一个例子来解释下这个说明的含义:比如该程序我们将其反编译成IL代码,查看IL代码的元数据.

  这里我要不去讲这个Token的由来,我只讲一下这个Token怎么去索引,前面比如这个02000002,前面的02代表在元数据表中的第二个表也就是TypeDef表,至于表内部的结构自己可以再进行研究。那么后面的02代表的是什么呢?代表的是表里面的第二条纪录。截图说明下:

  和IL图中描述一致:

  至于剩下的#Strings堆都是一些二进制形式存在的数据。为了节省篇幅就到此了!其他的自行分析!
三、结束语
  有可能这分析当中会存在一些问题,希望各位能人指出,我将其该正。抽时间将这篇文章整理出来!

时间: 2024-10-08 04:39:32

.NET的PE文件结构篇(转)的相关文章

浅析MSIL中间语言——PE文件结构篇

一.开篇 开篇我想讲一下于本文无关的话题,其实我很想美化一下自己博客园一直没时间弄,无意间找了博客园李宝亨的博客园里面有一篇分享自己主题的文章,我就将这个模板暂时用作我的blog主题,我要讲述一个关于PE文件结构的文章,这篇文章动手能力比较强,希望大家能够动手进行操作,这边文章篇幅有可能会长一些,为了方便大家阅读我可以将其分为几个部分进行讲解,主要分为以下几个部分: ①  PE文件头 ②  导入表 ③  导出表 ④  资源表 下面我来讲解下为什么要学PE文件结构,因为了解PE文件结构就会了解到数

PE文件基础

① PE (Portable Executable):微软参考COFF(Common Object File Format)规范,在Windows NT系统上制定的一种标准, 用于exe可执行文件.obj目标文件和dll动态链接库等文件格式.PE32+是PE的64位扩展,其并未添加额外结构,只是把原来32位的字段变成了64位. 与COFF一样,PE也是基于段(Segment,注:有时也被叫节Section)的结构, 按照不同属性将信息分段存放,常见的段有:代码段(.text).数据段(.data

深入剖析PE文件

不赖猴的笔记,转载请注明出处. 深入剖析PE文件 PE文件是Win32的原生文件格式.每一个Win32可执行文件都遵循PE文件格式.对PE文件格式的了解可以加深你对Win32系统的深入理解. 一.        基本结构. 上图便是PE文件的基本结构.(注意:DOS MZ Header和部分PE header的大小是不变的:DOS stub部分的大小是可变的.) 一个PE文件至少需要两个Section,一个是存放代码,一个存放数据.NT上的PE文件基本上有9个预定义的Section.分别是:.t

获取pe文件的文件类型

工程文件petype.cpp通过调用pefile类中的函数获取文件类型. 文件类型的判断通过5个监测点完成. 监测点1:dos头的e_magic 监测点2:nt头的Signature 监测点3:文件头的Characteristics 监测点4:可选头的Magic 监测点5:可选头的Subsystem 通过监测点1和2判断是否是pe文件: 通过监测点3判断文件是否是动态库文件 通过监测点4判断文件是pe32还是pe32+还是rom映像 通过监测点5判断文件是否是0环可执行文件[驱动文件],还是3环

PE文件的执行顺序

当一个PE文件被执行时,PE装载器首先检查DOS MZ header里的PE header的偏移量.如果找到,则直接跳转到PE header的位置. 当PE装载器跳转到PE header后,第二步要做的就是检查PE header是否有效.如果该PE header有效,就跳转到PE header的尾部. 紧跟PE header尾部的是节表.PE 装载器执行完第二步后开始读取节表中的节段信息,并采用文件映射方法将这些节段映射到内存,同时附上节表里指定节段的读写属性. PE文件映射入内存后,PE装载器

获取PE文件的输入表信息

输入表是PE文件结构中不可或缺的部分,输入表也称之为"导入表". 要想了解输入表,首先还得先从DLL文件入手.日常生活中我们会看见一些大型软件有很多的DLL格式的文件,这些文件中有很多的导入函数,这些函数不会直接被执行.当一个程序(EXE)运行时,导入函数是被程序调用执行的,其执行的代码是不在主程序(EXE)中的一小部分函数,其真正的代码却在DLL文件中.这时我们就会想,那么EXE主程序是如何找到这些需要导入的函数呢,这就要归结于“输入表”了,输入表就相当于EXE文件与DLL文件沟通的

计算PE文件校验和

// 计算PE校验和 #include <ImageHlp.h> #pragma comment(lib,"Imagehlp.lib") void Getchecksum(TCHAR* pszPath) { /* Code by Lthis 转载请注明出处 */ DWORD dwHeaderSum, dwCheckSum; // 一般PE文件中CheckSum字段存储的 // 是本代码中的dwHeaderSum获取的值 // 打开文件 HANDLE hFile = Crea

手写PE文件(二)

[文章标题]: 纯手工编写的PE可执行程序 [文章作者]: Kinney [作者邮箱]: [email protected] [下载地址]: 自己搜索下载 [使用工具]: C32 [操作平台]: win 7 [作者声明]: 只是感兴趣,没有其他目的.失误之处敬请诸位大侠赐教! ---------------------------------------------------------------------------------------------------------------

手写PE文件(一)

DOS Header(IMAGE_DOS_HEADER)->64 Byte DOS头部 DOS Stub 112字节 "PE"00(Signature) 4个字节 IMAGE_FILE_HEADER  20个字节 PE文件头       IMAGE_OPTIONAL_HEADER32  96个字节 数据目录表         16*8=128个字节 IMAGE_SECTION_HEADER    40个字节 块表              IMAGE_SECTION_HEADER