补:PE结构遍历输入表——因为不爱,所以都错

之前一直说再看一遍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里边存储着目标函数的名字.

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值