PE文件结构

PE文件

主要有exe、dll、sys文件,这些都是可执行文件

exe由多个exe文件组成,dll之类的

PE文件头解析

在内存中和硬盘中数据几乎是一样的,但是存在差异,且程序开始的位置不同,所以可以知道存储信息的位置是可以改变的

根据不同的功能分了不同的节,也就是程序不同的块,这样做1、可以节省硬盘空间,在内存中占用空间大于硬盘占用空间(节之间的间隔较小)

2、节省内存,当程序需要多开的时候,只需要将可读可写的数据段重新复制一份即可,不需要对只读的数据进行复制

硬盘(文件)对齐和内存对齐

目的都是为了提升读写的速度

老的编译器,硬盘对齐是200h个字节**(当不够200h字节,会自动填充)**,内存对齐是1000h个字节,所以PE结构执行(操作系统运行exe文件)时会发生拉伸的过程,即在内存中占用空间大于硬盘占用空间

新的编译器在硬盘和内存都采用1000h,这是因为编译器发展,这样内存和硬盘对齐一样,运行时可以减少运算,这样虽然牺牲了空间,但是缩短了时间

PE磁盘文件与内存映像结构图

块表(节表)概括节的基本信息(起始和终止位置,节的大小),DOS头和PE头对当前exe文件做的概要性描述(拉伸完后的大小,堆栈大小),节表存储信息

DOS头

DOS长度确定

解析exe文件时,先解析前两个字节,并通过DOS头找到exe文件真正开始的地方

第一个WORD MZSignature(e_magic)-MZ标记

最后一个LONG AddressOfNewExeHeader(e_lfanew)-指向了PE文件的真正开始地址

这里的0108表示从文件开始的地方计算0108个字节就是PE结构开始的地方

NT头

DOS头和NT头中间是一段空间,可以自由发挥,多与少不确定,我们可以在这一段写入shellcode( 利用软件漏洞而执行的代码 ),在程序中调用。有地址我们就可以访问和执行

PE的标记-DWORD Signature-四个字节

标准PE头-20字节

关键信息

enum IMAGE_MACHINE Machine-程序运行的CPU型号:0x0 任何处理器 /0x14C 386及后续处理器

WORD NumberOfSections-文件中存在的节的数量,如果需要新增或者合并节,就要修改这个值

time_t TimeDateStamp-文件创建时间,编译器填写

WORD SizeOfOptionalHeader-可选PE头的大小,32位PE文件默认E0h、64位PE文件默认为F0h 大小可以自定义.

struct FILE_CHARACTERISTICS Characteristics-每个位有不同的含义,可执行文件值为10F 即0 1 2 3 8位置1

这里的102拆成二进制

勾中即为1

可选PE头

结构

关键信息

enum OPTIONAL_MAGIC Magic-说明文件类型:10B 32位下的PE文件、20B 64位下的PE文件

DWORD SectionAlignment-内存对齐

DWORD FileAlignment-文件对齐

DWORD SizeOfCode-所有代码节的和,必须是FileAlignment的整数倍,编译器填的,不会被使用

DWORD SizeOfInitializedData-已初始化数据大小的和,必须是FileAlignment的整数倍,编译器填的,不会被使用

DWORD SizeOfUninitializedData-未初始化数据大小的和,必须是FileAlignment的整数倍,编译器填的,不会被使用

DWORD AddressOfEntryPoint-程序入口

DWORD BaseOfCode-代码开始的基址,编译器填的,不会被使用

DWORD BaseOfData-数据开始的基址,编译器填的,不会被使用

!!DWORD ImageBase-内存镜像基址(不从0开始是因为需要内存保护)

把文件拖入OD时会断在ImageBase+AddressOfEntryPoint这个地址,也就是OEP

程序入口可以修改-加壳

加壳之后入口点EP(RVA)和OEP(RAW)都会改变,但是内存镜像基址不变

