• PE文件详解(六)


    这篇文章转载自小甲鱼的PE文件详解系列原文传送门
    之前简单提了一下节表和数据目录表,那么他们有什么区别?
    其实这些东西都是人为规定的,一个数据在文件中或者在内存中的位置基本是固定的,通过数据目录表进行索引和通过节表进行索引都是可以找到的,也可以这么说,同一个数据在节表和数据目录表中都有一份索引值,那么这两个表有什么区别?一般将具有相同属性的值放到同一个节区中,这也就是说同一个节区的值只是保护属性相同,但是他们的用途不一定是一样的,但是在同一数据目录表中的数据的作用是相同的,比如输入函数表中只会保存输入函数的相关信息,输出函数表中只会保存输出函数的信息,而输入输出函数在PE文件中可能都位于.text这个节中。

    输入函数表

    输入函数:一般将那些在本程序中调用,但是它的代码不在本程序中的函数称为输入函数,输入函数一般都在另外一个独立的dll中。
    在之前谈到PE头的时候说到,在PE头中有一个结构是数据目录表,它的结构如下:

    IMAGE_DATA_DIRECTORY STRUCT
          VirtualAddress    DWORD       ?   ; 数据的起始RVA
          isize             DWORD       ?   ; 数据块的长度
    IMAGE_DATA_DIRECTORY ENDS

    这个结构大小为8,相对于PE文件头的偏移为0x78。
    在PE文件中,通过一个数组来保存多个数据目录表的信息,而输入函数表则是这个数组的第二个元素。
    而输入表是以一个 IMAGE_IMPORT_DESCRIPTOR(简称IID) 的数组开始。
    每个被 PE文件链接进来的 DLL文件都分别对应一个 IID数组结构。
    在这个 IID数组中,并没有指出有多少个项(就是没有明确指明有多少个链接文件),但它最后是以一个全为NULL(0) 的 IID 作为结束的标志。

    IMAGE_IMPORT_DESCRIPTOR

    IMAGE_IMPORT_DESCRIPTOR STRUCT 
        union 
            Characteristics           DWORD   ? 
            OriginalFirstThunk        DWORD   ? 
        ends 
        TimeDateStamp                 DWORD   ? 
        ForwarderChain                DWORD   ? 
        Name                          DWORD   ? 
        FirstThunk                    DWORD   ?
    IMAGE_IMPORT_DESCRIPTOR ENDS

    OriginalFirstThunk

    它指向first thunk,IMAGE_THUNK_DATA,该 thunk 拥有 Hint 和 Function name 的地址。

    TimeDateStamp

    该字段可以忽略。如果那里有绑定的话它包含时间/数据戳(time/data stamp)。如果它是0,就没有绑定在被导入的DLL中发生。
    在最近,它被设置为0xFFFFFFFF以表示绑定发生。

    ForwarderChain

    一般情况下我们也可以忽略该字段。在老版的绑定中,它引用API的第一个forwarder chain(传递器链表)。
    它可被设置为0xFFFFFFFF以代表没有forwarder。

    Name

    它表示DLL 名称的相对虚地址(译注:相对一个用null作为结束符的ASCII字符串的一个RVA,该字符串是该导入DLL文件的名称。
    如:KERNEL32.DLL)。

    FirstThunk

    它包含由IMAGE_THUNK_DATA定义的 first thunk数组的虚地址,通过loader用函数虚地址初始化thunk。
    在Orignal First Thunk缺席下,它指向first thunk:Hints和The Function names的thunks。
    这个OriginalFirstThunk 和 FirstThunk明显是亲家,两家伙首先名字就差不多哈。那他们有什么不可告人的秘密呢?
    这里写图片描述

    IMAGE_THUNK_DATA STRUC
        union u1
            ForwarderString      DWORD  ?        ; 指向一个转向者字符串的RVA
            Function             DWORD  ?        ; 被输入的函数的内存地址
            Ordinal              DWORD  ?        ; 被输入的API 的序数值
            AddressOfData        DWORD  ?        ; 指向 IMAGE_IMPORT_BY_NAME
        ends
    IMAGE_THUNK_DATA ENDS

    我们可以看出由于是union结构,所以IMAGE_THUNK_DATA 事实上是4个字节大小。
    这个共用体是怎么使用的呢:
    当 IMAGE_THUNK_DATA 值的最高位为 1时,表示函数以序号方式输入,这时候低 31位被看作一个函数序号。
    当 IMAGE_THUNK_DATA 值的最高位为 0时,表示函数以字符串类型的函数名方式输入,这时双字的值是一个 RVA,指向一个 IMAGE_IMPORT_BY_NAME 结构。
    接下来说明IMAGE_IMPORT_BY_NAME 结构:

    IMAGE_IMPORT_BY_NAME STRUCT
        Hint      WORD      ? 
        Name      BYTE      ?
    IMAGE_IMPORT_BY_NAME ENDS

    结构中的 Hint 字段也表示函数的序号,不过这个字段是可选的,有些编译器总是将它设置为 0。
    Name 字段定义了导入函数的名称字符串,这是一个以 0 为结尾的字符串。

    输入函数表的加载

    从上面的图上来看,OriginalFirstThunk与FirstThunk指向的是同一个数据结构,在PE文件中既可以通过OriginalFirstThunk来找到函数名,也可以通过FirstThunk来找到函数名,为什么会出现两个指针指向同一个数据结构的现象呢,其实这个与PE文件的加载有关
    第一个数组(由 OriginalFirstThunk 所指向)是单独的一项,而且不能被改写,我们前边称为 INT。
    第二个数组(由 FirstThunk 所指向)事实上是由 PE 装载器重写的。
    PE 装载器首先搜索 OriginalFirstThunk ,找到之后加载程序迭代搜索数组中的每个指针,找到每个 IMAGE_IMPORT_BY_NAME 结构所指向的输入函数的地址,然后加载器用函数真正入口地址来替代由 FirstThunk 数组中的一个入口,也就是说此时的FirstThunk 不在指向这个INAGE_IMPORT_BY_NAME结构,而是真实的函数的RVA。因此我们称为输入地址表(IAT)。
    所以,当我们的 PE 文件装载内存后准备执行时,刚刚的图就会转化为下图:
    这里写图片描述

    实验操作

    我们来编译一个具体的程序,源代码如下:

    #include <windows.h>
    
    int WINAPI WinMain( HINSTANCE hInstance, 
                       HINSTANCE hPrevInstance, 
                       PSTR szCmdLine, 
                       int iCmdShow
                       )
    {
          MessageBox( NULL, TEXT("Hello, welcome to Fishc.com!"), 
                      TEXT("Welcome!"), MB_OKCANCEL | MB_OK
                      );
    
          return 0;
    }

    这个程序就是弹出一个MessageBox,通过W32Dasm静态反汇编发现MessageBox函数所在地址应该在0x0042A2AC
    这里写图片描述

    在数据目录表中根据OriginalFirstThunk 项获取函数名称

    用UE打开这个PE文件,发现输入函数表的RVA = 0x0002A000
    这里写图片描述
    在节表中查询发现它是在.idata这个节中
    这里写图片描述
    通过之前说的公式,可以得到,这个RVA在文件中的偏移地址为0x0002A000 - 0x0002A000 + 0x00028000 = 0x00028000读取在这个位置的信息发现,OriginalFirstThunk = 0x0002A15C,这个偏移,发现它仍然在这个节中,通过上述公式计算得出,他在文件中的偏移地址为:0x0002815C
    这里写图片描述
    从这个位置得到的值来看,它的最高值为0,也就是IMAGE_THUNK_DATA保存的是函数名的字符串,字符串的RVA为0x0002A2DC,通过计算得到它在文件中的偏移为:0x000282DC.
    这里写图片描述
    从图上可以看出这个地址所对应的值正好是函数的名称MessgeBoxA

    通过FirstThunk成员找到函数名称

    首先根据PE文件的内容,可以知道,输入函数表在PE文件的偏移为0x00028000,而根据这个结构来看,FirstThunk在x00028000 + 16 = 0x00028010的位置,在这位置,我们发现它里面的值为0x0002A2AC
    这里写图片描述
    计算得到在磁盘中的偏移为0x000282AC,在PE文件中这个值为0x0002A2DC,它的最高位仍然为0,也就是说这个地址保存的内容为函数名称。
    另外我们发现这个值与之前用OriginalFirstThunk 寻址到的函数名称所在RVA一样,也就是说到此成功找到函数名称

    查找函数在内存的偏移地址

    根据上面所说的内容,只有当这个PE文件被加载到内存中,PE加载器才会将IMAGE_IMPORT_BY_NAME
    结构中的值替换为对应函数的地址,所以要查找函数的地址就需要先将PE文件加载到内存,然后再将内存中的数据抓取下来,最后再来分析得出这个函数的偏移地址。
    其实这个工作可以由lordPE工具来帮忙完成。首先是启动程序,然后打开lordPE,找到程序的进程,然后选择dump full抓取全部即可
    这里写图片描述
    这样会生成一个dump文件,分析这个文件,就可以得出相应的内容:
    由于这个是内存镜像的拷贝,所以在这在内存中的RVA就是在文件中的偏移。
    首先得到导入表的偏移为0x0002a000,这个值里面存储的值为0x0002A15C,这个值是OriginalFirstThunk的值,通过这个值找到对应的IMAGE_THUNK_DATA地址:0x0002A2DC
    我们发现这个值得高地址为是0,那么它所指向的应该就是函数名称,我们寻址到这个地址,发现它正好是函数名称
    这里写图片描述
    接下来,再来解析函数地址,在0x0002a010中找到对应的FirstThunk值,这个值为0x0002A2AC,它是指向一个IMAGE_THUNK_DATA结构,在这个地址处,发现它的值为0x77D507EA,这个值的最高位为1,所以它对应的是一个函数地址,它的低32位是一个函数编号,此时0x0002A2AC指向的不在是一个IMAGE_IMPORT_BY_NAME结构,而是函数地址的偏移,而这个程序是由VC6.0编译而成,VC6默认的加载地址为0x00400000,所以基址 + 偏移地址就是函数的正确地址,也就是0x0042A2AC,与之前用静态反汇编得到的值相同

  • 相关阅读:
    关于const_cast
    TinyXPath与TinyXML
    C++的异常
    如何让对象只能处于堆中
    股票的委托价、成交价与成本价的关系
    static_cast dynamic_cast reinterpret_cast const_cast
    C++ MD5,SHA1源码下载和简单调用
    C++中char[]转string
    HttpWorker
    winnet winhttp
  • 原文地址:https://www.cnblogs.com/lanuage/p/7725699.html
Copyright © 2020-2023  润新知