【文章标题】: PE文件结构处理经验总结1
【文章作者】: ddlx
【作者邮箱】: DdlxStudio@163.com
【作者QQ号】: 383394019
【作者声明】: 只是感兴趣,没有其他目的。失误之处敬请诸位大侠赐教!
--------------------------------------------------------------------------------
【详细过程】
  该贴不是扫盲贴,如果对Pe文件不是很熟悉,请先查阅其他人写的PE文章。
  该贴是我在写加壳工具的时候遇到问题的总结。记录下来,供以后温习,希望对各位也有一定帮助。
  由于本人接触该方面时间不长,免不了存在失误之处,敬请拍砖。
  
  1. IMAGE_DOS_HEADER
  Dos头需要注意的有两个字段:e_magic和e_lfanew。其它字段不用管它。这两个字段的意义大家都懂得~
  
  2. IMAGE_NT_HEADER
  通过Nt头的Signature字段和Dos头的e_magic来判断该文件是否为PE文件。
  
  3. IMAGE_FILE_HEADER
  字段:Machine。该字段通常用来判断该PE文件是否为64位格式。我原先使用字段Characteristics的IMAGE_FILE_32BIT_MACHINE位来判断是否为64位,发现这种方法不准确,32位文件可以不设置IMAGE_FILE_32BIT_MACHINE位。
  字段:Characteristics。这里面记录了好多文件属性。我当前只判断IMAGE_FILE_DLL,来确认该文件是否为动态链接库Dll.(IMAGE_FILE_32BIT_MACHINE标记判断是否为64位是不可靠的)
  字段: NumberOfSections。这个是区段(也叫节)个数,遍历区段头时使用该字段来控制是否遍历结束。我原先看PE文章说在遍历区段头时判断区段头数据是否为空就知道是否遍历结束了,经过我亲自试验,这种说法是错误的。
  字段: SizeOfOptionalHeader. 想知道可选文件头大小就使用这个字段。有人计算可选文件头大小时使用sizeof(IMAGE_OPTIONAL_HEADER32/64),这个也是错误的,因为数据目录的个数可能不止16个。
  
  4. IMAGE_OPTIONAL_HEADER
  该字段有32和64两种版本。
  通过BaseOfCode和SizeOfCode来标记代码段起始位置和大小是不可取的。数据段也一样。
  SectionAlignment和FileAlignment在增加或删除区段的时候有用。如果区段变更,一定要修正SizeOfImage。否则你的程序就无法被系统加载。
  SectionAlignment必须>=FileAlignment。并且都是2的幂数。
  字段: SizeOfHeaders。这个是整个Pe文件头大小。CopyPE文件头数据时使用。有些人在复制文件头时使用memcpy(pToModBase, pFromModBase, pSectionH[0].PointerToRawData);以后不要这样用了,因为有一些加过壳(特别是压缩壳)的程序pSectionH[0].PointerToRawData被置成了0.
  字段:CheckSum。校验和只有在驱动文件中有用。驱动加载失败首先要看一下校验和是否正确。
  字段:NumberOfRvaAndSizes。不是永远等于16。可以任意。
  
  
  5. IMAGE_SECTION_HEADER
  字段:Name.获取Name时不要使用strcpy(pSecName,pSecH[0].Name); 应该使用memcpy(pSecName, pSecH[0].Name,IMAGE_SIZEOF_SHORT_NAME);
  字段:VirtualAddress。该值必须是SectionAlignment的倍数。VirtualAddress不能为0
  字段:PointerToRawData。该值必须是FileAlignment的倍数。PointerToRawData可以为0.
  字段:VirtualSize:不必是SectionAlignment的倍数。
  字段:SizeOfRawData:不必是FileAlignment的倍数。可以为0.
  字段: Characteristics。 该字段包含了区段属性信息。可以通过IMAGE_SCN_MEM_EXECUTE标志来判断是否为可执行区段,不是很准。
  另外:
  1. 相邻的两个节数据不一定是连续的!
  2. 获取第一个区段头地址方法:
  #define IMAGE_FIRST_SECTION( ntheader ) ((PIMAGE_SECTION_HEADER)        \
      ((ULONG_PTR)ntheader +                                              \
       FIELD_OFFSET( IMAGE_NT_HEADERS, OptionalHeader ) +                 \
       ((PIMAGE_NT_HEADERS)(ntheader))->FileHeader.SizeOfOptionalHeader   \
      ))
  不要使用:
  #define IMAGE_FIRST_SECTION( ntheader ) ((PIMAGE_SECTION_HEADER)        \
      ((ULONG_PTR)ntheader +                                              \
       FIELD_OFFSET( IMAGE_NT_HEADERS, OptionalHeader ) +                 \
       sizeof(IMAGE_OPTIONAL_HEADER)   \
      ))
  3. 如果你要增加一个区段,一定要先判断一下区段头后面是否存在有用数据。如果你把有用数据给覆盖了,模块就加在不起来了。例如:记事本程序的区段头后面就是有用数据:Bound Import Directory。