内核重载:(有一些软件会通过hook技术,检测是否调用函数)相当于PEloader(加载)-把硬盘文件拷贝到内存中进而执行exe文件,但是在拷贝过程中还会有一个文件的缓冲区,这个缓冲区也是在内存中的。装载到内存时是以ImageBase为起点的

DWORD SizeOfImage-内存中整个PE文件的映射的尺寸,可以比实际的值大,但必须是SectionAlignment的整数倍,也就是PE文件在内存中被拉伸后的大小

DWORD SizeOfHeaders-所有头+节表按照文件对齐后的大小,否则加载会出错

DWORD CheckSum-校验和,一些系统文件有要求,用来判断文件是否被修改

DWORD SizeOfStackReserve-初始化时保留的堆栈大小

DWORD SizeOfStackCommit-初始化时实际提交的大小

DWORD SizeOfHeapReserve-初始化时保留的堆的大小

DWORD SizeOfHeapCommit-初始化时实际提交的大小

DWORD NumberOfRvaAndSizes-目录项数目

节表-每个节有28字节的信息

节表定位-DOS+PE+OPTIONPE

描述每个节的信息

节的结构

BYTE Name[8]是八个字节,不够补’\0’,在内存是0,所以不能使用char*,而要用char [9],且可以随便改

第二个成员是union Misc,双字,是该节在没有对齐前的真实尺寸,该值可以不准确(可能被别的软件加工过),但是不会妨碍运行,里面的成员DWORD VirtualSize-存放PE文件放入内存时的数据宽度,他的值可能大于文件对齐后的数据宽度(SizeofRawData),这是因为未初始化的变量不会被存入文件中

第三个成员-DWORD VirtualAddress-节区在内存中的相对偏移(拉伸-内存对齐后的),加上ImageBase才是在内存中的真正地址(跟文件中无关)

第四个成员-DWORD SizeOfRawData-节在文件中对齐后的尺寸

第五个成员-DWORD PointerToRawData-节区在文件中的偏移,所以.text是从400h开始的(跟内存中无关)

第二个和第四个是在调试时使用的,编译完这四个默认为0

struct SECTION_CHARACTERISTICS Characteristics-节的属性

最后一个成员struct SECTION_CHARACTERISTICS Characteristics-里面有32位,但不是每一位都用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
--> 标志(属性块) 常用特征值对照表:<--
[值:00000020h](*包含可执行代码)
[值:00000040h](*该块包含已初始化的数据)
[值:00000080h](*该块包含未初始化的数据)
[值:00000200h][Section contains comments or some other type of information.]
[值:00000800h][Section contents will not become part of image.]
[值:00001000h][Section contents comdat.]
[值:00004000h][Reset speculative exceptions handling bits in the TLB entries for this section.]
[值:00008000h][Section content can be accessed relative to GP.]
[值:00500000h][Default alignment if no others are specified.]
[值:01000000h][Section contains extended relocations.]
[值:02000000h][Section can be discarded.]
[值:04000000h][Section is not cachable.]
[值:08000000h][Section is not pageable.]
[值:10000000h](*该块为共享块).
[值:20000000h](*该块可执行)
[值:40000000h](*该块可读)
[值:80000000h](*该块可写)

运行状态

拉伸完之后还需要进行一些准备步骤

先把文件对齐后的数据拷贝到内存中,根据SizeofHeades,直接copy,因为头和节表是不会变化的,但是节表后面不一定是节区,这是因为文件对齐和内存对齐不同

循环赋值节的内容,复制到内存的地方由节表信息里面的VirtualAddress决定,PointerToRawData决定了文件复制到内存的起始位置

复制数据的大小根据SizeofRawData(Misc也可以,但是如果MIsc存在大量未初始化数据,会变得很大,可能将下一节的信息也copy到内存中)

根据内存数据存储的位置查找在文件中的位置

假设内存开始位置是500000,数据在内存存储位置是501234

1、确定节

先确定偏移501234-500000

根据节的不同偏移(VirtualAddress)

1234>VirtualAddress

1234<VirtualAddress+Misc.VirtualSize

2、计算距离节初始位置的长度

