《黑客免杀攻防学习笔记》——PE文件结构2

来源:互联网 发布:d3.js 电子书 编辑:程序博客网 时间:2024/06/05 15:13

文章中的图片大部分为自己做截屏,少量摘自网络,若有侵权请及时告知,我会尽快删除。转载请注明出处。

接上一篇,上一篇了解了PE头及可选头的基本结构,这一篇从区段表开始学习。

3.区段表

         PE文件头后面是区段表,用于描述各个区段的属性,文件至少拥有一个区段才能执行。区段表是多个相连的IMAGE_SECTION_HEADER结构组成。

3.1IMAGE_SECTION_HEADER结构

typedef struct _IMAGE_SECTION_HEADER {

   BYTE   Name[IMAGE_SIZEOF_SHORT_NAME]; //区段名,长度8字节的ASCII字符串

   union {

           DWORD   PhysicalAddress;

           DWORD   VirtualSize;

    }Misc;      //区段大小,实际被使用的区段大小,即未被对齐之前的大小

   DWORD   VirtualAddress;    //此区段加载到内存后的RVA,按照内存页对齐

   DWORD   SizeOfRawData;   //此区段在磁盘中的体积,按照文件页对齐

   DWORD   PointerToRawData;//此区段在文件中的偏移

   DWORD   PointerToRelocations;//此区段重定位表的偏移(用于OBJ文件)

   DWORD   PointerToLinenumbers;//行号表在文件中的偏移(用于调试)

   WORD    NumberOfRelocations;//重定位表项数量(用于OBJ文件)

   WORD    NumberOfLinenumbers;//行号表项数量

   DWORD   Characteristics;//区段属性值,具体的值和前面那张文件属性表相同

} IMAGE_SECTION_HEADER,*PIMAGE_SECTION_HEADER;

3.2区段名功能约定

区段名

描述

.text

代码段,里面的数据全都是代码

.data

可读写的数据段,存放全局变量或静态变量

.rdata

只读数据区

 .idata

导入数据区,存放导入表信息

.edata

导出数据区,导出表信息

.rsrc

资源区段,存放程序用到的所有资源,如图表,菜单等

.bss

未初始化数据区

.crt

用于支持C++运行时库所添加的数据

.tls

存储线程局部变量

.reloc

包含重定位信息

.sdata

包含相对于可被全局指针定位的可读写数据

.srdata

包含相对于可被全局指针定位的只读数据

.pdata

包含异常表

.debug$S

包含OBJ文件中的Codeview格式符号

.debug$T

包含OBJ文件中的Codeview格式类型的符号

.debug$P

包含使用预编译头时的一些信息

.drectve

包含编译时的一些链接命令

.didat

包含延迟装入的数据

         上面表格中空着的都是不常用的。

4.导出表

导出表是PE文件为其他应用程序提供API的一种函数导出方式

4.1IMAGE_EXPORT_DIRECTORY

typedef struct _IMAGE_EXPORT_DIRECTORY {

   DWORD   Characteristics;   //保留,恒为0x00000000

   DWORD   TimeDateStamp;//时间戳

   WORD    MajorVersion;       //主版本号

   WORD    MinorVersion;       //子版本号

   DWORD   Name;           //指向模块名称的RVA

   DWORD   Base;             //索引基数

   DWORD   NumberOfFunctions;    //导出地址表中的成员个数

   DWORD   NumberOfNames;        //导出名称表中的成员个数

   DWORD   AddressOfFunctions;     // RVA from base of image导出地址表RVA

   DWORD   AddressOfNames;         // RVA from base of image导出名称表RVA

   DWORD  AddressOfNameOrdinals;  // RVAfrom base of image指向导出序列号的数组

} IMAGE_EXPORT_DIRECTORY,*PIMAGE_EXPORT_DIRECTORY;

4.2识别导出表

         到处便可以由名称表、函数表和序号表,后两者是必须要的,名称表则是可选的。名称表和序号表起到索引找到函数表中的函数的作用,而函数表则是记录函数地址的。然而导出表的序号顺序和函数顺序并不是对应的,在内存中的数据是被完全打乱的,函数的顺序是按照名称表来确定的。

         首先需要在数据目录项中获得导出表的RVA,然后再观察所有段的RVA,这时就会知道导出表在所在的段的RVA,相减获得偏移地址,最后查看段表获得该段在文件中的偏移,最终获得导出表的偏移地址。公式如下:

导出表Offset = 导出表RVA – 有导出表的段RVA + 该段在文件中的偏移Offset

         这里把书上的例子dll文件拿来,导出表截图如下:


可以根据上面的数据结构获得每一项的值。比如此导出表的序号从1开始,导出地址表EAT偏移为0x1398,导出名称表ENT偏移为0x13A8。下面分别是ENT和EAT的截图:


根据上面的偏移可以知道从0x1398~0x13A7为EAT,后面的0x10字节为ENT,根据ENT中的地址计算出函数名在文件中的偏移量是从0x12C8开始;而0x13B8~0x13BF八个字节则是序号表。

         通过上面这张图很明显的看到函数名和函数地址的对应关系,但是有一个有点问题,就是最后一个导出函数的地址指向0x00003354,这是在.data段中的,是不可执行的,那为什么会这样呢?从ENT指向的对应函数名可以看到他的名字是nPEDemo而不是带有fn开头的字符串,其实这是一个导出的全局变量,所以会在.data 段中。

5.导入表

5.1导入表结构

         导入表机制是指PE文件从第三方程序中导入API以提供本函数调用的机制。事实上Windows平台下所有系统提供的API函数都是通过导入导出表完成的。所以想要看程序调用了哪些函数就要看导入表。但是其实也可以手工来调用而不是直接调用,比如用LoadLibrary()等函数加载dll文件然后获得里面函数的指针,最终直接通过地址调用函数。

typedef struct _IMAGE_IMPORT_DESCRIPTOR {

   union {

       DWORD   Characteristics;  // 0 for terminating null import descriptor

       DWORD   OriginalFirstThunk; // RVAto original unbound INT (PIMAGE_THUNK_DATA)

    };//指向输入名称表INT的RVA,INT是一个IMAGE_THUNK_DATA数组,而//IMAGE_THUNK_DATA数组又指向IMAGE_IMPORT_BY_NAME,数组的最后是内容为0

//的IMAGE_THUNK_DATA结构

   DWORD   TimeDateStamp; // 0 if notbound, -1 if bound, and real date\time stamp

    DWORD   ForwarderChain;//inIMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)

                            // O.W. date/time stampof DLL bound to (Old BIND)

                                                            //-1 if no forwarders。转发链,为0则不转发

   DWORD   Name;  //指向导入映像文件的名字

   DWORD   FirstThunk;  // RVA to IAT (if bound this IAT has actualaddresses)指向输入地址表IAT的RVA

} IMAGE_IMPORT_DESCRIPTOR;

         在32位系统中IMAGE_THUNK_DATA的结构为:

typedef struct _IMAGE_THUNK_DATA32 {

   union {

       PBYTE  ForwarderString;//转发字符串RVA,当上面的转发字段为0此值有效

       PDWORD Function;          //被导入函数的实际内存地址

       DWORD Ordinal;      //被导入函数的序号,当IMAGE_THUNK_DATA高位1则有效

       PIMAGE_IMPORT_BY_NAME AddressOfData;//指向输入名称表,上面3个均无效时有效

    }u1;

} IMAGE_THUNK_DATA32;

typedef IMAGE_THUNK_DATA32 *PIMAGE_THUNK_DATA32;

         下面再来看看最后两个值的意思。Ordinal当最高位为1表示使用序号导入函数,这时低31位就是序号。Function字段是系统所用,在系统加载程序之前先逐个遍历IAT然后从这个字段取出导入函数的内存地址,将这些地址逐一跳入对应的IAT。

         下面再来看看IMAGE_IMPORT_BY_NAME结构:

typedef struct _IMAGE_IMPORT_BY_NAME {

   WORD    Hint;      //需导入的函数序号

   BYTE    Name[1];//需导入的函数名称

} IMAGE_IMPORT_BY_NAME,*PIMAGE_IMPORT_BY_NAME;

5.2识别导入表

         还是使用上面的那个dll文件,现在数据目录表中第二项找到导入表RVA和大小,然后计算在文件中的偏移:

据此找到导入表及其范围:


OriginalFirstThunk的RVA偏移0x227C,偏移为0x107C,就跟在导入表后面,这就是IAT。由于IMAGE_IMPORT_DESCRIPTOR长度为0x14,可以看到这里有三个这样的结构组成数组表示从3个dll文件导入了函数,最后由填充了0x00的IMAGE_IMPORT_DESCRIPTOR结构结束。这里就拿第一个结构为例进行INT和IAT介绍,这里结构的头4字节OriginalFirstThunk指向INT是0x227C,转化为偏移0x107C:


里面的值为0x2304,指向的是对应的IMAGE_IMPORT_BY_NAME。再看块的最后4字节0x2000,偏移量0xE00:


里面的值0x2304,也是指向同一个IMAGE_IMPORT_BY_NAME,再来看这个结构:


可以看到前两字节0x0421为函数序号,后面的字符串就是函数名。下面这幅图很好的解释了导入表的结构:


6.1资源结构

         资源在PE文件中是以三级目录形式存在的,分别是资源类型、目录资源ID与资源代码页。这三个目录都是由一个IMAGE_RESOURCE_DIRECTORY为头部的,并且在后面跟着IMAGE_RESOURCE_DIRECTORY_ENTRY结构数组。头部为后面的数组指定成员个数,而后面的结构则指向下一层目录结构(或资源数据)。

typedef struct _IMAGE_RESOURCE_DIRECTORY {

   DWORD   Characteristics;   //资源属性,一般均为0

   DWORD   TimeDateStamp; //资源建立时间

   WORD    MajorVersion;       //资源的主版本,一般情况下为0x0004

   WORD    MinorVersion;       //资源的子版本,一般情况下为0x0000

   WORD    NumberOfNamedEntries;//用字符串作为资源标识的条目个数

   WORD    NumberOfIdEntries;//用数字ID作为资源表示的条目个数

// IMAGE_RESOURCE_DIRECTORY_ENTRY DirectoryEntries[];

} IMAGE_RESOURCE_DIRECTORY,*PIMAGE_RESOURCE_DIRECTORY;

         下面再来看看紧随其后的IMAGE_RESOURCE_DIRECTORY_ENTRY结构:

typedef struct_IMAGE_RESOURCE_DIRECTORY_ENTRY {

   union {

       struct {

           DWORD NameOffset:31;         // NameIsString为1时资源名字符串偏移,其指向一个保存资源名称的结构体

           DWORD NameIsString:1;//资源名为字符串

       };

       DWORD   Name;//当位于第一层目录时表示资源类型的值,当位于第三层时,保存资源语言区域类型值

       WORD    Id;          //资源数字ID

   };

   union {

       DWORD   OffsetToData;       //数据偏移地址

       struct {

           DWORD   OffsetToDirectory:31;// DataIsDirectory:1时指向下层目录偏移地址

           DWORD   DataIsDirectory:1;       //数据为目录

       };

   };

} IMAGE_RESOURCE_DIRECTORY_ENTRY,*PIMAGE_RESOURCE_DIRECTORY_ENTRY;

下面是资源类型的值:


上面这个IMAGE_RESOURCE_DIRECTORY_ENTRY结构由两个大小为4字节的联合体组成,不同情况下联合体的有效字段是不同的:

         第一个联合体由结构所处的目录层决定,位于第一层目录则Name字段有效,保存资源类型;位于第二层去取决于资源引用方式,若是采用编号则ID有效,否则结构体有效;位于第三层Name字段有效,保存的信息是资源语言类型。

         第二个联合体内的字段根据具体情况决定,如果下一级是子目录则结构体有效,否则OffsetToData字段有效。

         经过三层目录之后最终达到一个结构体IMAGE_RESOURCE_DATA_ENTRY,这个结构将指引我们找到资源数据:

typedef struct _IMAGE_RESOURCE_DATA_ENTRY {

   DWORD   OffsetToData;//资源数据的RVA

   DWORD   Size;               //资源数据长度

   DWORD   CodePage;    //代码页信息

   DWORD   Reserved;     //保留字段,恒为0x00000000

} IMAGE_RESOURCE_DATA_ENTRY,*PIMAGE_RESOURCE_DATA_ENTRY;

         下面这个图简要描述了资源的结构:


6.2识别资源

         还是和上面一样现在数据目录表中找到资源在文件中的偏移,这里为0x1600。下面还是通过例子来看:


头0x10字节是IMAGE_RESOURCE_DIRECTORY,最后两个字节是用数字作为ID的项目个数,这里两个表示后面有两个0x08字节的IMAGE_RESOURCE_DIRECTORY_ENTRY结构体。拿第一个作为例子。由于是第一层目录,所以头4字节是Name字段,表示资源类型为字符串,0x80000020中第一位为1表示DataIsDirectory,即为目录,后面31位表示偏移,所以这里二级目录偏移为0x20,可以得到相对文件偏移为0x1620,转到那里去看:


同样看到利用编号索引的子项目数为1,所以接下来只有0x8字节是第三级目录。由于使用编号索引,所以前4字节ID有效为0x07,后4字节计算的资源偏移为0x1650,到那里看看:


这里也是只有一个用编号索引的目录,由于是第三层,所以前4字节Name有效,保存资源语言区域类型,后4字节表示资源偏移,所以可以到0x1680处寻找资源结构IMAGE_RESOURCE_DATA_ENTRY:


可以看到数据RVA为0x40A0,长度为0x38,代码页信息为0x04E4。

         至此,关于资源的三层目录简要介绍就差不多了。


PE文件的前两篇主要是讲一些重要的结构,后面的一些内容不常用,所以第3篇会讲的简单一点。


0 0
原创粉丝点击