一个PE文件中的导入表,简单来说就是代表了该模块调用了哪些外部的API。当模块加载到内存后,PE加载器会修改该表,将导入地址表也就是常说的IAT修改为外部API重定位后的真实地址。下面结合实际PE文件来详细分析下导入表中的每一项。以及通过代码来对一个PE文件的导入表进行遍历,将其调用的函数显示出来。
首先解析导入表之前,先放三个结构体。
typedef struct _IMAGE_IMPORT_DESCRIPTOR{
union{
DWORD Characteristics;
DWORD OriginalFirstThunk;//导入名称表
};
DWORD TimeDateStamp; //时间戳
DWORD ForwarderChain;
DWORD Name; //dll名称
DWORD FirstThunk; //导入地址表
}IMAGE_IMPORT_DESCRIPTOR;
//OriginalFirstThunk和FirstThunk都指向的是_IMAGE_THUNK_DATA32结构体
//导入名称表最高位是0,就是名称导入
//最高位是1,就是序号导入
typedef struct _IMAGE_THUNK_DATA32 {
union {
DWORD ForwarderString; // PBYTE
DWORD Function; // PDWORD,导入函数的地址,在加载到内存后,这里才起作用
DWORD Ordinal; // 假如是序号导入的,会用到这里
DWORD AddressOfData; //PIMAGE_IMPORT_BY_NAME,假如是函数名导入的,用到这 里 ,它指向另外一个结 构体:PIMGE_IMPORT_BY_NAME
} u1;
} IMAGE_THUNK_DATA32;
typedef struct _IMAGE_IMPORT_BY_NAME{
WORD Hint; //序号
BYTE NAME[1]; //函数名
}IMAGE_IMPORT_BY_NAME,*PIMAGE_IMPORT_BY_NAME;
然后对照PE文件来一项一项的看。我这里随便拖了一个crackme的程序,使用CFF Explorer的一个PE查看工具进行查看。
左边可以看到在NT头中的扩展头(Optional Header)的最后一个成员数据目录表,第二个成员即为导入表结构体的信息。文件0x138偏移处存储着导入表的RVA,0x13c偏移处存储着导入表大小0x28
蓝色的框就是数据目录表的16个项,每一项以RVA和size两个子项组成,之后会挨个分析重要的一些项,现在就着重看导入表。
拿到RVA,这里就涉及到一个RVA转换FOA,0x35B4是一个在内存中的相对虚拟地址,(所谓的相对虚拟地址,就是PE文件加载到内存中后,某一个内存地址减去加载的基址,结果为这个内存地址相对于加载基址的偏移),想一下,虽然内存中对齐可能比文件中对齐的粒度大,但是这里内存地址相对于区段的起始位置的偏移是不会改变的,也就是说假设一个地址在文件中相对于起始区段的偏移是0x100,那么加载到内存中,它相对于起始区段偏移仍然是0x100。理解这个就比较好转换了。
所以就查看这个RVA落在下面哪两个节区之间。从下图中看到,红框中的是每个节区在内存中的起始位置,0x35B4正好落在.text和.data区段之间(0x1000~0x4000)。所以0x35B4-0x1000=0x25B4,这个算出来的是RVA在内存中相对于起始区段的偏移,也就是相对于.text段的偏移是0x25B4,那么这个偏移在内存中和在文件中是相同的,所以在文件中的位置,就很容易算出,看.text下面的字段,0x3000是文件中对齐后的大小,0x1000是文件中的起始位置。所以导入表在文件中的位置就可以算出0x1000+0x25B4=0x35B4。
跳转到0x35B4的位置,这时可以看第一个结构体了,_IMAGE_IMPORT_DESCRIPTOR,这个结构体有五个字段,每个字段四字节,所以大小为20B,其中比较有用的有三个,第一个字段,第四个字段,第五个字段。下面分别来看。要注意一点,导入表的结束是以同样结构体大小的全0来表示结束。看蓝色框的五个全为0的字段,就理解了。
第一个字段是个联合体 0x35DC,但一般用的是OriginalFirstThunk,也就是所谓的INT(导入名称表)。网上有很多关于导入地址表导入名称表的关系图,我就不贴了。可以对照着看。第四个字段 0x36B0,指向DLL的名称,也就是字符串,第五个字段FirstThunk 0x1000 这个就是所谓的IAT(导入地址表)。
这里需要说明的是OriginalFirstThunk和FirstThunk所指向的都是一个_IMAGE_THUNK_DATA32的结构体。同样,这个结构体也是以全0为结束
上面两图分别是导入名称表和导入地址表。先看导入名称表。
因为是指向一个_IMAGE_THUNK_DATA32结构体,可以看到结构体中成员是一个联合体,联合体中每个字段是一个DWORD,所以看导入名称表的第一个 0x36BE,这里面的所有地址都是RVA,之前也说过RVA转化FOA,所以跳到文件中的位置
可以看到之所以叫导入名称表,是它所指向的是每一个导入函数的名称,第四个字段,是指向DLL名称,也就是这些导入函数所在哪个DLL模块,0x36B0位置看到DLL的名称为MSVBVM60.DLL。之前说过,_IMAGE_IMPORT_DESCRIPTOR这个字段是以相同大小的结构体全0字段结束,说白了,就是每一个_IMAGE_IMPORT_DESCRIPTOR结构体,就代表是一个导入的DLL,如果全0结束,那说明导入的DLL完毕。下面这些每一个导入的函数,都和导入的DLL相对应,也就是说,一个DLL,导入的函数是属于这个DLL的,下一个DLL,导入的函数是属于下一个DLL。这里导入的DLL就一个。
然后再看 0x000036BE,它的最高位是0,所以它是以函数名导入的,假如它的最高位是1,那么它就是以序号导入的,序号导入,就直接使用第三个字段Ordinal;代表导入的序号,而名称导入就指向第三个结构体 _IMAGE_IMPORT_BY_NAME,也就是我们看到的函数名字符串。
最后再看FirstThunk指向的地址0x1000,它所指向的地址,在加载到内存中后,会再做调整,PE加载器做的填充IAT,就是在填充它。可能在内存中开始的时候,它和导入名称表都是指向函数名字符串,但加载到内存后,因为DLL加载的基址不定,所以需要使用GetProcAddress和LoadLibrary来动态获取实际导入函数的地址,导入函数名以及导入的DLL名称都存放在INT中,直接从中取出,然后再逐一获取每一个函数的实际地址,最后填充到IAT中,就完成了IAT的填充。
放到OD里面来看一下。
首先PE文件加载基址0x400000,扩展头的ImageBase得到
OD中查找0x400000的位置,看到文件已经被加载到内存中去。
往下翻找到导入表的偏移,0x35B4.
加上基址0x400000,就是0x4035B4
这时可以看到_IMAGE_IMPORT_DESCRIPTOR结构体的五个字段,和在PE工具中解析的数据基本一致,就看最后一个字段导入地址表,加载到内存中是什么东西。同样0x400000+0x1000=0x401000
返回头可以看下,在文件中的导入地址表那些数据都是填充使用,到内存中它会真正填充成函数实际地址。至此导入表的解析就先到这里。然后通过代码来遍历一个文件的导入表。
#include <iostream>
#include<windows.h>
#include<stdlib.h>
DWORD dwFileSize;
BYTE* g_pFileImageBase = 0;
PIMAGE_NT_HEADERS g_pNt = 0; //NT头
DWORD RVAtoFOA(DWORD dwRVA)
{
//区块数目,在文件头中
int nCountOfSection = g_pNt->FileHeader.NumberOfSections;
//第一个区段
PIMAGE_SECTION_HEADER pSec = IMAGE_FIRST_SECTION(g_pNt);
//内存中对齐大小
DWORD dwSecAligment = g_pNt->OptionalHeader.SectionAlignment;
for (int i = 0; i < nCountOfSection; i++)
{
//因为在区段这个结构体中,内存中的大小是没有对齐的,
//所以需要计算出它在内存中对齐后的大小
//VirtualSize记录的是在区段真实大小,并没有对齐,这里用VirtualSize%对齐粒度
//模等于0说明对齐了,直接取该值就可以,否则的话,VirtualSize/对齐粒度,计算出
//已经对齐的部分的大小,然后再加一个粒度的大小,完成对齐
DWORD dwVirFlieSize = pSec->Misc.VirtualSize%dwSecAligment ?
pSec->Misc.VirtualSize / dwSecAligment * dwSecAligment + dwSecAligment :
pSec->Misc.VirtualSize;
//VirtualAddress记录的是区段起始内存位置,该位置加上区段在内存中的对齐大小
//就是下一区段的起始位置。这里就是判断当前这个RVA是否落在这两个区段的范围内
if (dwRVA >= pSec->VirtualAddress&&dwRVA <= pSec->VirtualAddress + dwVirFlieSize)
{
//如果落在这个范围,RVA-内存起始算出了偏移,加上文件中区段的起始位 置,
//得到在文件中的位置
return dwRVA - pSec->VirtualAddress + pSec->PointerToRawData;
}
pSec++;
}
return 0;
}
void ShowIAT()
{
OPENFILENAME stOF{}; //打开文件的结构体
HANDLE hFile = NULL; //文件句柄
WCHAR szFileName[MAX_PATH]{}; //要打开的文件路径及名称名
RtlZeroMemory(&stOF, sizeof(stOF));
stOF.lStructSize = sizeof(stOF);
stOF.lpstrFile = szFileName;
stOF.nMaxFile = MAX_PATH;
stOF.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST;
if (GetOpenFileName(&stOF))
{
hFile = CreateFile(szFileName, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL,
OPEN_EXISTING, FILE_ATTRIBUTE_ARCHIVE, NULL);
if (hFile == INVALID_HANDLE_VALUE)
{
printf("打开文件失败!\n");
return;
}
dwFileSize = GetFileSize(hFile, NULL);
g_pFileImageBase = new BYTE[dwFileSize]{};
DWORD dwRead = 0;
bool bRet = ReadFile(hFile, g_pFileImageBase, dwFileSize, &dwRead, NULL);
//如果读取失败,释放内存,关闭句柄退出
if (!bRet)
{
delete[] g_pFileImageBase;
CloseHandle(hFile);
return;
}
//读取成功也关闭掉句柄,因为文件已经读到buffer中
CloseHandle(hFile);
}
//ODS头,DOS头+e_lfanew=PE标记位置
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)g_pFileImageBase;
if (pDos->e_magic != IMAGE_DOS_SIGNATURE)
{
//如果不是MZ标记
delete[] g_pFileImageBase;
return;
}
//NT 头
g_pNt = (PIMAGE_NT_HEADERS)(pDos->e_lfanew + g_pFileImageBase);
if (g_pNt->Signature != IMAGE_NT_SIGNATURE)
{
//不是PE标记
delete[] g_pFileImageBase;
return;
}
PIMAGE_OPTIONAL_HEADER32 option = &(g_pNt->OptionalHeader);
//导入表的RVA
DWORD dwImportRVA = option->DataDirectory[1].VirtualAddress;
if (dwImportRVA == 0)
{
printf("没有导入表\n");
delete[] g_pFileImageBase;
return;
}
//导入表在文件中的位置
DWORD dwImportInFile = (DWORD)(RVAtoFOA(dwImportRVA) + g_pFileImageBase);
PIMAGE_IMPORT_DESCRIPTOR pImport = (PIMAGE_IMPORT_DESCRIPTOR)dwImportInFile;
while (pImport->Name)
{
//导入地址表
PIMAGE_THUNK_DATA pIAT =
(PIMAGE_THUNK_DATA)(RVAtoFOA(pImport->FirstThunk) + g_pFileImageBase);
//导入名称表
PIMAGE_THUNK_DATA pINT =
(PIMAGE_THUNK_DATA)(RVAtoFOA(pImport->OriginalFirstThunk) + g_pFileImageBase);
//DLL名
char *pName = (char*)(RVAtoFOA(pImport->Name) + g_pFileImageBase);
printf("导入模块名:%s\n", pName);
while (pINT->u1.AddressOfData)
{
if (IMAGE_SNAP_BY_ORDINAL32(pINT->u1.AddressOfData))
{
//序号导入
printf(" 序号为:%-30x 地址:%X\n", pINT->u1.Ordinal & 0xFFFF, pIAT->u1.Function);
}
else
{
PIMAGE_IMPORT_BY_NAME pImport =
(PIMAGE_IMPORT_BY_NAME)(RVAtoFOA(pINT->u1.AddressOfData)+g_pFileImageBase);
printf(" 名称为:%-30s 地址:%X\n", pImport->Name, pIAT->u1.Function);
}
pIAT++;
pINT++;
}
pImport++;
}
delete[] g_pFileImageBase;
return;
}
int main()
{
ShowIAT();
system("pause");
return 0;
}