前面在PE文件笔记十四 导出表学习了导出表,接着学习导入表

导入表

导入表作用

当程序运行时,需要多个PE文件共同组成

PE文件提供哪些功能→导出表

PE文件需要依赖的模块以及依赖这些模块中的哪些函数→导入表


什么是导入表

导入表就是记录该PE文件还需要依赖的模块以及依赖这些模块中的哪些函数的一种结构


定位导入表

定位导入表原理

在上一个笔记:PE文件笔记十四 导出表中以及提到了,像导入表、导出表、重定位表、资源表等表,这些表的起始地址和大小都存储在扩展PE头里的DataDirectory这个数组的成员中,DataDirectory是一个数组,每个数组成员对应一张表

回顾先前的笔记,能得到导入表对应的下标为1

宏定义 含义
IMAGE_DIRECTORY_ENTRY_IMPORT 1 导入表

即DataDirectory[1]表示导入表

关于DataDirectory的具体描述在上一个笔记中已经详细说明过了,这里不再赘述:

IMAGE_DATA_DIRECTORY成员 数据宽度 说明
VirtualAddress DWORD(4字节) 表的起始位置(RVA)
Size DWORD(4字节) 表的大小

定位导入表流程

  1. 找到扩展PE头的最后一个成员DataDirectory
  2. 获取DataDirectory[1]
  3. 通过DataDirectory[1].VirtualAddress得到导入表的RVA
  4. 将导出表的RVA转换为FOA,在文件中定位到导入表

按流程定位导入表

要分析的实例

这次要分析的实例又回归到先前的EverEdit.exe了

程序在后面的附件中,有需要可以自行取用


找到DataDirectory

使用WinHex打开EverEdit.exe,先找到PE文件头的起始地址:0xF0

image-20210408115132092


再数24个字节(PE文件头标志大小+标准PE头大小),到达扩展PE头:0xF0+24=240+24=264=0x108

然后在数224-128=96个字节(扩展PE头大小减去DataDirectory大小)DataDirectory大小= _IMAGE_DATA_DIRECTORY大小×16=8*16

DataDirectory首地址 = 扩展PE头地址+96=0x108+96=264+96=360=0x168

image-20210408115736939


获取DataDirectory[1]

而导入表为DataDirectory[1],也就是从首地址开始的DataDirectory[0]的偏移之后的8个字节就是描述导入表的IMAGE_DATA_DIRECTORY

导入表地址 = DataDirectory首地址 + sizeof(IMAGE_DATA_DIRECTORY)=0x168+8=360+8=368=0x170

IMAGE_DATA_DIRECTORY成员 说明
VirtualAddress 0x001CF47C 表的起始位置(RVA)
Size 0x00000140 表的大小

得到导出表的RVA

于是得到导出表对应的RVA为:0x1CF47C


RVA转换FOA

但是IMAGE_DATA_DIRECTORY中的VirtualAddress是RVA,需要将其转换成FOA

关于RVA转FOA的内容在 PE文件笔记七 VA与FOA转换中已经详细说明了,这里不再赘述

直接使用在笔记七中写的转换代码计算出对应的FOA:

 复制代码 隐藏代码// PE.cpp : Defines the entry point for the console application.
//
#include <stdio.h>
#include <malloc.h>
#include <windows.h>
#include <winnt.h>
#include <math.h>
//在VC6这个比较旧的环境里,没有定义64位的这个宏,需要自己定义,在VS2019中无需自己定义
#define IMAGE_FILE_MACHINE_AMD64 0x8664

