文章中的图片大部分为自己做截屏,少量摘自网络,若有侵权请及时告知,我会尽快删除。转载请注明出处。
接上一篇,上一篇了解了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篇会讲的简单一点。