PE格式 – IAT问题

我正在尝试为Windows编写一个exe包装器。 到目前为止,我已经掌握了一些基础知识。 我要做的部分是阅读“BOUND IMPORT目录表”(或.idata部分?),基本上是PE文件的一部分,其中包含加载程序需要导入的DLL列表。

我想知道最好的方法是:

[A]找出IAT的位置(因为针对几个不同的.exe运行PEView似乎表明这个列表可以包含在多个不同的地方)然后读取列表

要么

[B]找到一种直接读取exe需要导入的DLL列表的方​​法。

有办法做到这一点吗? 有没有进一步的阅读,人们可以推荐IAT应该在哪里,如何读取它?

是的,您可以通过浏览可执行文件的标题找到IAT。 在winnt.h查找标头声明。

有关如何在标题中查找信息的详细信息,请参阅MSDN杂志中的 Matt Pietrek系列,“深入研究Win32可移植可执行文件格式”,第I部分和第II部分 。

您还可以从此处获取实际的Microsoft PE规范。

TL; DR:基本上查找序列如下:

  1. 从二进制文件的基址开始。 这是一个IMAGE_DOS_HEADER结构。
  2. 按照e_lfanew字段转到IMAGE_NT_HEADERS结构。
  3. 按照OptionalHeader进入IMAGE_OPTIONAL_HEADER结构(尽管它的名称,它不再是可选的)。
  4. 跟随DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]IMAGE_IMPORT_DESCRIPTOR结构数组。 每个导入的DLL都有一个条目。 此数组中的最后一个条目将被清零。
  5. 每个条目中的“ Name字段是指向DLL名称的RVA。 FirstThunk字段是指向DLL的IAT的RVA,IAT是IMAGE_THUNK_DATA结构的数组。

另外,这是一个pdf ,可以帮助您了解结构的命名和组织方式。 一些有用的程序: CFF Explorer和一个好的hex编辑器

我的回答与上面的不同之处在于它描述了一种在仍然在磁盘上的可执行文件中手动执行上述内容的方法。

要获取IAT的相对虚拟地址(运行时的地址,即RVA):

  1. 从二进制文件的基址开始。 这是一个IMAGE_DOS_HEADER结构。
  2. 按照e_lfanew字段转到IMAGE_NT_HEADERS结构。 从偏移0开始,向下跳转0x3c并取消引用以获得IMAGE_NT_HEADERS的开始。
  3. 遵循OptionalHeader (由IMAGE_NT_HEADERS包含,因此也是如此)。 要获得IMAGE_OPTIONAL_HEADER结构(尽管它的名称,它不再是可选的),请知道它是IMAGE_NT_HEADER中的第三个结构。 要访问OptionalHeader,请将0x18添加到之前取消引用的值
  4. 从OptionalHeader中,取消引用DataDirectory。 DataDirectory是OptionalHeader中的一个数组,位于IMAGE_NT_HEADERS中。 遵循DataDirectory数组中的第24个(如果0是第一个,如在0,1,2 ……中)条目到IMAGE_DIRECTORY_ENTRY_IAT。 将0xc0添加到您当前所在的地址以获取导入地址表目录