//VA转FOA 32位
//第一个参数为要转换的在内存中的地址:VA
//第二个参数为指向dos头的指针
//第三个参数为指向nt头的指针
//第四个参数为存储指向节指针的数组
UINT VaToFoa32(UINT va, _IMAGE_DOS_HEADER* dos, _IMAGE_NT_HEADERS* nt, _IMAGE_SECTION_HEADER** sectionArr) {
//得到RVA的值:RVA = VA - ImageBase
UINT rva = va - nt->OptionalHeader.ImageBase;
//输出rva
printf("rva:%X\n", rva);
//找到PE文件头后的地址 = PE文件头首地址+PE文件头大小
UINT PeEnd = (UINT)dos->e_lfanew + sizeof(_IMAGE_NT_HEADERS);
//输出PeEnd
printf("PeEnd:%X\n", PeEnd);
//判断rva是否位于PE文件头中
if (rva < PeEnd) {
//如果rva位于PE文件头中,则foa==rva,直接返回rva即可
printf("foa:%X\n", rva);
return rva;
}
else {
//如果rva在PE文件头外
//判断rva属于哪个节
int i;
for (i = 0; i < nt->FileHeader.NumberOfSections; i++) {
//计算内存对齐后节的大小
UINT SizeInMemory = ceil((double)max((UINT)sectionArr[i]->Misc.VirtualSize, (UINT)sectionArr[i]->SizeOfRawData) / (double)nt->OptionalHeader.SectionAlignment) * nt->OptionalHeader.SectionAlignment;

if (rva >= sectionArr[i]->VirtualAddress && rva < (sectionArr[i]->VirtualAddress + SizeInMemory)) {
//找到所属的节
//输出内存对齐后的节的大小
printf("SizeInMemory:%X\n", SizeInMemory);
break;
}
}
if (i >= nt->FileHeader.NumberOfSections) {
//未找到
printf("没有找到匹配的节\n");
return -1;
}
else {
//计算差值= RVA - 节.VirtualAddress
UINT offset = rva - sectionArr[i]->VirtualAddress;
//FOA = 节.PointerToRawData + 差值
UINT foa = sectionArr[i]->PointerToRawData + offset;
printf("foa:%X\n", foa);
return foa;
}

}

}

