IAT 的全称是。在可执行文件中使用其他 DLL 可执行文件的代码或数据,称为导入或者输入,当 PE 文件载入内存时,windows 加载器会定位所有导入的函数或数据将定位到的内容填写至可执行文件的某个位置供其使用,而这个操作是需要借助导入表来完成的。导入表中存放了程序所有使用的 DLL 模块名称及导入的函数名称或函数序号。本文所用文件及源代码下载地址:https://pan.baidu.com/s/1o9360AI学习 IAT 有什么用?在脱壳和加壳的研究中,导入表是非常关键的部分,加壳要尽可能的隐藏或破坏原始的导入表。脱壳一定要找到或者还原原本的导入表。举个简单的例子,我们已经找到了加壳程序的 OEP 并转存了下来,但该程序并不能正常运行,这时我们就要手工修复程序的 IAT。在反病毒的静态分析中,我们可以通过病毒的导入表,初步确定病毒的行为。在免杀中也有对 IAT 的操作,比如隐藏导入表,修改导入表描述信息,移动导入表函数等等。在 Hook 操作中也有相应的 IAT 钩子……IAT 的数据结构今天我们的目的是找出一个 PE 文件中所有的 DLL 以及每个 DLL 的导入函数并通过编程体会这一过程,先用一张图介绍一下 PE 结构吧:
(emmm…… 从网上找的,觉得做得很不错,如有侵权请联系我)在IMAGE_OPTIONAL_HEADER的IMAGE_DATA_DIRECTORY中定位到第二个目录,即IMAGE_DIRECTORY_ENTRY_IMPORT。该结构体保存了导入函数的 RVA 地址,通过该 RVA 地址可以定位到导入表的具体位置。描述导入表结构体是IMAGE_IMPORT_DESCRIPTOR,也就是说每一个导入的 DLL 都有一个对应的IMAGE_IMPORT_DESCRIPTOR,并且以数组的形式存放在文件中的,结构体的定义如下:
typedefstruct_IMAGE_IMPORT_DESCRIPTOR{
union{
DWORDCharacteristics;
DWORDOriginalFirstThunk;//该字段指向导入名称表(INT),该RVA是一个IMAGE_THUNK_DATA结构体
};
DWORDTimeDateStamp;//可以忽略,一般为0
DWORDForwarderChain;//一般为0
DWORDName;//指向DLL的名称的RVA地址
DWORDFirstThunk;//该字段包含导入地址表(IAT)的RVA,IAT是一个IMAGE_THUNK_DATA结构体数组
}IMAGE_IMPORT_DESCRIPTOR;
我们可以发现导入信息中并没有指定导入表的个数,而是以一个全 “0” 的IMAGE_IMPORT_DESCRIPTOR作为结束标志的。下面来看一下IMAGE_THUNK_DATA结构体的定义:
typedefstruct_IMAGE_THUNK_DATA32{
union{
DWORDForwarderString;// 一个RVA地址,指向forwarder string
DWORDFunction;// PDWORD,被导入的函数的入口地址
DWORDOrdinal;// 该函数的序数
DWORDAddressOfData;// 一个RVA地址,指向IMAGE_IMPORT_BY_NAME
}u1;
}IMAGE_THUNK_DATA32;
每一个IMAGE_THUNK_DATA对应一个 DLL 中的导入函数。与IMAGE_IMPORT_DESCRIPTOR类似,IMAGE_THUNK_DATA在文件中也是一个数组,并以一个全为 “0” 的IMAGE_THUNK_DATA结束。当该结构体值的最高位为 0 时,表示函数以函数名字符串的方式导入,这时该 DWORD 的值表示一个 RVA,并指向一个IMAGE_IMPORT_BY_NAME结构体:
typedefstruct_IMAGE_IMPORT_BY_NAME{
WORDHint;//该函数的导出序数
BYTEName[1];// 该函数的名字
}IMAGE_IMPORT_BY_NAME,*PIMAGE_IMPORT_BY_NAME;
这里简单介绍一下 IAT 与 INT 的区别:结构体中的和都指向结构体。当文件在磁盘上时,两者指向的是同一个,而当文件载入内存时中保存的仍然是指向函数的 RVA,而指向的内存变成了由装载器填充的导入函数地址,即 IAT。手动查找 IAT首先用 C32Asm 以十六进制模式打开 PE.exe,点击工具栏中的查看并单击PE信息,如图
这里要注意一下,给出的是 RVA 地址也就是载入内存后的地址,目前我们没有载入内存,只是在磁盘中打开而已,所以要把 RVA 转换为,手工转换我就不讲了,我只讲使用工具转换。打开 loadPE 载入 PE.exe,单击位置计算器。
输入,可以看到已经算了出来。
在 C32Asm 中,输入。
可以看到,有四个的结构体,但是第四个是一个全 “0” 的结构体。做成表格
Name 指向的是 Dll 的名称,注意这里 Name 也是 RVA 转成为、、,在 C32Asm 里查找
整理成表格
我们就以为例,查看和的内容,先查看,将 RVA 转为,转到
可以看到有 8 个结构体,也就说有 8 个导入函数,第一个结构体指向的 RVA 为转为等于,转到
处是一个结构体,前两个字节是 Hint 的值,所以导入函数的名称为。使用 loadPE 查看导入表,发现与我们分析的一致。
至于,大家可以参照上面的步骤一步一步查找。通过编程体会查找过程我分享给大家两种方法(源码已经上传到网盘中,供大家下载)操作系统:win 7IDE: vs2013第一种请看这张图
把文件映射入内存之后,要开始寻找IMAGE_IMPORT_DESCRIPTOR的位置,并用两层循环把 DLL 的名字和 DLL 中函数的名字输出,核心代码如下:
IMAGE_DOS_HEADER*dosHeader;
IMAGE_NT_HEADERS*ntHeader;
IMAGE_IMPORT_BY_NAME*ImportName;
//lpBase由MapViewOfFile函数返回
dosHeader=(IMAGE_DOS_HEADER*)lpBase;
//检测是否是有效的PE文件
if(dosHeader->e_magic!=IMAGE_DOS_SIGNATURE)
{
printf("This is not a windows file\n");
return;
}
//定位到PE header
ntHeader=(IMAGE_NT_HEADERS*)((BYTE*)lpBase+dosHeader->e_lfanew);//e_lfanew成员定位到PE header
//判断是否是一个有效的win32文件
if(ntHeader->Signature!=IMAGE_NT_SIGNATURE)
{
printf("This is not a win32 file\n");
return;
}
//定位到导入表
IMAGE_IMPORT_DESCRIPTOR*ImportDec=(IMAGE_IMPORT_DESCRIPTOR*)((BYTE*)lpBase+RVAToOffset(lpBase,ntHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress));
while(ImportDec->FirstThunk)
{
//得到DLL文件名
char*pDllName=(char*)((BYTE*)lpBase+RVAToOffset(lpBase,ImportDec->Name));
printf("\nDLL文件名:%s\n",pDllName);
//通过OriginalFirstThunk定位到PIMAGE_THUNK_DATA结构数组
PIMAGE_THUNK_DATApThunk=(PIMAGE_THUNK_DATA)((BYTE*)lpBase+RVAToOffset(lpBase,ImportDec->OriginalFirstThunk));
while(pThunk->u1.Function)
{
//判断函数是用函数名导入的还是序号导入的
if(pThunk->u1.Ordinal&IMAGE_ORDINAL_FLAG32)//高位为1
{
//输出序号
printf("从此DLL模块导出的函数的序号:%x\n",pThunk->u1.Ordinal&0xFFFF);
}
else//高位为0
{
//得到IMAGE_IMPORT_BY_NAME结构中的函数名
ImportName=(IMAGE_IMPORT_BY_NAME*)((BYTE*)lpBase+RVAToOffset(lpBase, (DWORD)pThunk->u1.AddressOfData));
printf("从此DLL模块导出的函数的函数名:%s\n",ImportName->Name);
}
pThunk++;
}
ImportDec++;
}
要注意计算,代码如下
//用来实现RVA到FileOffset的转换
DWORDRVAToOffset(LPVOIDlpBase,DWORDVirtualAddress)
{
IMAGE_DOS_HEADER*dosHeader;
IMAGE_NT_HEADERS*ntHeader;
IMAGE_SECTION_HEADER*SectionHeader;
intNumOfSections;//Section 的数量
//定位到PE head
dosHeader=(IMAGE_DOS_HEADER*)lpBase;
ntHeader=(IMAGE_NT_HEADERS*)((BYTE*)lpBase+dosHeader->e_lfanew);
NumOfSections=ntHeader->FileHeader.NumberOfSections;
for(inti=;i
{
SectionHeader=(IMAGE_SECTION_HEADER*)((BYTE*)lpBase+dosHeader->e_lfanew+sizeof(IMAGE_NT_HEADERS))+i;
//判断RVA是否在这个节区之内
if(VirtualAddress>SectionHeader->VirtualAddress&&VirtualAddressVirtualAddress+SectionHeader->SizeOfRawData)
{
DWORDAposRAV=VirtualAddress-SectionHeader->VirtualAddress;
DWORDOffset=SectionHeader->PointerToRawData+AposRAV;
returnOffset;
}
}
return;
}
程序运行效果如下
第二种由于把文件映射入内存这些操作比较麻烦,我们可以直接通过或者,将目标文件直接载入内存直接进行操作,代码如下。
HMODULEhExe=LoadLibraryW(L"c:\\PE.exe");
PIMAGE_IMPORT_DESCRIPTORpImportDesc=(PIMAGE_IMPORT_DESCRIPTOR)ImageDirectoryEntryToData((void*)hExe,TRUE,IMAGE_DIRECTORY_ENTRY_IMPORT,&size);
PIMAGE_IMPORT_DESCRIPTORDes=pImportDesc;
while(Des->Name)
{
printf("DllName = %s \r\n",(DWORD)hExe+(DWORD)Des->Name);
PIMAGE_THUNK_DATAthunk=(PIMAGE_THUNK_DATA)(Des->FirstThunk+(DWORD)hExe);
while(thunk->u1.Function)
{
if(thunk->u1.Ordinal&IMAGE_ORDINAL_FLAG)
{
printf("Ordinal = %08x \r\n",thunk->u1.Ordinal&0xFFFF);
}
else
{
PIMAGE_IMPORT_BY_NAMEPimName=(PIMAGE_IMPORT_BY_NAME)thunk->u1.Function;
printf("FuncName = %s\r\n",(DWORD)hExe+PimName->Name);
}
thunk++;
}
Des++;
}
因为 PE 文件已经载入到内存中了,所以我们不需要计算 FileOffset。程序效果图如下
第三种(只提供思路,不提供代码)最后算是给大家留一点思考的空间,先介绍几个 APIfopen(打开文件)FILE * fopen(const char * path,const char * mode);有下列几种形态字符串: r 打开只读文件,该文件必须存在。 r+ 打开可读写的文件,该文件必须存在。 w 打开只写文件,若文件存在则文件长度清为 0,即该文件内容会消失。若文件不存在则建立该文件。 w+ 打开可读写文件,若文件存在则文件长度清为零,即该文件内容会消失。若文件不存在则建立该文件。 a 以附加的方式打开只写文件。若文件不存在,则会建立该文件,如果文件存在,写入的数据会被加到文件尾,即文件原先的内容会被保留。 a+ 以附加方式打开可读写的文件。若文件不存在,则会建立该文件,如果文件存在,写入的数据会被加到文件尾后,即文件原先的内容会被保留。上述的形态字符串都可以再加一个 b 字符,如 rb、w+b 或 ab+ 等组合,加入 b 字符用来告诉函数库打开的文件为二进制文件,而非纯文字文件。由所建立的新文件会具有权限,此文件权限也会参考值。fseek(重定位流上的文件指针)int fseek(FILE *stream, long offset, int fromwhere);函数设置文件指针 stream 的位置。如果执行成功,stream 将指向以 fromwhere 为基准,偏移 offset 个字节的位置。如果执行失败(比如 offset 超过文件自身大小),则不改变 stream 指向的位置。fread(读文件数据)int fread(void *ptr, int size, int nitems, FILE *stream);用于接收数据的地址(指针)(ptr)单个元素的大小(size)元素个数(nitems)提供数据的文件指针(stream)思路如下:我们可以通过打开相应的 PE 文件,接着调用函数并设置相应的偏移,最后调用把对应偏移的数据读入相应的变量中,以下是代码片段(供大家参考)
pNewFile=fopen(newFileName,"rb+");//打开方式"rb+"
if(NULL==pNewFile)
{
puts("Open file failed");
exit();
}
fseek(pNewFile,,SEEK_SET);
fread(&DosHeader,sizeof(IMAGE_DOS_HEADER),1,pNewFile);//DosHeader是IMAGE_DOS_HEADER类型的变量
后面的操作跟上面两种类似。如果您还有其他方法欢迎在下方留言……小结相对于前两种方法,第三种方法在添加节区插入 stub 数据时会省去一些不必要的操作,简单方便。这里我想扩展一点,说到读取导入函数的信息,不知大家有没有想到在 shellcode 中动态获取函数地址的操作?首先查找 kernel32.dll 的基地址
movebx,fs:[edx+0x30]//[TEB+0x30]是PEB的位置
movecx,[ebx+0xc]//[PEB+0xc]是PEB_LDR_DATA的位置
movecx,[ecx+0x1c]//[PEB_LDR_DATA+0x1c]是ntdll.dll的位置
movecx,[ecx]//进入链表第一个就是ntdll.dll
movebp,[ecx+0x8]//ebp保存的是kernel32.dll的基地址
……
接着在中 kernel32.dll 查找相应的API函数
pushad//保护所有的寄存器中的内容
moveax,[ebp+0x3c]//PE头
movecx,[ebp+eax+0x78]//导入表的指针
addecx,ebp
movebx,[ecx+0x20]//导出函数的名字列表
……
popad
实际上跟上面都是类似的……本篇文章主要向大家介绍了 IAT 是什么东西、有什么用、怎么查找,下一篇我可能会向大家介绍一下 IATHook ……
领取专属 10元无门槛券
私享最新 技术干货