导出导入表

 

导出表
当一个EXE或DLL导出函数或变量时,其它EXE或DLL就可以使用这些导出的函数或变量。为了简单起见,我把导出的函数和导出的变量统称为“符号”。 当导出一些符号时,最起码导出符号的地址需要能够以一种已定义好的方式被获取。每个导出的符号都有一个与之关联的序数,它可以用来查找这个符号。同时,几 乎总有一个ASCII码格式的字符串名称与这个导出的符号关联。一般来说,导出的符号名与源文件中的符号名是一样的,尽管它们可以被修改的不一样。
通常,当可执行文件导入符号时,它使用的是符号的名称而不是它的序号。但是当通过名称导入时,系统仅使用这个名称去查找所需符号对应的导出序数,然后根据 这个序数值去获取相应的地址。如果先使用的是序数值的话查找过程会快一点。通过名称导出和导入只是为了让程序员使用方便罢了。
在.DEF文件中的Exports节中使用ORDINAL关键字可以告诉链接器创建一个导入库,这个导入库强制函数只能通过序数导入而不能通过名称导入。
我首先介绍IMAGE_EXPORT_DIRECTORY结构,如下表所示:
大小 域 描述
DWORD Characteristics 导出标志。当前未定义任何值。
DWORD TimeDateStamp 导出数据的创建时间。这个域的定义与IMAGE_NT_HEADERS.FileHeader.TimeDateStamp相同(从GMT时间1970年1月1日00:00以来的总秒数)。
WORD MajorVersion 导出数据的主版本号。未用,设置为0。
WORD MinorVersion 导出数据的次版本号。未用,设置为0。
DWORD Name 与导出符号相关的DLL的名称ASCII字符串的RVA(例如KERNEL32.DLL)。
DWORD Base 这个域包含了这个可执行文件的导出符号所使用的序数值的起始值。通常情况下这个值为1,但并不总是这样。当通过序数查找导出符号时,将序数值减去这个域的 值就得到了这个导出符号在导出地址表(Export Address Table ,EAT)中的索引。
DWORD NumberOfFunctions EAT中的元素数。注意EAT中的某些元素可能为0,这表明没有
代码/数据使用那个序数值导出。
DWORD NumberOfNames 导出名称表(Export Names Table,ENT)中的元素数。这个域的值总是小于或等于NumberOfFunctions域的值。当某些符号仅使用序数导出时,它就小于那个域的 值。如果导出序数之间有间隔,它同样也小于那个域的值。这个域的值也是导出序数表的大小(见下文)。
DWORD AddressOfFunctions EAT的RVA。EAT中的每个元素都是一个RVA。其中每个非0的RVA都对应一个导出符号。
DWORD AddressOfNames ENT的RVA。ENT中的每个元素都是一个ASCII码字符串的RVA。其中的每个ASCII码字符串都对应一个由名称导出的符号。这些字符串是按一定 顺序排列的。这就使得加载器在查找导出符号时可以进行二进制搜索。名称字符串的排序是按二进制(与C++运行时库函数strcmp类似),而不是与位置相 关的字母表顺序。
DWORD AddressOfNameOrdinals 导出序号表的RVA。这个表是一个WORD类型的数组。它将ENT中的索引映射到导出地址表中相应的元素上。

导出目录(Export Directory)指向三个数组和一个ASCII码字符串表。其中只有导出地址表是必需的,它是一个由指向导出函数的指针组成的数组。导出序数是这个数组的索引(见下图)。
让我们通过例子来看一下导出表的工作原理。下图显示了KERNEL32.DLL导出表的部分内容:
exports table:
Name:            KERNEL32.dll
Characteristics: 00000000
TimeDateStamp:   3B7DDFD8 -> Fri Aug 17 23:24:08 2001
Version:         0.00
Ordinal base:    00000001
# of functions: 000003A0
# of Names:      000003A0

Entry Pt Ordn Name
00012ADA     1 ActivateActCtx
000082C2     2 AddAtomA
•••remainder of exports omitted

假设你调用GetProcAddress来获取KERNEL32中的AddAtomA这个API的地址。这时系统开始查找KERNEL32的 IMAGE_EXPORT_DIRECTORY结构。它从那里获取了导出名称表的起始地址,知道了在这个数组中有0x3A0个元素,它通过二进制搜索来查 找字符串“AddAtomA”。
假设加载器发现AddAtomA是这个数组中的第二个元素。然后它从导出序数表(Export Ordinal Table)中读取相应的第二个值。这个值就是AddAtomA的导出序数。将这个导出序数作为EAT的索引(加上Base域的值),它最终获取 AddAtomA的相对虚拟地址(RVA)是0x82C2。将此值与KERNEL32的加载地址相加就得到了AddAtomA的实际地址。
导出转发
导出表一个特别聪明的地方是它能将一个导出函数转发(Forwarding)到其它DLL。例如在Windows NT®、Windows® 2000和Windows XP中,KERNEL32中的HeapAlloc函数被转发到了NTDLL导出的RtlAllocHeap函数上。转发是在链接时通过.DEF文件中的 EXPORTS节中的一种特殊语法形式来实现的。对于HeapAlloc这个例子,KERNEL32的.DEF文件一定包含下面的内容:
        EXPORTS
        •••
        HeapAlloc = NTDLL.RtlAllocHeap
