首先说几个基本问题:
1.导入表的作用是什么?没有它exe能运行么?
作用:记录了一个exe或者一个dll所用到的其他模块导出的函数;
所记录的信息有:用了哪些模块(用了哪些dll),用了dll的哪些函数
2.导出表的作用?没有它exe能运行么?
作用:记录了导出符号的地址,名称,与序号
(提示:exe文件中很少有导出表的,大多数dll都有导出表,某些存放资源文件的dll就没有导出表)
下面截取自笔记,便于理解导入表与导出表的关系
3.怎样才能知道一个exe用了哪些API?
通过遍历导入表就可以知道导入了哪些dll以及dll中的哪些API
4.已知一个dll名和dll导出函数的名字,如何得到这个函数名的地址
通过Loadlibrary(GetModelHandle)将dll模块映射进内存并返回一个可以被GetProcAddress函数使用的句柄,再利用GetProcAddress得到dll的加载地址,通过遍历导出表就可以得到该函数的地址.
5.如何判断导入函数是以序号导入还是以名称导入?
在IMAGE_THUNK_DATA32这个结构体(结构体里面就是一个联合体,大小为32位)里面,判断结构体字段中的最高位,
最高位为1:以序号导入
最高位为0:以名称导入
6.怎样才能知道导出函数是以序号导出还是以名称导出?
遍历序号表,判断地址表的下标有没有存在与序号表中,存在就说明是以名称导出,不存在就说明是以序号导出
双桥结构
导入表几个简图(笔记中的,将就看吧):
解析导入表:
#include "stdafx.h"
#include <windows.h>
DWORD RvaToOffset( IMAGE_NT_HEADERS* pNtHdr , DWORD dwRva ) {
// 1. 找Rva所在的区段
// 2. 用Rva减去所在区段的首Rva ,再用减出来的差,加上所在
// 区段的首区段偏移
IMAGE_SECTION_HEADER* pSechdr = NULL;
pSechdr = IMAGE_FIRST_SECTION( pNtHdr );
for( int i = 0 ; i < pNtHdr->FileHeader.NumberOfSections; ++i ) {
if( dwRva >= pSechdr[ i ].VirtualAddress
&& dwRva <= pSechdr[ i ].VirtualAddress + pSechdr[ i ].SizeOfRawData ) {
dwRva -= pSechdr[ i ].VirtualAddress;
dwRva += pSechdr[ i ].PointerToRawData;
return dwRva;
}
}
return -1;
}
int main( ) {
//typedef struct _IMAGE_IMPORT_DESCRIPTOR {
// union {
// DWORD Characteristics;
// DWORD OriginalFirstThunk; /*保存导入函数名称表(INT)的地址(RVA)*/
// } DUMMYUNIONNAME;
// DWORD TimeDateStamp;
// DWORD ForwarderChain;// 是否是dll转发
// DWORD Name;/*导入的模块名(DLL的名字)*/
// DWORD FirstThunk;/*导入函数地址表(IAT)(RVA)*/
//} IMAGE_IMPORT_DESCRIPTOR;
// 解析导入表
// 1. 读取文件到内存
printf( "请输入要解析的DLL的路径:" );
char szPath[ MAX_PATH ];
gets_s( szPath , MAX_PATH );
HANDLE hFile = INVALID_HANDLE_VALUE;
hFile = CreateFileA( szPath , GENERIC_READ , FILE_SHARE_READ ,
NULL , OPEN_EXISTING , FILE_ATTRIBUTE_NORMAL , NULL );
if( hFile == INVALID_HANDLE_VALUE ) {
printf( "文件不存在\n" );
system( "pause" );
return 0;
}
DWORD dwHeight = 0;
DWORD dwFileSize = GetFileSize( hFile , &dwHeight );
BYTE* pBuff = new BYTE[ dwFileSize ];
ReadFile( hFile , pBuff , dwFileSize , &dwFileSize , NULL );
// 2. 解析出Dos头,Nt头,扩展头
IMAGE_DOS_HEADER* pDosHdr = NULL;
IMAGE_NT_HEADERS* pNtHdr = NULL;
IMAGE_OPTIONAL_HEADER* pOptHdr = NULL;
IMAGE_DATA_DIRECTORY* pDataDir = NULL;
IMAGE_EXPORT_DIRECTORY* pExortTable = NULL;
pDosHdr = (IMAGE_DOS_HEADER*)pBuff;
pNtHdr = (IMAGE_NT_HEADERS*)( (ULONG_PTR)pDosHdr + pDosHdr->e_lfanew );
pOptHdr = &pNtHdr->OptionalHeader;
// 3. 得到扩展头中的数据目录表
pDataDir = pOptHdr->DataDirectory;
// 4. 通过数据目录表中的第1个元素得到导入表的RVA
DWORD dwImpRva = pDataDir[ 1 ].VirtualAddress;
if( dwImpRva == 0 ) {
printf( "没有导入表" );
system( "pause" );
return 0;
}
// 5. 将导入表的RVA转换成文件偏移
DWORD dwImpOfs = RvaToOffset( pNtHdr , dwImpRva );
// 6. 使用导入表结构体指针指向导入表所在的内存地址
IMAGE_IMPORT_DESCRIPTOR* pImpTab = NULL;
pImpTab = (IMAGE_IMPORT_DESCRIPTOR*)( dwImpOfs + (ULONG_PTR)pDosHdr );
// 7. 遍历所有的导入表
// 导入表是以一个全0元素作为结束的标志
while( pImpTab->Name != 0 ) {
// 7.1 获取这个导入所导入的DLL的名称,导入表里面的Name也是一个RVA
ULONG_PTR dwNameOfs = RvaToOffset( pNtHdr , pImpTab->Name );
char* pName = nullptr;
pName = (char *)( dwNameOfs + (ULONG_PTR)pDosHdr );
printf( "导入的DLL:%s\n" , pName );
//IMAGE_THUNK_DATA32
// 7.2 遍历从这个dll导入的函数名称
DWORD dwINToffset = RvaToOffset(pNtHdr, pImpTab->OriginalFirstThunk);
// INT : 导入名称表
// 这个表记录了所有导入函数名称的地址(RVA)
// 每个地址都是4/8字节(如果是32位PE文件,就是4字节,如果是64位的PE文件就是8字节)
ULONG_PTR* pInt = (ULONG_PTR*)( dwINToffset + (ULONG_PTR)pDosHdr );
while( *pInt != 0 ) {
// 导入函数有两种方式:
// 1. 以序号导入
// 2. 以名称导入
// 如果INT表中保存的值,最高位是0的时候,说明这个值是一个函数名称的RVA
// 否则这个值的低16位就是一个导入的序号。
if( IMAGE_SNAP_BY_ORDINAL( *pInt ) ) {
// 以序号方式导入
// 以序号方式导入, 保存的是序号,需要只有2字节
printf( "\t0x%04X\n" , *pInt & 0xFFFF );
}
else {
// 以名称方式导入,数组保存的RVA,并非是一个字符串的RVA
// 而是一个结构体(IMAGE_IMPORT_BY_NAME)的RVA
IMAGE_IMPORT_BY_NAME* pImpByName = NULL;
DWORD dwNameRva = *pInt;
dwNameOfs = RvaToOffset( pNtHdr , dwNameRva );
pImpByName = (IMAGE_IMPORT_BY_NAME*)( dwNameOfs + (ULONG_PTR)pDosHdr );
printf( "\t序号:%04X,名称:%s这是以名称方式导入的\n" ,
pImpByName->Hint ,
pImpByName->Name );
}
// 得到下一个导入函数的名称的地址
++pInt;
}
// 得到下一个导入表的地址
++pImpTab;
}
system( "pause" );
return 0;
}
另外一种方式:
遍历导入表中的INT修复IAT
while (exeimport->Name != NULL) //遍历模块
{
HMODULE h_dllModule = apis.pfnLoadLibraryA((char *)(exeimport->Name + ImageBase));
PIMAGE_THUNK_DATA import_Int = (PIMAGE_THUNK_DATA)(exeimport->OriginalFirstThunk+ImageBase);
PIMAGE_THUNK_DATA import_IAT = (PIMAGE_THUNK_DATA )(exeimport->FirstThunk+ ImageBase);
while (import_Int->u1.Ordinal != 0) //遍历函数
{
UCHAR *buf = (UCHAR *)apis.pfnHeapAlloc(heap, HEAP_ZERO_MEMORY, 10);
buf[0] = 0xb8;
buf[5] = 0xff;
buf[6] = 0xe0;
//new char[20]{ "\xB8\x00\x00\x00\0x00\0xff\0xe0" };
DWORD opl = 0;
apis.pfnVirtualProtect((LPVOID)buf, 20, PAGE_EXECUTE_READWRITE, &opl);
if (import_Int->u1.Ordinal &0x80000000) //这里是重点!!!序号导出, 最高位为1,这里是获取最高位,如果最高位为1,就执行下面里面的语句,即
//以序号导入 ,否则以名称导入,执行else中的语句
{
//获取序号函数
LPVOID apiaddr =
apis.pfnGetProcAddress(h_dllModule, (char *)(import_Int->u1.Ordinal & 0xFFFF));
*(DWORD*)&buf[1] = (DWORD)apiaddr; //函数写入shellcode
//DWORD funaddr = ;
apis.pfnVirtualProtect((LPVOID)(import_IAT ), 4, PAGE_EXECUTE_READWRITE, &opl);
*(DWORD*)((DWORD)import_IAT ) = (DWORD)buf; //将函数写入到iat
}
else
{
//DWORD Faddr = *(DWORD*)(import_Int->u1.AddressOfData + ImageBase);
PIMAGE_IMPORT_BY_NAME funname = (PIMAGE_IMPORT_BY_NAME)(import_Int->u1.AddressOfData+ImageBase);
LPVOID apiaddr =
apis.pfnGetProcAddress(h_dllModule, funname->Name);
*(DWORD*)&buf[1] = (DWORD)apiaddr; //函数写入shellcode
apis.pfnVirtualProtect((LPVOID)(import_IAT), 4, PAGE_EXECUTE_READWRITE, &opl);
*(DWORD*)((DWORD)import_IAT ) = (DWORD)buf; //将函数写入到iat
// DWORD funaddr =import_IAT->u1.Function ; //获取iat地址
//
// apis.pfnVirtualProtect((LPVOID)funaddr, 4, PAGE_EXECUTE_READWRITE, &opl);
// *(DWORD*)(funaddr) = (DWORD)buf; //将函数写入到iat
}
import_Int++;
import_IAT++;
}
exeimport++;
}
导入表:
(exe例子为:内存管理-VirtualAlloc)
LordPE查看其导入表RVA:
010editor中验证:
导入表位置,落在了.idata段:
1A000 【7800】
1A1C8 【79C8】
79C8:(这个值为文件中的偏移,就是它在文件中的起始位置,利用79C8在010editor里面可以找到导入表)
在010editor中:
typedef struct _IMAGE_IMPORT_DESCRIPTOR{
union {
DWORD Characteristics;
DWORD OriginalFirstThunk;//指向一个结构体数组的相对位移,RVA to original unbound IAT (PIMAGE_THUNK_DATA)
}DUMMYUNIONNAME;
DWORD TimeDataStamp;
DWORD ForwarderChain;
DWORD Name; //导入的PE文件的名字的相对位移(RVA)
DWORD FirstThunk ;//指向一个结构体数组的相对位移(RVA to IAT)
}
18 A2 01 00【OriginalFirstThunk:INT(Import Name Table)导入名称表地址RVA】
00 00 00 00
00 00 00 00
22 A4 01 00【DLL名称的RVA】
00 A0 01 00【IAT(Import Address Table)导入地址表地址RVA】
1.dll的名字【22 A4 01 00(小端)—>0001A422(因为1A000对应7800,故这里dll在文件中偏移为7C22)】
2.看下INT(OriginalFirstThunk):1A218【7A18】(以全0结尾)
函数名数组(对应IMAGE_THUNK_DATA32结构体数组,每一个结构体就是一个联合体)
E0 A3 01 00----0001A3E0【7BE0】最高位为0,说明是以名称导入的,不是序号导入的;
一共导入了25个函数,这里只写一个,其他依次类推!
注意:IAT和INT都指向下面的这个数据结构,4Byte
typedef struct _IMAGE_THUNK_DATA32{
union{
DWORD ForwarderString;
DWORD Function;//导入函数的地址,在加载到内存之后,这里才起作用
DWORD Ordinal;//假如是序号导入的,会用到这里
DWORD AddressOfData;//假如是函数名导入,会用到这里,它指向另外一个结构体PIMAGE_IMPORT_BY_NAME
}u1;
}IMAGE_THUNK_DATA32;
//如果是函数名导入的,AddressOfData会指向下面这个结构体
typedef struct _IMAGE_IMPORT_BY_NAME{
WORD Hint;//序号
CHAR Name[1];//不定长,字符串
}
由上可知:是按照函数名导入的(大多数都是按名称导入的),故上面的地址值,就会指向一个PIMAGE_IMPORT_BY_NAME的结构体
【7BE0】
看下IAT(00 A0 01 00->0001A000–>【7800】)
发现最高位也都是0,所以,也是名称导入的,另外,还可以发现,这个位置的值,和INT的值是一样的,因此,不再赘述了
这里的kernel32.dll里面有25个函数