peexplorer1.9: PE文件格式 1.9版 完整译文(附注释)(2)

  整理总结下:如果你想从“knurr”DLL中查找输入“foo”信息步你先找到数据目录中IMAGE_DIRECTORY_ENTRY_IMPORT(输入目录项)项得到个RVA再在原始节数据中找到那个地址现在你就得到个IMAGE_IMPORT_DESCRIPTOR(输入描述结构)通过查看根据它们“名称”被指向得到和“knurr”DLL有关这个成员(即个输入描述结构)在你找到正确IMAGE_IMPORT_DESCRIPTOR(输入描述结构)后顺着它“OriginalFirstThunk”(原始第个换长)得到被指向IMAGE_THUNK_DATA(换长数据);再通过查询RVA找到“foo”

  好了为什么我们有“两”列指向IMAGE_IMPORT_BY_NAME(输入名字)指针呢?这是在运行时应用不需要输入名字只需要地址在这里输入地址表又出现了加载器将从相关DLL文件输出目录中查找每个输入符号并用DLL文件入口点线性地址替换“FirstThunk”(第个换长)列表中IMAGE_THUNK_DATA(换长数据)元素(到现在的前它还是指向IMAGE_IMPORT_BY_NAME(输入名字))

  请记住带有象“__imp__symbol”标签地址列表;被数据目录IMAGE_DIRECTORY_ENTRY_IAT(输入地址表目录项)所指向输入地址表就是被“FirstThunk”(第个换长)所指向列表[在从好几个DLL文件输入情况下输入地址表是包含所有DLL文件“FirstThunk”(第个换长)目录项IMAGE_DIRECTORY_ENTRY_IAT(输入地址表目录项)可能会丢失但输入()仍能工作良好]

  “OriginalFirstThunk”(原始第个换长)保持不变因此你总能通过“OriginalFirstThunk”(原始第个换长)列表查找原始输入名字列表

  现在输入已经被用正确线性地址修正如下所示:

   原始第个换长    第个换长
      |          |
      |          |
      |          |
      V          V
      0-->  1    0--> 输出1
      1-->  2    1--> 输出2
      2-->  3    2--> 输出3
      3-->  foo     3--> 输出foo
      4-->  mumpitz   4--> 输出mumpitz
      5-->  knuff    5--> 输出knuff
      6-->0      0<--6
  这是简单情况下基本结构现在我们将要学习输入目录中需细讲东西

  第中IMAGE_THUNK_DATA元(换长数据)素IMAGE_ORDINAL_FLAG(序数标志)位(也是:MSB参见注释)被置1时表示列表中没有符号名字信息符号只以序数输入你可通过查看IMAGE_THUNK_DATA(换长数据)中低地址word来得到序数

  通过序数输入是不鼓励通过名字输入会更安全如果输出DLL文件不是预期版本时输出序数可能会改变

  第 2有所谓“绑定输入”

  请研究下加载器工作:当它想执行个 2进制文件需要个DLL中加载器会载入该DLL找到它输出目录查找RVA并计算入口点然后用这样找到地址修正“FirstThunk”(第个换长)列表

  假设员很聪明给DLL文件提供优先载入地址不会发生冲突那么我们就能认为入口点将总是相同它们在链接时能被算出并被补进“FirstThunk”(第个换长)列表中这就是“绑定输入”所发生(“绑定”工具就是干这个它是Win32SDK部分)      

  当然你得慎重:用户DLL可能是区别版本或者DLL必须重定位这些都会使先前修正“FirstThunk”(第个换长)列表不再有效;此时加载器仍能查寻“OriginalFirstThunk”(原始第个换长)列表找出输入符号并重新补正“FirstThunk”(第个换长)列表加载器知道这是必须当:1)输出DLL文件版本不符或2)输出DLL文件需要重定位时

  确定有没有重定位表对加载器来说不是问题但该怎样找出版本区别呢?这时IMAGE_IMPORT_DESCRIPTOR(输入描述结构)“时间戳”就派上用场了如果它是0表明输入列表没有被绑定加载器总是要修复入口点否则输入被绑定“时间戳”必须要和“文件头”中输出DLL文件“时间戳”相符;如果不符加载器就认为该 2进制文件被绑到个“DLL文件上并重新补正输入列表

  这里有另外个有关输入列表中“中转”怪事个DLL文件能输出个不定义在本DLL文件中却需从另个DLL文件中输入符号;这样符号据说就是被中转(参见上面输出目录描述)

  现在很明显你不能通过查看那个实际上并不包含入口点信息DLL文件时间戳来确定个符号入口点是否有效因此出于安全原因中转符号入口点必须总是被修正在 2进制文件输入列表中中转符号输入必须被找出以便加载器能补正它们

  这点可通过“ForwarderChain”(中转链)来做到它是个指向换长列表中索引值;被索引位置输入就是个中转输出并且此位置“FirstThunk”(第个换长)列表中内容就是“下个”中转输入索引值以此类推直到索引值为-1就表明已没有其他中转了如果根本就没有中转那么“ForwarderChain”(中转链)值本身就为-1

  这就是所谓“老式”绑定

  至此我们应该整理总结下我们目前已掌握情况:-)

  OK我将认为你已找到了IMAGE_DIRECTORY_ENTRY_IMPORT(输入目录项)并且已根据它找到了它输入目录位于某个节中现在你已处于IMAGE_IMPORT_DESCRIPTOR(输入描述结构)开头了此类最后个将以全0字节填充

  要读懂个IMAGE_IMPORT_DESCRIPTOR(输入描述结构)你得先查看它“名字”项根据它RVA你就能找到输出DLL文件名字步你得确定输入是否是绑定;如果输入是绑定“时间戳”就会是非“0”如果它们是绑定现在就是你通过比较“时间戳”来检查DLL文件版本是否相符好机会了

  现在你根据“OriginalFirstThunk”(原始第个换长)RVA来到了IMAGE_THUNK_DATA(换长数据);过完这些(它是0结尾)每个成员都将是个IMAGE_IMPORT_BY_NAME(输入名字)RVA(除非它高位被置1此时你找不到名字只有序数)根据那个RVA并跳过2字节(即‘提示’)现在你就得到个以0结尾这就是输入名字

  在绑定输入时要找到提供入口点先根据“FirstThunk”(第个换长)平行来到“OriginalFirstThunk”(原始第个换长)成员就是入口点线性地址(暂时不考虑中转话题)

  还有件我到现在都没有提及事情:明显地有些链接器在构建输入目录时会产生bug(我就发现个还在被个BorlandC链接器使用bug)这些链接器把IMAGE_IMPORT_DESCRIPTOR(输入描述结构)中“OriginalFirstThunk”(原始第个换长)设为0并只建立“FirstThunk”(第个换长)很明显这样输入目录不能被绑定(否则重修输入必须信息就会丢失----你根本找不到名字)在这种情况下你得根据“FirstThunk”(第个换长)来取得输入符号名字你将永远得不到预先补正入口地址我已发现个TIS文件(参考书目[6])讲述个在某种程度上和此bug兼容输入目录因此那个文件可能就是该bug起源

  TIS文件规定:

  IMPORTFLAGS(输入标志)

  TIME/DATESTAMP(时间/日期戳)

  MAJORVERSION-MINORVERSION(主版本号-小版本号)

  NAMERVA(名字RVA)

  IMPORTLOOKUPTABLERVA(输入查询表RVA)

  IMPORTADDRESSTABLERVA(输入地址表RVA)

  而别处使用对应结构是:

  OriginalFirstThunk(原始第个换长)

  TimeDateStamp(时间日期戳)

  ForwarderChain(中转链)

  Name(名字)

  FirstThunk(第个换长)

  最后个有关输入目录需要细讲就是所谓“新式”绑定(在参考书目[3]中讲述)它也可以由“绑定”工具来处理当使用这种方式时“时间日期戳”所有位被置为1并且没有中转链;此时所有输入符号地址都将被补正而不管它们是不是中转尽管如此你还是需要知道DLL版本并且你还是需要将序数符号从中转符号中区分开来为了达到这个目IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT(绑定输入目录项)目录被创建了就我所见它将不被放在节中而是被放在头中处于节头的后第节的前(咳这不是我发明我只是讲述它而已!)

  这个目录告诉你个已使用DLL文件中转输出是从哪些别DLL文件中来

  结构是IMAGE_BOUND_IMPORT_DESCRIPTOR(绑定输入描述结构)形式包括(按这个顺序):

  个32位数字“时间戳”

  个16位数字“OffModuleName(模块名字偏移量)”是从目录开头到以0结尾DLL文件名偏移量;

  个16位数字“NumberOfModuleForwarderRefs(模块中转参考数字)”给出这个DLL文件为它中转使用DLL文件数

  紧随这个结构的后你会发现“NumberOfModuleForwarderRefs(模块中转参考数字)”结构告诉你这个DLL文件中转所来自DLL文件名称和版本这些结构就是“IMAGE_BOUND_FORWARDER_REF(绑定中转参考)”结构:

  个32位数字“时间日期戳”(TimeDateStamp);

  个16位数字“模块名称偏移量”(OffModuleName)它就是从目录开头到中转来自那个DLL文件0结尾名字处偏移量;

  个16位未使用单元

  跟在“IMAGE_BOUND_FORWARDER_REF(绑定中转参考)”后是下个“IMAGE_BOUND_IMPORT_DESCRIPTOR(绑定输入描述结构)”以此类推;列表最终以个全部为0位IMAGE_BOUND_IMPORT_DESCRIPTOR(绑定输入描述结构)结束

  我对由此(描述)造成不便表示歉意但这就是它看起来样子:-)

  现在如果你有个新绑定输入目录你得载入所有DLL文件并使用目录指针IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT(绑定输入目录项)找到IMAGE_BOUND_IMPORT_DESCRIPTOR(绑定输入描述结构)扫描整个IMAGE_BOUND_IMPORT_DESCRIPTOR(绑定输入描述结构)并检查被载入DLL文件“时间日期戳”和这个目录中提供是否相符如果不符就将输入目录中“第换长”(FirstThunk)中全部修改过来

  8.资源(resources)

  -------------------

  资源比如对话框、菜单、图标等等都存储在IMAGE_DIRECTORY_ENTRY_RESOURCE(“资源目录项”)指向数据目录中它们处于个至少“IMAGE_SCN_CNT_INITIALIZED_DATA(已化数据内容节)”和“IMAGE_SCN_MEM_READ(内存可读节)”标志位都被置为1节中

  资源基础是“资源目录”(IMAGE_RESOURCE_DIRECTORY);它包含好几个“资源目录项”(IMAGE_RESOURCE_DIRECTORY_ENTRY)其中项反过来又可能指向个“资源目录”按照这种方式你就得到个以“资源目录项”为树叶“资源目录”树;它们树叶指向实际资源数据

  在实际使用中情况会稍微简单些般你不会遇到不可能理清特别复杂

  通常层次结构是这样:个目录作为根它指向很多目录每种资源类型都有这些目录又指向子目录每个子目录都有个名字或者ID号并指向这个资源所提供各种语言目录;每种语言你都能找到个资源项资源项最终指向(具体)数据(注意:多语言资源不能在Win95上运行即使有好几种语言Win95也总是使用相同资源----我没有查出是哪但我猜测肯定是它最先碰到那种多语言资源在NT系统上可以运行)

  没有指针树大致象这样:

             (根)
               |
      +----------------+------------------+
      |        |         |
      菜单     对话框       图标
      |        |         |
   +-----+-----+    +-+----+     +-+----+----+
   |     |    |   |     |   |  |
  ""   "popup" 0x10 "dlg"  0x1000x1100x120
   |      |   |   |     |   |  |
 +---+-+     |   |   |     |   |  |
 |  |  default english default  def. def. def.