1234-1000=234h

3、在文件中寻找

因为在文件中和在内存中距离节初始位置的长度相同,所以地址为400+234h

代码节空白区添加代码

MessageBoxA-四个参数,执行后会出现弹窗-一般exe都有这个函数-user32.dll里面

让程序执行我们的代码,需要先找到OEP,修改为call我们函数的地址,然后再jmp回到原程序的OEP

call=E8+四个字节,jmp=E9+四个字节

但是这四个字节不是直接的地址,需要进行计算

真正要跳转的地址=E8这条指令的下一行地址(当前指令地址+5,因为call长度是5)+X(X就是E8后边跟着的四个字节)

push=0x6A

所以我们要加的程序硬编码 = 6A 00 6A 00 6A 00 6A 00 E8 00 00 00 00 E9 00 00 00 00

我们加入的代码必须加在拉伸后的文件中,也就是内存中的,因为我们计算的地址是拉伸后的地址

添加代码过程-文件注入

1、先看代码空白区能不能填充那十八个字节——(SizeofRawData-VirtualSize)

2、找到添加的地方,假设我们开始是从1000h开始的,而SizeofRawData是1a0000,所以两者相加就是当前节结束的地址,而在1b0000之前为0的部分是代码块的空白部分

3、开始填充我们的那十八个字节

4、算地址,要计算内存对齐的地址,而不是文件对齐的,因为我们需要的是运行时的地址(内存对齐和文件对齐不同)

先找到MessageBoxA在内存中的地址,在OD里面使用命令bp MessageBoxA,按状态栏的B可以找到内存的地址

5、填充,注意小端序,E8跟着计算后的地址,E9跟着计算后的OEP(ImageBase+EntryPoint)

6、将原本的EntryOfPoint改为我们填充数据在内存中的地址

任意代码节空白区添加代码

从内存转为硬盘文件计算大小-最后一个节的初始位置+最后一个节的大小

新增节-添加代码

新增之后,修改NumberOfSections,节表信息,节表后面必须跟着一定长度的00数据(空白区),所以要计算增加之后SizeofHeaders会不会大于PointerOfRawData,并且在新增节最后加上一个节表信息长度的00

计算内存偏移VirtualAddress(RVA)时,根据上一个节区的VirtualAddress+(SizofRawData/VirtualSize[谁大加谁])按照内存对齐后的数

如果编译器在节表中加入一些数据,而我们不能修改,节表又不能断,只能将NT头前移(加入数据之前的),这样再去添加新的节表信息,就不会占用编译器加入的数据

当DOS到NT头大小不够开辟一个节表的信息,只能扩充最后一个节

扩大节-合并节-数据目录

扩大节:在内存中进行扩大,然后再还原回文件中,还原的时候要修改参数

1、拉伸到内存

2、分配新空间N=SizeofImage+Ex

3、将最后一个节的SizeOfRawdata和VirtualSize改为N

4、修改SizeofImage

合并节:合并之后节表有空间进行添加节区

VirtualSize=SizeofImage-VIrtualAddress

数据目录-里面存储各种表的信息

下面介绍几种比较重要的

DLL-动态链接库和静态链接库

使用的时候都需要include

程序编译的过程为将.h和.cpp等文件进行预编译,然后进行编译,再进行汇编,最后链接上(.a/.lib/.so/.dll)成为可执行文件

静态库、动态库区别来自【链接阶段】如何处理库(处理方式不同来区分),链接成可执行程序。分别称为静态链接方式、动态链接方式

静态链接

使用的时候需要include和#pragma comment(lib,“xxx.lib”)

因为头文件只包含函数声明,函数实现在lib文件中

即在链接阶段,将源文件用到的库函数与汇编生成的文件.o等合并(即函数被包含在exe文件中)生成可执行文件

好处:方便程序移植,因为可执行程序与库函数再无关系,放入任何环境当中都可以执行

缺点:可执行文件太大(因为包含了库函数),每次库文件升级都需要重新编译源文件,每个可执行程序都会合并库函数,存在很大的重复性,占用空间大