如果要遍历DLL的列表及其function地址,有一些背景知识:

  • IMAGE_IMPORT_DESCRIPTOR结构中的3个重要字段:OriginalFirstThunk(导入查找表的RVA),名称(以null结尾的ASCII Dll名称的RVA)和FirstThunk(IAT的RVA /由加载程序构建的线性地址数组)。
  • 需要两个数组,因为一个是导入例程(ILT)的名称数组,另一个是导入的例程地址(IAT)数组。 从DLL导入的例程可以通过其名称或序号导入。 要判断例程是否是序数导入,请检查ILT数组中IMAGE_THUNK_DATA结构的Ordinal字段中设置的标志。
  • 模块在加载时导入的每个函数都将由IMAGE_THUNK_DATA结构表示。 原始第一个Thunk和第一个Thunk都指向一个IMAGE_THUNK_DATA数组。 然而,IMAGE_THUNK_DATA结构是一个联合,它包含另一个结构IMAGE_IMPORT_BY_NAME。 这一点很重要,因为Original First Thunk使用IMAGE_IMPORT_BY_NAME结构,而First Thunk使用IMAGE_THUNK_DATA结构内部联合中的Function字段。
  • 磁盘上指定的RVA不允许您遍历文件,因为RVA表示二进制文件加载到内存时的地址; 要遍历磁盘上的二进制文件,您需要将RVA值转换为正确的forms。 公式很简单; HMODULE + RVA = PE元素的线性地址。 HMODULE也称为基地址。 但是获取基地址实际上需要一个有点冗长的算法,并且取决于你所讨论的RVA的实际价值。 要获取给定RVA的基址的值,以便计算磁盘上PE元素的线性地址:

    1. 获取sectionHeader; 为此,请浏览部分列表(例如.data,.text,ect),直到找到问题的RVA在currentSection.VirtualAddress和currentSection.VirtualAddress + currentSection.size中的部分。

      1.1)首先,在NT_HEADERS结构中找到FileHeader中的节数。 它是FileHeader中2字节机器号后面的2个字节。 *要手动执行此操作:将0x6添加到e_lfanew取消引用的值; 所以从偏移量0跳转0x3c,取消引用该值,然后将0x6取消引用。 然后读取两个字节并解释为整数。

      1.2)找到第一部分的位置; 它与OptionalHeader相邻。 请记住,OptionalHeader中的DataDirectories数组。 OptionalHeader长216个字节,加上结尾的2个字表示它的结束; 所以取hex的224(0xe0)并将其添加到从开始的0x3c处取消引用的值以获得第一个部分位置。

      1.3)要查找RVA所在的部分标题,请继续针对您当前的部分执行此测试。 如果测试失败,请转到下一部分。 如果您遍历所有部分并发现您到达结束的NULL字,那么该文件应该已损坏或您犯了错误。 测试如下:将要转换的RVA与该部分虚拟地址的可用指针进行比较; RVA应该> =到该部分的虚拟地址,<该部分的虚拟地址和虚拟大小的总和。 可以通过将12添加到该部分的地址来找到该部分的虚拟地址。 该部分的虚拟大小可以通过8到该部分的地址找到。 总结一下:传递if - (section.virtualAddress + section.virtualSize)> RVA> = section.virtualAddress。 *要迭代到下一节,节描述的长度为0x28; 你可以将0x28添加到当前节指针以进入下一节。 最后一节是一个空字节来表示结束。

    2. 从获得的节标题中,执行以下操作:(baseAddress + RVA) – (sectionHeader.virtualAddress – sectionhHeader.PointerToRawData)。 *如上所述,sectionHeader的virtualAddress与sectionHeader本身相距12个字节。 PointerToRawData距离节标题20。

    3. 获得的值表示指向RVA所需/表示的数据的实际指针。 您可以使用它来查找所需数据的实际文件位置。

那是满口的。 如果你想回顾一下,你应该阅读Rootkit Arsenal中第5章(挂钩呼叫表)的第257-60页,但是为了更容易理解的图形,请查看我在顶部附近给出的openrce.org pdf链接。

要做到这一点,请从……开始:

  1. 如上所述,转到OptionalHeader。 OptionalHeader包含一个(DataDirectory)IMAGE_DATA_DIRECTORY元素数组作为它的最后一个元素。 该数组中的第二个元素是IMAGE_DIRECTORY_ENTRY_IMPORT,它定位IAT。 因此,为了澄清,IMAGE_NT_HEADER包含OptionalHeader数组,该数组包含DataDirectory数组。 此数组中的最后一个条目将被清零。
  2. 从OptionalHeader取消引用到IMAGE_DIRECTORY_ENTRY_IMPORT。 下一个词是导入目录的大小。 从文件中的偏移量,跳转0x68。
  3. 此值是导入目录的RVA,它是IMAGE_IMPORT_DESCRIPTOR类型的结构数组(模块导入的每个DLL一个),其中最后一个的字段设置为零。 IMAGE_IMPORT_DESCRIPTOR中的第三个单词包含指向IMAGE_THUNK_DATA的FirstThunk指针。
  4. 使用上述算法,将导入目录的RVA转换为可用指针,并使用以下算法迭代导入目录数组。

    4.1)对于importDescriptor,将名称字段RVA转换为指针以获取名称。 它可能为空

    4.2)要获取导入的每个例程的名称和地址,请获取导入描述符的OriginalFirstThunk和FirstThunk RVA条目。 每个OFT和FT都可以为null,这表示它是空的,所以检查一下。

    4.3)将OFT RVA转换为指针; OFT对应于ILT,FT对应于IAT。 ILT和IAT可以为空,表示它们是空的。

    4.4)获取从ILT指针导入的函数的名称,以及IAT指针中函数的地址。 要移动到下一个导入的函数,请记住ILT和IAT是数组; 下一个元素是一个恒定的距离。

    4.5)检查获得的新ILT和IAT指针值是否不为零; 如果它们不为零,重复一遍。 如果其中任何一个为零,那么您已经点击为该dll导入的函数列表的末尾; 导入描述符也是重复数组,因此导入的下一个dll的偏移量是常量。 基本上,您正在遍历dll,并且对于每个dll,您正在迭代以这种方式导入的函数。 19