germanenglish
  个“资源目录项”(IMAGE_RESOURCE_DIRECTORY)包含:

  32位未使用标志叫做“特征”(Characteristics);

  32位“时间日期戳”(同样按常用time_t表示法)告诉你资源被创建时间(如果此项被设置话);

  16位“主版本号”(MajorVersion)和16位“小版本号”(MinorVersion)以允许你据此维护资源几个版本;

  16位“已命名项目数”(NumberOfNamedEntries)和另个16位“ID项目数”(NumberOfIdEntries)

  紧随此结构后是“已命名项目数”+“ID项目数”两结构体它们都是“资源目录项”格式都以名字开头它们可能指向下个“资源目录”或者指向实际资源数据

  个“资源目录项”由下面组成:

  32位单元提供你它所描述资源ID或者是目录;

  32位到数据偏移量或者是到下个子目录偏移量

  ID含义取决于树中层次;ID可能是个数字(如果最高位为0)也可能是个名字(如果最高位为1)如果是个名字低31位就是从资源节原始数据开始到这个名字(名字有16位长并由unicode而不是0结尾符作为结束)偏移量

  如果你位于根目录的中且如果ID是个数字那么它指就是下面种资源类型:

  1:光标

  2:位图

  3:图标

  4:菜单

  5:对话框

  6:字串表

  7:字体目录

  8:字体

  9:快捷键

  10:未格式化资源数据

  11:信息表

  12:组光标

  14:组图标

  16:版本信息

  任何其它数字都是用户自定义任何有类型名资源类型也是用户自定义

  如果你处于(树)下层当中此时ID定是个数字且就是资源个特例语言ID号;例如你可以(同时)拥有澳大利亚英语、加拿大法语和瑞士德语等本地化形式对话框并且它们分享同个资源ID系统会根据线程地点来选择要使用对话框反过来地点又反映了用户“区域设置”(如果资源找不到线程地点系统将先使用个中性子语言资源作为地点比如它将寻找标准法语而不是用户所拥有加拿大法语;如果它还是找不到就使用最小语言ID号那个例子必须注意所有这些只工作于NT系统的上)

  为便于辨认语言ID使用宏PRIMARYLANGID(意为“主语言ID”)和SUBLANGID(意为“子语言ID”)将它分开为主语言ID和子语言ID分别使用它0-9位和10-15位这些值定义在“winresrc.h”文件中

  语言资源只支持快捷键、对话框、菜单、资源数据或串等;其它资源类型必须为LANG_NEUTRAL/SUBLANG_NEUTRAL(中性语言/中性子语言)

  要确定资源目录层是不是另个目录你可查看它偏移量最高位如果它是1剩下31位就是从资源节原始数据开始到下层目录偏移量还是按“资源目录”后接“资源目录项”格式如果高位为0它就是从资源节原始数据开始到资源原始数据描述个资源数据项偏移量资源原始数据描述包含32位“OffToData”(到数据偏移量)(指是到原始数据偏移量从资源节原始数据开头算起)32位数据“Size”(大小)32位“CodePage”(代码页)和个未使用32位单元

  (不鼓励使用代码页你应该使用“语言”特性来支持多地域)

  原始数据格式依赖于资源类型;详细介绍可在微软SDK文档中找到注意:除了用户自定义资源资源中任何串总是按UNICODE格式明显用户自定义资源按是开发者选定格式

  9.重定位(relocations)

  -----------------------

  我将要描述最后个数据目录是基址重定位目录它是由可选头数据目录中IMAGE_DIRECTORY_ENTRY_BASERELOC(基址重定位目录项)项来指向典型它包含在自己节中名字象“.reloc”这样并且IMAGE_SCN_CNT_INITIALIZED_DATA(已化数据内容节)、IMAGE_SCN_MEM_DISCARDABLE(内存可丢弃节)和IMAGE_SCN_MEM_READ(内存可读节)等标志位被置1

  如果映象文件不能被加载到可选头中提到优先载入地址“ImageBase”(映象基址)时重定位数据对加载器来说就是必须此时链接器所提供固定地址就不再有效并且加载器将不得不对静态变量、串文字等使用绝对地址进行修正

  所谓重定位目录就是些连续块都包含4K映象文件重定位信息块由个“IMAGE_BASE_RELOCATION(基址重定位)”结构体开始这个结构体包含个32位“VirtualAddress(虚拟地址)”项和个32位“SizeOfBlock(块大小)”项跟在它们后面就是块实际重定位数据条都是16位

  “VirtualAddress(虚拟地址)”就是重定位所在块需要应用基本RVA;“SizeOfBlock(块大小)”就是整个块字节大小;跟在后面重定位数目是:('SizeOfBlock'-(IMAGE_BASE_RELOCATION))/2个当你碰到个“VirtualAddress(虚拟地址)”值为0“IMAGE_BASE_RELOCATION(基址重定位)”结构体时重定位信息就结束了

  每个16位重定位信息由低12位重定位位置和高4位重定位类型组成要得到重定位RVA你需要用这个12位位置加上“IMAGE_BASE_RELOCATION(基址重定位)”中“VirtualAddress(虚拟地址)”类型是下面的:

  IMAGE_REL_BASED_ABSOLUTE(0)

  这种不需操作;用于将块按32位边界对齐位置应该为0

  IMAGE_REL_BASED_HIGH(1)

  重定位高16位必须被用于被偏移量所指向那个16位WORD单元此WORD是个32位DWORD高位WORD

  IMAGE_REL_BASED_LOW(2)

Tags:  pe文件格式 peexplorer1.9

延伸阅读

最新评论

发表评论