怎样才能区别转发的函数与正常导出的函数呢?这需要一些技巧。通常EAT中包含的是导出符号的RVA。但是如果这个RVA位于导出表中(通过相应的DataDirectory中的VirtualAddress域和Size域进行判断),那么它就是转发的。

当转发一个符号时,它的RVA很明显不能是当前模块中的代码或数据的地址。实际上,它的RVA指向一个由DLL和转发到的符号名称组成的字符串。在前面的例子中,这个字符串就是NTDLL.RtlAllocHeap。
导入表
与导出函数或变量相反的就是导入它们。为了与前面保持一致,我仍然使用“符号”这个术语来指代导入的函数和变量。
导入数据被保存在IMAGE_IMPORT_DESCRIPTOR结构中。对应着导入表的数据目录项就指向由这个结构组成的数组。每个 IMAGE_IMPORT_DESCRIPTOR结构都与一个导入的可执行文件对应。这个数组的最后一个元素的所有域都被设置为0。下表是这个结构的内 容:
大小 域 描述
DWORD OriginalFirstThunk 这个域的命名太不恰当。它包含导入名称表的RVA。导入名称表是一个IMAGE_THUNK_DATA结构数组。这个域被设置为0表示IMAGE_IMPORT_DESCRIPTOR结构数组的结尾。
DWORD TimeDateStamp 如果可执行文件并未绑定导入的DLL,这个域的值为0。当使用老的绑定类型进行绑定(参考“绑定”一节)时,这个域包含日期/时间戳。当使用新的绑定类型进行绑定时,这个域的值为-1。
DWORD ForwarderChain 这是首个转发的函数的索引。如果没有转发的函数,这个域被设置为-1。它仅用于老的绑定类型,因为那种绑定类型不能很有效地处理转发的函数。
DWORD Name 导入的DLL名称字符串(ASCII码格式)的RVA。
DWORD FirstThunk 导入地址表的RVA。IAT是一个IMAGE_THUNK_DATA结构数组。

每个IMAGE_IMPORT_DESCRIPTOR结构指向两个数组,这两个数组实际上是一样的。它们有好几种叫法,但最常用的名称 是导入地址表(Import Address Table,IAT)和导入名称表(Import Name Talbe,INT)。下图显示的是可执行文件从USER32.DLL中导入一些API时的情况。

    这两个数组的元素均为IMAGE_THUNK_DATA类型的结构,这个结构是一个与指针大小相同的共用体(或者称为联合)。每个 IMAGE_THUNK_DATA结构对应着从可执行文件中导入的一个函数。这两个数组最后都以一个值为0的IMAGE_THUNK_DATA结构作为结 尾。这个共用体(实际是一个DWORD值)可以有如下几种含义:
DWORD ForwarderString;// 转发函数字符串的RVA(见上文)
DWORD Function;       // 导入函数的内存地址
DWORD Ordinal;        // 导入函数的序数
DWORD AddressOfData; // IMAGE_IMPORT_BY_NAME和导入函数名称的RVA(见下文)

IAT中的IMAGE_THUNK_DATA结构的用途可以分为两种。在可执行文件中,它们或者是导入函数的序数,或者是一个 IMAGE_IMPORT_BY_NAME结构的RVA。IMAGE_IMPORT_BY_NAME结构只是一个WORD类型的值,它后面跟着导入函数的 名称字符串。这个WORD类型的值是一个“提示(hint)”,它提示加载器导入函数的序号可能是什么。当加载器加载可执行文件时,它用导入函数的实际地 址来覆盖IAT中的每个元素。这一点是理解下文的关键。我强烈建议你读一读本期杂志中Russell Osterlund的文章——揭开Windows加载器的神秘面纱,这篇文章详细讲述了Windows加载器的行为。

在可执行文件被加载之前,是否存在一种方法能够区分IMAGE_THUNK_DATA结构中到底包含的是导入函数的序数呢,还是 IMAGE_IMPORT_BY_NAME结构的RVA呢?答案在IMAGE_THUNK_DATA结构的最高位。如果它为1,那么低31位(在64位可 执行文件中是低63位)中是导入函数的序数。如果最高位为0,那么IMAGE_THUNK_DATA结构的值就是 IMAGE_IMPORT_BY_NAME结构的RVA。

另一个数组INT,本质上与IAT是一样的。它也是一个IMAGE_THUNK_DATA结构数组。关键的区别在于当加载器将可执行文件加载进内存时,它 并不覆盖INT。为什么对于从DLL中导入的每组API都需要有两个并列的数组呢?答案在于一个称为绑定(binding)的概念。当在绑定过程(后面我 会讲到)中覆盖可执行文件的IAT时,需要以某种方式保存原来的信息。而作为这个信息的副本的INT,正是这个用途。