int main(int argc, char* argv[])
{
//创建DOS对应的结构体指针
_IMAGE_DOS_HEADER* dos;
//读取文件,返回文件句柄
HANDLE hFile = CreateFileA("C:\\Documents and Settings\\Administrator\\桌面\\user32.dll", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, 0);
//根据文件句柄创建映射
HANDLE hMap = CreateFileMappingA(hFile, NULL, PAGE_READWRITE, 0, 0, 0);
//映射内容
LPVOID pFile = MapViewOfFile(hMap, FILE_SHARE_WRITE, 0, 0, 0);
//类型转换,用结构体的方式来读取
dos = (_IMAGE_DOS_HEADER*)pFile;
//输出dos->e_magic,以十六进制输出
printf("dos->e_magic:%X\n", dos->e_magic);

//创建指向PE文件头标志的指针
DWORD* peId;
//让PE文件头标志指针指向其对应的地址=DOS首地址+偏移
peId = (DWORD*)((UINT)dos + dos->e_lfanew);
//输出PE文件头标志,其值应为4550,否则不是PE文件
printf("peId:%X\n", *peId);

//创建指向可选PE头的第一个成员magic的指针
WORD* magic;
//让magic指针指向其对应的地址=PE文件头标志地址+PE文件头标志大小+标准PE头大小
magic = (WORD*)((UINT)peId + sizeof(DWORD) + sizeof(_IMAGE_FILE_HEADER));
//输出magic,其值为0x10b代表32位程序,其值为0x20b代表64位程序
printf("magic:%X\n", *magic);
//根据magic判断为32位程序还是64位程序
switch (*magic) {
case IMAGE_NT_OPTIONAL_HDR32_MAGIC:
{
printf("32位程序\n");
//确定为32位程序后,就可以使用_IMAGE_NT_HEADERS来接收数据了
//创建指向PE文件头的指针
_IMAGE_NT_HEADERS* nt;
//让PE文件头指针指向其对应的地址
nt = (_IMAGE_NT_HEADERS*)peId;
printf("Machine:%X\n", nt->FileHeader.Machine);
printf("Magic:%X\n", nt->OptionalHeader.Magic);
//创建一个指针数组,该指针数组用来存储所有的节表指针
//这里相当于_IMAGE_SECTION_HEADER* sectionArr[nt->FileHeader.NumberOfSections],声明了一个动态数组
_IMAGE_SECTION_HEADER** sectionArr = (_IMAGE_SECTION_HEADER**)malloc(sizeof(_IMAGE_SECTION_HEADER*) * nt->FileHeader.NumberOfSections);

//创建指向块表的指针
_IMAGE_SECTION_HEADER* sectionHeader;
//让块表的指针指向其对应的地址
sectionHeader = (_IMAGE_SECTION_HEADER*)((UINT)nt + sizeof(_IMAGE_NT_HEADERS));
//计数,用来计算块表地址
int cnt = 0;
//比较 计数 和 块表的个数,即遍历所有块表
while (cnt < nt->FileHeader.NumberOfSections) {
//创建指向块表的指针
_IMAGE_SECTION_HEADER* section;
//让块表的指针指向其对应的地址=第一个块表地址+计数*块表的大小
section = (_IMAGE_SECTION_HEADER*)((UINT)sectionHeader + sizeof(_IMAGE_SECTION_HEADER) * cnt);
//将得到的块表指针存入数组
sectionArr[cnt++] = section;
//输出块表名称
printf("%s\n", section->Name);
}

VaToFoa32(nt->OptionalHeader.ImageBase +0x1CF47C,dos,nt,sectionArr);

break;
}

case IMAGE_NT_OPTIONAL_HDR64_MAGIC:
{
printf("64位程序\n");
//确定为64位程序后,就可以使用_IMAGE_NT_HEADERS64来接收数据了
//创建指向PE文件头的指针
_IMAGE_NT_HEADERS64* nt;
nt = (_IMAGE_NT_HEADERS64*)peId;
printf("Machine:%X\n", nt->FileHeader.Machine);
printf("Magic:%X\n", nt->OptionalHeader.Magic);

//创建一个指针数组,该指针数组用来存储所有的节表指针
//这里相当于_IMAGE_SECTION_HEADER* sectionArr[nt->FileHeader.NumberOfSections],声明了一个动态数组
_IMAGE_SECTION_HEADER** sectionArr = (_IMAGE_SECTION_HEADER**)malloc(sizeof(_IMAGE_SECTION_HEADER*) * nt->FileHeader.NumberOfSections);

//创建指向块表的指针
_IMAGE_SECTION_HEADER* sectionHeader;
//让块表的指针指向其对应的地址,区别在于这里加上的偏移为_IMAGE_NT_HEADERS64
sectionHeader = (_IMAGE_SECTION_HEADER*)((UINT)nt + sizeof(_IMAGE_NT_HEADERS64));
//计数,用来计算块表地址
int cnt = 0;
//比较 计数 和 块表的个数,即遍历所有块表
while (cnt < nt->FileHeader.NumberOfSections) {
//创建指向块表的指针
_IMAGE_SECTION_HEADER* section;
//让块表的指针指向其对应的地址=第一个块表地址+计数*块表的大小
section = (_IMAGE_SECTION_HEADER*)((UINT)sectionHeader + sizeof(_IMAGE_SECTION_HEADER) * cnt);
//将得到的块表指针存入数组
sectionArr[cnt++] = section;
//输出块表名称
printf("%s\n", section->Name);
}

break;
}

default:
{
printf("error!\n");
break;
}

}
return 0;
}

关键代码:

 复制代码 隐藏代码
VaToFoa32(nt->OptionalHeader.ImageBase +0x1CF47C,dos,nt,sectionArr);

因为先前写的函数是VA转FOA,这里得到的是RVA,于是要先用RVA+ImageBase得到VA

运行代码得到:

image-20210408122153869

获得了FOA为0x1CDA7C,也就是导入表的位置了,定位完成


导入表的结构

定位到了导入表后自然要了解导入表的结构才能解读导入表的内容

导入表的个数

与导出表不同,导入表通常要包含多个模块,而不像导出表只需要提供本PE文件需要提供的导出函数即可

因此,导出表只有一个,但导入表则可能有多个

当程序运行时,需要依赖几个模块,就对应有几张导入表


导入表的结构体

给出导入表在C语言中的结构体(在winnt.h中可以找到)

image-20210408124452121


即:

 复制代码 隐藏代码typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics; // 0 for terminating null import descriptor
