zoukankan      html  css  js  c++  java
  • 读取PE文件的导入表

        在上一篇文章里,我使用一个 TreeList 控件,展示了 PE 文件的内容。在那里可充分了解PE的文件头的信息,但是对 section(备注:常见译文为节,段,块)的一些信息我们还没有涉及。比如全局变量等数据,代码,资源,导入表等信息都位于相应的 section 中,有些 section 通常具有特定的名字,例如资源通常位于 .rsrc,代码通常位于 .text,导入表通常位于 .idata 段,等等。文本讲述的是把一个PE文件的导入表打印出来。我注意到 MS 提供了一个比较有用的函数,ImageRvaToVa,我们稍后主要借助这个函数去从RVA定位我们的目标数据。

        在开始之前,我们先做一些概念的定义和说明。

        Image: PE格式镜像文件,这通常就是我们的exe,dll文件。

        下面我们定义一些地址相关的概念,因为PE文件位于磁盘上,同时文件又可以被映射到虚拟内存中,在运行PE文件时它也被系统的Loader加载到内存中。所以这里就有了三个空间,如果我们不做一个清楚的说明,在后面我们很容易混淆。

        (1)磁盘空间:这里我们使用的地址叫做文件地址(距离文件头部的偏移)。在PE头的相关属性名称中,文件中的数据称为原始数据 (rawData),文件中的数据使用的对齐称为 FileAlignment。

        (2)虚拟内存空间:在这里的地址称为虚拟地址(Virtual Address)。同时PE文件的数据装载/映射到内存中后又分以下两种情况:

          (a)PE文件的内存视图,即PE文件被映射到内存(MapViewOfFile):

          BaseAddress:内存映射文件的基础地址,从这里看过去,就和从编辑器打开看到的文件内容完全一致。

          内存映射通常是处理大文件的一种有效方式。映射后我们在内存中看到的内容就是磁盘文件的一个视图。

          因此我们吧PE文件映射到内存以后,通过某个数据的RVA,调用 ImageRvaToVa 可得到某个数据的VA,再减去映射文件的起始地址,就是文件地址。

          (b) 进程空间,即在被调度之前,被loader装载到内存的时刻(例如双击执行一个exe)。

          这里和前者的视图方式不同,属于一种地址映射关系,文件中的节内容根据NT文件头的信息被加载到进程空间的相应位置。

          ImageBase:映射到进程空间的基础地址。

          RVA:相对ImageOfBase的偏移。它加上ImageBase就是进程空间的VA。

          在PE文件中的DataEntry,Section表中的VirutalAddress基本都属于RVA。

        下面介绍以下这个函数:ImageRvaToVa:(注意这个函数要求XP和win2000系统以上,在VC6自带的SDK中没有。。。)

        PVOID ImageRvaToVa(
            PIMAGE_NT_HEADERS NtHeaders,
            PVOID Base,
            ULONG Rva,
            PIMAGE_SECTION_HEADER* LastRvaSection
          );

        这个函数在(2).(a)即内存映射文件后使用,它把RVA根据NT头的信息,换算成内存映射文件中的实际VA。看起来不是在进程空间使用的,因为在进程空间中,ImageBase + Rva 就是VA了,装载后NT头等信息也不再重要了。最后一个参数是可选的,或许是因为如果调用方主动提供,此 API 可提高一定效率(可以直接在节中的地址信息去判断)。第二个参数也可以提供一个假的地址给这个函数,这个函数也能计算。即这个函数不去校验Base是否是一个有效地址,因此实际上我们可以读取出NtHeader的信息后,用一个假地址传给这个函数,再把结果减去这个假地址,即可换算出文件地址。

        好了,下面介绍下导入表的定位,这方面的资料和文章在看雪论坛的文集里面有很多。我再这里只做一个比较简洁的介绍。导入表本质上就是位于某个节中的一些数据,这些数据主要是一些C字符串(以0字符为结尾的dll和函数名称)以及一些指针(RVA地址),所以我们主要是需要了解如何定位到导入表,从而打印出导入表的信息。

        首先导入表的RVA地址,就在optional Header的DataDirectory的第二个元素中。通过它我们定位到导入表。

        导入表类似一个二级索引。一级是一个模块目录(IMAGE_IMPORT_DESCRIPTOR数组,这里把目录理解为一个以全0字节为结束的数组),它的每个元素代表了一个DLL。二级是导入地址表IAT(即 IMAGE_THUNK_DATA 数组,一个指针数组),每个元素指向一个 IMAGE_IMPORT_BY_NAME结构(该结构含有一个函数序号和一个函数名称字符串)。

        总结一下,我们的定位过程:

        (1)通过 NtHeaders.OptionalHeader.DataDirectory[1].VirtualBase --> 定位到导入表(IID Table)。

        (2)遍历每个 IID,直到遇到全0为止。

            通过 IID.Name -> 定位到 DLL 名字。

            通过 IID.OriginalFirstThunk 或者 FirstThunk -> 定位到IAT ( image_thunk_data32[] );

              遍历指针目录,知道遇到NULL为止。

                通过 thunk_data.AddressOfData -> 定位到一个 IMAGE_IMPORT_BY_NAME 的地址,再根据它寻址到真正的函数序号和函数名称。

        为什么存在二级指针呢,这是很容易解释的。所以我们需要一个DLL目录,由于DLL名称的长度和函数个数不固定,所以向下扩展了一级,而每个函数的函数名称又是不固定的,所以又要向下扩展一级,这样要找到真正的函数名称必须经过这样两级定向。

        因此这个二级索引的导入表就是这样的定位方式(每个数组都是一个高地址方向半开口的样子, C字符串也是这样的字符数组),如下图所示(注意每个数组的元素的size是固定的,但由于数组是半开口,所以数组本身属于size不固定):

          

        

        特点就是,每次遇到长度无法预测的成员,就用指针把它从元素中扩展出去(用一个指针指向它),这样我们就保证每个数组的元素都是固定的size,这样它才能成为线性表结构(满足用指针的加减或者[]操作符进行元素读取)。例如DLL的名称是可变长度的,因此它被扔到元素定义的外面去,在元素中保留为一个指针。每个DLL的函数目录也是可变长度的(函数个数是不确定的),因此它在元素中也是一个指针。而函数目录中函数信息又被扔出去,用指针指向它。

        下面看一下相关代码,很短,通过上面的讲解,下面的代码就很明了,很简单了:

    code_print_import_table
    // ImageRvaToVa.cpp : Defines the entry point for the console application.
    //

    #include 
    "stdafx.h"
    #include 
    <stdio.h>
    #include 
    <windows.h>
    #include 
    <Dbghelp.h> //ImageRvaToVa

    int main(int argc, char* argv[])
    {
        
    int i, j;
        HANDLE hFile 
    = CreateFile(
            
    "E:\\RfCard.exe"//PE文件名
            GENERIC_READ, 
            FILE_SHARE_READ,
            NULL,
            OPEN_EXISTING,
            FILE_ATTRIBUTE_NORMAL,
            NULL);

        
    if(hFile == INVALID_HANDLE_VALUE)
        {
            printf(
    "Create File Failed.\n");
            
    return 0;
        }

        HANDLE hFileMapping 
    = CreateFileMapping(hFile, NULL, PAGE_READONLY,    00, NULL);

        
    if (hFileMapping == NULL || hFileMapping == INVALID_HANDLE_VALUE) 
        { 
            printf(
    "Could not create file mapping object (%d).\n", GetLastError());
            
    return 0;
        }

        LPBYTE lpBaseAddress 
    = (LPBYTE)MapViewOfFile(hFileMapping,   // handle to map object
            FILE_MAP_READ, 000);
     
        
    if (lpBaseAddress == NULL) 
        { 
            printf(
    "Could not map view of file (%d).\n", GetLastError()); 
            
    return 0;
        }

        PIMAGE_DOS_HEADER pDosHeader 
    = (PIMAGE_DOS_HEADER)lpBaseAddress;
        PIMAGE_NT_HEADERS pNtHeaders 
    = (PIMAGE_NT_HEADERS)(lpBaseAddress + pDosHeader->e_lfanew);
        
        
    //导入表的rva:0x2a000;
        DWORD Rva_import_table = pNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress;

        
    if(Rva_import_table == 0)
        {
            printf(
    "no import table!");
            
    goto UNMAP_AND_EXIT;
        }

        
    //这个虽然是内存地址,但是减去文件开头的地址,就是文件地址了
        
    //这个地址可以直接从里面读取你想要的东西了
        PIMAGE_IMPORT_DESCRIPTOR pImportTable = (PIMAGE_IMPORT_DESCRIPTOR)ImageRvaToVa(
            pNtHeaders, 
            lpBaseAddress, 
            Rva_import_table,
            NULL
            );

        
    //减去内存映射的首地址,就是文件地址了。。(很简单吧)
        printf("FileAddress Of ImportTable: %p\n", ((DWORD)pImportTable - (DWORD)lpBaseAddress));

        
    //现在来到了导入表的面前:IMAGE_IMPORT_DESCRIPTOR 数组(以0元素为终止)
        
    //定义表示数组结尾的null元素!
        IMAGE_IMPORT_DESCRIPTOR null_iid;
        IMAGE_THUNK_DATA null_thunk;
        memset(
    &null_iid, 0sizeof(null_iid));
        memset(
    &null_thunk, 0sizeof(null_thunk));

        
    //每个元素代表了一个引入的DLL。
        for(i=0; memcmp(pImportTable + i, &null_iid, sizeof(null_iid))!=0; i++)
        {
            
    //LPCSTR: 就是 const char*
            LPCSTR szDllName = (LPCSTR)ImageRvaToVa(
                pNtHeaders, lpBaseAddress, 
                pImportTable[i].Name, 
    //DLL名称的RVA
                NULL);

            
    //拿到了DLL的名字
            printf("-----------------------------------------\n");
            printf(
    "[%d]: %s\n", i, szDllName);
            printf(
    "-----------------------------------------\n");

            
    //现在去看看从该DLL中引入了哪些函数
            
    //我们来到该DLL的 IMAGE_TRUNK_DATA 数组(IAT:导入地址表)前面
            PIMAGE_THUNK_DATA32 pThunk = (PIMAGE_THUNK_DATA32)ImageRvaToVa(
                pNtHeaders, lpBaseAddress,
                pImportTable[i].OriginalFirstThunk, //【注意】这里使用的是OriginalFirstThunk
                NULL);

            
    for(j=0; memcmp(pThunk+j, &null_thunk, sizeof(null_thunk))!=0; j++)
            {
                
    //这里通过RVA的最高位判断函数的导入方式,
                
    //如果最高位为1,按序号导入,否则按名称导入
                if(pThunk[j].u1.AddressOfData & IMAGE_ORDINAL_FLAG32)
                {
                    printf(
    "\t [%d] \t %ld \t 按序号导入\n", j, pThunk[j].u1.AddressOfData & 0xffff);
                }
                
    else
                {
                    
    //按名称导入,我们再次定向到函数序号和名称
                    
    //注意其地址不能直接用,因为仍然是RVA!
                    PIMAGE_IMPORT_BY_NAME pFuncName = (PIMAGE_IMPORT_BY_NAME)ImageRvaToVa(
                        pNtHeaders,    lpBaseAddress,
                        pThunk[j].u1.AddressOfData,
                        NULL);
                    
                    printf(
    "\t [%d] \t %ld \t %s\n", j, pFuncName->Hint, pFuncName->Name);
                }
            }
        }


    UNMAP_AND_EXIT:

        
    //关闭文件,句柄。。
        UnmapViewOfFile(lpBaseAddress);
        CloseHandle(hFileMapping);
        CloseHandle(hFile);
        getchar();
        
    return 0;
    }

         【注意】在上面的代码中我标示为高亮红色字体的部分,我使用的是 OriginalFirstThunk ,对于没有事先绑定的PE文件来说, OriginalFirstThunk 和 FirstThunk 是并行的内容相同的数组。但是对于已经经过绑定的PE文件来说, FirstThunk 数组中的元素会被设置成真正的函数地址(VA)!因此如果这时用 FirstThunk 数组尝试获取函数名称是得不到的。所以上面的代码应该使用 OriginalFirstThunk ,这样无论对于绑定还是未绑定的PE文件,都能够定向到相应的函数名称。

        【注意】:

        (1)请在项目属性中的链接选项中,添加 Dbghelp.lib;

        (2)上面对PE文件的名称采用了硬编码,如果为了灵活期间,可以从命令行获取PE文件的路径。

        下面是我用某个PE文件所产生的输出:

    output_result
    FileAddress Of ImportTable: 00028000
    -----------------------------------------
    [
    0]: USER32.dll
    -----------------------------------------
             [
    0]     198     EndDialog
             [
    1]     158     DialogBoxParamA
    -----------------------------------------
    [
    1]: KERNEL32.dll
    -----------------------------------------
             [
    0]     265     GetEnvironmentVariableA
             [
    1]     27      CloseHandle
             [
    2]     170     FlushFileBuffers
             [
    3]     294     GetModuleHandleA
             [
    4]     336     GetStartupInfoA
             [
    5]     202     GetCommandLineA
             [
    6]     372     GetVersion
             [
    7]     125     ExitProcess
             [
    8]     81      DebugBreak
             [
    9]     338     GetStdHandle
             [
    10]    735     WriteFile
             [
    11]    429     InterlockedDecrement
             [
    12]    501     OutputDebugStringA
             [
    13]    318     GetProcAddress
             [
    14]    450     LoadLibraryA
             [
    15]    432     InterlockedIncrement
             [
    16]    292     GetModuleFileNameA
             [
    17]    670     TerminateProcess
             [
    18]    247     GetCurrentProcess
             [
    19]    685     UnhandledExceptionFilter
             [
    20]    178     FreeEnvironmentStringsA
             [
    21]    179     FreeEnvironmentStringsW
             [
    22]    722     WideCharToMultiByte
             [
    23]    262     GetEnvironmentStrings
             [
    24]    264     GetEnvironmentStringsW
             [
    25]    621     SetHandleCount
             [
    26]    277     GetFileType
             [
    27]    373     GetVersionExA
             [
    28]    413     HeapDestroy
             [
    29]    411     HeapCreate
             [
    30]    415     HeapFree
             [
    31]    703     VirtualFree
             [
    32]    559     RtlUnwind
             [
    33]    282     GetLastError
             [
    34]    577     SetConsoleCtrlHandler
             [
    35]    440     IsBadWritePtr
             [
    36]    437     IsBadReadPtr
             [
    37]    423     HeapValidate
             [
    38]    191     GetCPInfo
             [
    39]    185     GetACP
             [
    40]    305     GetOEMCP
             [
    41]    409     HeapAlloc
             [
    42]    699     VirtualAlloc
             [
    43]    418     HeapReAlloc
             [
    44]    484     MultiByteToWideChar
             [
    45]    447     LCMapStringA
             [
    46]    448     LCMapStringW
             [
    47]    339     GetStringTypeA
             [
    48]    342     GetStringTypeW
             [
    49]    618     SetFilePointer
             [
    50]    636     SetStdHandle

        到这里,我们基本算了解了import table。不过还需要额外解释一下的是,在iid中含有两个指向thunk数组(即IAT:导入地址表)的指针(orignal first thunk 和 first thunk),两个指针指向不同的地址,但这两个thunk数组的元素值是相同的,最终指向的是同一个地址(函数序号和名称元素)。也就是说,thunk数组在PE文件中有两份,他们的地址不同,但最终指向完全相同。画出图形的话是类似下面这样(注意最终指向相同,4个指针画出来类似一个菱形):

        IID.OrignalFirstThunk---> thunk1[] -------\

        ---------------------------------------------   > IMAGE_IMPORT_BY_NAME ( hint, name [] )

        IID.FirstThunk-----------> thunk2[] -------/

        即在上面的代码里,用 iid 的无论哪一个thunk指针最终得到的都是相同的结果。PE文件中OrignalFirstThunk是一个对thunk表的原始备份,它始终是不变的。而FirstThunk在系统装载PE以后,将会修改,把他们替换成函数的真正地址(即进行绑定),这时它们就不再是指向函数序号和名称了,而是真正的函数地址(映射到进程空间之内的),你对某个DLL函数的调用在代码中本质上是调用一个存根(因为编译器不知道导入函数的实际地址),在存根处跳转到 FirstThunk 数组指明的实际函数地址。程序一旦运行起来,导入表将不再那么关键了,重要的是实际IAT(FirstThunk)。这时OrignalFirstThunk 数组就成为了一个被替换前的原始备份,当需要重新绑定时(例如DLL版本不对)需要用到它,这是“Orignal” 的含义。First的含义是,指向的是IAT的第一个元素(thunk)。Thunk可能是新造出来的词,没有确切的解释,大概是是被加载后被系统替换掉了值,有真真假假,虚虚实实的语义,同时也有一种说法是有“预先想到”的含义(备注:在ATL中也有Thunk这个词)。

        现在我们回头在看下IID和Thunk的定义,可以很好的理解那些注释,IID的OriginalFirstThunk指向的是未绑定之前的IAT(导入地址表),这个IAT表示的是应该导入该DLL的哪些名称(序号)的函数。而FirstThunk在绑定(被替换成实际函数地址)后指向的实际的函数地址。所以Thunk里面是一个union,里面的 AddressOfData 指是绑定之前它指向函数的hint和名称,Function 指的得是绑定后它就成了实际函数地址。

    typedef_iid_and_thunk 

    typedef 
    struct _IMAGE_IMPORT_DESCRIPTOR {
        union {
            DWORD   Characteristics;            
    // 0 for terminating null import descriptor
            DWORD   OriginalFirstThunk;         // RVA to original unbound IAT (PIMAGE_THUNK_DATA)
        };
        DWORD   TimeDateStamp;                  
    // 0 if not bound,
                                                
    // -1 if bound, and real date\time stamp
                                                
    //     in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)
                                                
    // O.W. date/time stamp of DLL bound to (Old BIND)

        DWORD   ForwarderChain;                 
    // -1 if no forwarders
        DWORD   Name;
        DWORD   FirstThunk;                     
    // RVA to IAT (if bound this IAT has actual addresses)
    } IMAGE_IMPORT_DESCRIPTOR;
    typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED 
    *PIMAGE_IMPORT_DESCRIPTOR;


    typedef 
    struct _IMAGE_THUNK_DATA32 {
        union {
            DWORD ForwarderString;      
    // PBYTE 
            DWORD Function;             // PDWORD
            DWORD Ordinal;
            DWORD AddressOfData;        
    // PIMAGE_IMPORT_BY_NAME
        } u1;
    } IMAGE_THUNK_DATA32;

        假如引用的 DLL 是确定的,那么在加载时的函数绑定工作就可以事先完成,在加载时只需要核对以下DLL的时间戳是否吻合(如果不能吻合则和没有绑定一样处理,再次人工绑定),这样就可以节省程序加载的时间,随操作系统提供的一些工具比如记事本,计算器等工具都经过了事先绑定。通过 VC 提供的 Dependency 工具可以查看到你链接的 DLL 是否经过了事先绑定。如果没有绑定,那些导入的函数的地址将显示未绑定(not bound)。借助 IDE 提供的 Bind 工具可以进行预先绑定,从而减少你的程序启动时间。或者通过编程方式,用 BindImageEx 函数去绑定一个 Image 文件。

        例如,如果你开发好了应用程序,在打包安装程序之前,可以用这两个命令行工具进行优化,从而加快程序启动的速度。

        (1)优化DLL的首选ImageBase:

        ReBase.EXE -b 10000000 *.dll

        (2)事先绑定DLL:(在客户端上,系统DLL未必是有效的绑定,但是对于你自己提供的DLL,该绑定一定有效)

        Bind.EXE -o -u -v 你的可执行文件.exe

     

        经过 Rebase 和 Bind 以后,可以提高你的程序的启动速度。系统提供的DLL都经过了Rebase,这样它们在被加载时几乎总能成功加载到首选的ImageBase。系统提供的工具也基本都经过事先绑定。

        IID中的ForwardChain,是函数转发器,即一个DLL文件可以导出一个假定的函数,这个函数是实际上位于另一个DLL中的函数,这样在逻辑上就成为一个“链式结构”,系统在加载时需要分析转发器,把引用的实际DLL映射到进程空间。在操作系统的 Kernel32.dll 中可以看到转发器,比如 Kernel32.dll 中的 HeapAlloc 被转发到 NTDLL 中的 RtlAllocateHeap。但转发器在实际应用中则比较少见。如果要指定一个转发器,需要在代码中使用形式如下面的语句:

        #pragma comment ( linker, "/export:FuncName = DllName.SomeOtherFuncName" )

        【参考资料】

        关于PE文件相关的知识,主要参考:

        (1)《看雪论坛五周年精华》(1~9)。

        关于函数转发器部分,主要参考:

        (2)《Window核心编程》第20章:DLL的高级操作技术。

        使用 Rebase 命令行工具重设 DLL 的首选ImageBase:

        (3)ms-help://MS.VSCC.v80/MS.MSDN.v80/MS.WIN32COM.v10.en/tools/tools/rebase.htm

        使用 Bind 命令工具进行预先绑定:

        (4)ms-help://MS.VSCC.v80/MS.MSDN.v80/MS.WIN32COM.v10.en/tools/tools/bind.htm

        【修订历史】

        (1)把文中范例代码中的 FirstThunk 替换为 OriginalFirstThunk,并给出原因。2010-12-28 0:18:53。

  • 相关阅读:
    摘录:测试各阶段的意义
    EF Code First Migrations数据库迁移
    MVC5中Model层开发数据注解
    Easyui 中的placeholder属性
    在VS2012后的版本中做数据报表时,提示尚未指定报表“Report1”的报表定义
    MVC 中与链接相关的辅助方法(Html.ActionLink,Html.RouteLink , Url.Action , Url.Content ,Url.RouteUrl)
    Asp.net MVC中Html.Partial, RenderPartial, Action,RenderAction 区别和用法
    C# 常用对象的的修饰符
    C# 静态构造函数
    1、Singleton 单件(创建模式)
  • 原文地址:https://www.cnblogs.com/hoodlum1980/p/1821778.html
Copyright © 2011-2022 走看看