INT对于可执行文件的加载并不是必需的。但是如果它不存在的话,那么这个可执行文件就不能被绑定。Microsoft链接器总是生成INT,但是长期以来,Borland链接器(TLINK)都不生成它。这样,由Borland链接器生成的可执行文件就不能被绑定。

在早期的Microsoft链接器中,导入节并不是专门针对于链接器的。组成可执行文件导入节的所有数据都来自导入库。你可以对一个导入库文件运行 DUMPBIN或PEDUMP来看一下。你会发现一些节名类似于.idata$3和.idata$4的节。链接器只是简单地遵守它的规则来组合节,所有的 结构和数组就神奇般地各就其位了。几年前Microsoft引进了一种新的导入库格式,这种导入库特别小,以便让链接器能在创建导入数据时更具主动性。

HMODULE hNtdll=GetModuleHandle(TEXT("ntdll.dll"));

PIMAGE_DOS_HEADER mzhead=(PIMAGE_DOS_HEADER)ibase;
if    ((mzhead->e_magic!=IMAGE_DOS_SIGNATURE) ||
   (ibaseDD[mzhead->e_lfanew]!=IMAGE_NT_SIGNATURE))
   return FALSE;
*pfh=(PIMAGE_FILE_HEADER)&ibase[mzhead->e_lfanew];
if (((PIMAGE_NT_HEADERS)*pfh)->Signature!=IMAGE_NT_SIGNATURE)
   return FALSE;
*pfh=(PIMAGE_FILE_HEADER)((PBYTE)*pfh+sizeof(IMAGE_NT_SIGNATURE));
*poh=(PIMAGE_OPTIONAL_HEADER)((PBYTE)*pfh+sizeof(IMAGE_FILE_HEADER));
if ((*poh)->Magic!=IMAGE_NT_OPTIONAL_HDR32_MAGIC)
   return FALSE;
*psh=(PIMAGE_SECTION_HEADER)((PBYTE)*poh+sizeof(IMAGE_OPTIONAL_HEADER));
return TRUE;

ped=(PIMAGE_EXPORT_DIRECTORY)(poh->DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress+(BYTE*)hNtdll);
   arrayOfFunctionNames=(DWORD*)(ped->AddressOfNames+(BYTE*)hNtdll);
   arrayOfFunctionAddresses = (DWORD*)( (BYTE*)hNtdll + ped->AddressOfFunctions);
   //arrayOfFunctionNames = (DWORD*)( (BYTE*)hNtdll + ped->AddressOfNames);
   arrayOfFunctionOrdinals = (WORD*)( (BYTE*)hNtdll + ped->AddressOfNameOrdinals);
   for (int i=0;i<(int)(ped->NumberOfNames);i++)
   {
    char* fun_name= (char*)((BYTE*)hNtdll + arrayOfFunctionNames[i]);
    functionOrdinal = arrayOfFunctionOrdinals[i] + ped->Base - 1;
    functionAddress = (DWORD)( (BYTE*)hNtdll + arrayOfFunctionAddresses[functionOrdinal]);
    if (fun_name[0]=='N'&&fun_name[1]=='t')
    {
     WORD number=*((WORD*)(functionAddress+1));
     if (number>ped->NumberOfNames) continue;
     lstrcpy(ssdt_list[number].fname,fun_name);
     printf(fun_name);
    }
   }

下面是导入表:

PIMAGE_DOS_HEADER imgDosHdr = (PIMAGE_DOS_HEADER)GetModuleHandle(NULL);
        PIMAGE_NT_HEADERS imgNtHdr = (PIMAGE_NT_HEADERS)((PBYTE)imgDosHdr + imgDosHdr->e_lfanew);
        PIMAGE_IMPORT_DESCRIPTOR importDes = (PIMAGE_IMPORT_DESCRIPTOR)((PBYTE)imgDosHdr + imgNtHdr->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);
        //对导入表进行检索
        for (; importDes->Name; importDes++)
        {
                //加载模块列表
                PCHAR modName = (PCHAR)imgDosHdr + importDes->Name;
                if (strnicmp(modName, "kernel32", 8) == 0)
                {
                        //指向一个IMAGE_THUNK_DATA的数据表
                        PIMAGE_THUNK_DATA orgTHunkData = (PIMAGE_THUNK_DATA)((PCHAR)imgDosHdr + importDes->OriginalFirstThunk);
                        PIMAGE_THUNK_DATA imgThunkData = (PIMAGE_THUNK_DATA)((PCHAR)imgDosHdr + importDes->FirstThunk);
                        for (; imgThunkData->u1.Function; imgThunkData++, orgTHunkData++)
                        {
                                //注意是imgThunkData是一个DWORD的联合数据结构
                                PIMAGE_IMPORT_BY_NAME impName = (PIMAGE_IMPORT_BY_NAME)((PCHAR)imgDosHdr + orgTHunkData->u1.AddressOfData);
                                if (strnicmp((PCHAR)impName->Name, "ExitProcess", 11) == 0)
                                {
                                        g_ExitProcess = (proc_ExitProcess)imgThunkData->u1.Function;
                                        *(PULONG)(&imgThunkData->u1.Function) = (ULONG)Hook_ExitProcess;
                                }
                        }
                }
        }

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值