加载不了Windows PE系统文件,每次都在同一个位置不动了?

参考书籍:《WindowsPE文件权威指南》
EXE文件与DLL文件之间的区别完全是语义上的,二者PE结构完全相同。唯一区别用一个字段标示处这个文件是exe还是dll。许多DLL扩展,如OCX控件,控制面板等都是DLL,它们有一样的实体。

64位的Windows只是对PE格式做了一些简单的修饰,新格式叫PE32+。没有新的结构加进去,其余的改变只是简单地将以前的32位字段扩展为64位字段。

1.PE文件基本结构:


PE文件的头分为DOS头、NT头、节头。注意,这是本人的分法。这样分法会更加合理,更易理解。因为这三个部分正好构成SizeOfHeaders所指的范围,所以将它们合为“头”。

用记事本打开任何一个镜像文件,其头2个字节必为字符串“MZ”,这是Mark Zbikowski的姓名缩写,他是最初的MS-DOS设计者之一。然后是一些在MS-DOS下的一些参数,这些参数是在MS-DOS下运行该程序时要用到的。在这些参数的末尾也就是文件的偏移0x3C(第60字节)处是是一个4字节的PE文件签名的偏移地址。该地址有一个专用名称叫做“E_lfanew”。这个签名是“PE00”(字母“P”和“E”后跟着两个空字节)。紧跟着E_lfanew的是一个MS-DOS程序。那是一个运行于MS-DOS下的合法应用程序。当可执行文件(一般指exe、com文件)运行于MS-DOS下时,这个程序显示“This mode(此程序不能在DOS模式下运行)”这条消息。用户也可以自己更改该程序,有些还原软件就是这么干的。同时,有些程序既能运行于DOS又能运行于Windows下就是这个原因。Notepad.exe整个DOS头大小为224个字节,大部分不能在DOS下运行的Win32文件都是这个值。MS-DOS程序是可有可无的,如果你想使文件大小尽可能的小可以省掉MS-DOS程序,同时把前面的参数都清0。

注意:DOS头后是一个整个DOS Stub字节块,其内容随使用的链接器的不同而不同,PE中并没有与之相关的结构

紧跟着PE文件签名之后,是NT头。NT头分为三个部分,1.PE文件标识2.PE文件头3.PE扩展头


(1)PE文件标准PE头(用于判断PE文件是exe还是dll,得到节的总量)

(2)PE扩展头(虽然是扩展头,但更像真正的PE头,非常重要,包含各种属性)

DWORD ImageBase; //映象(加载到内存中的PE文件)的基地址,这个基地址是建议,对于DLL来说,如果无法加载到这个地址,系统会自动为其选择地址。 DWORD SectionAlignment; //内存中节对齐粒度,PE中的节被加载到内存时会按照这个域指定的值来对齐,比如这个值是0x1000,那么每个节的起始地址的低12位都为0。 DWORD SizeOfImage; //映象的大小,PE文件加载到内存中空间是连续的,这个值指定占用虚拟空间的大小。


2.3.2 节表项的数据结构

}
1)准备要伪装的安装包以及CS木马

创建自解压文件,以真实安装包名字命名

    设置覆盖模式,如果解压目标目录下存在该文件则进行覆盖

    解压后运行自解压文件中真实的安装包和恶意文件,使用静默模式,以达成无感知效果

    利用ResourceHacker工具(该工具可查看,修改,添加,删除和重命名,提取Windows可执行文件和资源文件的资源替换工具)修改自解压文件资源

    将真正安装包内版本信息复制到自解压文件中后点击箭头所指按钮

    利用windows目录默认显示文件名长度特性,进行修改将文件名加长隐藏后缀名。

(简单可执行文件) .cpl(控制面板扩展) .src(屏幕保护程序)这些都属于鼠标点击可以直接运行的程序,在邮件钓鱼中主要解决的问题有三个:
  •     只需要受害者打开test.rar文件就会自动在受害者当前用户的启动选项下生产一个后门文件hi.exe,路径:

    RCE。因为大部分情况下js都无法有权限操作主机,这是风险很高的行为。但是在node.js环境下就不一样,若是集成了node.js后端的程序或者是软件,就可以使用Node.js来调用shellcode来实现恶意攻击。

    本公众号的下一篇还会发邮件攻击题材的文章,希望各位大佬指点呀,如果满意可以点点关注、再看呀!回见!【本文作者like粉色】


}

什么是PE文件及PE文件的结构和简述

