一、前言
PE是Portable Executable File Format
的简称,它是Windows系统上主要的可执行文件格式。
我们常看到的.exe
、.dll
等程序,就是这种格式的。
PE格式也有32位与64位的区别,但64位并没有引入新的结构,只是简单的将原本的32位字段扩展为了64位而已。
一个PE文件的结构大致如下图:
注意是从下往上看的。
二、基本概念
PE格式的文件本身也是一个文件,和你看到的.txt
文件是一样的,同样存储在硬盘上,并且就放在一个.exe
文件内。
所以很自然的,我们就可以得出结论,我们在代码中写的所有代码与数据,都会被统一按一定格式放入这一个文件中。
放在一起就很容易出问题,比如你解析的时候,这都是一堆二进制数据,你怎么知道这是代码还是数据呢?
所以PE格式的作用就出现了,它会将不同类型的数据存放在不同的块中,也就是上图的Section
,也被称为区段、节等。
为了方便解析,每个块的大小都是页大小的整数倍,这里的页大小是当前系统分配的,我们不用管,其目的是为了提高系统检索数据的速度。
也正因如此,各个块之间是首位相连的,因为它们占用的大小实际上就是占用多少个页大小,比如页大小为4kb,那么它们的开始位置与结束位置都很容易被计算出来。
同时为了能够判断出某个区块中数据是什么类型,每个区块都会进行记录,比如该区块是否包含代码,是否只读,或者可读可写。
然后当你双击这个可执行文件时,系统就会按照其存储的顺序,将其载入内存中去:
正如上图所示,它并不会直接将其复制进内容,而是会调整一下各个区块的大小后,再填入内容,至于各个区块之间多出来的数据,则会用0填充。
这样做的原因是硬盘的页大小与内存的页大小并不完全相同,为了提高速度,内存的页大小比硬盘的页大小要大。
所以总体看上去,就是将原本的数据‘拉伸’了一下,但数据本身并没有发生变化。
一般PE文件被载入内存后,就被称为了模块(Module),而模块句柄,实际上就是文件被载入内存后的起始地址。
在编程中,我们可以通过这个句柄访问其内存结构数据,而这个起始地址,也被称为基地址(ImageBase)。
在代码中,我们可以通过函数GetModuleHandle
来获取指定名称的模块句柄,如果传入NULL
,则是获取当前可执行文件的模块句柄。
值得注意的是,虽然这个起始地址,也就是基地址会有一个默认的地址,一般为0x400000h
,而dll一般会被默认加载到地址0x10000000h
,但这个地址是可以被改动的。
所以一般我们逆向数据的时候,并不会去寻找某个数据、函数的具体地址,而是会去找它相对于基地址偏移了多少。