原来在DOS操作系统上面的可执行文件(COM)只包含代码,程序被载入后第一句就是一条指令。这样让程序很不灵活,所有的代码,数据,堆栈都存放在一起,程序不能跨段操作。所有Windows操作系统为了解决这个问题就引入了PE文件结构。
-
PE文件头:包括文件的入口,堆栈位置,重定位表等信息
-
IMAGE_FILE_HEADER结构
-
MACHINE成员:运行的平台
-
NumberOfSections:节数
-
TimeDateStamp:文件创建的日期
-
SizeOfOptionalHeader:IMAGE_OPTIONAL_HEADER32结构的大小(永远是0x00E0)
-
Characteristics:属性。一般EXE文件为0x010F,dll文件为0x210E
-
-
IMAGE_OPTIONAL_HEADER32结构
-
AddressOfEntryPoint:文件被执行时的入口RVA。一般为0x00100000
-
ImageBase:PE文件被映射到内存的基地址。一般EXE:0x00400000 DLL:0x10000000
-
SectionAlignment/FileAlignment:段加载到内存中的对齐方式/段在PE文件中的对齐方式
-
DataDirectory:是由16个IMAGE_DATA_DIRECTORY组成,在这里插入点关于为什么要用到IMAGE_DATA_DIRECTORY的原因。其实PE文件中的数据块按功能其实是可以分成很多部分的,如导入表,导出表以及.const段指定的只读数据,这些不同用途的数据块可能被放到具有同一属性的节中。PE文件就用一系列的IMAGE_DATA_DIRECTORY来分别指明这些数据的位置。下面就把这16个IMAGE_DATA_DIRECTORY所要指定的数据块贴出来。
IMAGE_DATA_DIRECTORY结构包含数据块的起始RVA和数据块的长度索 引
索引值在Windows.inc中的预定义值
对应的数据块
0
IMAGE_DIRECTORY_ENTRY_EXPORT
导出表
1
IMAGE_DIRECTORY_ENTRY_IMPORT
导入表
2
IMAGE_DIRECTORY_ENTRY_RESOURCE
资源
3
IMAGE_DIRECTORY_ENTRY_EXCEPTION
异常(具体资料不详)
4
IMAGE_DIRECTORY_ENTRY_SECURITY
安全(具体资料不详)
5
IMAGE_DIRECTORY_ENTRY_BASERELOC
重定位表
6
IMAGE_DIRECTORY_ENTRY_DEBUG
调试信息
7
IMAGE_DIRECTORY_ENTRY_ARCHITECTURE
版权信息
8
IMAGE_DIRECTORY_ENTRY_GLOBALPTR
具体资料不详
9
IMAGE_DIRECTORY_ENTRY_TLS
Thread Local Storage
10
IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG
具体资料不详
11
IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT
具体资料不详
12
IMAGE_DIRECTORY_ENTRY_IAT
导入函数地址表
13
IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT
具体资料不详
14
IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR
具体资料不详
15
未使用
-
-
-
节表:所有节的地址,属性等信息被定义在节表中。一个节表对应一个节,却2者的顺序一样,节表由一系列的IMAGE_SECTION_HEADER排列而成。一个IMAGE_SECTION_HEADER描述一个节。节表最终以一个全0的IMAGE_SECTION_HEADER结尾。所以IMAGE_SECTION_HEADER要比节多一个。
-
IMAGE_SECTION_HEADER结构包括节的名称(不能有重复),节被装入内存的RVA,节在磁盘文件中所处的位置,节属性(PAGE_READERWRITE...)
-
-
节
到此,PE文件的基本结构就理完了。下面再介绍下导入表。用例子来说明下。先贴一段反汇编代码出来:
:00401000 6A 00 push 00000000
:00401002 6800304000 push 00403000
:00401007 680F 304000 push 0040300F
: 0040100C 6A 00 push 00000000
:0040100E E807000000 Call 0040101A ;MessageBox
:00401013 6A 00 push 00000000
:00401015 E806000000 Call 00401020 ;ExitProcess
: 0040101A FF2508204000 Jmp dword ptr [00402008]
:00401020 FF2500204000 Jmp dword ptr [00402000]
这里的对MessageBox和ExitProcess的调用都是Call到一段子程序,而子程序的第一条指令都是一个跳转。现在我们再来看下0x00402008和0x00402000处到底是什么 。首先得先看下这个地址在什么段中
----------------------------------------------------------
节区名称 节区大小 虚拟地址 Raw_尺寸 Raw_偏移 节区属性
----------------------------------------------------------
.text 00000026 00001000 00000200 00000400 60000020
.rdata 00000092 00002000 00000200 00000600 40000040
.data 00000022 00003000 00000200 00000800 C 0000040
发现是.rdata段,而这个段的Raw_偏移是0x00000600,现在找一个16进制编辑器打开PE文件来看看这个地址到底是什么:
0600 76 20 00 00 00 00 00 00 -5C 20 00 00 00 00 00 00 v ....../ ......
0610 54 20 00 00 00 00 00 00-00 00 00 00 6A 20 00 00 T ..........j ..
0620 08 20 00 00 4C 20 00 00-00 00 00 00 00 00 00 00 . ..L ..........
0630 84 20 00 00 00 20 00 00-00 00 00 00 00 00 00 00 . ... ..........
0640 00 00 00 00 00 00 00 00-00 00 00 00 76 20 00 00 ............v ..
0650 00 00 00 00 5C 20 00 00-00 00 00 00 BB 01 4D 65 ..../ ........Me
0660 73 73 61 67 65 42 6F 78-41 00 55 53 45 52 33 32 ssageBoxA.USER32
0670 2E 64 6C 6C 00 00 75 00-45 78 69 74 50 72 6F 63 .dll..u.ExitProc
0680 65 73 73 00 4B 45 52 4E-45 4C 33 32 2E 64 6C 6C ess.KERNEL32.dll
0690 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
这个地址的内容是0x0276,显然不是真实函数的地址,但是如果把它看成是一个RVA,就是0x00000276,再找到对应到PE文件中的地址0x00000676,找到这个地址,发现再过去两个字节就是这个函数的函数名(ExitProcess)。现在整理下,首先Call a,然后程序跳到Jmp xxxxxxxx的地方,而xxxxxxxx又是指向这个函数的函数名的字符串。似乎没有任何意义。但是如果告诉你,当PE文件被装载的时候,WINDOWS装载器会根据这个用户名找到真正的函数地址,并且把xxxxxxxx替换成真正的函数地址,那么问题就迎刃而解了。
接下来就说说,如何查找导入表,以及导入表中的数据是如何组织,以便windows装载器能够顺利的进行完转换工作。
首先导入表的地址可以通过查找IMAGE_OPTIONAL_HEADER32的DataDirectory成员的第2个IMAGE_DATA_DIRECTORY来得到RVA。现在就得到了导入表的数据块,导入表数据块由一系列的IMAGE_IMPORT_DESCRIPTOR组成(一个dll模块对应一个结构,最终以一个全0的结构结束)。
IMAGE_IMPORT_DESCRIPTR最重要的两个成员就是Name1和FirstThunk。Name1就是dll的名字(如Kernal32.DLL)而FirstThunk是经过一系列的转换后,最终就是真实函数的地址。