继续PE系列笔记的更新
PE其它笔记索引可前去:
PE文件笔记一 PE介绍
前面在PE文件笔记十四 导出表学习了导出表,接着学习导入表
导入表
导入表作用
当程序运行时 ,必要多个 PE文件共同组成
PE文件提供哪些功能→导出表
PE文件必要依赖的模块以及依赖这些模块中的哪些函数→导入表
什么是导入表
导入表就是记录该PE文件还必要依赖的模块以及依赖这些模块中的哪些函数的一种结构
定位导入表
定位导入表原理
在上一个笔记:PE文件笔记十四 导出表中以及提到了,像导入表、导出表、重定位表、资源表等表,这些表的起始地址和大小都存储在扩展PE头里的DataDirectory这个数组的成员中,DataDirectory是一个数组,每个数组成员对应一张表
回首先前的笔记,能得到导入表对应的下标为1
宏界说值寄义IMAGE_DIRECTORY_ENTRY_IMPORT1导入表即DataDirectory[1]体现导入表
关于DataDirectory的具体形貌在上一个笔记中已经详细说明过了,这里不再赘述:
IMAGE_DATA_DIRECTORY成员数据宽度说明VirtualAddressDWORD(4字节)表的起始位置(RVA)SizeDWORD(4字节)表的大小定位导入表流程
找到扩展PE头的最后一个成员DataDirectory
获取DataDirectory[1]
通过DataDirectory[1].VirtualAddress得到导入表的RVA
将导出表的RVA转换为FOA,在文件中定位到导入表
按流程定位导入表
要分析的实例
这次要分析的实例又回归到先前的EverEdit.exe了
程序在背面的附件中,有必要可以自行取用
找到DataDirectory
使用WinHex打开EverEdit.exe,先找到PE文件头的起始地址:0xF0
再数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
获取DataDirectory[1]
而导入表为DataDirectory[1],也就是从首地址开始的DataDirectory[0]的偏移之后的8个字节就是形貌导入表的IMAGE_DATA_DIRECTORY
导入表地址 = DataDirectory首地址 + sizeof(IMAGE_DATA_DIRECTORY)=0x168+8=360+8=368=0x170
IMAGE_DATA_DIRECTORY成员值说明VirtualAddress0x001CF47C表的起始位置(RVA)Size0x00000140表的大小得到导出表的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 #include #include #include #include //在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->Misc.VirtualSize, (UINT)sectionArr->SizeOfRawData) / (double)nt->OptionalHeader.SectionAlignment) * nt->OptionalHeader.SectionAlignment; if (rva >= sectionArr->VirtualAddress && rva < (sectionArr->VirtualAddress + SizeInMemory)) { //找到所属的节 //输出内存对齐后的节的大小 printf("SizeInMemory:%X\n", SizeInMemory); break; } } if (i >= nt->FileHeader.NumberOfSections) { //未找到 printf("没有找到匹配的节\n"); return -1; } else { //计算差值= RVA - 节.VirtualAddress UINT offset = rva - sectionArr->VirtualAddress; //FOA = 节.PointerToRawData + 差值 UINT foa = sectionArr->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
运行代码得到:
得到了FOA为0x1CDA7C,也就是导入表的位置了,定位完成
导入表的结构
定位到了导入表后自然要了解导入表的结构才能解读导入表的内容
导入表的个数
与导出表不同,导入表通常要包含多个模块,而不像导出表只必要提供本PE文件必要提供的导出函数即可
因此,导出表只有一个,但导入表则大概有多个
当程序运行时 ,必要依赖几个模块,就对应有几张导入表
导入表的结构体
给出导入表在C语言中的结构体(在winnt.h中可以找到)
即:
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;结构体分析
成员数据宽度说明CharacteristicsDWORD(4字节)标志 为0体现结束 没有导入形貌符了OriginalFirstThunkDWORD(4字节)RVA指向IMAGE_THUNK_DATA结构数组(桥1)TimeDateStampDWORD(4字节)时间戳ForwarderChainDWORD(4字节)链表的前一个结构NameDWORD(4字节)RVA,指向DLL名字,该名字以''\0''结尾FirstThunkDWORD(4字节)RVA指向IMAGE_THUNK_DATA结构数组(桥2)Characteristics
标志 为0体现结束 没有导入形貌符了
IMAGE_THUNK_DATA
在介绍OriginalFirstThunk之前,要先了解一下OriginalFirstThunk和FirstThunk所指向的结构数组
指向的数组中每一项为一个结构,此结构名称是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文件笔记十四 导出表中由导出表得到导出函数所需的两种方法
即:
根据函数名称获取导出函数地址
根据函数序号获取导出函数地址
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文件加载前
PE文件加载后
加载前后对比
在PE文件加载前:桥1指向的INT和桥2指向的IAT的数据值是雷同 的,但是其存储位置是不同的
在PE文件加载后:桥1指向的INT不变 ,但桥2指向的IAT的数据值变为了函数相应的RVA地址
PS:函数相应的RVA地址是根据IAT中的函数名称或者导出表中的序号得到的
按结构分析导入表
回到先前得到的导入表的FOA,在WinHex中找到FOA:0x1CDA7C
这里取第一个_IMAGE_IMPORT_DESCRIPTOR进行分析
将对应的数据填入结构体成员中得到:
成员值说明Characteristics0x001CF790标志 为0体现结束 没有导入形貌符了OriginalFirstThunk0x001CF790RVA指向IMAGE_THUNK_DATA结构数组(桥1)TimeDateStamp0x00000000时间戳ForwarderChain0x00000000链表的前一个结构Name0x001D0788RVA,指向DLL名字,该名字以''\0''结尾FirstThunk0x0019B1D4RVA指向IMAGE_THUNK_DATA结构数组(桥2)Characteristics
不为0,体现另有导入形貌符
OriginalFirstThunk
指向IMAGE_THUNK_DATA结构数组,先将RVA:0x001CF790转换为FOA:0x1CDD90
转换代码为:
VaToFoa32(nt->OptionalHeader.ImageBase +0x001CF790,dos,nt,sectionArr);用WinHex找到0x1CDD90的位置:
得到结构体数组 INT为:
数组下标IMAGE_THUNK_DATA(RVA)对应FOA00x001D07740x1CED7410x001D26200x1D0C2020x001D26060x1D0C0630x001D25F40x1D0BF440x001D25E00x1D0BE0...............................................................n00由于这里前面的几个IMAGE_THUNK_DATA的最高位都为0,于是其体现的为内容体现指向_IMAGE_IMPORT_BY_NAME的RVA
PS:若最高位为1,则其体现的内容去掉最高位后为:导出函数的序号
先查看下标为0 对应的FOA:
将得到的数据填入_IMAGE_IMPORT_BY_NAME
_IMAGE_IMPORT_BY_NAME成员值Hint0x018FNameANSI码为"GetComputerNameW"这里就得到了必要导入的函数的名称和Hint,这个Hint不一定准确,不使用
TimeDateStamp
值为0,编译器并未填写时间戳
ForwarderChain
值为0,没有链表的前一个结构
Name
值为0x001D0788,是个RVA地址,先将其转换成FOA:0x1CED88
转换代码:
VaToFoa32(nt->OptionalHeader.ImageBase +0x001D0788,dos,nt,sectionArr);用WinHex找到0x1CED88的位置:
得到第一个导入形貌符 形貌的导入模块名为:"KERNEL32.dll"
FirstThunk
值为0x0019B1D4,是个RVA地址,先将其转换成FOA:0x1997D4
转换代码:
VaToFoa32(nt->OptionalHeader.ImageBase +0x0019B1D4,dos,nt,sectionArr);用WinHex找到0x1997D4的位置:
得到结构体数组 IAT为:
数组下标IMAGE_THUNK_DATA(RVA)对应FOA00x001D07740x1CED7410x001D26200x1D0C2020x001D26060x1D0C0630x001D25F40x1D0BF440x001D25E00x1D0BE0...............................................................n00这里会发现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
选中数据窗口
然后按快捷键:Ctrl+G,弹出窗口
在弹出的窗口中填写要跳转的RVA地址
然后会发现内存窗口中的内容发生了改变,但显示方式并不是很友好
于是修改一下显示方式,在内存窗口中 右键→长型→ASCII数据地址
可以看到:
可以看到这里的kerner32.GetComputerNameW正是前面分析出来的名称
验证了IAT表在PE文件加载后发生了变化
代码实现分析导入表
// PE.cpp : Defines the entry point for the console application.//#include #include #include #include #include //在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->Misc.VirtualSize, (UINT)sectionArr->SizeOfRawData) / (double)nt->OptionalHeader.SectionAlignment) * nt->OptionalHeader.SectionAlignment; if (rva >= sectionArr->VirtualAddress && rva < (sectionArr->VirtualAddress + SizeInMemory)) { //找到所属的节 //输出内存对齐后的节的大小 //printf("SizeInMemory:%X\n", SizeInMemory); break; } } if (i >= nt->FileHeader.NumberOfSections) { //未找到 printf("没有找到匹配的节\n"); return -1; } else { //计算差值= RVA - 节.VirtualAddress UINT offset = rva - sectionArr->VirtualAddress; //FOA = 节.PointerToRawData + 差值 UINT foa = sectionArr->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;}运行结果
可以看到运行结果和前面手动分析的一致,而且既能解析出函数名也能解析出函数序号
再用PE工具:DIE验证一下
结果是一致的,代码部分完成q(≧▽≦q)
代码说明
这次的代码部分其实和先前的解析导出表难度差不多
要留意的就是解析IMAGE_THUNK_DATA时,要先判断其最高位;根据最高位是否为1来进行类型转换和解读
总结
导出表最多 只有一张,而导入表通常不只一张
导入表具有双桥结构 ,双桥结构中的IAT 在PE文件运行前和PE文件运行后内容不同
无论是INT还是IAT在PE文件运行前其内容是一致 的,结构都为IMAGE_THUNK_DATA
无论是INT还是IAT在PE文件运行前,其存储的内容归根结底要么是导出函数序号 ,要么就是导出函数名称
通过导出函数序号和导出函数名称再加上模块名就可以根据导出表获取到对应的函数地址
附件
附上本笔记中分析的EverEdit文件:点我下载
来源:http://www.12558.net
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有帐号?立即注册
x
楼主热帖