PE文件结构部分解析以及输入的定位

 

此图非常详细,可以看到每个字段的内容和大概含义,根据此图动手实践了PE文件结构,下面给出我自己理解的简约结构:

为了更加形象生动,特把实验过程记录如下,从实验过程中我们可以发现不小细节: 

使用一个简单的PE文件举例,下图是PE文件概要信息:

 

图中没有列出DOS头信息,直接跳到了NT头的Image_File_Header,其中Machine=014C,有四个段,可选头大小为EO;其他是一些标志位。

接下来是可选头部分image base=400000也就是镜像文件载入地址为400000,

段落对齐1000(1024字节),文件对齐200(512字节),还有一些堆栈默认大小;

接下来是可选头里面的Data Directory数据目录,这个目录表明了导入表的虚拟地址:

2244(此表在PE文件是.rdata段,地址:E44,.rdata的地址为C00,也就是.rdata偏移244就可找到导入表,

因为.rdata虚拟地址是2000所以导入表虚拟地址为2000+244=2244),以及导出表、IAI等的虚拟地址计算方法同上。

 

接下来就是一个段表,里面标示了有几个段落,每个段落的PE文件地址是多少,大小多少,虚拟地址多少,虚拟地址空间中大小多少等信息。

.txt段落PE地址400,大小800,虚拟地址1000,大小7B6,为什么大小一会儿是800,一会儿是7B6呢?

7B6为段实际大小,PE文件要求段对齐为200,400+7B6 = BB6,那么下一个段起址是BB7,不符合200的倍数关系,

C00才是200的倍数,因此填充了一些无用的0使下一个段地址为200的整数倍;

这个段放的都是代码,实际映射到进程的地址空间是1000。

.rdat段,PE地址C00,大小800,虚拟地址2000,大小656。这个段放的是一些只读量,里面就有导入表中的函数虚拟地址的值。

因此修改这个值就可以HOOK API;还有字符串常量,例如char * a = "wo shi shui",这个字符串就放在这里面。

.data PE地址1400,大小200,虚拟地址3000,大小394,这里394>200是因为有些变量没有分配空间,

所以没有占磁盘空间,但是到虚拟地址中就必须为其预留空间。

也就是说PE文件将全局变量压缩存储了。这个段都是数据,有全局初始化数据和未初始化数据,有静态变量。

.rsrc PE地址1600,大小200,虚拟地址4000,大小1AC。这个段是资源段,可能用来存放图片。 

在接下来就是各个具体段的数据了.txt,.rdat,.data,.rsrc,而导入表信息在.rdat里面存放着。

接下来是填充,为了让PE文件512字节对齐,从而进行填充。

然后PE文件结束了。 

好了,下面来具体看看PE文件:

下图是用WINHEX打开PE文件后的实际情况:

下面是NT头开始的数据,包含了image file header、image optional header、四个段的信息表、以及四个段的具体数据

上图已经圈出了头部的数据结构,每个颜色代表一个头,最下面是各段的具体数据,当然首先是被全0填充的,直到400才开始是真实的段数据。

测试代码如下:,此代码最终生成dlltest.exe文件,并调用test.dll,主要是打印出EXE所在的堆栈地址,DLL和EXE如何协作。

#include "stdafx.h"
#include "../TestDelayDll/TestDelayDll.h"
char abc;

int cba = 0x0000a0a0;

int _tmain( int argc, _TCHAR* argv[ ] )
{
  int b = 0x1111;
  char *ok = "wo shi shui";
  
  char *xx = new
  char[4096];
  printf( "[exe]no-init=%X, init=%X, stack=%X, heap=%X, %X:%s\r\n", &abc, &cba,
    &b, xx, ok, ok );
//load
  fnTestDelayDll( );
  
  delete []xx;

  getchar( );
  
  return 0;
}

test.dll主要代码:
int fnTestDelayDll()
{ 
  int sp = 0xabcd;
  char *a = new char[1024];
  printf("[dll]no-init=%X init=%X stack=%X heap=%X\r\n", &nTestDelayDll, &g_testValue, &sp, a);
  delete []a;
  return 42;
}

 

上面两段代码可能会被编译器优化掉,因此你必须关闭编译器优化才能看到真实的情况。

结果如下:

进程的内存结构如下(我自己整理的,可能不是很对,堆是各个线程公用的,栈是一个线程一个,用完还给系统):

 

我自己的理解是这样的,在执行一个EXE程序时,父进程负责为EXE创建进程地址空间,并将EXE文件映射到此空间,当然是以EXE文件所在的存储器为后备存储器,映射过程中是有一定流程的,首先将PE文件头(这个头包含了所有的小头)直接映射到400000开始的虚拟地址空间,然后根据头中的信息将4个段分别映射到地址空间,注意,这里的映射并不是和PE文件存储结构一致,而是根据PE头中的信息进行映射,例如.txt段在PE中偏移是400,那么按道理应该映射到400000+400=400400这是地址,可是实际上是映射到401000地址,就是因为PE头中已经指示了必须映射到此地址,其他各段以此类推。

 

映射完成后,系统开始找导入表,按照导入表中的信息,导入EXE依赖的DLL,例如需要导入Test.dll文件,这时候执行的映射和EXE一致,只不过地址为定位从10000000开始映射,实际上具体的系统可能不是从这边映射,因为一般我们编写的DLL都从10000000开始映射,这样2个以上的DLL会出现重叠,因此这时候系统会重定位该DLL,重定位DLL后,因为载入地址不在是10000000,所以DLL代码中所有可能引用地址的地方都需要更新,这时候系统会找到DLL的重定位段,根据重定位段记录的地址表,逐个更新地址。

对于DLL重定位我也做了一实验,让testdll.exe加载了两个DLL文件,这两个DLL文件镜像载入的虚地址都是10000000,测试发现,第一个DLL被正常映射到10000000地址,一切都是按照PE约定的虚拟地址进行映射,而第二个DLL镜像被映射到了3b0000地址,也就是被映射到了exe镜像的上面,哈哈,.txt代码段本应该是10001000的,却被映射到了3b1000地址,看来DLL镜像在映射的时候被重定位了,并且还发现,原来代码段中的内容被改掉了,果然是系统修复了地址,如图:

 

原始汇编:

看来DLL重定位确实会动态修改汇编代码段内容.

 

posted @ 2015-09-06 17:35  IAmAProgrammer  阅读(2170)  评论(0编辑  收藏  举报