动态链接

两个程序应用一个库,目标文件在内存中只有一份,供所有程序使用,但是可移植性太差,如果两台电脑运行环境不同,动态库存放位置不同,可能会导致程序运行失败

隐式链接

1、将dll,lib文件添加到调用文件中

2、将#pragma comment(lib,“xxx.lib”)添加到调用文件中

3、加入函数的声明

extern “C” _declspec(dllimport) _stdcall int Plus(int x,int y);

导出则为dllexport,dll导出和导入的方式必须一致,即导出的时候使用了_stdcall,导入的时候也应该是__stdcall

这样的话如果需要修改函数,只需要在dll文件修改即可,程序不用编译

显示链接

1、定义函数指针

typedef int (__stdcall *lpPlus)(int,int);

2、声明函数指针变量

lpPlus myPlus

3、动态加载dll到内存中

HINSTANCE hModule=LoadLibrary(“DllDemo.dll”);

4、获取函数地址

myPlus=(lpPlus)GetProcAddress(hModule,“_Plus@8”)😭__stdcall为了区分会自动加符号),如果导入的时候没加stdcall就不需要加符号,即myPlus=(lpPlus)GetProcAddress(hModule,“Plus”);

5、调用函数

int a=myPlus(1,2);

特别说明

Handle是代表系统内核的对象,如文件句柄、线程句柄、进程句柄

HMODULE代表应用程序载入的模块-在内存中拉伸的起始位置

HINSTANCE在win32是和前一个一样的 win16遗留

HWND是窗口句柄

上面的都是无符号整数,四个字节

这样做是为了方便区分,操作系统给的

def文件导出

为了隐藏函数的名字

和前面的类似,先创建动态链接库,头文件只需要函数声明

创建def文件后,在def文件中写入

EXPORTS

(函数名) 编号

Plus @12——导出序号是12

在编号后面+NONAME 可以隐藏函数名

dll导出函数的方式

extern表示是个全局函数,可以供各个其他的函数调用

声明导出:_declspec(dllexport)

def文件导出

dll函数调用

隐式链接

包含头文件,载入lib库

显式链接

LoadLibary,GetProAddress

导出表

作用:记载我们写的dll或者exe导出的函数

导出表重要成员

1、指向导出表文件名的字符串-DWORD Name

2、导出函数的起始序号-DWORD Base

3、导出函数地址表RVA-DWORD AddressOfFunctions

4、函数名称地址表RVA-DWORD AddressOfNames

5、函数序号地址表RVA-DWORD AddressOfNameOrdinals

定位导出表

1、找出导出表RVA的偏移

首先我们要在数据目录的导出表信息的第一项,也就是导出表地址的RVA偏移是多少

可以看出c000h是RVA偏移,大小是164个字节

判断属于哪一个节

根据节区的VIrtualAddress判断

所以可以知道在.edata节

算出FOA位置

我们知道是在.rdata节的,可以算出FOA

FOA=RVA-节区的RVA(VirtualAddress)+节.PointerOfRawData

计算发现为8200h

通过FOA找到导出表位置

找到后根据前面的导出表大小可以知道导出表的范围

跳转过去即可

导出表的存储方式

一个导出表大小为0x28个字节,也就是两行半

分段讲解

前面十二个字节没用,就不讲了

Dword Name

绿框的4个字节存储的是dll名称的RVA,想查看的话,可以计算FOA,和前面一样的,得到8250地址存储的是我们的dll名称,以00结尾

Dword Base

导出函数的起始序号

DLL导出的函数如果给序号了,那么就从这个序号开始

Dword NumberOfFunctions-所有的导出函数的个数

前四个

DWORD NumberOfNames-以名字(有名字的)导出的函数的个数

可以将两个不同的名字指向相同的地址

有一些是无名函数不会在里面

后四个

下面就是子表了,三个子表都是RVA,要转为FOA

分别是函数地址表0x8228,函数名称表0x8238,函数序号表0x8248