DWORD OriginalFirstThunk; // RVA to original unbound IAT (PIMAGE_THUNK_DATA)
};
DWORD TimeDateStamp; // 0 if not bound,
// -1 if bound, and real date\time stamp
// in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)
// O.W. date/time stamp of DLL bound to (Old BIND)

DWORD ForwarderChain; // -1 if no forwarders
DWORD Name;
DWORD FirstThunk; // RVA to IAT (if bound this IAT has actual addresses)
} IMAGE_IMPORT_DESCRIPTOR;

结构体分析

成员 数据宽度 说明
Characteristics DWORD(4字节) 标志 为0表示结束 没有导入描述符了
OriginalFirstThunk DWORD(4字节) RVA指向IMAGE_THUNK_DATA结构数组(桥1)
TimeDateStamp DWORD(4字节) 时间戳
ForwarderChain DWORD(4字节) 链表的前一个结构
Name DWORD(4字节) RVA,指向DLL名字,该名字以’’\0’’结尾
FirstThunk DWORD(4字节) RVA指向IMAGE_THUNK_DATA结构数组(桥2)

Characteristics

标志 为0表示结束 没有导入描述符了


IMAGE_THUNK_DATA

在介绍OriginalFirstThunk之前,要先了解一下OriginalFirstThunk和FirstThunk所指向的结构数组

image-20210408141537209


指向的数组中每一项为一个结构,此结构名称是IMAGE_THUNK_DATA

数组最后以一个内容全为0的IMAGE_THUNK_DATA作为结束

IMAGE_THUNK_DATA实际上只是一个DWORD,但在不同的时刻却拥有不同的解释

IMAGE_THUNK_DATA有两种解释

  • DWORD最高位为0,那么该数值是一个RVA,指向_IMAGE_IMPORT_BY_NAME结构,表明函数是以字符串类型的函数名导入
  • DWORD最高位为1,那么该数值的低31位就是函数的导出函数的序号

_IMAGE_IMPORT_BY_NAME结构:

 复制代码 隐藏代码typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint;
