PE文件结构入门——导入表结构


  • 作为刚进入逆向行业不久的新人,也作为刚进入论坛不久的新人,在此分享一篇短篇文章活跃一下~
    该篇文章的主题是Windows PE(x86)文件结构基础的知识点之一,也是PE文件结构中最难最复杂的结构之一,文中所有若有误,欢迎指正。

    64位的PE文件与32位PE文件的区别请移步坛主的文章:
    https://bbs.schnee.moe/topic/55/simpledpack_pe%E7%BB%93%E6%9E%84%E4%B8%8E%E5%A3%B3%E7%9A%84%E5%8E%9F%E7%90%86%E8%AE%B2%E8%A7%A3

    一、 导入表介绍
    简单来说,导入表描述了一个PE文件从外部引用了哪些动态库的导出函数,当然,这其中并不包含通过LoadLibrary显式动态加载的动态库。

    以调用MessageBox为例,我们看一下调用和没有调用该函数的情况下,两个PE文件中导入表内容的区别:
    【不调用MessageBox】
    1.png
    【调用MessageBox】
    2.png
    在代码中调用MessageBox,程序在链接时自动将MessageBox函数所属的USER32.dll及其内部的导出函数——MessageBox加入到导入表中,打开Winhex看导入表内容验证一下:
    3.png
    接下来就以这个程序为样本,深入导入表结构。我们在程序中多调用两个windows API函数——LoadLibrary和CreateFile。

    二、 导入表结构
    现在假设你已经定位到了PE文件中导入表的位置,那么此时你遇到的是一个类似于其它子结构的结构体数组,它的每一个数组元素结构体如下:
    4.png
    一个结构体共占用0x14字节。

    主要成员说明:
    DUMMYUNIONNAME 共用体,4字节,指向一个DLL的INT指针表(INT可称作
    函数名称表)的RVA。
    Name 4字节,指向一个Dll名称的RVA。
    FirstThunk 4字节,是一个DLL对应的IAT的指针(IAT可称作函数地
    址表),也是一个RVA地址。
    这个结构体数组以全0的0x14字节作为数组的结束。

    例子程序中的导入表结构:
    5.png
    每一个IMAGE_IMPORT_DESCRIPTOR对应一个DLL。
    以图中第一个IMAGE_IMPORT_DESCRIPTOR为例
    DUMMYUNIONNAME = 0x1B25C
    Name = 0x1B472
    FirstThunk = 0x1B000
    我们来通过这3个RVA地址分别找到对应的Dll名称、INT、IAT的位置,也就是它们的FA值(文件地址),RVA转换成FA的过程也比较复杂,以后有机会我开一个专门的帖子讲这一块知识原理。
    现在咱们直接用CFF工具帮我们算出FA:
    6.png
    Dll名称的FA是0x8072
    7.png
    INT指针表的FA是0x7E5C
    8.png
    IAT的FA是0x7C00

    首先看一下Dll名称的内容:
    9.png
    说明这个导入表元素对应的DLL是KERNEL32.dll。
    接下来看一下INT的内容,由于定位INT需要通过INT指针表中转,所以先定位到INT指针表后再算一次RVA->FA :
    10.png
    这是INT指针表,是一个数组,以4字节全0的元素作为数组结尾。
    现在取第一个RVA 0x1B454,换算成FA是0x8054,得到函数名称:
    11.png
    注意,这里函数名称是一个结构体,结构体如下:
    12.png
    前2字节Hint没有实际用处,可以忽略,Name是API的名称,是一个柔性数组。

    接下来看一下IAT内容:
    IAT的FA是0x7C00,直接定位到——
    13.png
    IAT的总大小以及每个元素的大小与INT指针表一样,且下标内容一一对应,也就是说,IAT[0]描述的API地址与INT_PTR[0]指向的API名称是一一对应的。
    我们拿IAT[0]的RVA,将它转换成VA和FA——
    14.png
    可以看到,这个RVA 0x1B454实际上就是INT指针表第1个元素的值,它们描述的API都是同一个。
    这里的VA指的是内存中的地址,这个PE文件被作为进程加载到内存之后,这里的VA值将会被填入内存中的IAT,汇编调用这个API时就可以通过IAT描述的VA地址找到要调用的API的位置。
    对导入表结构所涉及的各个数据结构之间的关系做一个图形化总结:
    【通过导入表访问DllName数组】
    15.png
    【通过导入表访问INT】
    16.png
    【通过导入表访问IAT】
    17.png

    三、 操作系统访问导入表的流程
    这里简单描述一下操作系统访问导入表的流程。
    以这边文章中的例子程序作为样本,我这里用winhex手动示范操作系统访问导入表的流程,并且假设已经通过了DOS头部、NT头部、FILE头部、选项头部的定位过程,直接找到数据目录表(IMAGE_DATA_DIRECTORY数组)的位置。

    1. 找数据目录表下标1的元素项,取RVA值
      18.png
      导入表RVA=0x1B1F8
    2. 遍历各节表(Section Header),取VirtualAddress成员以及VirtualAddress+SizeOfRawData的和(该节表描述的节区范围)跟导入表RVA进行比较,如果这个RVA被包含在这个节区范围中,那么导入表就在这个节区中,利用这个节表的某些成员将其换算成FA定位到导入表位置;如果遍历完所有的节表,还没有节区范围跟导入表RVA匹配的,那么这就是个无效的导入表RVA地址,在程序启动时操作系统会报“无效的Win32程序”错误,停止加载程序。
      19.png
    3. 遍历导入表数组,检查导入表项是否全0,全0则表示导入表没有内容。
    4. 检查导入表项中第4个DWORD值(Name),也就是DLL名称,名称过长则报错,系统内部通过LoadLibrary加载目标DLL。这也是我前面讲导入表结构的时候为什么按照Name->DUMMYUNIONNAME->FirstThunk的顺序来讲,因为操作系统访问它们的顺序就是“4->1->5”,也就是第4个DWORD->第1个DWORD->第5个DWORD的访问顺序,记“415”比较好记。
      20.png
    5. 检查导入表项中第1个DWORD,也就是INT指针表中的成员是否为NULL,若是,再检查IAT内容是否为NULL,若是,就认为PE程序不合法;若不是,就找INT中存放的API序号(INT中有时存放的是API的序号而非名称)或API名称,通过GetProcAddress得到API地址覆盖这个IAT元素项。
      21.png
    6. 若INT指针表不为NULL,判断INT元素最高位是不是1,若是,就取低2字节作为API序号调用GetProcAddress(序号高2字节必须是0);若不是,整个DWORD作为API名称的字符串指针(RVA)调用GetProcAddress。
    7. 将GetProcAddress得到的API地址填入INT同下标的IAT表中。

    四、 总结
    本文只对导入表的基本结构以及操作系统访问导入表的基本流程做了介绍,导入表相关的知识还涉及到:
    RVA、VA、FA三种地址的相互转换
    导出函数序号调用
    IAT中转API
    导入表修复或修改
    利用修改导入表做劫持
    ……
    以后有机会再进行总结和介绍。

yurisbbs by @devseed since 2020, powered by nodebb.