先去找函数名称表,根据名称的下标再去序号表找对应,然后再去函数地址表找

函数地址表-AddressOfFunctions

函数地址表指向一个偏移,这个偏移存放了所有导出函数的地址,每个地址占四个字节,存放的是RVA地址

ImageBase+函数偏移就是函数在内存中的实际地址

可以看到这就是函数

还需要注意的就是,如果你按照序号导出1 3 4 5这4个函数,在导入表中我们的函数地址表中的地址会有5个,原因就是:中断的序号会给我们用0填充,2虽然没有,但是也会给我们导出.

函数名称表-AddressOfNames

函数名称表也是存储名称的RVA,四个字节存储一个,RVA的个数由以函数名称导出函数个数来决定(DWORD NumberOfNames)

转为FOA就是0x805e,0x8071,0x8082,0x8093

排序的时候是按照字母顺序排列的,而不是按照导出时函数的顺序进行排序的

例如:

EXPORT

SUB

ADD

MUL

导出三个函数,那么第一项就为 ADD,因为按照字母排序,A在前边,后面依次类推,所以我们上面看到的函数名称 ACquireSRW 这个函数名称,并不是第一个导出的函数.

函数序号表-AddressOfNumberOrdinals-存储的是RVA

给名称用的中转表

和函数名称表大小相同

按名称导出

得到函数名称后去AddressOfNames中的函数名称对比,得到索引后根据索引取出AddressOfNameOrdinals存储的值,以此为索引去函数地址表找到函数的地址

按名称导出过程

函数数量计算方式=最大序号-最小序号+1,(连续的算得准)不连续的话在函数地址表会留有多余的地址,但是多余的地址没有被使用

按序号导出

当使用序号导出的时候不需要使用序号表,直接将序号-Base作为索引去函数地址表找即可

导出的过程

系统和我们前面说的不一样

1

根据函数地址表找,如果索引在序号表中,说明是按名字导出的,再去名称表找对应,索引和序号表中的相同

这里的100a索引是3,在序号表中没有,所以不是按照序号导出的,序号可算,为索引+Base=005

重定位表-struct IMAGE_DATA_DIRECTORY BaseRelocationTable

程序的加载过程

系统加载的dll一般在高空间运行,一般是71**************

先像贴图一样把dll贴在内存中,最后指向exe文件的入口点,exe开始运行

我们自己写的dll的imageBase都是1000000,所以当多个dll同时使用,会存在地址已经被占用的问题,所以dl只能往后放,但是:

编译器生成的地址=ImageBase+RVA,这个地址在程序编译完成后,已经写入文件了。但是如果当程序加载时没有按照原来的ImageBase载入,但是此时程序还是会按照前面生成的地址进行使用

所以一般exe不存在重定位表,而dll一般都有,用于记录需要修改的地方

重定位表解析

重定位表是分块,根据RVA计算得到FOA,跳转过去发现有几个重定位表

这两个数据分别存储RVA和大小

下面的数据中,每两个字节代表这一段有多少个数据需要修复,一般是绝对地址需要修改

只需要基础地址加上小表的值就可以定位到需要修改的地方,而不用四个字节来存储地址,节省了空间,而这个基础地址就是前面记录的RVA

而基础地址可以不同,所以才需要分块操作

可以发现每一块的间隔是1000h,也就是内存对齐的大小

一个页是1000h,所以只需要1000个十六进制的地址就可以记录完一页的每个位置,对应十进制的4096个

2^12=4096,所以只需要十二个二进制位就可以表示完所有可能,两个字节有十六位,所以最后找地址的时候,只用取低十二位,高四位有别的用处-如果高四位的值是3就说明这个地方需要修改

判断块数

下一块的开始地址都可以通过RVA+SizeOfBlock来得到

直到遇到全为零的块

计算需要修改的数据个数:(SizeOfBlock-8)/2,因为前面两个数据是8个字节,剩下的两个字节为1组

移动导出表-重定位表