BYTE Name[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

该结构即为:”编号—名称”(Hint/Name)描述部分

  • Hint:导出函数地址表的索引编号,可能为空且不一定准确,由编译器决定,一般不使用该值
  • Name:这个是一个以”\0”结尾的字符串,表示函数名

这里不难发现,IMAGE_THUNK_DATA最终提供的数据也只有2个:

  • DWORD最高位为0时:需要导入函数的名称(Hint不一定准确,所以不使用)
  • DWORD最高位为1时:需要导入的函数在导出表中的序号

正好对应了在上一个笔记:PE文件笔记十四 导出表中由导出表获得导出函数所需的两种方法

即:

  1. 根据函数名称获取导出函数地址
  2. 根据函数序号获取导出函数地址

OriginalFirstThunk

因为它是指向另外数据结构的通路,因此简称为桥1。该字段指向一个包含了一系列结构的数组:IMAGE_THUNK_DATA

桥1所指向的地址列表被定义为:INT(Import Name Table) 导入名称表


TimeDateStamp

时间戳,一般不用,大多情况下都为0。如果该导入表项被绑定,那么绑定后的这个时间戳就被设置为对应DLL文件的时间戳。操作系统在加载时,可以通过这个时间戳来判断绑定的信息是否过时


ForwarderChain

链表的前一个结构


Name

这里的Name是一个RVA,它指向该结构对应的DLL文件的名称,而这个名称是以”\0”结尾的ANSI字符串

ANSI编码是一种对ASCII码的拓展


FirstThunk

与OriginalFirstThunk相同,它指向的链表定义了针对Name这个动态链接库引入的所有导入函数,简称桥2

桥2所指向的地址列表被定义为:IAT(Import Adress Table) 导入地址表


导入表的双桥结构

桥1和桥2最终的目的地是一致的,都指向了引入函数的”编号—名称”(Hint/Name)描述部分

桥1到目的地的过程中,经过了:INT(Import Name Table)导入名称表

而桥2到目的地的过程中,经过了:IAT(Import Address Table)导入地址表


PE文件加载前

双桥


image-20210408162001903

PE文件加载后

image-20210408162839413


image-20210408162937094


加载前后对比

  • 在PE文件加载前:桥1指向的INT和桥2指向的IAT的数据值是相同的,但是其存储位置是不同的
  • 在PE文件加载后:桥1指向的INT不变,但桥2指向的IAT的数据值变为了函数相应的RVA地址

PS:函数相应的RVA地址是根据IAT中的函数名称或者导出表中的序号获得的


按结构分析导入表

回到先前得到的导入表的FOA,在WinHex中找到FOA:0x1CDA7C

这里取第一个_IMAGE_IMPORT_DESCRIPTOR进行分析

image-20210408182130674


将对应的数据填入结构体成员中得到:

成员 说明
Characteristics 0x001CF790 标志 为0表示结束 没有导入描述符了
OriginalFirstThunk 0x001CF790 RVA指向IMAGE_THUNK_DATA结构数组(桥1)
TimeDateStamp 0x00000000 时间戳
ForwarderChain 0x00000000 链表的前一个结构
Name 0x001D0788 RVA,指向DLL名字,该名字以’’\0’’结尾
FirstThunk 0x0019B1D4 RVA指向IMAGE_THUNK_DATA结构数组(桥2)

Characteristics

不为0,表示还有导入描述符


OriginalFirstThunk

指向IMAGE_THUNK_DATA结构数组,先将RVA:0x001CF790转换为FOA:0x1CDD90

转换代码为:

 复制代码 隐藏代码
VaToFoa32(nt->OptionalHeader.ImageBase +0x001CF790,dos,nt,sectionArr);

用WinHex找到0x1CDD90的位置:

image-20210408144944386


得到结构体数组 INT为:

数组下标 IMAGE_THUNK_DATA(RVA) 对应FOA
0 0x001D0774 0x1CED74
1 0x001D2620 0x1D0C20
2 0x001D2606 0x1D0C06
3 0x001D25F4 0x1D0BF4
4 0x001D25E0 0x1D0BE0
………….. ………………………………. …………
n 0 0

因为这里前面的几个IMAGE_THUNK_DATA的最高位都为0,于是其表示的为内容表示指向_IMAGE_IMPORT_BY_NAME的RVA

PS:若最高位为1,则其表示的内容去掉最高位后为:导出函数的序号


先查看下标为0 对应的FOA:

image-20210408150058936


将得到的数据填入_IMAGE_IMPORT_BY_NAME

_IMAGE_IMPORT_BY_NAME成员
Hint 0x018F
Name ANSI码为”GetComputerNameW”

这里就获得了需要导入的函数的名称和Hint,这个Hint不一定准确,不使用


TimeDateStamp

值为0,编译器并未填写时间戳


ForwarderChain

值为0,没有链表的前一个结构


Name

值为0x001D0788,是个RVA地址,先将其转换成FOA:0x1CED88

转换代码:

 复制代码 隐藏代码
VaToFoa32(nt->OptionalHeader.ImageBase +0x001D0788,dos,nt,sectionArr);

用WinHex找到0x1CED88的位置:

image-20210408150719798

得到第一个导入描述符 描述的导入模块名为:”KERNEL32.dll”


FirstThunk

值为0x0019B1D4,是个RVA地址,先将其转换成FOA:0x1997D4

转换代码:

 复制代码 隐藏代码
VaToFoa32(nt->OptionalHeader.ImageBase +0x0019B1D4,dos,nt,sectionArr);

用WinHex找到0x1997D4的位置:

image-20210408152225017


得到结构体数组 IAT为:

数组下标 IMAGE_THUNK_DATA(RVA) 对应FOA
0 0x001D0774 0x1CED74
1 0x001D2620 0x1D0C20
2 0x001D2606 0x1D0C06
3 0x001D25F4 0x1D0BF4
4 0x001D25E0 0x1D0BE0
………….. ………………………………. …………
n 0 0

这里会发现IAT和INT中的内容是一致的,但是它们存储在不同的地址上(FOA不同,前面INT的FOA为:0x1CDD90,这里IAT的FOA为:0x1997D4)

验证了:在PE文件加载前,桥1指向的INT和桥2指向的IAT的数据值是相同的,但是其存储位置是不同的


验证PE文件加载后的IAT变化

上面只分析了PE文件加载前的IAT,其内容和INT一致;当程序运行后,再用OD来查看其对应的IAT的变化:

先前获得的IAT地址为:0x0019B1D4(RVA),用OD打开EverEdit.exe

选中数据窗口

image-20210408164539480


然后按快捷键:Ctrl+G,弹出窗口

在弹出的窗口中填写要跳转的RVA地址

image-20210408165048250


然后会发现内存窗口中的内容发生了改变,但显示方式并不是很友好

于是修改一下显示方式,在内存窗口中 右键→长型→ASCII数据地址

image-20210408165318175


可以看到:

image-20210408165346670

可以看到这里的kerner32.GetComputerNameW正是前面分析出来的名称

验证了IAT表在PE文件加载后发生了变化


代码实现分析导入表

 复制代码 隐藏代码// PE.cpp : Defines the entry point for the console application.
//
#include <stdio.h>
#include <malloc.h>
#include <windows.h>
#include <winnt.h>
#include <math.h>
//在VC6这个比较旧的环境里,没有定义64位的这个宏,需要自己定义,在VS2019中无需自己定义
#define IMAGE_FILE_MACHINE_AMD64 0x8664

//VA转FOA 32位
//第一个参数为要转换的在内存中的地址:VA
//第二个参数为指向dos头的指针
//第三个参数为指向nt头的指针
//第四个参数为存储指向节指针的数组
UINT VaToFoa32(UINT va, _IMAGE_DOS_HEADER* dos, _IMAGE_NT_HEADERS* nt, _IMAGE_SECTION_HEADER** sectionArr) {
//得到RVA的值:RVA = VA - ImageBase
UINT rva = va - nt->OptionalHeader.ImageBase;
//输出rva
//printf("rva:%X\n", rva);
//找到PE文件头后的地址 = PE文件头首地址+PE文件头大小
UINT PeEnd = (UINT)dos->e_lfanew + sizeof(_IMAGE_NT_HEADERS);
//输出PeEnd
//printf("PeEnd:%X\n", PeEnd);
//判断rva是否位于PE文件头中
if (rva < PeEnd) {
//如果rva位于PE文件头中,则foa==rva,直接返回rva即可
//printf("foa:%X\n", rva);
return rva;
}
else {
//如果rva在PE文件头外
//判断rva属于哪个节
int i;
for (i = 0; i < nt->FileHeader.NumberOfSections; i++) {
//计算内存对齐后节的大小
UINT SizeInMemory = ceil((double)max((UINT)sectionArr[i]->Misc.VirtualSize, (UINT)sectionArr[i]->SizeOfRawData) / (double)nt->OptionalHeader.SectionAlignment) * nt->OptionalHeader.SectionAlignment;

if (rva >= sectionArr[i]->VirtualAddress && rva < (sectionArr[i]->VirtualAddress + SizeInMemory)) {
//找到所属的节
//输出内存对齐后的节的大小
//printf("SizeInMemory:%X\n", SizeInMemory);
break;
}
}
if (i >= nt->FileHeader.NumberOfSections) {
//未找到
printf("没有找到匹配的节\n");
return -1;
}
else {
//计算差值= RVA - 节.VirtualAddress
UINT offset = rva - sectionArr[i]->VirtualAddress;
//FOA = 节.PointerToRawData + 差值
UINT foa = sectionArr[i]->PointerToRawData + offset;
//printf("foa:%X\n", foa);
return foa;
}

}

}

void getImportTable(_IMAGE_DOS_HEADER* dos, _IMAGE_NT_HEADERS* nt, _IMAGE_SECTION_HEADER** sectionArr) {
_IMAGE_DATA_DIRECTORY importDataDirectory = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT];

//计数,用来记录导入了多少个模块
int cnt = 0;
while (true) {
UINT importAddress = VaToFoa32(importDataDirectory.VirtualAddress + nt->OptionalHeader.ImageBase, dos, nt, sectionArr);
_IMAGE_IMPORT_DESCRIPTOR* importDirectory = (_IMAGE_IMPORT_DESCRIPTOR*)((UINT)dos + importAddress + sizeof(_IMAGE_IMPORT_DESCRIPTOR) * cnt) ;
if (importDirectory->OriginalFirstThunk != 0) {
UINT nameOffset = VaToFoa32(importDirectory->Name + nt->OptionalHeader.ImageBase, dos, nt, sectionArr);
char* name = (char*)((UINT)dos + nameOffset);
cnt++;
UINT offset=VaToFoa32(nt->OptionalHeader.ImageBase+importDirectory->OriginalFirstThunk, dos, nt, sectionArr);
if (offset == -1)return;
IMAGE_THUNK_DATA* INTTableBegin=(IMAGE_THUNK_DATA*)((UINT)dos + offset);
//计数,用来记录导入了该模块多少个函数
int cnt2 = 0;
while (true) {

IMAGE_THUNK_DATA* address = INTTableBegin + cnt2;
if (address->u1.AddressOfData == 0) {
break;
}
else {
//判断最高位
if ((UINT)address->u1.AddressOfData >= 0x80000000) {
//最高位为1
printf("模块名:%s\t函数序号:%X\n", name, address->u1.Ordinal-0x80000000);
}
else {
//最高位为0
UINT functionNameOffset= VaToFoa32(nt->OptionalHeader.ImageBase + (UINT)address->u1.AddressOfData, dos, nt, sectionArr);
_IMAGE_IMPORT_BY_NAME* functionName=(_IMAGE_IMPORT_BY_NAME*)((UINT)dos + functionNameOffset);
printf("模块名:%s\t函数名:%s\n", name,functionName->Name);
}
}
cnt2++;
}

printf("模块%s\t函数数量%d\n", name,cnt2);

}
else {
break;
}
}
printf("引用模块数:%d\n", cnt);

}

