WindowsPE文件格式入门04.导入表
PE 内部保存了导入的dll 和 api信息,这些信息保存到一个表里面.称为导入表,导入表就是 记住一个可执行文件导入了那些dll,以及导入了这些dll中的哪些函数一个可执行文件会调用其他DLL里的函数或数据,当PE文件被加载时,Windows加载器会定位这些所有被输入的函数和数据,并让正在被载入的文件也可以使用那些地址(函数), 这个过程是通过PE的导入表(Import Table,简称IT,也称为导入表)完成的。
**导入表保存了什么?**
- 输入表中保存加载动态链接库所需要的函数名和驻留DLL名等信息(LoadLibaray)。
# 为什么需要导入表
假设要调用一个WINAPI-->MessageBoxA,调用API最终是call某一个API的地址,尴尬的是API地址在DLL里,地址是不确定的,因此这行代码是不能编译的。
编译器有一个巧妙的办法,将地址保存在一个全局变量中,编译器生成的代码就是call一个全局变量地址:call dword ptr ,这样就可以编译成功。
接下来在软件运行的时候,有OS将对应API的地址填入到对应的全局变量的位置,程序就可以调用正确的API。
这种工作当靠编译器或者OS一方是无法完成的,编译器知道API的地址要放在什么地方,要拿哪个API,但是不知道API的地址到底是多少
而OS知道各个API的地址,但是不知道应该放在那里。
这时导入表就至关重要,编译器将它所知道的数据整理成数据表放在PE中,软件运行时OS读取,又OS填入正确的API地址,程序就可以正确调用API。
- 整理OS需要的信息:API名字(name/order)、填充位置(address)、库名称(lib name)
# 导入表结构
一个可执行文件 和 dll 之间是 一对多 的关系, 一个 dll 和dll中的导出函数也是一对多的关系,根据数据关系是多存一,因此正常的建表情况如下图
!(./mdimg/pe/1655728566974-52f1f637-0b1f-4759-9063-b539b76979dc.png)
但是当我们想要知道那些dll 有哪些函数时 , 如果 dll 和 每个 dll 中的函数很多时,这种效率很低假设 dll 有20个 ,每个dll的导数函数都是30个 ,那么总共需要遍历 20 * (20 * 30) = 12000 次
但是我们可以可以改变数据结构(不遵循数据关系原则)来提高速率
!(./mdimg/pe/1655729296022-2bc86d13-1438-4cd9-bbb8-3c8fccd45d55.png)
如图,把每个 dll 的导出函数用之中表存起来,然后 dll 后面保存 该表地址,这样我们就只需要遍历 20+ (20*60) = 620 次,可以极大的提高效率,这种防暑效率虽然很高,但是插入删除就会有问题,但是对于可执行文件来说,他没有增删改的需求,因此这个问题对于可执行文件来说不是问题
但是对于系统来说,拿到 函数的名称并没有用,主要是拿导入函数的地址,,因为我们调函数时,用的是函数的地址,并不是字符串,理论上来说,是哪个地方调用了函数,就把调用函数的对应地址填过去,但是,可能调用函数的地方很多,这样就需要把每个地方的地址填过去,这样没办法知道哪个地方调用了该函数,否则就需要建表保存,这样很麻烦,因此,编译器在运行时,就会准备一张空白表,当可执行文件加载时,就会把所有函数的地址 保存进去,当去掉api或者导出函数的时候,就会从这张表去拿导出函数的地址.
## 导入名称表(INT)
用OD 打开 winmine
!(./mdimg/pe/1655730325284-f8b0a0b2-5015-481e-92ee-03c9d6d6da01.png)
!(./mdimg/pe/1655730356754-a11566d8-91d1-4647-99cc-37cae8fe617a.png)
!(./mdimg/pe/1655730387044-7adea20e-c28d-4912-8207-22f34b25bd30.png)
!(./mdimg/pe/1655730607613-3f6341ca-1a80-4492-a4be-e2f6409e0e68.png)
可以看出,不同地方调用同一个函数,调式调用保存函数地址的地址,因此当系统调用函数时.只需要哪保存函数地址的值传填进去就可以了
!(./mdimg/pe/1655730761530-ab1678b1-8901-4937-9dbd-b597a72da638.png)
- **导入表定位:IMAGE_OPTIONAL_HEADER-> DIrSectons**
- - **导入表位于数据目录第二项。**
- **注意:数据目录里的地址全部是RVA**
- **为什么是RVA而不是FA呢?**
- - - **处理导入表的时候,DLL已经映射进入内存了,通过RVA更加的方便。**
-
- 数据目录结构体
- typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress; //该项数据的RVA地址
DWORD Size; //所占的大小,大小可不要,可修改。
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
- ### 数据在PE中的位置:Option.Header的最后一项
- 从下图可以看出导入表的地址是内存偏移
- !(./mdimg/pe/1655731860047-fbac57ea-0457-41f2-9285-23a33da0c906.png)
-
- 从节表可以看出,内存偏移2000的地方,对应得文件偏移是 600 ,因此内存偏移位 002010的地址对应的文件偏移为
- 610
- !(./mdimg/pe/1655732218688-0bcd277c-f619-4a86-82ae-9b97a2143a9b.png)
-
- ### (一)_IMAGE_IMPORT_DESCRIPTOR
- 导入目录表,描述了DLL和其API信息 大小 20 字节
- !(./mdimg/pe/1655732755761-ab9251b6-9a56-47f7-af5b-902edf155f51.png)
导出表结构体 IMAGE_IMPORT_DESCRIPTOR
```
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
//INT
union {
DWORD Characteristics; // 不是给Windows使用
// 一个PIMAGE_THUNK_DATA结构体 保存 INT信息
DWORD OriginalFirstThunk; //一个API指向一个名称/序号表的中的表项,包括了序号和API名称两个字段 //可以理解为需要的API
} DUMMYUNIONNAME;
//下面2个字段没用
DWORD TimeDateStamp; // 时间戳
DWORD ForwarderChain; // -1 if no forwarders
DWORD Name; // 重要,指向库名称
//IAT
DWORD FirstThunk; // IAT(导入地址表),IMAGE_THUNK_DATA32,一样的格式,和OriginalFirstThunk
//可以理解为API地址要填入的全局变量的地址
//FirstThunk是该库需要的第一个API要填入的位置,
//下一个偏移4的位置是第二个API要填入的位置
} IMAGE_IMPORT_DESCRIPTOR;
```
#### 1. 导入目录表个数:
导入表指向的数组中,并没有指明导入表的个数。但它的最后一个单元(一个IDD结构体大小)是“NULL”。由此可计算出项数。
!(./mdimg/pe/1636602401149-4cf6d161-9ca9-42ef-846f-0d23c84050dd.png)
### (二)IMAGE_THUNK_DATA
导入查找表,也叫导入地址表,描述API名称/序号 信息
IMAGE_THUNK_DATA
```
typedef struct _IMAGE_THUNK_DATA32 {
union {
DWORD ForwarderString; // PBYTE
DWORD Function; // PDWORD
DWORD Ordinal;
DWORD AddressOfData; // PIMAGE_IMPORT_BY_NAME
} u1;
} IMAGE_THUNK_DATA32;
typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32;
```
导入函数导入的时候可以用名称导入,也可以用序号导入,因此要进行区分
- **ul:**
- - 如果高位位1,说明是序号导入,低WORD(序号最大是 65535) 位导入函数的序号值。
- 如果最高位为 0,说明是名字导入,该DWORD 指向结构体 IMAGE_IMPORT_BY_NAME 的偏移值
- 微软提供了一个宏来判断最高位有效值
- - 32位:`#defineIMAGE_ORDINAL_FLAG32 80000000h `
- 64位:`#defineIMAGE_ORDINAL_FLAG64 8000000000000000h `
!(./mdimg/pe/1655733870162-43f30a26-b606-4e41-8e9f-66b64236e03a.png)
!(./mdimg/pe/1655734609469-827f28bd-3238-423c-8965-936d22597940.png)
### (三)MAGE_IMPORT_BY_NAME
导入名称/序号表,结构体
```
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint; //OS并不看序号,不同版本DLL的API的序号不一定一样,
CHAR Name; //导入函数的名称
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
```
#### 名称导入示例
!(./mdimg/pe/1636603359503-7d46d4fa-db66-4bfc-b5b8-0836693a7643.png)
#### 1. 序号导入示例
#### OriginalFirstThunk 和 FirstThunk 分别指向本质相同的两个数组
**在程序加载到进程之后,****FirstThunk 所指的 INT 值会被 改写成导入函数的地址 IAT。**
`**jmp XXXXXXX**` **该地址指的就是FirstThunk 的入口地址**
!(./mdimg/pe/1636603936916-654b29ce-b47a-45b9-a9d6-04fb5e651045.png)
### (四)填充IAT的流程
加载器内部调用
1. LoadLibary(_IMAGE_IMPORT_DESCRIPTOR.Name)
2. GetProcAddress(_IMAGE_IMPORT_BY_NAME);
3. 拿到IAT的地址,填入到对应的地址中
4. 1. 需要注意的时,FirstThunk是一个数组,有和OriginalFirstThunk一样多的项,一一对应
2. 第一个API写到FirstThunk的地址
3. 第二个API写道FirstThunk + 4的地址,依次类推
#### 具体流程:
1. PE装载器先搜索 OriginalFirstThunk ,如果找到。加载程序就会迭代搜索数组中的每个指针。找出IMAGE_IMPORT_BY_NAME 结构体所指的输入函数的地址。
然后加载器用函数真正的函数入口地址替代由FirstThunk 指向的数组中的一个入口。因此称为输入地址表 IAT
!(./mdimg/pe/1636605663967-54e0a5cf-307f-423f-be30-ffae91479b8a.png)
## 导入名称表
!(./mdimg/pe/1655735396406-984b93c4-b13c-4460-a93b-c52c3e7078c5.png)
!(./mdimg/pe/1655735583098-d86ede4b-9d3c-4871-818f-4c9051c3e020.png)
!(./mdimg/pe/1655735689845-a7bd1983-4bce-4c89-97e3-f14acc775a16.png)
!(./mdimg/pe/1655735705349-983a2f3f-4033-4990-bf9d-0a43c872e863.png)
!(./mdimg/pe/1655736023299-6d94eeae-28b5-4e6d-b61b-95ef42fcb984.png)
!(./mdimg/pe/1655736403789-a9449063-335d-4c2b-b81b-3c08489cc58c-170456035436045.png)
!(./mdimg/pe/1655736473415-704ecf01-1619-453e-8dcb-a4f0b5749c8b.png)
!(./mdimg/pe/1655736598220-42029ea1-5798-4b95-b77d-62527a2d106d.png)
!(./mdimg/pe/1655737341516-dbcd3fc5-e316-45f2-b56e-73908d9b5eaa.png)
!(./mdimg/pe/1655737035696-ef1ee1df-3de6-4675-95d1-c99b3f35c821.png)
!(./mdimg/pe/1655737272088-0a7dd25c-fa29-41c8-a90c-babfd50d1458.png)
!(./mdimg/pe/1655737499548-6184cf6f-ed7d-462a-a2ee-a5bdc941fcbc.png)
!(./mdimg/pe/1655737582432-59a8fcd8-a500-4aca-939a-b03e5e049242.png)
## 导入地址表(IAT)
!(./mdimg/pe/1655736403789-a9449063-335d-4c2b-b81b-3c08489cc58c.png)
!(./mdimg/pe/1655738085266-5945d2f5-721a-4492-a5e3-3f0dad3c8751.png)
!(./mdimg/pe/1655738211967-2a4e04e5-47f3-4611-aefc-960cc773e672.png)
再看看低6个 dll 的 IAT (000020A4) 导出名称表地址(2b1c)
先看导出的dll名
!(./mdimg/pe/1655738536706-729f098b-8ce0-4cb1-8864-77fcf383e658.png)!(./mdimg/pe/1655738692612-612712da-e082-4910-9a5b-f8bce54c92e1.png)
在看导出函数,可以看出只有一个
!(./mdimg/pe/1655738986044-2726bc13-d4cb-4f9d-8c7c-0bed75a47b69.png)
!(./mdimg/pe/1655739057334-4e7ad0a2-6797-413f-89e9-2e52edbb3161.png)
!(./mdimg/pe/1655739097004-891ec26b-6b07-4963-8499-baecad08413e.png)
!(./mdimg/pe/1655739129102-220e3b2e-cc46-4556-a12c-ab49907433a4.png)
从上面可以看出,编辑器遍历的时候 实现获取 dll ,再从dll 对应的 INT表(导入名称表)拿到地址 ,填入 对应的 IAT (导入地址表)中
## 小细节
### 名称导入
!(./mdimg/pe/1655739464991-39f0697b-8888-4b3b-984a-c6cc72b52e0e.png)
!(./mdimg/pe/1655739628182-325275aa-5cbb-4fac-95e7-d0f6de68a227.png)
可以看到程序未加载时INT 个 IAT 指向的 值是一样的,但是程序加载是 IAT 指向的值就填成了 函数指向的地址
进程加载前
!(./mdimg/pe/1655739761637-03b1460e-1f0c-463a-87d2-a04b89edb900.png)
进程加载后
!(./mdimg/pe/1655740311404-acfd0cae-9b78-49cc-b7d9-230732fa0a92.png)
### 序号导入
序号导入一般 MFC 的比较多
找一个32位的MFC程序,必须在共享dll中使用 MFC
!(./mdimg/pe/1655741714909-7e6de5fe-b562-4031-b9a8-21038bde73f7.png)
!(./mdimg/pe/1655742092305-da2113a8-8376-426b-80c6-829e41f37096.png)
!(./mdimg/pe/1655742575750-325e1f7b-a7d8-4fdf-b275-f0dc238a3ddf.png)
!(./mdimg/pe/1655742617773-595348d8-4b3e-446f-9321-6d6a8642611c.png)
!(./mdimg/pe/1655742665069-6bcad299-2784-4aa7-89fb-28380f591711.png)
!(./mdimg/pe/1655742951645-bbd0df7f-0588-4068-8797-2186df0318ad.png)
OD也无法看到对应的函数名称,还原要去写工具,要找到序号和地址的对应关系,这个对应关系要到 lib 中去提取,
lib中有名称,dll中没有,都是序号
## 导入表字段对文件的影响
!(./mdimg/pe/1655743662673-80332009-e319-4d15-9064-5b340ea1621e.png)
这个结构体中,除了上图3个成员,其他2个是没用的
#### 清掉IAT
!(./mdimg/pe/1655743796192-864d1ca7-7e49-4557-a072-a931581fd768.png)
程序可以正常运行
#### 清掉NAME
清掉名字就想当于到了导出表的结尾项后面的函数
!(./mdimg/pe/1655743889400-d86db2e9-98e5-4901-b747-6f31216be99c.png)
程序无法正常运行用OD查看,发现后面的dll都没办法加载
!(./mdimg/pe/1655744291258-8f341ff5-9f81-4d7d-bd39-24b099d85e61.png)
把第二个dll的名字清了,第一个dll的导出函数有了,但是后面的都没了
!(./mdimg/pe/1655744457092-1e6ac39c-5c49-4496-b81a-89b8cc980712.png)
#### 清调INT
!(./mdimg/pe/1655744605946-ffc5990c-94ae-47cc-a3ff-fb417b37b540.png)
跟清NAME的效果一样
!(./mdimg/pe/1655744641060-0c87b929-6bab-4c7c-9f68-985bcf23333f.png)
#### 清掉INT表的第一项
!(./mdimg/pe/1655744776155-d5bb3fb4-3c2b-47d8-8409-4ce883269838.png)
程序不能正常运行,而且第一个 dll 的函数无法加载,后面的正常加载,如果清dll 的 INT表 的非第一项 后面的项,程序正常运行
!(./mdimg/pe/1655744905812-4ee5efe4-0065-4024-8815-34475d966d1e.png)
#### 清 NAME 表的第一项
!(./mdimg/pe/1655745415513-030d3057-8531-4e14-a3b5-fe5478cd4a70.png)
对应的dll的导出函数就不会加载, ,清后面的项,则dll中 该项前面的 函数可以正常加载, 这项开始
的函数无法正常加载!(./mdimg/pe/1655745443169-d4ccfd19-d8d2-4114-9a37-802bcee4478d.png)
#### 清掉IAT表的第一项
!(./mdimg/pe/1655745731565-4ecb88de-d3ed-4a15-bfb5-5cea5c131252.png)
对应 dll 的导数无法加载,只清该项后面的项,程序正常运行
!(./mdimg/pe/1655745759948-ae65e82a-3f5c-41b2-b117-e3971fa8c536.png)
#### 总结
1. 遍历函数的时候,如果 NAME 或者 IAT 为 0,则遍历结束
2. 如果NAME 或者 IAT都不为0 情况下
- ```
如果 INT 不存在,那么加载函数就会用IAT 表
```
- ```
如果 INT 存在,那么加载函数就会用INT 表,但是会检查IAT 表第一项是否为0, 0就结束
```
# 导入表遍历
---
导入表遍历顺序 = 导入表加载的顺序
1. 检查Name 和FirstThunk ,如果任一为NULL,则停止遍历
2. 取FirstThunk 的项(数组中的元素),如果为NULL, 就取OriginalFirstThunk对应的项,如果为NULL,则遍历下一项
3. 判断项的最高位,如果为1,则取低WORD为序号,如果为0,则作为RVA 取出IMAGE_IMPORT_BY_NAME 中的函数名
4. 循环遍历下一项
```
while TRUE
{
pItem = 取出导入表一项;
//判断
if(Name == NULL || FirstThunk(IAT) == NULL)
{
//遍历结束
break;
}
//加载dll
LoadLibrary(Name);
//判断OriginalFirstThunk(INT)是否存在
pINT = FirstThunk(IAT);
if(OriginalFirstThunk != NULL)
{
pINT = OriginalFirstThunk(INT);
}
//遍历导入名称表
while(*pINT != NULL)
{
//判断是否是序号
if(pINT 最高位为1)
{
//序号导入
导入函数地址 = GetProcAddress(低字序号);
}
else
{
导入函数地址 = GetProcAddress(字符串地址);
}
导入函数地址填入IAT表中相等下标索引位置。
}
}
```
```
while(Name != NULL && FirstThunK != NULL)
{
IMAGE_DATA_THUNK* pTmpThunk = OriginalFirstThunk;
if(OriginalFirstThunk == NULL)
{
pTmpThunk = FirstThunk;
}
while(*pTmpThunk != NULL)
{
if(*pTmpThunk & 0x80000000)
{
WORD dwNumber = *pTmpThunk & 0xffff; //低字为序号
}
else
{
IMAGE_IMPORT_BY_NAME* pName = *pTmpThunk;//获取导入函数名称的RVA
}
pTmpThunk++;
}
}
```
# 导入表注入
---
- **导入表中数据:导入表中保存的是函数名和DLL等动态链接所需的信息。**
- **导入表的作用:**系统读取导入表,加载对应的DLL 和把导入的地址填入IAT表中。
- **导入表注入:**在导入表中加一项导入信息,让系统加载新增的一项所对应的DLL和数据。
导入表注入是将自己的DLL,添加到导入目录表中。目的是让系统根据导入表加载自己的DLL。从而实现代码注入的目的。
如果原导入目录表后有足够多的位置,那么可以直接加一项。
如果原导入目录表后没有足够多的位置,可以采用增加最后一个节的大小的方式来增加节,然后将导入目录表移到增加的节上,修改PE头的选项头中的数据表 --> 导入表中保存的导入目录表的地址。
新增的导入目录项,要是OS可以加载它,必须包含的信息有库名、IAT,IAT中必须保存一个DLL导出函数的名称/序号表项
### 步骤:
1. 定位导入表
2. 挪动导入表,因为一般导入表后面都没有空位置。
3. 新增导入表项
4. 修改导入表项的函数地址等
### 演示
在 PE.exe中j注入一个dll
!(./mdimg/pe/1655825360746-f6c42731-d3e3-43f6-a1a3-cccdb464458a.png)
!(./mdimg/pe/1655825513523-8b5854b2-b25f-42a9-9383-a1034ce00326.png)
!(./mdimg/pe/1655825677343-f016e34b-7e93-405b-b5bc-7f69ddb8cc1c.png)!(./mdimg/pe/1655825743712-dff05147-cd42-4974-92fb-32d9a871134b.png)
因为如果 INT (导入名称表) 那么就会直接用导入地址表(IAT),如果有INT就会用INT,Name说明 INT 可有可无,为了方便,我们可以不要 INT 只要 NAME 和 IAT2项的值就可以了
!(./mdimg/pe/1655826344992-ed8514d3-abca-4ae3-a0a1-e6fcee542c90.png)
注意:如果移动数据涉及到跨分页,要考虑 内存属性问题,是否可读写,而且移动数据 不要覆盖有用的数据
### 用代码给可执行程序注入一个dll
代码没办法像人一样用肉眼判断是否数据移动位置,因此可以通过添加 和 拓展节来实现
有附加数据的没法加
页:
[1]