表的数据也在节区中,加密代码时,表的信息也会被加密,这样程序无法初始化,所以要先增节,再移动表,学会移动各种表是对程序加密和破解的基础

移动导出表

比较繁琐,因为要移动多张表

在DLL文件中新增节

先计算大小

并且返回新增节的FOA,因为后面的数据还要从新增节的开头开始

复制数据

需要复制的数据类型和长度

复制AddressOfFunctions

长度:4×NumberOfFunctions

复制AddressOfNameOrdinals

长度:2×NumberOfNames

复制AddressOfNames

长度:4×NumberOfNames

复制所有函数名

长度不确定,复制时直接修复AddressOfName(因为名字的地址也改变了),每复制完一个名字,都需要计算下一个复制的地方

复制导出表的整体结构

修复地址

AddressOfName

目录项指向新的导出表的位置

函数地址表、序号表、名称表都需要修复

将目录项中的RVA修正

指向我们新的导出表的位置

移动重定位表

直接复制过去,修改RVA即可

加载程序:(当前面已经有DLL被加载)

1、将新的DLL复制到新的位置

2、修复重定位表,修改每一个重定位表的偏移

导入表

作用:从其他第三方程序导入API,以供本程序调用

在exe运行的时候加载器会遍历导入表,将导入表中所有dll加载到进程中,被加载的DLL的DLLMain就会被调用

通过导入表可以知道程序使用了哪些函数

导入表,是为了提供的要导入的dll的函数的地址,只不过由于dll地址不固定,所以每次都会重新修正

导入表解析

重要字段

1
DWORD OriginalFirstThunk;

指向导入名称表(INT)的RVA

1
DWORD   FirstThunk;

指向导入地址表(IAT)的RVA

1
DWORD   Name;

指向导入映像文件的名字

计算后跳转过去,注意要算的是FOA,但是这里RVA和FOA一样

其中IAT与INT都指向 IMAGE_THUNK_DATA32(虽然二者的地址不同)

INT-以全0结尾

函数名数组(对应IMAGE_THUNK_DATA32结构体数组,每一个结构体就是一个联合体-4字节)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef struct  _IMAGE_THUNK_DATA32{
union{//联合体,实际是4字节
DWORD ForwarderString;
DWORD Function;//导入函数的地址,在加载到内存之后,这里才起作用
DWORD Ordinal;//假如是序号导入的,会用到这里
DWORD AddressOfData;//假如是函数名导入,会用到这里,它指向另外一个结构体PIMAGE_IMPORT_BY_NAME
}u1;
}IMAGE_THUNK_DATA32;

//如果是函数名导入的,AddressOfData会指向下面这个结构体
typedef struct _IMAGE_IMPORT_BY_NAME{
WORD Hint;//序号
CHAR Name[1];//不定长,字符串

}

导入表结构

在可选PE头最后的十六个数组的导入表结构,这里存放的是RVA

跳转过去发现存储的是struct IMAGE_IMPORT_DESCRIPTOR ImportDescriptor[0],这里存放的是真正的导入表结构

有多少个DLL,对应的结构体数组就有几个

关键的数据有下面三个,也要注意这里的TimeDateStamp-时间戳

INT表

ULONG OriginalFirstThunk-RVA,存放的是IMAGE_THUNK_DATA这个结构体数组,四字节数,以0结尾

跳转过去

但是之前学导出表有了解到,导出函数可以以名字导出,亦可以序号导出。所以为了方便区分,就将这INT表的每个值做了细微调整。

如果这个4字节数的最高位(二进制)为1,那么抹去这个最高位之后,所表示的数就是要导入的函数的序号;如果最高位是0,那这个数就也是一个RVA,指向IMAGE_IMPORT_BY_NAME结构体(包含真正的导入函数的名字字符串,以0结尾)。INT表以4字节0结尾。

根据INT存储的RVA进行跳转

struct _IMAGE_IMPORT_BY_NAME

1、WORD Hint,可能为0,编译器决定,如果不为0,则是函数在导出表中的索引

2、BYTE Name[1],函数名称,以0结尾