int main(int argc, char* argv[])
{
//创建DOS对应的结构体指针
_IMAGE_DOS_HEADER* dos;
//读取文件,返回文件句柄
HANDLE hFile = CreateFileA("C:\\Users\\lyl610abc\\Desktop\\EverEdit\\EverEdit.exe", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, 0);
//根据文件句柄创建映射
HANDLE hMap = CreateFileMappingA(hFile, NULL, PAGE_READWRITE, 0, 0, 0);
//映射内容
LPVOID pFile = MapViewOfFile(hMap, FILE_SHARE_WRITE, 0, 0, 0);
//类型转换,用结构体的方式来读取
dos = (_IMAGE_DOS_HEADER*)pFile;
//输出dos->e_magic,以十六进制输出
printf("dos->e_magic:%X\n", dos->e_magic);

//创建指向PE文件头标志的指针
DWORD* peId;
//让PE文件头标志指针指向其对应的地址=DOS首地址+偏移
peId = (DWORD*)((UINT)dos + dos->e_lfanew);
//输出PE文件头标志,其值应为4550,否则不是PE文件
printf("peId:%X\n", *peId);

//创建指向可选PE头的第一个成员magic的指针
WORD* magic;
//让magic指针指向其对应的地址=PE文件头标志地址+PE文件头标志大小+标准PE头大小
magic = (WORD*)((UINT)peId + sizeof(DWORD) + sizeof(_IMAGE_FILE_HEADER));
//输出magic,其值为0x10b代表32位程序,其值为0x20b代表64位程序
printf("magic:%X\n", *magic);
//根据magic判断为32位程序还是64位程序
switch (*magic) {
case IMAGE_NT_OPTIONAL_HDR32_MAGIC:
{
printf("32位程序\n");
//确定为32位程序后,就可以使用_IMAGE_NT_HEADERS来接收数据了
//创建指向PE文件头的指针
_IMAGE_NT_HEADERS* nt;
//让PE文件头指针指向其对应的地址
nt = (_IMAGE_NT_HEADERS*)peId;
printf("Machine:%X\n", nt->FileHeader.Machine);
printf("Magic:%X\n", nt->OptionalHeader.Magic);
//创建一个指针数组,该指针数组用来存储所有的节表指针
//这里相当于_IMAGE_SECTION_HEADER* sectionArr[nt->FileHeader.NumberOfSections],声明了一个动态数组
_IMAGE_SECTION_HEADER** sectionArr = (_IMAGE_SECTION_HEADER**)malloc(sizeof(_IMAGE_SECTION_HEADER*) * nt->FileHeader.NumberOfSections);

//创建指向块表的指针
_IMAGE_SECTION_HEADER* sectionHeader;
//让块表的指针指向其对应的地址
sectionHeader = (_IMAGE_SECTION_HEADER*)((UINT)nt + sizeof(_IMAGE_NT_HEADERS));
//计数,用来计算块表地址
int cnt = 0;
//比较 计数 和 块表的个数,即遍历所有块表
while (cnt < nt->FileHeader.NumberOfSections) {
//创建指向块表的指针
_IMAGE_SECTION_HEADER* section;
//让块表的指针指向其对应的地址=第一个块表地址+计数*块表的大小
section = (_IMAGE_SECTION_HEADER*)((UINT)sectionHeader + sizeof(_IMAGE_SECTION_HEADER) * cnt);
//将得到的块表指针存入数组
sectionArr[cnt++] = section;
//输出块表名称
printf("%s\n", section->Name);
}

getImportTable(dos, nt, sectionArr);

break;
}

case IMAGE_NT_OPTIONAL_HDR64_MAGIC:
{
printf("64位程序\n");
//确定为64位程序后,就可以使用_IMAGE_NT_HEADERS64来接收数据了
//创建指向PE文件头的指针
_IMAGE_NT_HEADERS64* nt;
nt = (_IMAGE_NT_HEADERS64*)peId;
printf("Machine:%X\n", nt->FileHeader.Machine);
printf("Magic:%X\n", nt->OptionalHeader.Magic);

//创建一个指针数组,该指针数组用来存储所有的节表指针
//这里相当于_IMAGE_SECTION_HEADER* sectionArr[nt->FileHeader.NumberOfSections],声明了一个动态数组
_IMAGE_SECTION_HEADER** sectionArr = (_IMAGE_SECTION_HEADER**)malloc(sizeof(_IMAGE_SECTION_HEADER*) * nt->FileHeader.NumberOfSections);

//创建指向块表的指针
_IMAGE_SECTION_HEADER* sectionHeader;
//让块表的指针指向其对应的地址,区别在于这里加上的偏移为_IMAGE_NT_HEADERS64
sectionHeader = (_IMAGE_SECTION_HEADER*)((UINT)nt + sizeof(_IMAGE_NT_HEADERS64));
//计数,用来计算块表地址
int cnt = 0;
//比较 计数 和 块表的个数,即遍历所有块表
while (cnt < nt->FileHeader.NumberOfSections) {
//创建指向块表的指针
_IMAGE_SECTION_HEADER* section;
//让块表的指针指向其对应的地址=第一个块表地址+计数*块表的大小
section = (_IMAGE_SECTION_HEADER*)((UINT)sectionHeader + sizeof(_IMAGE_SECTION_HEADER) * cnt);
//将得到的块表指针存入数组
sectionArr[cnt++] = section;
//输出块表名称
printf("%s\n", section->Name);
}

break;
}

default:
{
printf("error!\n");
break;
}

}
return 0;
}

运行结果

image-20210408203544390


image-20210408203623096

可以看到运行结果和前面手动分析的一致,并且既能解析出函数名也能解析出函数序号


再用PE工具:DIE验证一下

image-20210408204013688

结果是一致的,代码部分完成q(≧▽≦q)


代码说明

这次的代码部分其实和先前的解析导出表难度差不多

要注意的就是解析IMAGE_THUNK_DATA时,要先判断其最高位;根据最高位是否为1来进行类型转换和解读


总结

  • 导出表最多只有一张,而导入表通常不只一张
  • 导入表具有双桥结构,双桥结构中的IAT在PE文件运行前和PE文件运行后内容不同
  • 无论是INT还是IAT在PE文件运行前其内容是一致的,结构都为IMAGE_THUNK_DATA
  • 无论是INT还是IAT在PE文件运行前,其存储的内容归根结底要么是导出函数序号,要么就是导出函数名称
  • 通过导出函数序号和导出函数名称再加上模块名就可以根据导出表获取到对应的函数地址

附件

附上本笔记中分析的EverEdit文件:点我下载

转载自吾爱破解上的一位大佬