一个操作系统的可执行文件格式在很多方面是这个系统的一面镜子。虽然学习一个可执行文件格式通常不是一个程 序员的首要任务,但是你可以从这其中学到大量的知识。在这篇文章中,我会给出 MicroSoft 的所有基于 win32系统(如winnt,win9x)的可移植可执行(PE)文件格式的详细介绍。在可预知的未来,包括 Windows2000 , PE 格式已经普遍应用,并且在不短的将来仍是不可避免的。现在是时候找出这种新的可执行文件格式为操作系统带来的东西了。
我最后不会让你盯住无穷无尽的十六进制Dump,也不会详细讨论页面的每一个单独的位的重要性。代替的,我会向你介绍包含在 PE 文件中的概念,并且将他们和你每天都遇到的东西联系起来。比如,线程局部变量的概念,如下所述:
我快要发疯了,直到我发现它在可执行文件中实现起来是如此的简单并且优雅。既然你们中的许多人都有使用 16 Windows 的背景,我将把 Win32 PE 文件的构造追溯到和它等价的16 位 NE 文件。
除 了一个不同的可执行文件格式, MicroSoft 还引入了一个用它的编译器和汇编器生成的新的目标模块格式。这个新的 OBJ 文件格式有许多和PE 文件共同的东东。我做了许多无用功去查找这个新的 OBJ 文件格式的文档。所以我以自己的理解对它进行解析,并且,在这里,除了 PE 文件,我会描述它的一部分。
大家都知道,Windows NT 继承了 VAX? VMS? 和 UNIX? 的传统。许多 Windows NT 的创始人在进入微软前都在这些平台上进行设计和编码。当他们开始设计 Windows NT 时,很自然的,为了最小化项目启动时间,他们会使用以前写好的并且已经测试过的工具。用这些工具生成的并且工作的可执行和 OBJ 文件格式叫做 COFF (Common Object File Format 的首字母缩写)。COFF 的相对年龄可以用八进制的域来指定。COFF 本身是一个好的起点,但是需要扩展到一个现代操作系统如 Windows 95 和 Windows NT 的需要。这个更新的结果就是(PE格式)可移植可执行文件格式。它被称为"可移植的"是因为在所有平台(如x86,Alpha,MIPS等等)上实现的 WindowsNT 都使用相同的可执行文件格式。当然了,也有许多不同的东西如二进制代码的CPU指令。重要的是操作系统的装入器和程序设计工具不需要为任何一种CPU完全 重写就能达到目的。
MicroSoft 抛弃现存的32位工具和可执行文件格式的事实证实了他们想让 WindowsNT 升级并且运行的更快的决心。为16位Windows编写的虚拟设备驱动程序用一种不同的32位文件布局--LE 文件格式--WindowsNT出现很早以前就存在了。比这更重要的是对 OBJ 文件的替换!在 WindowsNT 的 C 编译器以前,所有的微软编译器都用 Intel 的 OMF 的公司为了使用多个不同的编译器,不得不为每个不同的编译器分发这些库的不同版本(如果他们不这么做)。
PE 文件格式在 winnt.h 头文件中文档化了(用最不精确的语言)!大约在 winnt.h 的中间部分标题为"Image Format"的一个快。在把 MS-DOS 的 MZ 文件头和 NE 文件头移入新的PE文件头之前,这个块就开始于一个小栏。WINNT.H提供PE文件用到的生鲜数据结构的定义,但只有很少有助于理解这些数据结构和标志 变量的注释。不管谁为PE文件格式写出这样的头文件都肯定是一个信徒无疑(突然持续地冒出Michael J. O'Leary的名字来)。描述名字,连同深嵌的结构体和宏。当你配套winnt.h进行编码时,类似下面这样的表达式并不鲜见:
为了有助于逻辑的理解这些winnt.h中的信息,阅读可移植可执行和公共对象文件格式的规格说明,这些在MSDN既看光盘中是可用的,一直包括到2001年8月。
现 在让我们转换到COFF格式的OBJ文件的主体上来,WINNT.H包括COFF OBJ和LIB的结构化定义和类型定义。不幸的是,我还没有找到上面提到的可执行文件格式的类似文档。既然PE文件和COFF OBJ文件是如此的相似,我决定是时间把这些文件带到重点上来,并且把它们也文档化。仅仅读过了关于PE文件的组成,你自己也想Dump一些PE文件来看 这些概念。如果你用微软基于32位WINDOWS的开发工具,DUMPBIN 程序可以将PE文件和COFF OBJ/LIB文件转化为可读的形式。在所有的PEDump器中,DUMPBIN是最容易理解的。它恰好有一些很好的选项来反汇编它正解析的文件的代码 块,Borland用户可以使用tdump来浏览PE文件,但tdump不能解析 COFF OBJ/LIB 文件。这不是一个重要的东西因为Borland的编译器首先就不生成 COFF 格式的OBJ文件。
我写了一个PE和COFF OBJ 文件的Dump程序--PEDUMP(见表1),我想提供一些比DUMPBIN更加可理解的输出。虽然它没有反汇编器以及和LIB库文件一起工作,它在其 他方面和DUMPBIN是一样的,并且加入了一些新的特性来使它值得被认同。它的源代码在任何一个MSJ电子公报版上都可以找到,所有我不打算在这里把他


让我们复习一下几个透过PE文件的设计了解到的基本概 念(见图1)。我用术语"MODULE"来表示一个可执行文件或一个DLL载入内存的代码(CODE)、数据(DATA)、资源(RESOURCES), 除了代码和数据是你的程序直接使用的,一个模块还可以由WINDOWS用来确定数据和代码载入的位置的支撑数据结构组成。在16位WINDOWS中,这些 支撑数据结构在模块数据库(用一个HMODULE来指示的段)中。在WIN32里面,这些数据结构在PE文件头中,这些我将会简要地解释一下。

关于PE文件最重要的是,磁盘上的可执行文件和它被WINDOWS调入内存之后是非常相像的。 WINDOWS载入器不必为从磁盘上载入一个文件而辛辛苦苦创建一个进程。载入器使用内存映射文件机制来把文件中相似的块映射到虚拟空间中。用一个构造式 的分析模型,一个PE文件类似一个预制的屋子。它本质上开始于这样一个空间,这个空间后面有几个把它连到其余空间的机件(就是说,把它联系到它的DLL 上,等等)。这对PE格式的DLL是一样容易应用的。一旦这个模块被载入,Windows 就可以有效的把它和其它内存映射文件同等对待。
和16 位Windows不同的是。16位NE文件的载入器读取文件的一部分并且创建完全不同的数据结构在内存中表示模块。当数据段或者代码段需要载入时,载入器 必须从全局堆中新申请一个段,从可执行文件中找出生鲜数据,转到这个位置,读入这些生鲜数据,并且要进行适当的修正。除此而外,每个16位模块都有责任记 住当前它使用的所有段选择器,而不管这个段是否被丢弃了,如此等等。
对Win32来讲,模块所使用的所有代码,数据,资源,导入表,和其它需要的模块数据结构都在一个连续的内存块中。在这种形势下,你只需要知道载入器把可执行文件映射到了什么地方。通过作为映像的一部分的指针,你可以很容易的找到这个模块所有不同的块。
另 一个你需要知道的概念是相对虚拟地址(RVA)。PE文件中的许多域都用术语RVA来指定。一个RVA只是一些项目相对于文件映射到内存的偏移。比如说, 载入器把一个文件映射到虚拟地址0x10000开始的内存块。如果一个映像中的实际的表的首址是0x10464,那么它的RVA就是0x464。
为 了把一个RVA转化成一个有用的指针,只需要把RVA值加到模块的基地址上即可。基地址是内存映射EXE和DLL文件的首址,在Win32中这是一个很重 要的概念。为了方便起见,WindowsNT 和 Windows9x用模块的基地址作为这个模块的实例句柄(HINSTANCE)。在Win32中,把模块的基地址叫做HINSTANCE可能导致混淆, 因为术语"实例句柄"来自16位Windows。一个程序在16位Windows中的每个拷贝得到它自己分开的数据段(和一个联系起来的全局句柄)来把它 和这个程序其它的拷贝分别开来,就形成了术语"实例句柄"。在Win32中,每个程序不必和其它程序区别开来,因为他们不共享相同的地址空间。术语 译注:如果 dllname 为 NULL,则得到执行体自己的模块句柄。这是非常有用的,如通常编译器产生的启动代码将取得这个句柄并将它作为一个参数hInstance传给WinMain !
你 最终需要理解的PE文件的概念是"块(Section)"。PE文件中的一个块和NE文件中的一个段或者资源等价。块可以包含代码或者数据。和段不同的 是,块是内存中连续的空间,而没有尺寸限制。当你的连接器和库为你建立,并且包含对操作系统非常重要的信息的其它的数据块时,这些块包含你的程序直接声明 和使用的代码或数据。在一些PE格式的描述中,块也叫做对象。术语对象有如此多的涵义,以至于只能把代码和数据叫做"块"。
和 其它可执行文件格式一样,PE文件在众所周知的地方有一些定义文件其余部分面貌的域。首部就包含这样象代码和数据的位置和尺寸的地方,操作系统要对它进行 干预,比如初始堆栈大小,和其它重要的块的信息,我将要简短的介绍一下。和微软其它可执行格式相比,主要的首部不是在文件的最开始。典型的PE文件最开始 的数百个字节被DOS残留部分占用。这个残留部分是一个可以打印如"这个程序不能在DOS下运行!"这类信息的小程序。所以,你在一个不支持Win32的 系统中运行这个程序,便可以得到这类错误信息。当载入器把一个Win32程序映射到内存,这个映射文件的第一个字节对应于DOS残留部分的第一个字节。那 是无疑的。和你启动的任一个基于Win32

连接器产生这个文件的日期(对OBJ文件是编译器),这个域保存的数是从1969年12月下午4:00开始到现在经过的秒数。

关于这个文件信息的标志。一些重要的域如下:

0x0001 这个文件中没有重定位信息
0x0002 可执行文件映像(不是OBJ或LIB文件)
0x2000 文件是动态连接库,而非程序

所有代码块的进位尺寸。通常大多数文件只有一个代码块,所以这个域和 .TEXT 块匹配。

已初始化的数据组成的块的大小(不包括代码段)。然而,和它在文件中的表现形式并不一致。

载入器在虚拟内存中申请空间,但在磁盘上的文件中并不占用空间的块的尺寸。这些块在程序启动时不需要指定初值,因此术语名就是"未初始化的数据"。未初始化的数据通常在一个名叫 .bss 的块中。

代码块起始地址的RVA 。在内存中,代码块通常在PE首部之后,数据块之前。在微软的连接器产生的EXE文件中,这个值通常是0x1000 。Borland 的连接器 TLINK32 也一样,把映像第一个代码块的RVA和映像基址相加,填入这个域。
译注:这个域好像一直没有什么用

数据块起始地址的RVA 。在内存中,数据块经常在最后,在PE首部和代码块之后。
译注:这个域好像也一直没有什么用

连接器创建一个可执行文件时,它假定这个文件被映射到内存中的一 个指定的地方,这个地址就存在这个域中,假定一个载入地址可以使连接器优化以便节省空间。如果载入器真的把这个文件映射到了这个地方,在运行之前代码不需 要任何改变。在为WindowsNT 创建的可执行文件中,默认的ImageBase 是0x10000。对DLL,默认是0x40000。在Window95中,地址0x10000不能用来载入32位EXE文件,因为这个区域在一个被所有 进程共享的线性地址空间中。因此,微软把Win32可执行文件的默认基址改为0x40000,假定基址为0x10000 的老程序坐在Windows95 中需要更长的载入时间,这是因为载入器需要重定位基址。
译注:这个域即"Prefered Load Address",如果没有什么意外,这就是该PE文件载入内存后的地址。

在PE文件中,组成每个块的生鲜数据必须保证开始于这个 值的整数倍。默认值是0x200 字节,也许是为了保证块都开始于一个磁盘扇区(一个扇区通常是 512 字节)。这个域和NE文件中的段/资源对齐(segment/resource alignment)尺寸是等价的。和NE文件不同的是,PE文件通常没有数百个的块,所以,为了对齐而浪费的通常空间很少。

载入器必须关心的这个映像所有部分的大小总和。是从映像的开始到最后一个块结尾这段区域的大小。最后一个块结尾按SectionAlignment进位。
译注:这个很重要,可以大,但不可以小!

PE首部和块表的大小。块的实际数据紧跟在所有首部组件之后。

这个文件的CRC校验和。在微软可执行格式中,这个域被忽略并且置为0 。这个规则的一个例外情况是信任服务,这类EXE文件必须有一个合法的校验和。

指定在何种环境下一个DLL的初始化函数(比如DllMain)将被调用的标志变量。这个值经常被置为0 。但是操作系统在下面四种情况下仍然调用DLL的初始化函数。


1 DLL第一次载入到进程中的地址空间中时调用
2 一个线程结束时调用
4 一个线程开始时调用

为初始线程保留的虚拟内存总数。然而并不是所有这些内存都被提交(见下一个域)。这个域的默认值是0x100000(1Mbytes)。如果你在CreateThread 中把堆栈尺寸指定为 0 ,结果将是用这个相同的值(0x10000)。

开始提交的初始线程堆栈总数。对微软的连接器,这个域默认是0x1000字节(一页),TLINK32 是两页。

为初始进程的堆保留的虚拟内存总数。这个堆的句柄可以用GetPocessHeap 得到。并不是所有这些内存都被提交(见下一个域)。

从WINNT.H中可以看到,这些标志是和调试支持相联系的。我从没有见到过在哪个可执行文件中这些位都置位了,清除它让连接器来设置它。下面的值定义为:
1. 在开始进程前调用一个端点指令
2. 进程被载入时调用一个调试器

在WINNT.H中的定义。这个数组允许载入器迅速查找这个映像的一个指定的块(例如,导入函数表),而不需要遍历映像的每个块,通过比较名字来确定。大 部分数组条目描述一整块数据。然而,IMAGE_DIRECTORY_ENTRY_DEBUG项只包括 .rdata 块的一小部分字节。


在PE首部和映像块之间的是块表。块表本质上是包含映像中每个块信息的电话本。映像中的块以他们的起始地址(RVA)排列,而不是按字母排列。
现 在,我进一步澄清什么是一个块。在NE文件中,你的程序代码和数据存储在相互区别开来的段中。NE首部的一部分是一个结构数组,每个对应你的程序用到的一 个段。数组中的每个结构包含一个段的信息。这些信息存储了段的类型(代码或数据)、大小、和它在文件中的位置。在PE文件中,块表和NE文件中的段表类 似。和NE文件的段表不同,PE块表项不存储一个代码和数据块的选择子。代替的,每个块表项存储文件的生鲜数据映射到内存中以后的地址。于是块就和32位 段类似,但他们实际上不是单独的段。它们实际上是进程虚拟空间的一个内存范围。
另一个PE文件和NE文件的不同之处是它怎样管理你的程序不用,但 操作系统要用的支持数据;例如可执行文件使用的DLL列表或修正表的位置。在NE文件中,资源不被当作段。甚至分配给他们的选择子,资源的相关信息并未存 储在NE文件首部的段表中。代替的,提交给一个分隔表的资源朝向PE首部的结尾。关于导入和导出函数的信息也没有授权给它自己的段;它交织在NE首部中。
PE文件的故事就不一样了。任何可能被认为是关键的代码或数据都存在一个完备的块中。于是,导入函数表的信息就存在它自己的块中,导出表也一样。对重定位数据也是一样的。程序或操作系统可能需要的任何代码或数据都可以得到它们自己的块。
在 我讨论特定块之前,我需要先描述操作系统管理这些块的数据。在内存中紧跟在PE首部的是一个IMAGE_SECTION_HEADER数组。数组的元素个 数在PE首部中给定(IMAGE_NT_HEADER.FileHeader.NumberOfSections域)。我用PEDUMP来输出块表和块的 所有的域及其属性。表5 描述了用PEDUMP输出的一个典型EXE文件的块表,表6

每个IAMGE_SECTION_HEADER都有一个如图7 描述的格式。注意每个块中存储的信息缺失了什么是很有趣的。首先,注意没有指明任何预载入的属性。NE文件格式允许你指定应该和模块一起载入的预载入段的 属性。OS/2? 2.0 LX 格式有点类似,允许你指定预载入八页(内存页:译注,下同) 。PE格式就没有任何类似的东西。微软必须确保Win32 这 是一个为块命名的8字节ANSI名字(不UNICODE)。大部分块名开始于一个 ". "(比如".text"),但这并非必须的,就像你可能相信的一些PE文档一样。你可以在汇编语言中用任何一个段指示你自己的块。或者在微软C/C++编 译器中用"#pragma data_seg"来指示。需要注意的是如果块名占满8个字节,就没有NULL结束字节了。如果你热衷于 printf ,你可以用 %8s来避免把这个名字拷贝到一个缓冲区中,然后又在结尾加上一个NULL字节。
在EXE 和OBJ中,这个域的意义不同。在EXE中,它保存代码或者数据的实际尺寸。这个尺寸是未经过校准文件对齐尺寸并进位的。后面要讲到的这个结构的 SizeOfRawData 域(这个词有点不确切)保存了校准文件对齐尺寸并进位后的尺寸。Borland 的连接器调换了这两个域的意思,于是看上去就是正确的了。对OBJ文件,这个域指示块的物理尺寸。第一个块开始于地址0 。为找到OBJ 文件中的下一个块,把SizeOfRawData加到当前块基址上即可。

在EXE中,这个域保存决定载入器把这个块映射到内存 中哪个位置的RVA 。为计算一个给定的块在内存中的实际起始地址,把这个映像的基址加上存储在这个域的VirtualAddress即可。用微软的工具,第一个块的默认 RVA是0x1000 。在OBJ文件中,这个域没有意义,被置为0 。

在EXE中,这个域包含这个块按文件对齐尺寸进位后的尺 寸。比如说,假定一个文件的对齐尺寸是0x200 。如果这个块的VirtualAddress域(前面那个域)的是0x35a ,那么这个域就是0x400 。在OBJ文件中,这个域包含由编译器或汇编器提供的块的精确尺寸。换句话说,对OBJ ,它等价于EXE中的VirtualSize域。

这是一个基于文件的偏移,通过这个偏移,可以找到 由编译器或汇编器产生的生鲜数据。如果你的程序自己要把一个PE或COFF文件映射到内存(而不是让操作系统来载入),那么这个域比 VirtualAddress更重要。在这种情况下你有一个完全线性的文件映射,所以你会在这个偏移处找到块的数据,而不是在 VirtualAddress域指定的RVA 处找到。
在OBJ中,这是指向块 的重定位信息的基于文件的偏移值。每个OBJ块的重定位信息紧跟在这个块的生鲜数据之后。在EXE中,这个域(和后面的)是没有意义的,被置为0 。连接器产生EXE时,它解决了大部分的这种修正值,只剩下基址的重定位和导入函数,将在载入时解决。关于基本重定位信息和导入函数保留在他们自己的块 中,所以对一个EXE ,没有必要在每个块的生鲜数据之后都紧跟它的重定位信息。

这是行号表基于文件的偏移量。行号表把源 文件的一行和(编译器)为这一行产生的(机器)代码的首址联系起来。在如CodeView格式的现代调试格式中,行号信息存储为调试信息的一部分。然而, 在COFF调试格式中,行号信息和符号名/型信息的存储是分开的。通常只有代码块(如 .text )有行号信息。在EXE文件中,行号信息在块的生鲜数据之后,朝着文件的结尾方向收集。在OBJ文件中,一个块的行号信息跟在生鲜块数据和这个块的重定位 表之后。

大部分程序员的称之为标志,COFF/PE格式称之为特征。这个域是指示块属性的标志集(如代码/数据,可读,可写)。一个对所有可能的块属性的完整的列表,见WINNT.H中的IMAGE_SCN_XXX_XXX的定义。如下是比较重要的一些标志:

0x 这个块包含代码。通常和可执行标志(0x)一起置位。
0x 这个块包含已初始化的数据。除了可执行块和 .bss 块之外几乎所有的块的这个标志都置位。
0x 这个块包含未初始化的数据(如 .bss 块)
0x 这个块包含注释或其它的信息。这个块的一个典型用法是编译器产生的 .drectve 块,包含链接器命令。
0x 这个块的内容不应放进最终的EXE文件中。这些块是编译器或汇编器用来给连接器传递信息的。0x 这个块可以被丢弃,因为一旦它被载入,其进程就不需要它了。最通常的可丢弃块是基本重定位块( .reloc )。
0x 这个块是可共享的。和DLL一起使用时,这个块的数据可以在使用这个DLL的进程之间共享。默认时数据块是非共享的,这意味着使用这个DLL的各个进程都 有自己对这个块的数据的副本。在更专业的术语中,共享块告诉内存管理器把使用这个DLL的所有进程把的这个块的页面映射到内存中相同的物理页面。为使一个 块可共享,在连接时用SHARE属性。如:
告诉连接器叫做"MYDATA"的块是可读的,可写的,共享的。
0x 这个块是可执行的。这个标志通常在"包含代码"标志(0x)被置位时置位。
0x 这个块是可读的。在EXE文件中,这个域几乎总被置位。
0x 这个块是可写的。如果在一个EXE块中这个块未被置位,载入器会把这块的内存映射页面标为只读或"只执行"。有此属性的典型的块是 .data 和 .bss 。有趣的是,.idata 块也有这个属性。
PE 格式中还缺少"页表"的概念。在LX格式中,OS/2的IMAGE_SECTION_TABLE等价物不直接指向文件中的代码或数据块。代替的,它指向一 个指示块中特定范围的属性和位置的页查找表。PE格式分配所有的,并且确保所有的块中的数据将连续的存储在文件中。比较这两种格式:LX可以允许更大的灵 活性,但PE风格更简单,更容易协同工作。我已经写了这两种文件的Dumper 。
PE格式另一个值得欢迎的改变是所有项目的位置都存储为简单的 双字(DWORD)偏移。在NE格式中,几乎所有东西的位置都存储为它们的扇区值。为了得到实际的偏移,你第一步需要查找NE首部的对齐单元尺寸并把它转 化为扇区尺寸(典型的是 16 和512 字节)。然后你需要把扇区尺寸乘以指定的扇区偏移才得到实际的文件偏移。如果NE文件的某些东西偶然存储为一个扇区偏移,这可能是相对于NE首部的。因为 NE首部并不在文件的开始,你需要在自己的代码中调整这个文件的NE首部。总之,PE格式比NE,LX,或LE格式更容易协同工作(假定你能使用内存映像 文件)。

已经看到了大体上块是什么和它们位于何处,让我们看一下你将会在EXE和OBJ文件中找到的通用块。这个列表决不是完整的,但包含了你每天都碰到的块(甚至你没有意识到的)。
.text 块是编译器或汇编器结束时产生的通用代码块。因为PE文件运行在32位模式下,并且没有16位段的限制,没有理由根据分开的源文件把代码分为分开的块。代 替的,连接器把从不同的OBJ文件得来的 .text 块连接起来放到EXE文件中的一个大 .text 块中。如果你用 Borland C++ ,编译器把产生的代码放到名为 CODE 的块中。Borland C++ 生成的PE文件有一个名为 CODE 的块而不是名为 .text 。我将会简短的解释一下。

对 我来说,除了我用编译器创建的或从运行时库中得到的代码外,在 .text 块中找到附加的代码是比较有趣的。在一个PE文件中,当你在另一模块中调用一个函数时(比如在USER32.DLL中的GetMessage ),编译器产生的CALL 指令并不把控制直接转移到在DLL中的这个函数(见图8)。代替的,CALL 指令把把控制转移到一个也在 .text 中的
指 令处。这个 JMP 指令(译注1)通过一个在 .idata 中的DWORD变量间接的转移控制。 .idata 块的DWORD包含操作系统函数入口的实际地址。在对这进行一会儿回想之后,我开始理解为什么DLL调用用这种方式来实现。通过一个位置传送所有的对一个 给定的DLL函数的调用,载入器不需要改变每个调用DLL的指令。所有的PE载入器必须做的是把目标函数的正确地址放到 .idata 的一个 DWORD 中。不需要改变任何call指令。在NE文件中就不同了,每个段都包含一个需要应用到这个段上的一个修正表。如果这个段把一个给定的DLL函数调用了20 次,载入器必须把这个函数的地址写入到这个段的每个调用指令中。PE方法的缺点是你不能用一个DLL函数的真实地址来初始化一个变量。比如,你要考虑这样 的情况:
开始的字节,你将不能如愿(除非你自己做跟在 .idata 指针后的工作)。后面我将会返回到这个话题上--在导入表的讨论中。
译注1:英文 thunk,正统的计算机专业术语为"形实转换程序",类似宏(macro)替换,故我将它译为"替换指示",指在具体指令中xxxxxxxx 被替换,后面出现的替换指示同。
译 注2:现在的编译器如VC6以上等等,产生的导入函数调用代码不再是先来一个相对Call指令到 jmp [xxxx] 处,然后再到 xxxx 处(真正的导入函数入口),而是用了一种效率更高,也更容易让人理解的方式:call [xxxx] 。以前用那种间接的方式多是为兼容编译器。但是现在仍有一些编译器,如MASM,直到版本7.0,还是用前面那种间接的方式,从这里也可以看出微软对 虽然 Borland 可以让编译器输出的代码块名为 .text ,但它是选择 NAME 作为默认的段名。为了确定PE文件中的块名,Borland 的连接器(TLINK32.EXE)从OBJ文件中取出段名并把它截断为8字符(如果有必要)。
当 块名的不同只是一个小问题时,Borland PE 文件怎样链接到其它模块就是一个重要的不同。就像我在 .text 的描述中提到的,所有到OBJ的调用通过一个JMP DWORD PTR [XXXXXXXX]替换指示。在微软系统下,这条指令通过一个导入库到达 .text 块。因为库管理器(LIB32)当你链接外部DLL时才创建导入库(和这个替换指示),连接器自己不需要"知道"怎样生成这这个替换指示。导入库实际上只 不过是链接到这个PE文件的一些更多的代码和数据。
Borland 处理导入函数的系统只是一个简单的16位NE文件方式扩展。Borland 连接器使用的导入库实际上只不过是一个函数名连同它所在的DLL名的列表。于是TLINK32就有责任确定外部DLL的修正,并生为它成一个适当的JMP DWORD PTR [XXXXXXXX] 替换指示 。TLINK32把这个替换指示存储在它创建的名为 .icode 块中。正像 .text 是默认的代码块,.data 块是已初始化数据的归宿。这些数据包含编译时初始化的全局和静态局部变量。它还包括文字字符串。连接器把从OBJ/LIB文件得来的所有 .data 块组合到EXE文件的一个 .data 块中。局部变量载入到一个线程的堆栈中,在 .data 或 .bss 中不占空间。
.bss 块是存储未初始化的全局和静态局部变量的地方。连接器把 OBJ/LIB 文件中的所有 .bss 块链接到EXE文件的一个 .bss 块中。在块表中,.bss 块的RawDataOffset 域置为0 ,表示这个块在文件中不占用任何空间。TLINK 不产生这个块。代替的,它扩展 DATA 块的虚拟尺寸(virtual size)。
.CRT 块是微软 C/C++ 运行时库利用的另一个已初始化数据的块(从名字)。我不能理解为什么这些数据不放在 .data 中。(译注)
译注:从CRT的字面意思看,应该是"C Run Time",即C运行时库。
.rsrc 块这个模块的所有资源。在Windows NT的早期,16位RC.EXE输出的RES文件是微软的PE连接器不能识别的格式。CVTRES 程序把这种格式的RES文件转换成COFF格式的OBJ文件,把资源数据放在 OBJ 的 .rsrc 块中。连接器就可以把这个资源OBJ当作另一个OBJ来链接了,允许连接器"知道"关于资源的特殊东西。微软最近发布的更多连接器可以直接处理RES文 .idata 块包含关于这个模块从其它DLL导入的函数(和数据)的信息(译注)。这个块和NE文件的模块引用表是等价的。一个关键的不同是PE文件导入的每个函数都明确的列在这个块中。为找到NE文件中的等价信息,你必须去挖掘这个段生鲜数据的结尾的重定位信息。
.edata 块是这个PE文件导出到其它模块的函数和数据的列表。它的NE文件等价物是条目表的联合,驻留名表,和非驻留名表,和16位Windows不一样,很少有 理由从一个EXE文件导出一些东西,所以你通常只在DLL中看到 .edata 块。当使用微软的工具时,.edata 块中的数据通过EXP文件来到PE文件中。换种方法,连接器不为它自己生成这个信息。代替的,它依赖库管理器(LIB32)来扫描OBJ文件,并创建 EXP文件,连接器要把它要链接的模块的列表加入其中。是的,好!这些麻烦的EXP文件实际上只是扩展名不同的OBJ文件而已。
.reloc 块保持一个基本重定位表。基本重定位是一个对一条指令或已初始化的变量值的调整,如果载入器不能把这个文件载入到连接器假定的位置,这就是很重要的了。如 果载入器能把这个映像载入到连接器建议(prefer)的基地址,载入器就完全忽略这个块的重定位信息。如果你愿意冒险,并且希望载入器可以始终把这个映 像载入到假定的基址,你可以通过 /FIXED 选项告诉链接器去除这个信息。这样可以在可执行文件中节省空间,但会导致这个可执行文件在其它的Win32实现中不能工作。比如,假定你为Windows NT建立了一个EXE文件,并且把基址设为 0x10000 。如果你让连接器去除重定位信息,这个EXE文件在Windows95下将不能运行,因为在这里地址0x10000已被系统使用了。
注意编译器生 成的JMP和CALL指令是很重要的,首选它使用相对偏移量的版本,而非32位平坦段中的真实偏移量版本。如果映像需要被载入非连接器假定的基址处,这些 指令不需要改变,因为它使用的是相对寻址。结果就是,并不需要你想象的那么多的重定位。重定位通常只需要使用指向一些数据的32位偏移。举个例子,让我们 看一下,你有如下的全局变量声明:
如果连接器假定一个0x10000的映像 基址,变量i的地址将最终是一个特定值如0x12004 。在用来存放指针"ptr"的内存中,连接器将写进0x12004 ,因为这是变量 i 的地址。如果载入器由于某种原因决定把这个文件载入基址0x70000处,变量i的地址将是0x72004 。.reloc 块是映像中的一些内存位置的列表,这些内存位置在连接时连接器假定的载入地址和实际需要的载入地址是不同的,这个因素需要考虑。
当你使用编译器指 令 __declspec(thread) 时,你定义的数据不在 .data 和 .bss 块种。它最终在 .tls 块中,这个块指示"线程局部存储",并且和Win32的TlsAlloc函数族相联系。处理 .tls 块时,内存管理器设置页表以便进程在任何时刻切换线程时,都有一个新的物理内存页集映射到 .tls 块的地址空间。这就允许线程内的全局变量。在大部分情况下,利用这种机制,比基于线程分配内存并把其指针存在一个 "TlsAlloc 过的"(注:原文TlsAlloc'ed)槽(注:原文Slot)中要容易的多。
不幸的是,有一点需要注意--必须深入研究.tls 块和 __declspec(thread) 的变量。在WindowsNT 和Windows95 中,如果DLL是被载入库动态载入的,这种线程局部存储机制将不能在这个DLL中工作。然而在EXE中或一个隐含载入的DLL中,一切都工作正常。如果你 不隐含链接到这个DLL ,但需要按线程的数据,你必须会到过去并使用 TlsAlloc 和 TlsGetValue 这种原始方式来设置线程动态内存分配。
虽然 .rdata 块通常在 .data 和 .bss 块之间,你的程序一般看不见并使用这些块中的数据。.rdata 块至少在两种东西中使用。第一,在微软连接器生成的EXE中,.rdata 块存放调试目录,这只在EXE文件中出现。(在 TLINK32 的 EXE 中,调试目录在名为

调试目录不必在 .rdata 块的开始找到。为找到调试目录表的开始,使用数据目录的第七个条目(IMAGE_DIRECTORY_ENTRY_DEBUG)的RVA。数据目录在文件 的PE首部结尾部分。为确定微软连接器生成的调试目录的条目数,用调试目录的尺寸(在数据目录条目的尺寸域)除以一个 .rdata 域的另一个有用的部分是"描述串"。如果你在程序的DEF文件中指定一个DESCRIPTION条目,这个指定的描述串就出现在 .rdata 块中。在NE格式中,描述串总是非驻留名表的第一个条目。描述串是用来保持一个描述这个文件的有用的文本串的。不幸的是,我还没找到一条便捷的途径来得到 它。我看到有些描述串在PE文件的调试目录之前,在另一些文件中它在调试目录之后。我找不到得到这个描述串的一致的方法(或甚至这种方法根本就不存在)。
.debug$S 和 .debug$T 块只出现在 OBJ 中。他们保存 CodeView 调试符号和类型信息。这些块名是从以前16位编译器($$SYMBOLS 和 $$TYPE)使用的段名继承来的。.debug$T 块的唯一用途是保持包含工程中所有OBJ的CodeView信息的PDB文件的路径。连接器从PDB中读取并且使用它来创建CodeView信息的组成部 分,这些CodeView信息放置在PE文件的结尾。
.drectve 块只出现在OBJ文件中。它包含用文本表示的连接器命令。比如,在我用微软编译器编译的任一OBJ中,下面的字符串都出现在 .drectve 块中:
在玩弄 PEDUMP 的过程中,我不时的遇到其它块。例如,在Window95的KERNEL32.DLL中,有LOCKCODE和LOCKDATA块。大概这是一种特殊的页处理方法,是为了避免缺页(译注)。
译注:缺页,在页式内存管理中,一条指令访问的虚拟内存未映射到物理内存中,此时将发生缺页中断,关于缺页中断,请参阅操作系统相关书籍。
从 这里学到两个教训。第一:不要以为有约束而只使用编译器或汇编器提供的标准块。如果由于某种原因你需要一个分开的块,不要犹豫,自己去创建!在C/C++ 编译器中,使用 #pragma code_seg 和 #pragma data_seg 。在汇编语言中,只不过是创建一个名字和和标准块不同的32位的段(将成为一个块)。如果使用TLINK32 ,你必须使用一个不同的类,或者关掉代码段包装(packing)。其它要记住的东西是使用非标准块名你将会更透彻的理解特殊PE文件的意图和实现。

前面,我描述了函数调用怎样到一个外部DLL中而不直接调用这个DLL 。代替的,在执行体中的 .text 块中(如果你用Borland C++ 就是 .icode 块),CALL指令到达一条
指令处。JMP指令寻找的地址把控制转移到实际的目标地址。PE文件的 .idata 会包含一些必要的信息,这些信息是载入器用来确定目标函数的地址以及在执行体映像中去修正他们的。

这个域联系到前向链。前向链包括一个DLL函数向另一 个DLL转送引用。比如,在WindowsNT中,NTDLL.DLL就出现了的一些前向的它向KERNEL32.DLL导出的函数。应用程序可能以为它 调用的是NTDLL.DLL中的函数,但它最终调用的是KERNEL32.DLL中的函数。这个域还包含一个FirstThunk数组的索引(即刻描 述)。用这个域索引得函数会前向引用到另一个DLL 。不幸的是,函数怎样前向引用的格式没有文档,并且前向函数的例子也很难找。

这个域是指向 IMAGE_THUNK_DATA联合的偏移(RVA)。几乎在任何情况下,这个域都解释为一个指向的IMAGE_IMPORT_BY_NAME结构的指 针。如果这个域不是这些指针中的一个,那它就被当作一个将从这个被导入的DLL的导出序数值。如果你实际上可以从序数导入一个函数而不是从名字导入,从文 档看,这是不清楚的。
结构。表3以图形显示了这种布局。表12显示了PEDUMP对一个导入表的输出。

第一个域是导入函数的导出序数的最佳猜测。和NE文件不同,这个值不是必须正确的。于是,载入器指示把它当作一个进行二分查找的建议开始值。下一个是导入函数的名字的ASCIIZ字符串。
为 什么有两个平行的指针数组指向结构IMAGE_IMPORT_BY_NAME ?第一个数组(由Characteristics域指向的)单独的留下来,并不被修改。经常被称作提名表。第二个数组(由FirstThunk域指向的) 将被PE载入器覆盖。载入器在这个数组中迭代每个指针,并查找每个IMAGE_IMPORT_BY_NAME结构指向的函数的地址。载入器然后用找到的函 对Borland用户,上面的描述有点别 扭。由TLINK32产生的PE文件缺少其中一个数组。在这样一个执行体中,IMAGE_IMPORT_DESCRIPTOR(提名数组)中 Characteristics域的是0 。于是,仅有的由FirstThunk域(导入地址表)指向的数组在PE文件中就是必须的了。故事到这里应该结束了,除非在我写PEDUMP时深入一个有 趣的问题中。在优化上无止境的探索,微软在WindowsNT中"优化"了系统DLL(KERNEL32.DLL等等)的thunk数组。在这个优化中, 这个数组中的指针不再指向IMAGE_IMPORT_BY_NAME结构,它们已经包含了导入函数的地址。换句话说,载入器不需要去查找函数的地址并用导 入函数的地址覆盖thunk数组(译注)。对希望这个数组包含指向IMAGE_IMPORT_BY_NAME结构的指针的PEDump程序,这导致了一个 问题。你可能正在思考,"但是,Matt ,为什么呢不顺便使用提名表数组?"这可能是一个完美的解决方案,除非提名表数组在Borland文件中不存在。PEDUMP处理所有这些情况,但是代码 理所当然的就有些杂乱。
因为导入地址表在一个可写的块中,拦截一个EXE或DLL对另一个DLL的调用就相对容易。只需要修改适当地导入地址条目去指向希望拦截的函数。不需要修改调用者或被调者的任何代码。
注 意微软产生的PE文件的导入表并不是完全被连接器同步的,这一点很有趣。所有对另一个DLL中的函数的调用的指令都在一个导入库中。当你连接一个DLL 时,库管理器(LIB32.EXE或LIB.EXE)扫描将要被连接的OBJ文件并且创建一个导入库。这个导入库完全不同于16位NE文件连接器使用的导 入库。32位库管理器产生的导入库有一个.text块和几个.idata$块。导入库中的.text块包含 JMP [XXXX] 的替换指示,这个替换指示在OBJ文件的符号表中有一个名字来存储它。这个符号名对将从DLL中导出的所有函数名都是唯一的(例如: _Dispatch_Message@4)。导入库中的一个.idata$块包含一个从其中引用的替换指示(译注:即JMP [XXXX]中的XXXX)。另一个.idata$块有一个导入函数名之前的提示序号(hint ordinal)的空间。这两个域就组成了IMAGE_IMPORT_BY_NAME结构。当你晚连接一个使用导入库的PE文件时,导入库的块被加到连接 器需要处理的在OBJ文件中的你的块的列表中。一旦导入库中的这个替换指示的名字和和要导入的函数名相同,连接器就假定这个替换指示就是这个导入函数,并 修正对这个导入函数,使其指向这个替换指示。导入库中的这个替换指示在本质上就被当作这个导入函数本身了。
除了提供一个导入函数替换指示的代码部 分,导入库还提供PE文件的.idata块(或称导入表)的片断。这些片断来自于库管理器放入导入库中的不同的.idata$块。简而言之,连接器实际上 不知道出现在不同的OBJ文件中的导入函数和普通函数之间的不同。连接器只是按照它的边框调整规则去建立并结合块,于是,所有的事情就自然顺理成章了。
生鲜数据:原文"RawData",意指未加工过的数据,即原原本本从磁盘上读入而未经过任何改动的数据。
替换指示:原文"thunk",本质上是一条指令,这条指令中有浮动的地址域。如文中的 jmp [xxxx],其中xxxx是一个浮动地址(floating address),或称可重定位地址(relocatable address)。
OBJ文件:Object文件,即编译器编译产生的目标文件,这种文件只有在(和LIB)连接之后,才能形成可执行文件。
LIB文件:库文件,这种文件中包含一些二进制的代码(数据)及其符号,一般情况下,用到LIB中的哪个符号,连接器连接时,关于那个符号的二进制代码(数据)才会放入最终的执行体中。
EXE文件:不用多说Windows下的可执行文件,这类文件一般有导入表(Import Table)。有少数这类文件有导出表(Export Table)。
DLL文件:Dinamic Link Library ,即动态连接库,用来向其它执行体导出函数(或数据等)。

}

我要回帖

更多关于 pe系统卡在windows界面 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信