NAME

DLL的名字,以0结尾

ULONG FirstThunk

根据RVA跳转,发现存储的值和INT的一样,这个存储的是IAT,当文件加载后,会发现IAT会改变

IAT表-导入地址表

记录程序正在使用哪些库中的哪些函数

分为两个过程,在文件中存储的是存放函数的地址,在内存中存放函数

在文件运行前类似于call [地址]

在文件运行时类似于call [上面地址存放的值,也就是函数]

IAT表存在的原因

一般程序在调用自身函数的时候,自身函数地址RVA是固定的;但是当程序在调用dll里的函数的时候,由于dll的地址会发生重定位,导致dll里的函数地址每次都会发生变化。

为了准确调用dll函数的地址,构造了IAT表来存储程序运行时,即DLL文件重定位之后的dll函数的位置。

导入表加载过程

PE文件加载前

加载过程

先找DLL,再确定使用的函数

寻找DLL

先根据导入表的NAME找到DLL名称和DLL的INT和IAT表,开始的时候INT和IAT数据相同,但是存储位置不同,INT和IAT表都指向IMAGE_THUNK_DATA

查INT表

因为导出的时候可以按序号导出也可以按函数名称导出,所以为了区分,image_thunk_data最高位为1时,表示其存储的是序号,而剩下的31位表示序号,否则image_thunk_data存储的是IMAGE_BY_NAME的RVA

因为GetProc可以通过序号找到函数地址,也可以使用名称找到,所以通过这种方式找到函数的地址,然后填充至IAT中

修改IAT

使用GETProAddr(module)

将前面查找到的函数地址填充进IAT表中

PE文件加载后

可以看到IAT表变成了函数的地址

IAT表会存储dll的函数的地址,方便调用该函数时,直接取IAT表这个地址内的值,作为函数地址,去CALL。

绑定导入表- struct _IMAGE_BOUND_IMPORT_DESCRIPTOR

可选NT头里的结构

作用

有些windows程序,如notepad,为了提高加载速度,会直接把DLL中的函数地址写入到IAT表,省去了加载时的计算。

可能存在的问题

第一,当DLL没有占住ImageBase时,IAT中的地址就是错的;第二,当链接的DLL被修改了,那IAT里写的地址也是错的。遇到这两种情形之一,加载时就必须修复IAT了。

对于第二种情形,DLL是否被修改,是根据比较DLL的时间戳和绑定导入表中的记录的DLL时间戳来判断的,如果不一致,说明DLL被修改了。

加载程序时,操作系统根据导入表中的时间戳来判断程序是否使用了绑定导入。当时间戳为0,表示不使用绑定导入表;当时间戳为0xFFFFFFFF,说明该程序使用绑定导入。

绑定导入表结构

1
2
3
4
5
6
typedef struct _IMAGE_BOUND_IMPORT_DESCRIPTOR {
DWORD TimeDateStamp;
WORD OffsetModuleName;
WORD NumberOfModuleForwarderRefs;
// Array of zero or more IMAGE_BOUND_FORWARDER_REF follows
} IMAGE_BOUND_IMPORT_DESCRIPTOR, *PIMAGE_BOUND_IMPORT_DESCRIPTOR;

TimeDateStamp 是时间戳,用于和DLL中的时间戳比较,判断DLL是否已经发生变化;

OffsetModuleName 是当前模块名距离第一个 _IMAGE_BOUND_IMPORT_DESCRIPTOR 的偏移。

NumberOfModuleForwarderRefs 是该模块依赖的模块数量;

依赖模块结构

1
2
3
4
5
typedef struct _IMAGE_BOUND_FORWARDER_REF {
DWORD TimeDateStamp;
WORD OffsetModuleName;
WORD Reserved;
} IMAGE_BOUND_FORWARDER_REF,*PIMAGE_BOUND_FORWARDER_REF;

除了第三个属性保留,其他与 _IMAGE_BOUND_IMPORT_DESCRIPTOR 相同。

绑定导入表