4. 不要看见区段名叫.text或Code就认为是代码段。区段名可以任意写的。
  
  6. IMAGE_EXPORT_DIRECTORY导出表
  字段:Base。意思是基数。通常为1。可以为任意数。不能为0.
  字段:NumberOfNames。NumberOfNames <= NumberOfFunctions
  字段:NumberOfFunctions。该值可以为0,就是没有导出函数。
  另外:1. 导出表只有1个。有的童鞋可能认为存在多张导出表。
  2. 转发函数:这是一个特殊的导出函数。如果你自己实现GetProcAddress函数,可能会遇到这个问题。如果该导出函数是一个转发函数,那么这个函数地址就是不函数指令了,而是转发的那个函数字符串。例如Kernel.dll中得导出函数HeapAlloc,函数地址处是字符串NTDLL.RtlAllocateHeap。你如果调用系统的GetProcAddress(pKernel32Base, "HeapAlloc"),得到的地址是Ntdll中得RtlAllocateHeap函数地址。
  3. 转发函数判断方法:得到函数地址后,判断这个函数是否在导出表数据内。如果是,则为转发函数。
  4. 根据函数序号得函数地址的方法: 1. 先得函数地址表索引:索引 = 序号 - 基数;2. 根据索引得函数地址。pAddressOfFunctions[序号 - 基数]
  
  7. IMAGE_IMPORT_DESCRIPTOR导入表
  字段:OriginalFirstThunk。该字段可以为0.也可以FirstThunk == OriginalFirstThunk
  字段:FirstThunk,该字段必须不为0.如果OriginalFirstThunk为0,则在模块加载时FirstThunk担任OriginalFirstThunk角色。
  
  8. IMAGE_BASE_RELOCATION 重定位表
  判断重定位表结束的方法:VirtualAddress==0 && SizeOfBlock==0. 只判断VirtualAddress是不行的。我就犯过这个错误~
  
  9. IMAGE_TLS_DIRECTORY TLS
  当时为了很好的处理TLS,让加好壳的程序TLS运行正常,伤了很多脑细胞。
  TLS分为两块:静态数据 和 TLS CallBack
  TLS的静态数据,最需要注意的是初始化时不为0的数据。不要都给他填充为0.
  TLS CallBack,当时需要增加TLS反调试功能,所以对CallBack表重新构造了一下。
  
  字段:StartAddressOfRawData:TLS静态数据开始地址。
  字段:EndAddressOfRawData:TLS静态数据结束地址。
  字段:SizeOfZeroFill,直接填充为0的大小。
  上面3个字段都是跟静态数据大小有关的。静态数据总大小就是EndAddressOfRawData-StartAddressOfRawData+SizeOfZeroFill.
  字段:AddressOfIndex。跟TLS静态数据有关。如果你要压缩数据,一定要对该字段特殊处理。这个是地址索引,系统设置的,跟进程中拥有TLS的模块个数有关。
  为什么说要特别注意?因为该数据存在数据段,如果数据段被压缩,加载时在解压的话,AddressOfIndex会在你解压时填充为0.由于AddressOfIndex的索引是在你解压之前设置的,就是说你把系统分配的索引又摸回去了。
  
  
  10. Load Config Directory
  该数据目录项,在加壳时一定要给他清空。否则壳中的异常可能得不到处理。
  该结构中得最后两个字段保存了该模块出现异常时的异常处理地址表。如果你的异常处理地址不在那张列表中,系统就不会处理你的异常。
  
--------------------------------------------------------------------------------
  不足之处,欢迎拍砖。 未完待续。。。
  
--------------------------------------------------------------------------------
【版权声明】: 本文原创于看雪技术论坛, 转载请注明作者并保持文章的完整, 谢谢!

                                                       2011年11月12日 13:35:58