PE导入表和IAT表的原理及工作关系
文章目录
0.说明
观看滴水逆向视频总结(部分截图来自于滴水课件)
编译器:vc++6.0
编写语言:c
观看滴水逆向视频总结(部分截图来自于滴水课件)
欢迎大家留言交流😃^ __ ^
可以加我qq一起学习:1245885144😄🤣
1.PE导入表和IAT表大致工作关系
(1)为什么会有IAT表
IAT:Import Address Table 导入(函数)地址表
一般程序在调用自身函数的时候,自身函数地址RAV是固定的;但是当程序在调用dll里的函数的时候,由于dll的地址会发生重定位,导致dll里的函数地址每次都会发生变化。
为了每次都能准确的调用dll函数的地址,就特意构建了一张表,用于存储每次程序运行,dll发生重定位之后,dll的函数的地址。
而这样之后,那我自身程序在调用dll函数的时候,就可以用“类似指针”指向这张表格,取其值为函数新的地址。即可准确调用dll的函数。
//类似这样的调用函数。这里的0x401234就是IAT的地址,
CALL DWORD PTR DS:[0x401234]
CALL DWORD PTR DS:[<&KERNEL32.GetVersion>; kernel32.GetVersion
(2)为什么会有导入表
说到导入表,肯定就是对应导出表。而导出表的作用是自身提供一张清单,表明自己又哪些函数,大多是dll为exe提供函数,存在导出表。那相反,exe为了表明自身需要哪些dll的函数,也会生成一张表,那这张表就是导入表。
有些dll一会需要使用别的dll的函数,那这类dll也会有导入表。
同样,导出表是提供的自身要导出的函数的地址,对应导入表,也是为了提供的要导入的dll的函数的地址,只不过由于这个地址不固定,所以每次都会重新修正。(大致,具体看下面原理)
2.导入表和IAT表的真正关系(原理)
导入表再目录项中的第二项(导出表之后)。对应目录项中的VirtualAddress(RVA)即指向的导入表。
这个结构体有5个4字节数据,占20字节,但只需特别记住这三个RVA即可,下面会分别详细说明。
三个RVA所指向的地址大概是这样的:
注意这是PE文件在加载前的样子!
上面涉及到的IMAGE_THUNK_DATA这个结构数组,其实就是一个4字节数,本来是一个union类型,能表示4个数,但我们只需掌握两种即可,其余两种已经成为历史遗留了。
(1)OriginalFirstThunk
OriginalFirstThunk这个RVA所指向的是INT表(Import Name Table),这个表每个数据占4个字节。顾名思义就是表示要导入的函数的名字表。
但是之前学导出表有了解到,导出函数可以以名字导出,亦可以序号导出。所以为了方便区分,就将这INT表的每个值做了细微调整:
INT
:如果这个4字节数的最高位(二进制)为1,那么抹去这个最高位之后,所表示的数就是要导入的函数的序号;如果最高位是0,那这个数就也是一个RVA,指向IMAGE_IMPORT_BY_NAME结构体(包含真正的导入函数的名字字符串,以0结尾)。INT表以4字节0结尾。
IMAGE_IMPORT_BY_NAME
:前两个字节是一个序号,不是导入序号,一般无用,后面接着就是导入函数名字的字符串,以0结尾。
(2)Name
这个结构体变量也是一个RVA,直接指向一个字符串,这个字符串就是这个导入表对应的DLL的名字。说到这,大家明白,一个导入表只对应一个DLL。那肯定会有多个导入表。所以对应目录项里的VirtualAddress(RVA)指向的是所有导入表的首地址,每个导入表占20字节,挨着。最后以一个空结构体作为结尾(20字节全0结构体)。
(3)FirstAddress
FirstAddress(RVA)指向的就是IAT表!IAT表也是每个数据占4个字节。最后以4字节0结尾。
注意上图PE文件加载前,IAT表和INT表的完全相同的,所以此时IAT表也可以判断函数导出序号,或指向函数名字结构体。
而在加载后,差别就是IAT表发生变化,系统会先根据结构体变量Name加载对应的dll(拉伸),读取dll的导出表,对应原程序的INT表,匹配dll导出函数的地址,返回其地址,贴在对应的IAT表上,挨个修正地址(也就是GetProcAddress的功能)。
所以上文说到,IAT表会存储dll的函数的地址,方便调用该函数时,直接取IAT表这个地址内的值,作为函数地址,去CALL。
(注意IAT表发生变化)
(4)关键点
实际上,PE文件在加载过程中,只会检索IAT表里存储的数据,而不会管INT表是否有数据。(也就是说,即使INT表置零,只要IAT表数据还在,都能正常识别导入函数,从而正常运行;而如果IAT表置零了,即使INT表还在,都无法正常运行)
3.C语言解析导出表
(1)建议
解析的时候要注意,每个地址都是RVA,RVA转FOA会很多,比较绕。
有两个点很容易错:
- IMAGE_IMPORT_BY_NAME这个结构体前两个字节是一个数,不用管,但是在读取函数名字的时候,一定要记住这里是个结构体,要跳过前两个字节的数再读取。
- 由于RVA互转FOA很多,可能会误把INT表直接当做IMAGE_IMPORT_BY_NAME给读取了。
建议:
- 使用PIMAGE_IMPORY_DESCRIPTOR定义导入表结构体指针pImportDescriptor,然后直接pImportDescriptor++,即可实现导入表的遍历。
- 同理,也可以直接使用四字节指针去指向INT表或IAT表,指针++,即可实现遍历表结构。
(2)C源代码
这是功能子函数,定义的变量名称都挺直白的。顾名思义即可。
//功能:解析、打印重定位表
//参数:指向该程序被读取到内存的指针
//返回值:无
void ReadPE( LPVOID pFileBuffer )
{
PIMAGE_DOS_HEADER pDosHeader = NULL;
PIMAGE_NT_HEADERS32 pNtHeader = NULL;
PIMAGE_FILE_HEADER pFileHeader = NULL;
PIMAGE_OPTIONAL_HEADER pOptionHeader = NULL;
PIMAGE_DATA_DIRECTORY pDataDirectory = NULL;
PIMAGE_IMPORT_DESCRIPTOR pImportDescriptor = NULL;
PDWORD pThunkINT = NULL;//INT表
PDWORD pThunkIAT = NULL;//IAT表
PIMAGE_IMPORT_BY_NAME pImportByName = NULL;//名称结构体
pDosHeader = (PIMAGE_DOS_HEADER)pFileBuffer ;
pNtHeader = (PIMAGE_NT_HEADERS32)( (DWORD)pDosHeader + pDosHeader->e_lfanew );
pFileHeader = (PIMAGE_FILE_HEADER)( (DWORD)pNtHeader + 4 );
pOptionHeader = (PIMAGE_OPTIONAL_HEADER)( (DWORD)pFileHeader + IMAGE_SIZEOF_FILE_HEADER );
pDataDirectory = (PIMAGE_DATA_DIRECTORY )pOptionHeader->DataDirectory ;
//导入表寻址
pImportDescriptor = (PIMAGE_IMPORT_DESCRIPTOR)( (DWORD)pDosHeader + ConvertRvaToFoa( pDataDirectory[1].VirtualAddress , pFileBuffer ) );
while( pImportDescriptor->Name )
{
printf("**************************************************\n");
printf("Dll Name : %s\n",(PDWORD)( (DWORD)pDosHeader + ConvertRvaToFoa( pImportDescriptor->Name ,pFileBuffer) ) );
pThunkINT = (PDWORD)( (DWORD)pDosHeader + ConvertRvaToFoa( pImportDescriptor->OriginalFirstThunk , pFileBuffer) );
while( *pThunkINT )
{
if( *pThunkINT & 0x80000000 )
printf("Import by series number : %d\n", (DWORD)pThunkINT & 0x7fffffff);
else
{ //寻址函数名字结构体
pImportByName = (PIMAGE_IMPORT_BY_NAME)( (DWORD)pDosHeader + ConvertRvaToFoa( *pThunkINT , pFileBuffer) );
printf("Import by name! Hint: %-10XFunction Name: %s\n",pImportByName->Hint ,pImportByName->Name );
}
pThunkINT++;
// getchar();
}
putchar('\n');
pImportDescriptor++;
}
}
4.移动导出表到新增节区
(1)流程
(2)我遇到的困难
- IAT表不能移动,因为在调用dll函数时,call命令指向的地址是IAT表,再取IAT表内的值。除了IAT表,其余都可以移动。
- 新增节区的属性必须包含可读可写(0xC0000000),不然系统无法正常解析导入表。PE文件:节表(区块表) 节表属性格式分析
- 有的PE在文件状态下,实际大小(查看二进制数据)比PE结构表明的大小(最后一个节区PointerToRawData + SizeOfRawData)要大。造成问题:RVA转FOA出错;新增节区的与原始最后一个节区不连贯。原因:那一部分多出的数据是无效的,可以覆盖0或直接删除(可能是编译时自带的)。解决:(新增节区时)可以在遍历节表头的时候,根据最后一个节表头的PointToRawData+SizeOfRawData修正FileSize。
(3)C源代码
主要功能子函数。
//功能:移动导入表到新增节
//参数:指向文件内存的指针
//返回值:指向新增节区后再移动导出表过后的PE内存的指针
LPVOID MoveImportDescriptorToNewSection( LPVOID pFileBuffer )
{
DWORD Offset = FileSize;//复制导入表时的偏移等于新增节区前的文件大小
DWORD SIZEOF_INT = 0;
PIMAGE_DOS_HEADER pDosHeader = NULL;
PIMAGE_NT_HEADERS32 pNtHeader = NULL;
PIMAGE_FILE_HEADER pFileHeader = NULL;
PIMAGE_OPTIONAL_HEADER pOptionHeader = NULL;
PIMAGE_DATA_DIRECTORY pDataDirectory = NULL;
PIMAGE_IMPORT_DESCRIPTOR pImportDescriptor = NULL;
PDWORD pThunkINT = NULL;//INT表
PDWORD pThunkIAT = NULL;//IAT表
PIMAGE_IMPORT_BY_NAME pImportByName = NULL;
//第一步: 新增一个节区(注意新增节区的大小是不装的下导入表)
pFileBuffer = IncreaseNewSection( pFileBuffer , 0x3000 );//第二个参数是新增节的大小
//初始化PE头
pDosHeader = (PIMAGE_DOS_HEADER)pFileBuffer;
pNtHeader = (PIMAGE_NT_HEADERS32)( (DWORD)pDosHeader + pDosHeader->e_lfanew );
pFileHeader = (PIMAGE_FILE_HEADER)( (DWORD)pNtHeader + 4);
pOptionHeader = (PIMAGE_OPTIONAL_HEADER)( (DWORD)pFileHeader + IMAGE_SIZEOF_FILE_HEADER);
pDataDirectory = (PIMAGE_DATA_DIRECTORY)pOptionHeader->DataDirectory;
//第二步: 根据目录项里的RVA和Size复制整块导入表到到新增节区(Size大小内同时包多个dll的导入表)
memcpy((PDWORD)( (DWORD)pDosHeader + Offset) , (PDWORD)( (DWORD)pDosHeader + ConvertRvaToFoa(pDataDirectory[1].VirtualAddress , pFileBuffer ) ) , pDataDirectory[1].Size );
//同时修正目录下里的RVA
pDataDirectory[1].VirtualAddress = ConvertFoaToRva(Offset , pFileBuffer );
//修改偏移
Offset += pDataDirectory[1].Size ;
//初始化指向导入表的指针
pImportDescriptor = (PIMAGE_IMPORT_DESCRIPTOR)( (DWORD)pDosHeader + ConvertRvaToFoa( pDataDirectory[1].VirtualAddress , pFileBuffer ) );
//第六步: 遍历其余dll的导入表,重复以上第三步至第五步(写的时候这一步要通过while循环放在第二步之后)
while(pImportDescriptor->Name )
{
//第三步: 根据第一块导入表的OriginalFirstThunk(RVA),复制整块INT表(先遍历计算大小,再通过memcpy复制)
pThunkINT = (PDWORD)( (DWORD)pDosHeader + ConvertRvaToFoa( pImportDescriptor->OriginalFirstThunk ,pFileBuffer) );
do
SIZEOF_INT+=4;
while( *(pThunkINT++) );//让SIZEOF_INT多加一个4的大小,为了给末尾留4字节0作为结尾
memcpy( (PDWORD)( (DWORD)pDosHeader + Offset) , (PDWORD)( (DWORD)pDosHeader + ConvertRvaToFoa(pImportDescriptor->OriginalFirstThunk ,pFileBuffer) ) , SIZEOF_INT);
//同时修正导入表中OriginFirstThunk(RVA)
pImportDescriptor->OriginalFirstThunk = ConvertFoaToRva( Offset , pFileBuffer );
//修改偏移
Offset += SIZEOF_INT;
//第四步: 遍历INT表,判断,复制对应IMAGE_IMPORT_BY_NAME这个结构体到新增节区(注意:所有涉及到的字符串末尾要留一个0)
//同时修正INT表中的RVA
//同时遍历IAT表,修正IAT表中的RVA
//(IAT表不能移动,因为在调用dll函数时,call命令指向的地址是IAT表,再取IAT表内的值)
pThunkINT = (PDWORD)( (DWORD)pDosHeader + ConvertRvaToFoa( pImportDescriptor->OriginalFirstThunk , pFileBuffer) );
pThunkIAT = (PDWORD)( (DWORD)pDosHeader + ConvertRvaToFoa( pImportDescriptor->FirstThunk , pFileBuffer) );
for( ; *pThunkINT ; pThunkINT++ , pThunkIAT++)
if( !(*pThunkINT & 0x80000000) )
{
memcpy( (PDWORD)( (DWORD)pDosHeader + Offset) , (PDWORD)( (DWORD)pDosHeader + ConvertRvaToFoa(*pThunkINT ,pFileBuffer) ) , 2 + strlen( (PBYTE)( (DWORD)pDosHeader + ConvertRvaToFoa(*pThunkINT+2 , pFileBuffer) ) ) );
//同时修正INT表中的RVA
*pThunkINT = ConvertFoaToRva( Offset , pFileBuffer);
//同时修正IAT表中的RVA
*pThunkIAT = *pThunkINT;
//修改偏移
Offset += 2 + strlen( (PBYTE)( (DWORD)pDosHeader + ConvertRvaToFoa(*pThunkINT+2 , pFileBuffer) ) ) + 1;//多加一个1的大小,给字符串末尾留一个0的结尾符。
}
//第五步: 根据导入表的Name(RVA),复制dll名字符串到新增节区
memcpy( (PDWORD)( (DWORD)pDosHeader + Offset) , (PDWORD)( (DWORD)pDosHeader + ConvertRvaToFoa( pImportDescriptor->Name , pFileBuffer) ), strlen( (PBYTE)(DWORD)pDosHeader +ConvertRvaToFoa(pImportDescriptor->Name ,pFileBuffer) ) );
//同时修正导入表中的Name(RVA)
pImportDescriptor->Name = ConvertFoaToRva( Offset , pFileBuffer);
//修改偏移
Offset += strlen( (PBYTE)( (DWORD)pDosHeader + ConvertRvaToFoa( pImportDescriptor->Name , pFileBuffer) ) ) + 1;//多加一个1的大小,给字符串末尾留一个0的结尾符。
pImportDescriptor++;//遍历导入表。
}
return pFileBuffer ;
}
欢迎大家留言评论交流。