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

转自 https://www.cnblogs.com/dwlsxj/p/4052871.htmlhtml

1、开篇数组

  开篇我想讲一下于本文无关的话题,其实我很想美化一下本身博客园一直没时间弄,无心间找了博客园李宝亨的博客园里面有一篇分享本身主题的文章,我就将这个模板暂时用做个人blog主题,我要讲述一个关于PE文件结构的文章,这篇文章动手能力比较强,但愿你们可以动手进行操做,这边文章篇幅有可能会长一些,为了方便你们阅读我能够将其分为几个部分进行讲解,主要分为如下几个部分:数据结构

  ①  PE文件头编辑器

  ②  导入表spa

  ③  导出表命令行

  ④  资源表3d

  下面我来说解下为何要学PE文件结构,由于了解PE文件结构就会了解到数据字典中第十五存放的就是元数据经过这个能够进一步研究元数据结构,至于.NET的 PE文件结构下一次进行分析版本控制

2、.NET的特殊之处

    这里咱们不讲普通程序的PE文件结构,咱们只针对当前.NET程序进行分析,了解普通的PE文件结构后,咱们会知道.NET的PE结构不一样之处在于在PE头中的IMAGE_OPTIONAL_HEARDER这个结构中的数据目录DataDirectory这个包括了映像文件中的CLR头的RVA和大小。这就使咱们可以很快的进行扩展.NET的PE文件结构,下面咱们就对文件进行分析,随便找一个.NET的程序,我这里有一个程序,咱们用16进制编辑器打开,找到数据目录的第十五个,这个对应的2个字节的CLR头RVA和2个字节的大小。code

  如今咱们来记录下这个记录:
  CLR头:RVA:0x2008   size:0x48htm

  既然咱们知道了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文件中的存储格式,咱们能够在上图中寻找到:

  其中元数据(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个流数据。

  其中的红色“|”标示着下一个流数据结构的开始,相应对应的结果我用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,那么将其换算成二进制,看一下哪一些表是有效的,二进制数据以下图所示:

  其中红色部分表示表数据是有效的一共有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堆都是一些二进制形式存在的数据。为了节省篇幅就到此了!其余的自行分析!

相关文章
相关标签/搜索