之前一直说再看一遍PE结构的东西,想着温故知新,能看到之前没有注意到的东西吧。加上上一篇的遍历导出表,这一次的遍历导入表,纯手工制作,鲜香出炉,之后会再补上一篇手动实现PE加载器的代码,望诸君笑纳共勉。
先看源码:
#include <iostream>
#include<windows.h>
DWORD RVA_RAW(DWORD RVA, unsigned char * Base)
{
//定位DOS头;
PIMAGE_DOS_HEADER DosHeader = (PIMAGE_DOS_HEADER)Base;
//定位NT头;
PIMAGE_NT_HEADERS Nt_Header = (PIMAGE_NT_HEADERS)(Base + DosHeader->e_lfanew);
//获取节区数目
WORD SectionNum = Nt_Header->FileHeader.NumberOfSections;
//获取节区头的地址
PIMAGE_SECTION_HEADER SectionHeader = (PIMAGE_SECTION_HEADER)(Base + DosHeader->e_lfanew + 0x18 + Nt_Header->FileHeader.SizeOfOptionalHeader);
//定位目标RVA所在节区
DWORD RAW = 0;
for (int i = 0; i < SectionNum; i++)
{
DWORD SectionStart = SectionHeader[i].VirtualAddress;
DWORD SectionEnd = SectionStart + SectionHeader[i].Misc.VirtualSize;
if (RVA >= SectionStart && RVA <= SectionEnd)
{
RAW = RVA - SectionStart + SectionHeader[i].PointerToRawData;
break;
}
}
return RAW;
}
int main()
{
HANDLE hFile = CreateFileA(
"C:\\Users\\Administrator\\source\\repos\\TestExport\\TestExport\\user32.dll",
GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
NULL, OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL
, NULL
);
if (hFile == INVALID_HANDLE_VALUE)
{
printf("文件打开失败");
printf("失败的原因%d", GetLastError());
}
//获取文件的大小
DWORD FileSize = GetFileSize(hFile, NULL);
LPDWORD SizeToRead = 0;
//申请一块和文件大小相同的新内存
unsigned char * pBuf = new unsigned char[FileSize];
//将这块内存用0填充
ZeroMemory(pBuf, FileSize);
//填充之后,将文件读取到这段内存空间里边,这个时候文件将保存在磁盘上的状态,也就是需要进行地址转换
int i = ReadFile(hFile, pBuf, FileSize, SizeToRead, NULL);
if (i == 0)
{
printf("文件读取失败");
}
//定位DOS头;
PIMAGE_DOS_HEADER DosHeader = (PIMAGE_DOS_HEADER)pBuf;
//定位NT头;
PIMAGE_NT_HEADERS Nt_Header = (PIMAGE_NT_HEADERS)(pBuf + DosHeader->e_lfanew);
//定位OPtionHeader
PIMAGE_OPTIONAL_HEADER32 OptionHeader = &(Nt_Header->OptionalHeader);
//定位数据目录表
PIMAGE_DATA_DIRECTORY DataDir = (PIMAGE_DATA_DIRECTORY)(OptionHeader->DataDirectory);
//定位输入表的RVA
DWORD ImportRVA = (DWORD)DataDir[1].VirtualAddress;
//进行地址转换,获取输入表在磁盘文件中的偏移
DWORD ImportRAW = RVA_RAW(ImportRVA,pBuf);
//定位输入表的数据结构
PIMAGE_IMPORT_DESCRIPTOR ImportTable =(PIMAGE_IMPORT_DESCRIPTOR) (ImportRAW + pBuf);
//判断输入表是不是为空,因为输入表以全零作为结束标志
while (ImportTable->FirstThunk)
{
//获取输入表中对应的输入模块的名字的RVA
DWORD ImportNameRVA=ImportTable->Name;
//获取对应模块名在文件中的偏移
DWORD ImportNameRAW=RVA_RAW(ImportNameRVA,pBuf);
//输出模块名
printf("目标DLL名称是%s\n",ImportNameRAW+pBuf);
//获取输入名称表的RVA
DWORD ImportNameTableRVA = ImportTable->OriginalFirstThunk;
//地址转换
DWORD ImportNameTableRAW = RVA_RAW(ImportNameTableRVA, pBuf);
//定位导出名称表的结构
PIMAGE_THUNK_DATA INT = (PIMAGE_THUNK_DATA)(ImportNameTableRAW+pBuf);
//定位导出地址表的结构
PIMAGE_THUNK_DATA IAT = (PIMAGE_THUNK_DATA)(RVA_RAW(ImportTable->FirstThunk,pBuf)+pBuf);
while (INT->u1.AddressOfData)
{
//判断导入名称表的最高位是否为1
if (IMAGE_SNAP_BY_ORDINAL32(INT->u1.AddressOfData))
{
//最高位为1,代表该函数是以序号导入的
printf("目标导入函数的序号是:%x\t,目标导入函数的地址是:%x\t\n",INT->u1.Ordinal & 0xFFFF,IAT->u1.Function);
}
//如果最高位是0,表示这个函数是以名字导出的
else
{
//如果该函数以名称导入,那么就需要用到另一个结构体
PIMAGE_IMPORT_BY_NAME ImportByName = (PIMAGE_IMPORT_BY_NAME)(RVA_RAW(INT->u1.AddressOfData, pBuf) + pBuf);
printf("目标函数的名字是:%s\t,目标函数的地址是%x\n", ImportByName->Name, IAT->u1.Function);
}
INT++;
IAT++;
}
ImportTable++;
}
}
接下来说一些需要注意的点:
1 也是最重要的一点:不管在这次的遍历导入表还是上次的遍历遍历导出表,代码部分其实都有一个安全隐患.这个点就是.容易造成内存泄露,因为再读取文件的时候,我使用new来申请了一块新的内存空间,但是再遍历完毕之后并没有去释放这块内存空间,这个算是比较危险的地方了,释放的话,直接用Delete去释放就好了.
2:遍历导入表的过程其实相对比遍历导出表较为简洁.下图记录了导入表的结构图:
每一个INT的结构都对应一个IMAGE_THUNK_DATA,当然实际上,INT和IAT指向的都是IMAGE_THUNK_DATA结构,而且还有重要的一点就是,需要注意导入的方式,再判断导入的方式之后,需要注意,当目标函数是以序号导入的时候,正常输出就好.但是当目标函数是以名称导入的时候,需要注意这个时候的INT所对应的IMAGE_THUNK_DATA->Address指向IMAGE_IMPORT_BY_NAME结构体,也就是上图的Hint Name 所对应的结构,这个时候IMAGE_IMPORT_BY_NAME->Name才是我们的目标导入函数的名称.
3:至于目标函数是以什么方式导入的,可以使用IMAGE_SNAP_BY_ORDINAL32这个宏来进行判断,这个宏的定义如下:(64位和32位都包含在内)
#define IMAGE_ORDINAL_FLAG64 0x8000000000000000
#define IMAGE_ORDINAL_FLAG32 0x80000000
#define IMAGE_ORDINAL64(Ordinal) (Ordinal & 0xffff)
#define IMAGE_ORDINAL32(Ordinal) (Ordinal & 0xffff)
#define IMAGE_SNAP_BY_ORDINAL64(Ordinal) ((Ordinal & IMAGE_ORDINAL_FLAG64) != 0)
#define IMAGE_SNAP_BY_ORDINAL32(Ordinal) ((Ordinal & IMAGE_ORDINAL_FLAG32) != 0)
4:对于PE加载器来说,INT和IAT意味着什么?
首先,获取到IMAGE_IMPORT_DESCRIPTION结构中Name字段的数据(也就是获取目标模块的名字)
第二步执行LoadLibrary(“目标模块的名称”,将目标模块加载到内存中)
第三步:获取到INT的地址
第四步:逐一循环获取INT数组的值(前边已经说过,每一个INT项都指向一个IMAGE_THUNK_DATA的数据结构),如果是以序号导出的,获取相应函数的序号和地址,如果是以名称导出的,先指向相应的IMAGE_IMPORT_BY_NAME结构,之后获取该结构里的Name字段和Hint的数据.
第五步:根据上边获取到的name字段的数据,使用GetprocAddress()函数来获取到函数的地址
第六步:获取到IAT的地址
第七步:将上边获取到的函数的地址填充到获取到的IAT的地址,循环进行.直到遇到全零的IMAGE_IMPORT_DESCRIPTION结构.
从上边的过程,我们不难发现,在实际的使用过程中,IAT的数据可能跟INT的数据是相同的,治理之所以使用可能,是因为,还存在一些特殊情况.
其实从名字来理解也更容易理解,IAT里边存储着目标函数的地址,INT里边存储着目标函数的名字.