PE文件格式详解

PE文件格式详解
PE文件格式详解

PE文件格式详解

(一)基础知识

什么是PE文件格式:

我们知道所有文件都是一些连续(当然实际存储在磁盘上的时候不一定是连续的)的数据组织起来的,不同类型的文件肯定组织形式也各不相同;PE文件格式便是一种文件组织形式,它是32位Window系统中的可执行文件EXE以及动态连接库文件DLL的组织形式。为什么我们双击一个EXE文件之后它就会被Window运行,而我们双击一个DOC文件就会被Word打开并显示其中的内容;这说明文件中肯定除了存在那些文件的主体内容(比如EXE文件中的代码,数据等,DOC 文件中的文件内容等)之外还存在其他一些重要的信息。这些信息是给文件的使用者看的,比如说EXE文件的使用者就是Window,而DOC文件的使用者就是Word。Window可以根据这些信息知道把文件加载到地址空间的那个位置,知道从哪个地址开始执行;加载到内存后如何修正一些指令中的地址等等。那么PE文件中的这些重要信息都是由谁加入的呢?是由编译器和连接器完成的,针对不同的编译器和连接器通常会提供不同的选项让我们在编译和联结生成PE文件的时候对其中的那些Window需要的信息进行设定;当然也可以按照默认的方式编译连接生成Window中默认的信息。例如:WindowNT默认的程序加载基址是0x40000;你可以在用VC连接生成EXE文件的时候使用选项更改这个地址值。在不同的操作系统中可执行文件的格式是不同的,比如在Linux上就有一种流行的ELF格式;当然它是由在Linux上的编译器和连接器生成的,所以编译器、连接器是针对不同的CPU架构和不同的操作系统而涉及出来的。在嵌入式领域中我们经常提到交叉编译器一词,它的作用就是在一种平台下编译出能在另一个平台下运行的程序;例如,我们可以使用交叉编译器在跑Linux的X86机器上编译出能在Arm上运行的程序。

程序是如何运行起来的:

一个程序从编写出来到运行一共需要那些工具,他们都对程序作了些什么呢?里面都涉及哪些知识需要学习呢?先说工具:编辑器-》编译器-》连接器-》加载器;首先我们使用编辑器编辑源文件;然后使用编译器编译程目标文件OBJ,这里面涉及到编译原理的知识;连接器把OBJ文件和其他一些库文件和资源文件连接起来生成EXE文件,这里面涉及到不同的连接器的知识,连接器根据OS的需要生成EXE文件保存着磁盘上;当我们运行EXE文件的时候有Window的加载器负责把EXE文件加载到线性地址空间,加载的时候便是根据上一节中说到的PE文件格式中的哪些重要信息。然后生成一个进程,如果进程中涉及到多个线程还要生成一个主线程;此后进程便开始运行;这里面涉及的东西很多,包括:PE文件格式的内容;内存管理(CPU内存管理的硬件环

境以及在此基础上的OS内存管理方式);模块,进程,线程的知识;只有把这些都弄清楚之后才能比较清楚的了解这整个过程。下面就让我们先来学习PE文件格式吧。

PE文件的总体结构:

下图便是PE文件的一个总体结构:注意,图2是在图1的基础上进一步细化了,不过图2的顺序是从下向上代表文件的从头到尾的顺序。

我们的EXE文件在磁盘上就是按照上面的格式顺序存储的,当运行的时候它就很容易被加载器加载到线性地址空间;但是在线性空间中和在磁盘上不同,在线性空间中各个部分不一定是占据连续的线性地址空间。下面对PE文件格式的介绍就按照上图中对从头到尾对每个部分进行介绍。好的,今天刚去医院回来有些累了,就先写到这儿吧。

嗯,不行,还有几个重要而又基础的概念需要在这儿先澄清一下,否则后面就会出乱子了。

几个重要的基本概念:

1)节:PE文件的真正内容划分成块,称之为sections(节)。每节是一块拥有共同属性的数据,比如代码/数据、读/写等。我们可以把PE文件想象成一逻辑磁盘,PE header 是磁盘的boot 扇区,而sections就是各种文件,每种文件自然就有不同属性如只读、系统、隐藏、文档等等。值得我们注意的是---- 节的划分是基于各组数据的共同属性: 而不是逻辑概念。重要的不是数据/代码是如何使用的,如果PE文件中的数据/代码拥有相同属性,它们就能被归入同一节中。不必关

心节中类似于"data", "code"或其他的逻辑概念: 如果数据和代码拥有相同属性,它们就可以被归入同一个节中。(节名称仅仅是个区别不同节的符号而已,类似"data", "code"的命名只为了便于识别,惟有节的属性设置决定了节的特性和功能)如果某块数据想付为只读属性,就可以将该块数据放入置为只读的节中,当PE装载器映射节内容时,它会检查相关节属性并置对应内存块为指定属性。

注意:上面已经说过了“节的划分是基于各组数据的共同属性: 而不是逻辑概念。重要的不是数据/代码是如何使用的,如果PE文件中的数据/代码拥有相同属性,它们就能被归入同一节中”所以上面表中列出的节并不一定单独成节,也就是说即使存在上面表中的某一节,在节表(sect ion table)(后面会讲到)中也不一定就有于之对应的项,因为它可能和别的具有共同属性的节共同组成了一节。比如 .idata 可以和 .text 合成一节而命名为 .text,而在节表中只有和 .text对应的项。这也就是后面的optional header中数据目录(DataDirectory)存在的作用,因为很多有用的节被合并了,因此加载器无法通过节表来定位它们,所以这就是数据目录(DataDirectory)发挥作用的时候了(具体作用后面会讲到)。

2)虚拟地址:虚拟地址即程序中使用的地址,也就是从程序员的角度看到的地址,有时也叫逻辑地址;通常使用段地址:偏移量的形式表示,不过在32位系统中使用的是平坦(Flat)内存模式,所以我们可以不用管段地址,只考虑32位的偏移量即可,认为32位的偏移量就是虚拟地址,这样一来程序员就可以认为他是在一个段中写程序,这个段的大小是232 = 4G的容量,当然这部分地址空间是程序和OS共享的,程序员可以利用的大约有2G(具体可以参考Win98和WinNT的内存布局);所以我们平时在写程序申请内存的时候实际上申请的就是这2G的线性地基空间,由于所有的4G线性地址空间都被OS作为资源来管理(这4G的线性地址空间是通过页表来表现出来的,OS分配线性地址空间給进程也就是分配相应的页表給进程),所以我们无论用什么方式使用

内存最终都是转换为OS为我们分配线性地址空间,至于分配的线性地址空间又如何被映射为真正的物理内存完全是有OS负责的(更详细资料参见“Windows 内存管理”),程序员不必操心。

3)相对虚拟地址:「相对虚拟地址(Relative VirtualAddress,RVA)」即相对于上面的基地址的偏移量。PE 文件中的许多字段内容都是以RVA 表示,一个RVA 是某一资料项的offset(偏移)值-- 从文件被映像进来的起点(即基地址)算起。举个例子,我们说Windows加载器把一个PE 文件映像到虚拟地址空间的0x400000 处,如果此image 有一个表格开始于0x401464,那么这个表格的RVA 就是0x1464:虚拟地址0x401464 - 基地址0x400000 = RVA 0x1464只要把RVA 加上基地址,RVA 就可以被转换为一个有用的指针。在PE文件中大多数地址多是RVA 而RVA只有当PE文件被PE装载器装入内存后才有意义。如果我们直接将文件映射到内存而不是通过PE装载器载入,那么我们就不能直接使用那些RVA。必须先将那些RVA转换成文件偏移量,RVAToOffset函数就起到这个作用。

4)基地址:「基地址(base address)」是一个重要概念,用来描述被映像到内存中的EXE 或DLL 的起始地址。为了方便,Windows NT 和Windows 95 都以模块的基地址做为模块的ins tance handle(HINSTANCE,实例句柄)。Windows95加载器把一个PE 文件映像到虚拟地址空间的0x400000 处;而WindowNT加载器把一个PE 文件映像到虚拟地址空间的0x10000 处。

5)文件偏移量:文件中的地址与内存中表示不同,它是用偏移量(File offset)来表示的,文件中的第一个字节的偏移量是0,后面的字节依次递增。在SoftICE和W32Dasm下显示的地址值是内存线性地址,或称之为虚拟地址(Virual Address,VA)。而十六进制工具里,如:Hiew、Hex Workshop等显示的地址就是文件地址,称之为偏移量(File offset) 或物理地址(RAW offset,注意这个物理地址不是内存寻址中说到的物理地址)。

6)模块:「模块(module)」一词表示一个EXE 或DLL 被加载内存后的程序代码、数据和资源(就是被加载到内存后的EXE或DLL整体,包括代码、数据和资源,而不是说代码、数据、资源分别都是模块)。除了程序代码和数据是你的程序直接使用的之外,模块还内含一些支持性数据,Windows 用它来决定程序代码和数据放在内存的什么地方,在Win32,这些信息保留在PE

头部(即图1中的PE header,实际上它是一个IMAGE_NT_HEADERS 结构)中。

7)逻辑地址:见“虚拟地址”

8)线性地址:线性地址是由虚拟地址(逻辑地址)转换来的,转换需要CPU和OS共同合作来完成;里面涉及到全局描述符表GDT和局部描述符表LDT;不过由于32位的Window系统采用flat内存模式,所以我们可以认为虚拟地址就是线性地址,即我们可以认为逻辑地址中的32位偏移量就是线性地址。

9)物理地址:即最终发往地址总线上的地址,它对应着实际的物理内存,在32位的Window 存储管理中它是通过页表由线性地址转换出来的。

10)实际地址:即“物理地址”。

其中前面的6个概念是学习PE文件格式需要知道的,后面的几个主要在内存管理里面提到,在这里为了便于区别一起列了出来。

(二)PE格式总览

上一节我们已经了解了PE文件格式的作用和其总体结构,从这节开始我们就开始按照上一节中的总体结构从上到下来解析PE文件各个部分的具体结构和作用,当然我不会对每个部分的每一个字段都详细描述它的作用,因为讲解PE文件格式的资料很多,讲解的都很详细,所以我在这里只是按照程序执行的线索和基本原理把那些最重要的字段讲解一下,为了让我们对PE文件格式有个比较清楚的宏观认识,在具体讲解每一部分之前先让我们大概了解一下各部分的作用。1.DOS MZ header 和DOS Stub:

如果在DOS下执行PE格式文件就会执行后面的DOS Stub,显示字符串"This program can not run in DOS mode",如果在Window下执行PE格式文件,PE加载器就会根据DOS MZ he ader中的最后一个域e_lfnew跳过DOS Stub直接转到PE Header , DOS MZ header 和DOS Stub的贡献仅此而已。

2. PE Header:

当加载器跳到PE Header后,根据里面的各个域首先检查这是不是有效的PE文件格式,能否在当前的CPU架构下运行,优先加载基址是多少,一共有几个节(section),这是一个EXE文件还是DLL文件等总体信息,有了这些总体信息之后加载器就会跳到下面的Section table。

3.Section table:

有了上面从PE Header获得的总体信息后,加载器并不能准确的加载文件,因为要准确的加载文件,加载器还需要一些关于每一节的更具体的信息,比如:每一节在磁盘文件上的起始位置、大小,应该被加载的线性地址空间的哪一部分,这一节是代码还是数据,读写属性如何等等。所有这些信息都保存在Section table里面,Section table是一个结构数组,数组里面的每一个结构对应PE文件中的一个节。PE加载器就会遍历这个结构数组把PE文件的每一节准确的加载到线性地址空间。(这里还要注意两点:一是PE加载器把PE文件的每一节加载到线性地址空间并不是说把磁盘上的文件调入物理内存;而只是为它分配线性地址空间,分配线性地址空间意味着申请本进程需要的页表,并把相应的信息添入页表中。线性地址空间也可以看作是一种资源,它是通过页表来体现的,当一个页表被添入相应的信息被占用之后那么这个页表对应的那块线性地址空间也就被分配出去了。需要注意的另一点是PE加载器对每一节采用文件映射的方式把相应的磁盘文件映射到内存,而不是把整个PE文件采用文件映射的方法把磁盘文件映射到内存。更具体的解释我会在“W indows 内存管理”中提到。)

4.Sections:

PE文件最后的部分就是各个节了,比如.text , .data , .idata等等,各种节的作用后面会有一个简要介绍。

思考一下:既然加载器不一定把程序加载到PE头中指定的优先加载基址,那么如果在没有加载到PE头中指定的优先加载基址的情况下,指令中的地址是不是都要依次修改呢?首先我们要明确的一点是程序指令中的地址分两大类,其中一类是在编译过程就可以确定的,这类地址采用的是相对虚拟地址(RVA),所以即使程序没有被加载到希望的基址这些地址也无需修改。另一类地址是编译过程和连接过程都无法确定的,比如那些引用外部库的函数地址,因为外部库之后在被加载器加载后里面的函数地址才能确定下来,所以程序中的这类地址要在程序被加载后进行修改。那么编译器和连接器对这类无法确定的地址是如何处理的呢?加载器又是根据什么如何来对它们进行修改的呢?个人感觉PE文件格式学习中这一部分内容有些繁杂,所以希望大家读后面各节的时候最好时常思考一下这两个问题。从下一节开始我们将对PE文件的各个部分作更为详尽的讲解。重点部分会放在对上面两个问题的解决上。

(三)DOS Header & PE Header

上一节中我们对PE文件的各个部分的作用有了一个总体的认识,从这节起我们会对PE文件的每个部分作更进一步的解释,当然别忘记了上一节中我提出的两个问题。

1.DOS MZ header 和DOS Stub:

所有PE文件(甚至32位的DLLs) 必须以一个简单的DOS MZ header 开始。我们通常对此结构没有太大兴趣。有了它,一旦程序在DOS下执行,DOS就能识别出这是有效的执行体,然后运行紧随MZ header 之后的DOS stub。DOS stub实际上是个有效的EXE,在不支持PE 文件格式的操作系统中,它将简单显示一个错误提示,类似于字符串"This program requires Wi ndows" 或者程序员可根据自己的意图实现完整的DOS代码。通常我们也不对DOS stub 太感兴趣: 因为大多数情况下它是由汇编器/编译器自动生成。通常,它简单调用中断21h服务9来显示字符串"This program cannot run in DOS mode"。在Window95下运行32位程序的时候这个部分并不会被加载器映射的线性地址空间,当Win32 加载器把一个PE 文件映像到内存,内存映像文件(memory mapped file)的第一个字节对应到DOS Stub 的第一个字节。WINNT.H 为DOS stub 表头DOS MZ header定义了一个结构,第一个域e_magic ,被称为魔术数字,它被用于表示一个MS-DOS兼容的文件类型,其作用类似于PE header中的Signature域,所有MS-DOS 兼容的可执行文件都将这个值设为0x5A4D,表示ASCII字符MZ。MS-DOS头部之所以有的时候被称为MZ头部,就是这个缘故。还有许多其它的域对于MS-DOS操作系统来说都有用,但是对于Windows NT来说,这个结构中只有一个有用的域——最后一个域e_lfnew ,PE头部就是由它定位的。循此我们将非常容易找到PE头部,它是一个相对偏移值(或说是RVA),指向真正的PE头部(PE header)。为了获得指针,你必须为RVA 加上image 的基地址:

pNTHeader = dosHeader + dosHeader->e_lfanew;

有了这个指向PE Header的指针我们就可以取得很多有用的信息了,既然我们研究的是PE 文件格式,因此PE Header才是我们研究的重点。总之,DOS MZ header和DOS Stub之间的关系相当于PE header和EXE或者DLL之间的关系。

2.PE Header:

PE header 是PE相关结构IMAGE_NT_HEADERS 的简称,其中包含了许多PE装载器用到的重要域。当我们更加深入研究PE文件格式后,将对这些重要域耳目能详。执行体在支持PE 文件结构的操作系统中执行时,PE装载器将从DOS MZ header 中找到PE header 的起始偏移量。因而跳过了DOS stub 直接定位到真正的文件头PE header。PE头部整个是个IMAGE_NT _HEADERS 结构,定义于WINNT.H。这个结构正是Windows 95 的module database(“模块”的概念在第一节中说过了,操作系统就是利用这个结构感知“模块”的存在、获得“模块”的信息等;这个结构我会在以后的“模块”学习当中提及)。每一个被载入的EXE 或DLL 都以一个IMAGE_NT_ HEADERS 结构表现出来。此结构有一个DWORD 和两个子结构:

DWORD Signature;

IMAGE_FILE_HEADER FileHeader;

IMAGE_OPTIONAL_HEADER OptionalHeader;

(1)对于PE格式的文件Signature 字段内容应该是ASCII 的PE\0\0。

(2)IMAGE_FILE_HEADER FileHeader:

IMAGE_FILE_HEADER结构比较简单,也比较容易理解,在此不做过多的解释;简言之,只有三个域对我们有一些用: Machine, NumberOfSections 和Characteristics。通常不会改变Mac hine 和Characteristics 的值,但如果要遍历节表就得使用NumberOfSections。

(3)比较复杂也更有趣的是第三个东东即:IMAGE_OPTIONAL_HEADER,现在我们学习IMAGE_NT_HEADERS 中的最后成员optional header 结构,它包含了PE文件的逻辑分布信息。该结构共有31个域,一些是很关键,另一些不太常用。这里只介绍那些真正有用的域。

上面表格里最难理解的也是很重要的一个域是最后一个,即:DataDirectory;它是一个结构数组,它一共包含16个元素即共含16个结构;每一个结构对应于一个section(注意这里的secti on是按照第一节中按作用进行划分的section,不是最终生成的PE文件中包含的节),结构中的两个域分别描述了该section的RVA 和SIZE; 这样一来加载器就能够通过这个数组迅速在image 中找到特定的section,后面讲到的导入表,引出表都要用到这个数组中相应的元素,到时候还会有进一步的解释。

(四)Section Table(节表)

到本节为止,我们已经学了许多关于DOS header 和PE header 的知识。接下来就该轮到section table(节表)了。节表其实就是紧挨着PE header 的一结构数组,它的作用我们在前面已经说过了。该数组成员的数目由file header (IMAGE_FILE_HEADER) 结构中NumberOfSect ions 域的域值来决定。节表结构又命名为IMAGE_SECTION_HEADER。我们把它的主要成员列

好的,现在PE文件的前半部分结构我们已经了解的差不多了,下面就让我们模拟一下加载器加载PE文件的过程吧:

加载器加载PE文件的主要步骤:

1. 当PE文件被执行,PE装载器检查DOS MZ header 里的PE header 偏移量。如果

找到,则跳转到PE header。

2. PE装载器检查PE header 的有效性。如果有效,就跳转到PE header的尾部。

3. 紧跟PE header 的是节表。PE装载器读取其中的节信息,并采用文件映射方法将这些

节映射到内存,同时付上节表里指定的节属性。

4. PE文件映射入内存后,PE装载器将处理PE文件中类似import table(引入表)逻辑部

分。

加载器检查PE文件有效性步骤总结如下:

1. 首先检验文件头部第一个字的值是否等于IMAGE_DOS_SIGNATURE,是则DOS MZ

header 有效。

2. 一旦证明文件的DOS header 有效后,就可用e_lfanew来定位PE header 了。

3. 比较PE header 的第一个字的值是否等于IMAGE_NT_HEADER。如果前后两个值都

匹配,那我们就认为该文件是一个有效的PE文件。

现在我们已知晓IMAGE_SECTION_HEADER 结构,再来模拟一下PE装载器的工作吧:

1. 读取IMAGE_FILE_HEADER 的NumberOfSections域,知道文件的节数目。

2. SizeOfHeaders 域值作为节表的文件偏移量,并以此定位节表。

3. 遍历整个结构数组检查各成员值。

4. 对于每个结构,我们读取PointerToRawData域值并定位到该文件偏移量。然后再读取S

izeOfRawData域值来决定映射内存的字节数。将VirtualAddress域值加上ImageBase

(基地址)域值等于节起始的虚拟地址。然后就准备把节映射进内存,并根据Character

istics域值设置属性。

5. 遍历整个数组,直至所有节都已处理完毕。

遍历节表的步骤:

1. PE文件有效性校验。

2. 定位到PE header 的起始地址。

3. 从file header 的NumberOfSections域获取节数。

4. 通过两种方法定位节表: ImageBase+SizeOfHeaders 或者PE header的起始地址+ P

E header结构大小。(节表紧随PE header)。如果不是使用文件映射的方法,可以用S

etFilePointer 直接将文件指针定位到节表。节表的文件偏移量存放在SizeOfHeaders域

里。(SizeOfHeaders 是IMAGE_OPTIONAL_HEADER(PE header)的结构成员) //

定位节表位置

5. 处理每个IMAGE_SECTION_HEADER 结构。(这是结构数组)

好的,到此为止我们已经清楚了加载器加载PE文件的大部分过程,但是别忘了我们的问题,现在问题还没有解决,要解决这个问题就好弄清楚后面两节:Import Table和Export Table,这两节是最重要的当然也是最复杂的。

(五)Improt Table(引入表)

这节即将学习的Import Table和下节的Export Table关系密切,两者联合起来就可以解决我们开始提出的问题。在说明Import Table和Export Table的作用之前先让我们明白编译器是如何处理我们调用外部库函数的。在PE 文件中,当你调用另一模块中的函数(例如USER32.DLL 中的GetMessage),编译器制造出来的CALL 指令并不会把控制权直接传给DLL 中的函数,而是传给一个JMP DWORD PTR [XXXXXXXX] 指令,后者也位于.text 中。JMP 指令跳到一个地址

去,此地址储存在.idata 的一个DWORD之中。这个DWORD 内含该函数的真正地址(函数进入点),如图1所示:

图1

那么,这样做有什么好处呢?试想一下,如果CALL指令后面跟的直接就是DLL中的函数地址,那么加载器就需要修补每一个调用DLL 的指令。而现在PE 载入器需要做的,就只是把DLL 函数的真实地址放到.idata 的那个DWORD 之中,根本就没有程序代码需要修补。嗯,现在比较清除了,加载器首先要知道所加载的程序调用了哪些DLL的哪些函数,然后找出这些函数的地址,把他们添入到.idata 的那些DWORD 之中。那么加载器如何知道所加载的程序调用了哪些DLL的哪些函数,这就是Import Table的作用;加载器又是如何找出这些函数的地址呢,这又是Export Tabl e的作用。现在两者的作用都很清除了,剩下的关键问题就是PE加载其如何利用这两个东东来完成上面的任务,完成了这个任务也就解决了我们开始提出的问题。这节我们先讨论前半部分,也就是加载器如何利用Import Table找出所加载的程序调用了哪些DLL的哪些函数。

首先,最基本的是加载器如何找到Import Table呢?这就要利用前面我们提到的数据目录(D ata Directory),它是Option Header结构中的最后一个域。Data Directory 是一个IMAGE_DAT A_DIRECTORY 结构数组,共有16个成员,每个成员包含了一个重要数据结构的信息(RVA 和大小),并且这些重要数据结构的信息在IMAGE_DATA_DIRECTORY 结构数组中的位置是固定的,这样加载器就很容易找到需要的信息了。这就好比你有一个书架每一层都放着不同种类的书籍,并且它们始终固定,即:第一层始终放小说,第二层始终放散文…等等,当你需要散文的时候你就可以毫不犹豫的去拿第二层上的书就可以了。下面的表就是IMAGE_DATA_DIRECTORY 结构数组的布置情况:(第0,1,12三项是和我们这两节解决问题有关系的;第5项在最后一节中用到;第9项在线程中用到)。

如果您还记得节表可以看作是PE文件各节的根目录的话,也可以认为Data Directory 是存储在这些节里的逻辑元素的根目录。明确点,Data Directory 包含了PE文件中各重要数据结构的位置和尺寸信息。Data Directory的每个成员都是IMAGE_DATA_DIRECTORY 结构类型的,其定义如下所示:

IMAGE_DATA_DIRECTORY STRUCT

VirtualAddress dd ?

isize dd ?

IMAGE_DATA_DIRECTORY ENDS

VirtualAddress 实际上是数据结构的相对虚拟地址(RVA)。比如,如果该结构是关于import s ymbols的,该域就包含指向IMAGE_IMPORT_DESCRIPTOR 数组的RVA。isize 含有Virtual Address所指向数据结构的字节数。

现在我们知道如何找到引入表了。Data Directory数组第二项的VirtualAddress包含引入表地址。引入表实际上是一个IMAGE_IMPORT_DESCRIPTOR 结构数组。每个结构包含PE文件引入函数的一个相关DLL的信息。比如,如果该PE文件从10个不同的DLL中引入函数,那么这个数组就有10个成员。该数组以一个全0的成员结尾。下面详细研究结构组成: IMAGE_IMPORT_DESCRIPTOR STRUCT

union

Characteristics dd ?

OriginalFirstThunk dd ?

ends

TimeDateStamp dd ?

ForwarderChain dd ?

Name1 dd ?

FirstThunk dd ?

IMAGE_IMPORT_DESCRIPTOR ENDS

结构第一项是一个union子结构。事实上,这个union子结构只是给OriginalFirstThunk 增添了个别名,您也可以称其为"Characteristics"。该成员项是指向一个DWORD 数组的RVA。数组里的每一个DWORD 事实上是一个IMAGE_THUNK_DATA union。那么,IMAGE_THUNK _DATA又是什么呢? 每个IMAGE_THUNK_DATA union对应于一个输入函数,这个DWORD(即IMAGE_THUNK_DATA union)的内容视文件被加载与否(即:加载前后内容不同,原因后面会解释),以及函数被以名称输入或以序号输入而定(即:输入方式不同,内容不同)。以名称输入是比较共同的方式。当一个函数以序号输入,EXE 文件的IMAGE_THUNK_DATA DWORD 中的最高位(0x80000000)设立。例如,考虑一个IMAGE_THUNK_DATA,其值为0x80000112,放在GDI32.DLL 数组中。表示这IMAGE_THUNK_DATA 将输入GDI32.DLL 中的第112号输出(e xported)函数。如果函数以名称输入,IMAGE_THUNK_DATA DWORD 就内含一个RVA(Rel ative Virtual Address),指向IMAGE_IMPORT_BY_NAME 结构,由于一个输入函数对应一个I MAGE_THUNK_DATA union,而当函数以名称输入的时候IMAGE_THUNK_DATA又指向一个I MAGE_IMPORT_BY_NAME 结构,所以此时一个输入函数对应一个IMAGE_IMPORT_BY_NAM E 结构。现在让我们看看IMAGE_IMPORT_BY_NAME 结构里面有些什么东东,我们希望里面存有一个引入函数的相关信息。奥,原来它真的如我们所愿:

IMAGE_IMPORT_BY_NAME STRUCT

Hint dw ?

Name1 db ?

IMAGE_IMPORT_BY_NAME ENDS

Hint 指示本函数在其所驻留DLL的引出表中的索引号。该域被PE装载器用来在DLL的引出表里快速查询函数。Name1 含有引入函数的函数名。函数名是一个ASCIIZ字符串。现在请看这里: 假设程序中调用了N个输入函数,那么就对应着有N个IMAGE_IMPORT_BY_NAME 结构,我们收集起这些结构的RVA 放在IMAGE_THUNK_DATA结构中组成一个数组,并以0结尾,然后再将此数组的RVA放入OriginalFirstThunk。这样一来我们就可以利用OriginalFirstThunk这条线把从某一个DLL中调用的函数全部給揪出来。

奥,刚才我们被IMAGE_IMPORT_BY_NAME 结构给中断了一下,现在让我们返回IMAGE_I MPORT_DESCRIPTOR结构,继续看里面的其他域。为了和上面的讨论保持连贯性先让我们来看一下最后一个域FirstThunk,FirstThunk 与OriginalFirstThunk 非常相似,也是一个RVA,它也是指向一个IMAGE_THUNK_DATA 结构数组(当然这是另外一个IMAGE_THUNK_DATA 结构数组,不过这个IMAGE_THUNK_DATA结构数组和OriginalFirstThunk指向的IMAGE_THUNK_DA

TA结构数组内容是完全一样的)。好的让我们理顺一下:现在有几个IMAGE_IMPORT_BY_NAM E 结构,同时您又创建了两个结构数组,并同样存入指向那些IMAGE_IMPORT_BY_NAME 结构的RVA,这样两个数组就包含相同数值了(可谓相当精确的复制啊)。最后您决定将第一个数组的R VA赋给OriginalFirstThunk,第二个数组的RVA赋给FirstThunk,这样一切都很清楚了。如果你对上面一堆的RVA和结构数组的关系感到头晕的话那么请往下看,或许下面这个图解能让你清醒一些:

OriginalFirstThunk IMAGE_IMPORT_BY_NAME FirstThunk | |

---> --->

---> ---> ---> ---> <--- <--- <--- <--- <--- <---

现在您应该明白我的意思。不要被IMAGE_THUNK_DATA这个名字弄糊涂: 当以名字引入函数时,它仅是指向IMAGE_IMPORT_BY_NAME 结构的RVA。如果将IMAGE_THUNK_DATA 字眼想象成RVA,就更容易明白了。OriginalFirstThunk 和FirstThunk 所指向的这两个数组大小取决于PE文件从DLL中引入函数的数目。比如,如果PE文件从kernel32.dll中引入10个函数,那么IMAGE_IMPORT_DESCRIPTOR 结构的Name1域包含指向字符串"kernel32.dll"的RVA,同时每个IMAGE_THUNK_DATA 数组有10个元素。

下一个问题是: 为什么我们需要两个完全相同的数组? 为了回答该问题,我们需要了解当PE 文件被装载到内存时,第一个数组(由Characteristics 指向)从不被修改,这样一来所以若还反过头来查找引入函数名,PE装载器还能找寻到。第二个数组(由FirstThunk 指向)则被载入器改写。载入器一一检阅每一个IMAGE_THUNK_DATA ,然后进一步通过其他结构找出引入函数的地址。然后用引入函数真实地址来替代由FirstThunk指向的IMAGE_THUNK_DATA 数组里的元素值。因此当PE文件准备执行时,上图已转换成:(这也证实了前面所说的IMAGE_THUNK_DATA 结构的内容不仅和函数是按序号或者名字引入有关,而且和文件是否加载有关。由于此时FirstThu

nk 指向的IMAGE_THUNK_DATA 数组已经被修改,所以它和IMAGE_IMPORT_BY_NAME 数组的关系也被切断。)

OriginalFirstThunk

IMAGE_IMPORT_BY_NAME FirstThunk

---> ---> ---> ---> ---> --->

<-\- <-\- <-\- <-\- <-\- <-\-

稍早我曾经说过对DLL 函数的调用会导至一个JMP DWORD PTR [XXXXXXXX] 指令。[X XXXXXXX] 事实上参考到FirstThunk 数组中的一个元素。由于这个IMAGE_THUNK_DATA 数组内容已被加载器改写为输入函数的地址,所以它又被称做Import Address Table (IAT ),由于这块数据区的重要性,为了查找方便在数据目录(Data Directory )中有一项(第12项)就是来描述它的。下面是一个更具体的图例:

图2

下面让我们看看IMAGE_IMPORT_DESCRIPTOR 结构中剩下的几个域吧: ForwarderChain :这个字段关系到所谓的forwarding (转交),意味一个DLL 函数再参考(调用、利用)另一个DLL ,由于涉及的东西比较复杂,所以不做进一步介绍。

Name1:是一个指向DLL 名字的RVA ,即指向DLL 名字的指针,也是一个ASCIIZ 字符串。 有了上面的知识之后假设我们要列出某个PE 文件的所有引入函数,可以照着下面步骤走:

1. 校验文件是否是有效的PE 。

2. 从DOS header 定位到PE header。

3. 获取位于OptionalHeader 数据目录地址。

4. 转至数据目录的第二个成员提取其VirtualAddress值。//import

5. 利用上值定位第一个IMAGE_IMPORT_DESCRIPTOR 结构。

6. 检查OriginalFirstThunk值。若不为0,顺着OriginalFirstThunk 里的RVA值转入那个

RVA数组。若OriginalFirstThunk 为0,就改用FirstThunk值。有些连接器生成PE文

件时会置OriginalFirstThunk值为0,这应该算是个bug。不过为了安全起见,我们还是

检查OriginalFirstThunk值先。

7. 对于每个数组元素,我们比对元素值是否等于IMAGE_ORDINAL_FLAG32。如果该元素

值的最高二进位为1,那么函数是由序数引入的,可以从该值的低字节提取序数。

8. 如果元素值的最高二进位为0,就可将该值作为RVA转入IMAGE_IMPORT_BY_NAME

数组,跳过Hint 就是函数名字了。

9. 再跳至下一个数组元素提取函数名一直到数组底部(它以null结尾)。现在我们已遍历完一

个DLL的引入函数,接下去处理下一个DLL。

10. 即跳转到下一个IMAGE_IMPORT_DESCRIPTOR 并处理之,如此这般循环直到数组见

底。(IMAGE_IMPORT_DESCRIPTOR 数组以一个全0域元素结尾)。

这一节已经够长了,也已经把Import Table解释的比较清楚了,但是我们的问题解决了吗?没有,我们的目的是要找出程序中引入的所有函数,并且把它们的真实地址找出来然后添入FirstThu nk指向的IMAGE_THUNK_DATA 数组里,那么我们这节解决了哪些问题呢?奥,原来我们已经可以知道程序中引入了哪些DLL,以及这些DLL中的哪些函数了,下面的任务就是要找出这些函数对应的地址了。

(六)Exprot Table(引出表)

这节我们来解决上一节遗留下来的问题:寻找引入函数的真实地址,那么去哪里找呢?当PE 装载器执行一个程序,它将相关DLL都装入该进程的地址空间。然后根据主程序的引入函数信息,查找相关DLL中的真实函数地址来修正主程序。PE装载器搜寻的是DLL中的引出函数,这些引出函数的信息正是放在Exprot Table(引出表:引出表放的是函数的实际地址)中。

DLL/EXE要引出一个函数给其他DLL/EXE使用,有两种实现方法: 通过函数名引出或者仅仅通过序数引出(这恰好对应相应的两种引入方式)。比如某个DLL要引出名为"GetSysConfig"的函数,如果它以函数名引出,那么其他DLLs/EXEs若要调用这个函数,必须通过函数名,就是Get SysConfig。另外一个办法就是通过序数引出。什么是序数呢?序数是唯一指定DLL中某个函数

的16位数字,在所指向的DLL里是独一无二的。例如在上例中,DLL可以选择通过序数引出,假设是16,那么其他DLLs/EXEs若要调用这个函数必须以该值作为GetProcAddress调用参数。这就是所谓的仅仅靠序数引出。不过通常不提倡仅仅通过序数引出函数这种方法,这会带来DLL维护上的问题。一旦DLL升级/修改,程序员无法改变函数的序数,否则调用该DLL的其他程序都将无法工作。

现在我们开始学习引出结构。象引出表一样,可以通过数据目录找到引出表的位置。这儿,引出表是数据目录的第一个成员,又可称为IMAGE_EXPORT_DIRECTORY。该结构中共有11 个

上面也许无法让您完全理解引出表,下面的简述将助您一臂之力。

引出表的设计是为了方便PE装载器工作。首先,模块必须保存所有引出函数的地址以供PE 装载器查询。模块将这些信息保存在AddressOfFunctions域指向的数组中,而数组元素数目存放在NumberOfFunctions域中。因此,如果模块引出40个函数,则AddressOfFunctions指向的数组必定有40个元素,而NumberOfFunctions值为40。现在如果有一些函数是通过名字引出的,那么模块必定也在文件中保留了这些信息。这些名字的RVA存放在一数组中以供PE装载器查询。该数组由AddressOfNames指向,NumberOfNames包含名字数目。考虑一下PE装载器的工作机制,它知道函数名,并想以此获取这些函数的地址。至今为止,模块已有两个数组: 函数名字数组和函数地址数组,但两者之间还没有联系的纽带。因此我们还需要一些联系函数名及其地址的东东。先让我们想一下,如果我们要按照名字查找一个函数的地址,那么如果我们能知道该函数地址在函数地址数组中的索引就好了,好,那么我们现在就是需要一个根据函数名字找到该函数地址在函数地址数组中的索引的这么一个东东。有了这个东东PE装载器在名字数组中找到匹配名字的同时,也就获取了指向地址表中对应元素的索引。那么索引存储在哪里呢?原来这些索引保存在

由AddressOfNameOrdinals域指向的另一个数组(最后一个)中。由于该数组是起了联系名字和地址的作用,所以其元素数目必定和名字数组相同,比如,每个名字有且仅有一个相关地址,反过来则不一定: 每个地址可以有好几个名字来对应。因此我们给同一个地址取"别名"。(也就是说名字数组的大小应该大于或者等于地址数组的大小)为了起到连接作用,名字数组和索引数组必须并行地成对使用,譬如,索引数组的第一个元素必定含有第一个名字的索引,这样一来一旦我们在名字数组中的第一个位置找到了匹配的函数名,那么我们就可以从索引数组的第一个位置取出一个索引值,用这个索引值去地址数组中取出函数地址,以此类推。具体可以参考下面的图示:(注意图中的双箭头只是表示一一对应的关系,而不是指针的指向关系。)(这就形成了一个一一映射的关系);

AddressOfNames AddressOfNameOrdinals

| |

<-->

<-->

<-->

<-->

...

<-->

下面是一个更具体的图示:

//使用索引输出的函数应该是遍历整个数组后得到的结果,这样就增加了寻找时间

下面举一两个例子说明问题。如果我们有了引出函数名并想以此获取地址,可以这么做:

1. 定位到PE header。

2. 从数据目录读取引出表的虚拟地址。

3. 定位引出表获取名字数目(NumberOfNames)。

4. 并行遍历AddressOfNames和AddressOfNameOrdinals指向的数组匹配名字。如果在A

ddressOfNames 指向的数组中找到匹配名字,从AddressOfNameOrdinals 指向的数组

中提取索引值。例如,若发现匹配名字的RVA存放在AddressOfNames 数组的第77个元素,那就提取AddressOfNameOrdinals数组的第77个元素作为索引值。如果遍历完

NumberOfNames 个元素,说明当前模块没有所要的名字。

5. 从AddressOfNameOrdinals 数组提取的数值作为AddressOfFunctions 数组的索引。也

就是说,如果值是5,就必须读取AddressOfFunctions 数组的第5个元素,此值就是所要函数的RVA。

现在我们在把注意力转向IMAGE_EXPORT_DIRECTORY 结构的nBase成员。您已经知道A ddressOfFunctions 数组包含了模块中所有引出符号的地址。当PE装载器索引该数组查询函数地址时,让我们设想这样一种情况,如果程序员在.def文件中设定起始序数号为200,这意味着Add ressOfFunctions 数组至少有200个元素,甚至这前面200个元素并没使用,但它们必须存在,因为PE装载器这样才能索引到正确的地址。这种方法很不好,所以又设计了nBase 域解决这个问题。如果程序员指定起始序数号为200,nBase 值也就是200。当PE装载器读取nBase域时,它知道开始200个元素并不存在,这样减掉一个nBase值后就可以正确地索引AddressOfFunctions 数组了。有了nBase,就节约了200个空元素。

注意nBase并不影响AddressOfNameOrdinals数组的值。尽管取名"AddressOfNameOrdinal s",该数组实际包含的是指向AddressOfFunctions 数组的索引,而不是什么序数啦。讨论完nBa se的作用,我们继续下一个例子。假设我们只有函数的序数,那么怎样获取函数地址呢,可以这么做:

1. 定位到PE header。

2. 从数据目录读取引出表的虚拟地址。

3. 定位引出表获取nBase值。

4. 减掉nBase值得到指向AddressOfFunctions 数组的索引。

5. 将该值与NumberOfFunctions作比较,大于等于后者则序数无效。

6. 通过上面的索引就可以获取AddressOfFunctions 数组中的RVA了。//这里解释了nBa

se的作用,这样可以加快速度

可以看出,从序数获取函数地址比函数名快捷容易。不需要遍历AddressOfNames 和Addre ssOfNameOrdinals 这两个数组。然而,综合性能必须与模块维护的简易程度作一平衡。

总之,如果想通过名字获取函数地址,需要遍历AddressOfNames 和AddressOfNameOrdi nals 这两个数组。如果使用函数序数,减掉nBase值后就可直接索引AddressOfFunctions 数组。

如果一函数通过名字引出,那在GetProcAddress中可以使用名字或序数。但函数仅由序数引出情况又怎样呢? 现在就来看看。"一个函数仅由序数引出"意味着函数在AddressOfNames 和Ad dressOfNameOrdinals 数组中不存在相关项。记住两个域,NumberOfFunctions 和NumberOfN ames。这两个域可以清楚地显示有时某些函数没有名字的。函数数目至少等同于名字数目,没有名字的函数通过序数引出。比如,如果存在70个函数但AddressOfNames数组中只有40项,这就意味着模块中有30个函数是仅通过序数引出的。现在我们怎样找出那些仅通过序数引出的函数呢?这不容易,必须通过排除法,比如,AddressOfFunctions 的数组项在AddressOfNameOrdinal s 数组中不存在相关指向,这就说明该函数RVA只通过序数引出。

到此为止故事好像结束了,因为我们知道应用程序中都引入了哪些函数,也能找到它们对应的地址了,但是我们从地址数组中找到的地址是不是直接添入FirstThunk指向的IMAGE_THUNK_D ATA数组呢?先让我们看看地址数组中的函数地址是什么吧,奥,原来这些地址是一些RVA,这些RVA是相对于DLL的ImageBase而言的,然而程序执行时需要的指令地址是进程地址空间的地址,所以加载器要把这些RVA转换成进程地址空间的地址,其实这很容易,只需要把RVA和DLL的I mageBase相加即可转换成进程地址空间的地址,然后把这些地址添入FirstThunk指向的IMAGE _THUNK_DATA数组。到此为止我们的问题已经解决了。这一节也就圆满结束了。下一节我们将对剩下的一些其他PE格式相关知识简要介绍一下简要作个总结这个PE格式学习笔记就算结束了(七)PE 文件的基底重定位(Base Relocations)

这节是最后一节了,其实PE格式里面还有很多东西,比如资源,也是挺复杂的一个东东,不过我对它不感兴趣,写点儿自己感兴趣的东东吧――PE 文件的基底重定位(Base Relocations)。前面我们说过了每个模块有一个优先加载地址ImageBase,这个值是连接器给出的,因此连接器生成指令中的地址时是在假设模块被加载到ImageBase的前提之下生成的,这样一来一旦模块没有按照预期的加载到ImageBase,那么程序中的指令就需要修改。下面是一个例子:假设有一个可执行文件,基地址是0x400000。在这个image 偏移位置0x2134 处是一个指针,指向一个字符串。字符串始于实际地址0x404002 处,所以指针内容应该是0x404002。你可以把文件加载,但是加载器决定把它映像到实际地址0x600000处。连接器假设的基地址和实际加载的起始地址之间的差额称为delta。此例之delta 为0x20000。整个image 的位置提高了0x20000,其中的字符串当然也是(现在应该是0x604002)。所以指向字符串的指针就错误了,delta 应该加到指针值中。为了让Windows 加载器有能力做这样的调整,可执行文件内含许多个「基底重定位资料项」,给那些存放指针的位置(本例为0x2134)使用。加载器必须把delta 加到各个位址上。本例之中加载器

pe文件格式

PE文件格式详解(一)――基础知识 什么是PE文件格式: 我们知道所有文件都是一些连续(当然实际存储在磁盘上的时候不一定是连续的)的数据组织起来的,不同类型的文件肯定组织形式也各不相同;PE文件格式便是一种文件组织形式,它是32位Wind ow系统中的可执行文件EXE以及动态连接库文件DLL的组织形式。为什么我们双击一个EXE文件之后它就会被Window运行,而我们双击一个DOC文件就会被Word打开并显示其中的内容;这说明文件中肯定除了存在那些文件的主体内容(比如EXE文件中的代码,数据等,DOC文件中的文件内容等)之外还存在其他一些重要的信息。这些信息是给文件的使用者看的,比如说EXE文件的使用者就是Window,而DOC文件的使用者就是Word。Window可以根据这些信息知道把文件加载到地址空间的那个位置,知道从哪个地址开始执行;加载到内存后如何修正一些指令中的地址等等。那么PE文件中的这些重要信息都是由谁加入的呢?是由编译器和连接器完成的,针对不同的编译器和连接器通常会提供不同的选项让我们在编译和 联结生成PE文件的时候对其中的那些Window需要的信息进行设定;当然也可以按照默认的方式编译连接生成Window中默认的信息。例如:WindowNT默认的程序加载基址是0x40000;你可以在用VC连接生成EXE文件的时候使用选项更改这个地址值。在不同的操作系统中可执行文件的格式是不同的,比如在Linux上就有一种流行的ELF格式;当然它是由在Linux上的编译器和连接器生成的,

所以编译器、连接器是针对不同的CPU架构和不同的操作系统而涉及出来的。在嵌入式领域中我们经常提到交叉编译器一词,它的作用就是在一种平台下编译出能在另一个平台下运行的程序;例如,我们可以使用交叉编译器在跑Linux的X86机器上编译出能在Arm上运行的程序。 程序是如何运行起来的: 一个程序从编写出来到运行一共需要那些工具,他们都对程序作了些什么呢?里面都涉及哪些知识需要学习呢?先说工具:编辑器-》编译器-》连接器-》加载器;首先我们使用编辑器编辑源文件;然后使用编译器编译程目标文件OBJ,这里面涉及到编译原理的知识;连接器把OBJ文件和其他一些库文件和资源文件连接起来生成EXE文件,这里面涉及到不同的连接器的知识,连接器根据OS的需要生成EXE文件保存着磁盘上;当我们运行EXE文件的时候有W indow的加载器负责把EXE文件加载到线性地址空间,加载的时候便是根据上一节中说到的PE文件格式中的哪些重要信息。然后生成一个进程,如果进程中涉及到多个线程还要生成一个主线程;此后进程便开始运行;这里面涉及的东西很多,包括:PE文件格式的内容;内存管理(CPU内存管理的硬件环境以及在此基础上的OS内存管理方式);模块,进程,线程的知识;只有把这些都弄清楚之后才能比较清楚的了解这整个过程。下面就让我们先来学习PE文件格式吧。

PE格式基础及程序的装入

DOS MZ header部分是DOS时代遗留的产物,是PE文件的一个遗传基因,一个Win32程序如果在DOS下也是可以执行,只是提示:“This program cannot be run in DOS mode.”然后就结束执行,提示执行者,这个程序要在Win32系统下执行。 DOS stub 部分是DOS插桩代码,是DOS下的16位程序代码,只是为了显示上面的提示数据。这段代码是编译器在程序编译过程中自动添加的。 PE header 是真正的Win32程序的格式头部,其中包括了PE格式的各种信息,指导系统如何装载和执行此程序代码。 Section table部分是PE代码和数据的结构数据,指示装载系统代码段在哪里,数据段在哪里等。对于不同的PE文件,设计者可能要求该文件包括不同的数据的Section。所以有一个Section Table 作为索引。Section多少可以根据实际情况而不同。但至少要有一个Section。如果一个程序连代码都没有,那么他也不能称为可执行代码。在Section Table后,Section数目的多少是不定的。 二、程序的装入 当我们在explorer.exe(资源管理器)中双击某文件,执行一个可执行程序,系统会根据文件扩展名启动一个程序装载器,称之为Loader。Loader会首先检查DOS MZ Header,如果存在,就继续寻找PE header,如果这两项都不存在,就认为是DOS 16位代码,如果只存在DOS MZ Header,而其中又指示了而其中又指示了PE Header 的位置,那么Loader 就判定此文件不一个有效的PE文件,拒绝执行。 如果DOS Header 和PE Header都正常有效,那么Loader就会根据PE Header 及Section Table的指示,将相应的代码和数据映射到内存中,然后根据不同的Section进行数据的初始化,最后开始执行程序段代码。 三、PE格式高级分析 下面我们以一个真实的程序为例详细分析PE格式,分析PE格式最好有PE分析器,常用的软件是Lord PE,也有其它的分析工具和软件如PE Editor 、Stud PE等。 先分析一下磁盘文件的内容,这里我们使用UltraEdit32(UE)工具,这是一个实用的文件编辑器,可以编辑文本和二进制文件。

PE文件头解析大全

PE可选头部 PE可执行文件中接下来的224个字节组成了PE可选头部。虽然它的名字是“可选头部”,但是请确信:这个头部并非“可选”,而是“必需”的。OPTHDROFFSET宏可以获得指向可选头部的指针: PEFILE.H #define OPTHDROFFSET(a) ((LPVOID)((BYTE *)a + \ ((PIMAGE_DOS_HEADER)a)->e_lfanew + \ SIZE_OF_NT_SIGNATURE + \ sizeof(IMAGE_FILE_HEADER))) 可选头部包含了很多关于可执行映像的重要信息,例如初始的堆栈大小、程序入口点的位置、首选基地址、操作系统版本、段对齐的信息等等。IMAGE_OPTIONAL_HEADER结构如下: WINNT.H typedef struct _IMAGE_OPTIONAL_HEADER { // // 标准域 // USHORT Magic; UCHAR MajorLinkerVersion; UCHAR MinorLinkerVersion; ULONG SizeOfCode; ULONG SizeOfInitializedData; ULONG SizeOfUninitializedData; ULONG AddressOfEntryPoint; ULONG BaseOfCode; ULONG BaseOfData; // // NT附加域 // ULONG ImageBase; ULONG SectionAlignment;

ULONG FileAlignment; USHORT MajorOperatingSystemVersion; USHORT MinorOperatingSystemVersion; USHORT MajorImageVersion; USHORT MinorImageVersion; USHORT MajorSubsystemVersion; USHORT MinorSubsystemVersion; ULONG Reserved1; ULONG SizeOfImage; ULONG SizeOfHeaders; ULONG CheckSum; USHORT Subsystem; USHORT DllCharacteristics; ULONG SizeOfStackReserve; ULONG SizeOfStackCommit; ULONG SizeOfHeapReserve; ULONG SizeOfHeapCommit; ULONG LoaderFlags; ULONG NumberOfRvaAndSizes; IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; } IMAGE_OPTIONAL_HEADER, *PIMAGE_OPTIONAL_HEADER; 如你所见,这个结构中所列出的域实在是冗长得过分。为了不让你对所有这些域感到厌烦,我会仅仅讨论有用的——就是说,对于探究PE文件格式而言有用的。 标准域 首先,请注意这个结构被划分为“标准域”和“NT附加域”。所谓标准域,就是和UNIX可执行文件的COFF 格式所公共的部分。虽然标准域保留了COFF中定义的名字,但是Windows NT仍然将它们用作了不同的目的——尽管换个名字更好一些。 ·Magic。我不知道这个域是干什么的,对于示例程序EXEVIEW.EXE示例程序而言,这个值是0x010B

PE文件结构

检验PE文件的有效性 <1>首先检验文件头部第一个字的值是否等于IMAGE_DOS_SIGNATURE,是则表示DOS MZ header有效 <2>一旦证明文件的Dos header 有效后,就可用e_lfanew来定位PE header <3>比较PE header 的第一个字的值是否等于IMAGE_NT_HEADER,如果前后两个值都匹配. PS.WinHex使用方法 1.Alt+G跳到指定位置 2.Ctrl+Shift+N放入新文件 3.大文件扩容,新建一个扩容大小+1的文件,把这个文件的数据复制后写入整个文件的尾地址. 4.文本搜索ctrl+F 5.十六进制搜索ctrl+alt+x 6.文本显示F7 7.打开内存alt+F9 8.进制转换器F8 9.分析选块F2 10.计算HASH ctrl+F2 11.收集文本信息ctrl+F10 12.编辑模式F6 一.IMAGE_DOS_HEADER <1>位置00H,WORD(2个字节)的e_magic为4D5A,即MZ <2>位置3CH,60,LONG(4个节节)的e_lfanew为64+112=176即B0H, 二.IMAGE_NT_HEADERS <1>位置B0H,DWORD(4个字节),PE开始标记,写入50450000,即PE <2>位置B4H,WORD,PE所要求的CPU,对于Intel平台,为4C01 <2>位置B6,WORD,PE中段总数,计划有3个段,.text代码段,.rdata只读数据段,.data全局变量数据段,所以值为0300, <3>位置C4,WORD,表示后面的PE文件可选头的占空间大小,即224字节(E0),值为E000 <4>位置C6,WORD,表示文件是EXE还是DLL,如果是可执行文件写0200,如果是dll,写0020, <5>位置C8,WORD,表示文件格式,如果是0B01表示.exe,如果是0701表示ROM映像

pe文件结构 入门 教程

三年前,我曾经写了一个手工打造可执行程序的文章,可是因为时间关系,我的那篇文章还是有很多模糊的地方,我一直惦记着什么时候再写一篇完美的,没想到一等就等了三年。因为各种原因直到三年后的今天我终于完成了它。现在把它分享给大家,希望大家批评指正。 我们这里将不依赖任何编译器,仅仅使用一个十六进制编辑器逐个字节的手工编写一个可执行程序。以这种方式讲解PE结构,通过这个过程读者可以学习PE结构中的PE头、节表以及导入表相关方面的知识。为了简单而又令所有学习程序开发的人感到亲切,我们将完成一个Hello World! 程序。功能仅仅是运行后弹出一个消息框,消息框的内容是Hello World!。 首先了解一下Win32可执行程序的大体结构,就是通常所说的PE结构。 如图1所示PE结构示意图: 图1 标准PE结构图 由图中可以看出PE结构分为几个部分: MS-DOS MZ 头部:所有PE文件必须以一个简单的DOS MZ 头开始。有了它,一旦程序在DOS下执行,DOS就能识别出这是有效的执行体,然后运行紧随MZ header 之后的DOS程序。以此达到对Dos系统的兼容。(通常情况DOS MZ header总共占用64byte)。 MS-DOS 实模式残余程序:实际上是个有效的EXE,在不支持PE文件格式的操作系统中,它将简单显

示一个错误提示,大多数情况下它是由汇编编译器自动生成。通常,它简单调用中断21h,服务9来显示字符串"This program cannot run in DOS mode"。(在我们写的程序中,他不是必须的,可以不予以实现,但是要保留其大小,大小为112byte,为了简洁,可以使用00来填充。) PE文件标志:是PE文件结构的起始标志。(长度4byte, Windows程序此值必须为0x50450000) PE文件头:是PE相关结构 IMAGE_NT_HEADERS 的简称,其中包含了许多PE装载器用到的重要域。执行体在支持PE文件结构的操作系统中执行时,PE装载器将从DOS MZ header中找到PE header的起始偏移量,跳过了MS-DOS 实模式残余程序,直接定位到真正的文件头PE header,长度20byte。 PE文件可选头:虽然它的名字是“可选头部”,但是请确信:这个头部并非“可选”,而是“必需”的。(长度 224byte )。 各段头部:又称节头部,一个Windows NT的应用程序典型地拥有9个预定义段(节),它们是“.text”、“.bss”、“.rdata”、“.data”、“.rsrc”、“.edata”、“.idata”、“.pdata”和“.debug”。一些应用程序不需要所有的这些段,同样还有些应用程序为了自己特殊的需要而定义了更多的段。(每个段头部占40byte,我们这里也不需要所有的段,仅需3个段。) 通常我们是将PE整个结构分成四个部分,把MS-DOS MZ 头部和MS-DOS 实模式残余程序作为第一部分,可以称他为DOS部分,而PE文件标志、PE文件头、PE文件可选头三个部分作为第二部分,称之为PE头部分,因为这部分才是Windows下真正需要的部分,所以从PE文件标志开始才是真正的PE部分。各段头部是第三部分,称之为节表。它详细描述了PE文件中各个节的详细信息。最后就是各个节的实体部分了,称为节数据。 以上仅仅是对PE结构各部分的大体讲解。接下来再手写这个Hello World!程序过程中,我将详细介绍每个部分的含义。 首先准备一下工具,一个十六进制编辑器足以。我们这里使用VC++ 6.0所携带的十六进制编辑器,您也可以使用如WinHex等十六进制编辑工具。 打开VC,选择文件,新建菜单项,然后选择一个二进制文件,单击确定。一切就绪了,下面就开始手写可执行程序,如图2所示:

PE结构4——区段与代码类型

甲壳虫免杀VIP教程 https://www.360docs.net/doc/bd18930838.html, 专业的免杀技术培训基地 我们的口号:绝对不一样的免杀教程!绝对不一样的实战体验!清晰的思路!细致全面的讲解!让你感到免杀原来可以这么简单! 动画教程只是起到技术交流作用.请大家不用利用此方法对国内的网络做破坏. 国人应该团结起来一致对外才是我们的责任.由此动画造成的任何后果和本站 无关. -------------------------------------------------------------------- 【免杀PE结构班】制作:Just41(carrieyz) 第四节【PE文件常见区段及其代码类型】 一、区段表的结构 PE文件格式中,所有的区段信息位于可选PE头之后。每个区段信息为40个字节长,并且没有任何填充信息。区段信息被定义为以下的结构: 学名:免杀技术说明大小LOADPE Name:区段名称,如".text" [8h] SizeOfRawData:RV A偏移大小[4h] VSize VirtualAddress:区段RV A起始地址[4h] VOffset PointerToRawData:区段物理偏移大小(偏移量)[4h] RSize PhysicalAddress:区段物理起始地址[4h] ROffset VirtualSize:真实长度[4h] PointerToRelocations:重定位的偏移[4h] PointerToLinenumbers:行号表的偏移[4h] NumberOfRelocations:重定位项数目[2h] NumberOfLinenumbers:行号表的数目[2h] Characteristics:区段属性[4h] 标志 计算方式: 区段表的文件偏移地址=PE头的文件偏移地址+14h+可选PE头大小+1 首先从0X3Ch处得到PE头的文件偏移地址,然后由PE头的文件偏移地址+14h得到可选PE头大小,再将上面三个数据相加再+1就得到区段表的文件偏移地址了。 VSize的大小只是效验下是否跨越下一个节了,或者是否超出了SizeOfImage,如果出现越界问题,提示非法32位应用程序,否则的话,它的值没有意义,节的大小不是由它决定的......对非最后一个节,按节间VOffset之差,最后一节用SizeOfImage-VOffset。

pe文件格式

pe文件格式:PE文件格式(1) 疯狂代码 https://www.360docs.net/doc/bd18930838.html,/ ?:http:/https://www.360docs.net/doc/bd18930838.html,/Waigua/Article60255.html 介绍说明:希望本文能够对初级入门CRACKER有定帮助翻译存在疏漏或者不准确希望来信指出感谢您指导!感谢看雪为我们提供这个交流平台让我们技术和时俱进!! 前言: PE("portableexecutable")文件格式是针对MSwindowsNT,windows95and win32s可执行 2进制代码(DLLsandprograms)在windowsNT内,驱动也是这个格式也可以用于对象文件和库 这个格式是Microsoft设计并在1993经过TIS(toolerfacestandard)委员会 (Microsoft,Intel,Borland,Watcom,IBM等)标准化了它基于在UNIX和VMS上运行对象文件和可执行文件COFF"commonobjectfileformat"格式 win32SDK包括个头文件包括对PE格式定义我将提及成员名和定义你也可能发现DLL文件"imagehelp.dll"非常有用它是NT部分但文档很少它些在"DeveloperNetwork"被描述 总览: 在PE文件开始我们可以发现MSDOS执行部分("stub");这使得任何个PE文件是有效DOS执行文件在DOS-stub的后是32位魔数0x00004550(IMAGE_NT_SIGNATURE).然后是个COFF格式文件头指明在何种机器上运行多少个节在里面连接时间是否是可执行文件或者DLL等DLL和可执行文件区别:DLL不能够启动只可以被其他可执行文件使用个可执行文件不能够连接到另个可执行文件 接着我们看到个可选文件头optionalheader(虽然叫“可选”它实际上直存在) COFF把可选文件头用于库不用于目标文件这里告诉我们文件如何被调入:起始地址预留堆栈数数据段尺寸 个有趣部分是尾巴上数据目录datadirectories这些目录包含指向节内数据指针例如如果文件有输出目录可以在成员IMAGE_DIRECTORY_ENTRY_EXPORT内发现个指针指向那个目录(目录描述结构->THUNKDATA结构->BYNAME结构)他将指向个节 在头后面是节头实际上节内容就是真正需要运行个所需要东西所有头和目录成员就是帮你找到它每个节有几个标志:对齐包含数据类型(化数据等)是否可以共享等及数据自身多数节含有个或多个通过“可选头”内数据目录项引用目录没有目录类型内容是化数据或者可执行代码(节是物理意义上内容组织目录是逻辑意义上内容

【翻译】“PE文件格式”1.9版 完整译文(附注释)

标题:【翻译】“PE文件格式”1.9版完整译文(附注释) 作者:ah007 时间: 2006-02-28,13:32:12 链接: https://www.360docs.net/doc/bd18930838.html,/showthread.php?t=21932 $Id: pe.txt,v 1.9 1999/03/20 23:55:09 LUEVELSMEYER Exp $ PE文件格式系列译文之一---- 【翻译】“PE文件格式”1.9版完整译文(附注释) ========================================================= 原著:Bernd. Luevelsmeyer 翻译:ah007 [注意:本译文的所有大小标题序号都是译者添加,以方便大家阅读。圆圈内的数字是注释的编号,其中注释②译自微软的《PECOFF规范》,其它译自网络。----译者] 一、前言(Preface) ------------------ PE(“portable executable”,可移植的可执行文件)文件格式,是微软WindwosNT,Windows95和Win32子集①中的可执行的二进制文件的格式;在WindowsNT中,驱动程序也是这种格式。它还能被应用于各种目标文件②和库文件中。 这种文件格式是由微软设计的,并于1993年被TIS(tool interface standard,工具接口标准)委员会(由Microsoft,Intel,Borland,Watcom,IBM,等等组成)所批准,它明显的基于COFF文件格式的许多知识。COFF (“common object file fromat”,通用目标文件格式)是应用于好几种UNIX系统③和VMS④系统中的目标文件和可执行文件的格式。 Win32 SDK⑤中包含一个名叫的头文件,其中含有很多用于PE格式的#define和typedef定义。我将逐步地提到其中的很多结构成员名字和#define定义。 你也可能发现DLL文件“imagehelp.dll”很有用途,它是WindowNT的一部分,但其书面文件却很缺乏。它的一些功用在“Developer Network”(开发者网络)中有所描述。 二、总览(General Layout) ------------------------- 在一个PE文件的开始处,我们会看到一个MS-DOS可执行体(英语叫“stub”,意为“根,存根”);它使任何PE文件都是一个有效的MS-DOS可执行文件。 在DOS-根之后是一个32位的签名以及魔数0x00004550 (IMAGE_NT_SIGNATURE)(意为“NT签名”,也

PE文件加密方法

对于一个软件开发商来说,要想了解PE文件加密处理的技术,第一步必须要研究PE文件格式,研究PE文件格式不但可以给我们洞悉Windows结构的良机,而且对于如何去保护自己开发的软件,有非常好的帮助,那么PE文件能否加密呢?下面我们就给大家介绍一下如何给PE文件加密。 一、什么是PE文件 PE文件的意思就是Portable Executable(可移植的执行体)。它是Win32环境自身所带的执行体文件格式,它的一些特性继承自Unix的Coff(common object fileformat)文件格式。“portable executable”(可移植的执行体)意味着此文件格式是跨win32平台的,即使Windows运行在非Intel的CPU上,任何win32平台的PE装载器都能识别和使用该文件格式。当然,移植到不同的CPU上PE执行体必然得有一些改变。所有win32执行体都使用PE文件格式,包括动态库NT的内核模式驱动程序(kernel?mode drivers)。EXE 与DLL的差别完全是语义上的,它们使用格式,其实都是PE文件格式。唯一的区别就是其中有一个字段标识出是EXE还是DLL,还有很多DLL的扩展比如OCX,CPL等都是PE文件格式。 二、PE文件加密 对被保护软件的可执行文件加密是软件保护产品的重要组成部分,一般的加密思路是对可执行文件进行加一层保护壳,即对原始文件的代码段、数据段、引入表和原始入口点等进行加密,只有在运行完加壳的代码,才对以上内容进行解密。对PE文件加密是软件开发商保护自己合法软件的一个重要步骤,它可以极大的提高被保护软件的安全性。下面介绍一下加过密的可执行文件的装载过程。

4各种文件格式的不同分析

JPEG/BMP/TIF/PNG四种图像格式有什么不同? 一、BMP格式 BMP是英文Bitmap(位图)的简写,它是Windows操作系统中的标准图像文件格式,能够被多种Windows应用程序所支持。随着Windows操作系统的流行与丰富的Windows应用程序的开发,BMP位图格式理所当然地被广泛应用。这种格式的特点是包含的图像信息较丰富,几乎不进行压缩,但由此导致了它与生俱生来的缺点--占用磁盘空间过大。所以,目前BMP在单机上比较流行。 二、GIF格式 GIF是英文Graphics Interchange Format(图形交换格式)的缩写。顾名思义,这种格式是用来交换图片的。事实上也是如此,上世纪80年代,美国一家著名的在线信息服务机构CompuServe针对当时网络传输带宽的限制,开发出了这种GIF图像格式。GIF格式的特点是压缩比高,磁盘空间占用较少,所以这种图像格式迅速得到了广泛的应用。最初的GIF 只是简单地用来存储单幅静止图像(称为GIF87a),后来随着技术发展,可以同时存储若干幅静止图象进而形成连续的动画,使之成为当时支持2D动画为数不多的格式之一(称为GIF89a),而在GIF89a图像中可指定透明区域,使图像具有非同一般的显示效果,这更使GIF风光十足。目前Internet上大量采用的彩色动画文件多为这种格式的文件,也称为GIF89a格式文件。此外,考虑到网络传输中的实际情况,GIF图像格式还增加了渐显方式,也就是说,在图像传输过程中,用户可以先看到图像的大致轮廓,然后随着传输过程的继续而逐步看清图像中的细节部分,从而适应了用户的"从朦胧到清楚"的观赏心理。目前Internet 上大量采用的彩色动画文件多为这种格式的文件。但GIF有个小小的缺点,即不能存储超过256色的图像。尽管如此,这种格式仍在网络上大行其道应用,这和GIF图像文件短小、下载速度快、可用许多具有同样大小的图像文件组成动画等优势是分不开的。 三、JPEG格式 JPEG也是常见的一种图像格式,它由联合照片专家组(Joint Photographic Experts Group)开发并以命名为"ISO10918-1",JPEG仅仅是一种俗称而已。JPEG文件的扩展名为.jpg 或.jpeg,其压缩技术十分先进,它用有损压缩方式去除冗余的图像和彩色数据,获取得极高的压缩率的同时能展现十分丰富生动的图像,换句话说,就是可以用最少的磁盘空间得到较好的图像质量。同时JPEG还是一种很灵活的格式,具有调节图像质量的功能,允许你用不同的压缩比例对这种文件压缩,比如我们最高可以把1.37MB的BMP位图文件压缩至20.3KB。当然我们完全可以在图像质量和文件尺寸之间找到平衡点。由于JPEG优异的品质和杰出的表现,它的应用也非常广泛,特别是在网络和光盘读物上,肯定都能找到它的影子。目前各类浏览器均支持JPEG这种图像格式,因为JPEG格式的文件尺寸较小,下载速度快,使得Web页有可能以较短的下载时间提供大量美观的图像,JPEG同时也就顺理成章地成为网络上最受欢迎的图像格式。 四、JPEG2000格式 JPEG2000同样是由JPEG组织负责制定的,它有一个正式名称叫做"ISO15444",与JPEG相比,它具备更高压缩率以及更多新功能的新一代静态影像压缩技术。JPEG2000作为JPEG的升级版,其压缩率比JPEG高约30%左右。与JPEG不同的是,JPEG2000同时支持有损和无损压缩,而JPEG只能支持有损压缩。无损压缩对保存一些重要图片是十分有用的。JPEG2000的一个极其重要的特征在于它能实现渐进传输,这一点与GIF的"渐显"有异曲同工之妙,即先传输图像的轮廓,然后逐步传输数据,不断提高图像质量,让图象由朦胧到清晰显示,而不必是像现在的JPEG一样,由上到下慢慢显示。此外,JPEG2000还支持所谓的"感兴趣区域"特性,你可以任意指定影像上你感兴趣区域的压缩质

PE文件格式分析及修改

PE文件格式分析及修改(图) PE 的意思是 Portable Executable(可移植的执行体)。它是 Win32环境自身所带的执行文件格式。它的一些特性继承自Unix的Coff(common object file format)文件格式。“Portable Executable”(可移植的执行体)意味着此文件格式是跨Win32平台的;即使Windows运行在非Intel的CPU上,任何win32平台的PE装载器都能识别和使用该文件格式。 PE文件在文件系统中,与存贮在磁盘上的其它文件一样,都是二进制数据,对于操作系统来讲,可以认为是特定信息的一个载体,如果要让计算机系统执行某程序,则程序文件的载体必须符合某种特定的格式。要分析特定信息载体的格式,要求分析人员有数据分析、编码分析的能力。在Win32系统中,PE 文件可以认为.exe、.dll、.sys 、.scr类型的文件,这些文件在磁盘上存贮的格式都是有一定规律的。 一、PE格式基础 下表列出了PE的总体结构 一个完整的PE文件,前五项是必定要有的,如果缺少或者数据出错,系统会拒绝执行该文件如下图

DOS MZ header部分是DOS时代遗留的产物,是PE文件的一个遗传基因,一个Win32程序如果在DOS 下也是可以执行,只是提示:“This program cannot be run in DOS mode.”然后就结束执行,提示执行者,这个程序要在Win32系统下执行。 DOS stub 部分是DOS插桩代码,是DOS下的16位程序代码,只是为了显示上面的提示数据。这段代码是编译器在程序编译过程中自动添加的。 PE header 是真正的Win32程序的格式头部,其中包括了PE格式的各种信息,指导系统如何装载和执行此程序代码。 Section table部分是PE代码和数据的结构数据,指示装载系统代码段在哪里,数据段在哪里等。对于不同的PE文件,设计者可能要求该文件包括不同的数据的Section。所以有一个Section Table 作为索引。Section多少可以根据实际情况而不同。但至少要有一个Section。如果一个程序连代码都没有,那么他也不能称为可执行代码。在Section Table后,Section数目的多少是不定的。 二、程序的装入 当我们在explorer.exe(资源管理器)中双击某文件,执行一个可执行程序,系统会根据文件扩展名启动一个程序装载器,称之为Loader。Loader会首先检查DOS MZ Header,如果存在,就继续寻找PE header,如果这两项都不存在,就认为是DOS 16位代码,如果只存在DOS MZ Header,而其中又指示

PE文件格式对定位病毒特征码的作用

摘要:电脑病毒时刻在网络中传播,影响人们的生活和工作。本文根据杀毒软件查杀电脑病毒的过程和原理,分析pe文件格式在查杀病毒过程中所起的作用。 关键词:pe文件格式;杀毒软件;病毒 中图分类号:tp309.5 文献标识码:a 文章编号:1007-9599(2013)01-0067-02 1 引言 互联网时代,病毒在网络上到处传播,窃取用户信息,破坏用户数据。杀毒软件起着“伸张正义”的角色,对病毒的破坏行为进行拦截和对病毒进行查杀。杀毒软件和病毒之间的关系是矛与盾的较量关系。虽然杀毒软件处于强者地位,一直在追杀着病毒,然而杀毒软件却是一个被动的强者。杀毒软件永远都是在病毒出现后才知道去追杀(杀毒软件预知病毒能力目前还不是很强),而病毒发现自己被追杀后,也会通过伪装、隐藏、变种等方式来躲过杀毒软件的查杀。 面对病毒的伪装、隐藏、变种,杀毒软件如何能准确而快速的对病毒进行查杀呢?本文从杀毒软件查杀病毒的原理出发,分析pe文件格式在杀毒软件定位病毒特征码中的作用。杀毒软件通过快速准确定位病毒特征码,对伪装、隐藏、变种病毒进行查杀。 2 杀毒软件查杀病毒的原理概述 2.1 电脑病毒 对于操作系统来说,电脑病毒和其他应用程序是没有差别的,都是一个exe程序。只是功能上有所区别,通常病毒程序的功能是窃取用户信息、破坏电脑中的数据等,而一般的应用程序不会这么做。 2.2 杀毒软件查杀病毒的原理及特征码的作用 由于电脑病毒也是一个exe程序,杀毒软件怎样判断一个exe程序是一个病毒程序呢? 通常是先经过杀毒软件公司的技术人员来分析,验证某程序是否具有窃取用户信息,破坏电脑数据的行为。如果有,则认为是病毒程序。在确定为病毒程序后,则需要提取该病毒文件的特征码并把特征码录入病毒库。如果杀毒软件遇上某程序文件的特征码存在于病毒库中,那么杀毒软件则判断该程序文件是一个病毒文件。从这里可以看出来,杀毒软件对病毒文件的判断主要就是通过特征码比较来判断的。 世界上有无数的exe应用程序,如果某一个非病毒程序文件与一个病毒程序文件有相同的特征码,那么杀毒软件就是“杀错好人”了。“杀错好人”的情况就是“误杀”。 一个病毒程序也可能有无数个变种,如果某一个病毒,杀毒软件只杀了第一次出现时病毒版本,而不杀它的变种版本,那么杀毒软件就是“放走坏人”了。“放走坏人”的情况就是“漏杀”。 为了减少误杀和漏杀,特征码可能不止一个,而是一组。一组特征码按照一定的排列组合来确定某程序文件是否为病毒,这样会大大减少误杀的情况。然而增加特征码的个数会增加特征码的对比次数,从而增加查杀时间。 从杀毒软件查杀病毒的原理可以知道,杀毒软件杀毒的过程中有如下矛盾: 减少“查杀时间”,那就需要减少特征码的个数,但是这样子会提高“误杀率”和“漏杀率”。 为了降低“误杀率”和“漏杀率”,需要增加特征码的个数,但是这样子会增多“查杀时间”。 也就是说“查杀时间”与“误杀率”是一对矛盾。而其中解决这对矛盾的关键因素就是特征码的个数了。 针对这对矛盾,杀毒软件需要做的事情就是要定位出精准和稳定的特征码。定位出精准和稳定的特征码就是要找到最少的特征码来标识一个病毒,而且要这些特征码在病毒的变种

PE结构详解(64位和32位的差别)

1 基本概念下表描述了贯穿于本文中的一些概念:

图1 解释了Microsoft PE可执行文件格式: PE文件总体上分为“头”和“节”。“头”是“节”的描述、简化、说明,“节”是“头”的具体化。 3 文件头 PE文件的头分为DOS头、NT头、节头。注意,这是本人的分法,在此之前并没有这种分法。这样分法会更加合理,更易理解。因为这三个部分正好构成SizeOfHeaders所指的范围,所以将它们合为“头”。这里的3个头与别的文章的头的定义会有所区别。 节头紧跟在NT头后面。 3.1 DOS头(PE文件签名的偏移地址就是大小) 用记事本打开任何一个镜像文件,其头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 program cannot be run in DOS mode(此程序不能在DOS模式下运行)”

这条消息。用户也可以自己更改该程序,有些还原软件就是这么干的。同时,有些程序既能运行于DOS又能运行于Windows下就是这个原因。Notepad.exe整个DOS头大小为224个字节,大部分不能在DOS下运行的Win32文件都是这个值。MS-DOS程序是可有可无的,如果你想使文件大小尽可能的小可以省掉MS-DOS程序,同时把前面的参数都清0。 3.2 NT头(244或260个字节) 紧跟着PE文件签名之后,是NT头。NT头分成3个部分,因为第2部分在32与64位系统里有区别,第3部分虽然也是头,但实际很不像“头”。 第1部分(20个字节) 第2部分(96或112个字节)

PE文件格式实验

PE文件格式分析实验 使用工具LordPE/PEview、winhex 选择一个exe或者DLL文件 阶段一:(本次实验) 1.DOS头部查看、对应DOS头结构进行数据逐项分析 2.PE头部查看、对应PE头结构进行数据逐项分析 3.Exe文件和DLL文件均是PE格式,他们的区别在哪里? 4.Section表结构的查看(是否可以增加一个新的section表?对齐边界是多少?) 5.PE文件section查看、对应section 块表结构进行数据分析 6.VA、RVA、RA计算 7.问题:你查看的PE文件DOS、PE头部的空隙是多大? 8.问题:你查看的PE文件在那个section的空隙最大/最小? 9.问题:如果手工增加一个section,要修改哪些字段,请手工试验。(提高:你的新块表 增加是否引起原文件的对齐位置的改变?) 10.问题(提高):一个section的属性字节如何设置,请在上一个问题基础上实验。 本次实验要求对照以上要求,自行选择文件进行分析,撰写报告。 阶段二:(后一阶段的工作) 1.资源查看、修改 2.编写PE文件分析程序 3.编写PE病毒程序 附录1PE格式详细讲解(一) 前几天发了一个PE信息查看器的小工具,本来想用那个获取邀请码的,可是觉得几率不是太大,于是再献上一篇教程,既是为了自己能获得邀请码,也是帮助那些想学习PE格式的人,让知识来源于网络再回归网络。 N年没写文章了,不知道句子还能不能写通顺,最近正在看《软件加密技术内幕》,刚看完PE结构那部分内容,所以想起来写篇教程作为读书笔记,既可加强记忆又可帮助别人,何乐而不为呢。 好了,废话少说好戏正式上场,PE是英文Portable Executable(可移植的执行体)的缩写,从缩写可以看出它是跨平台的,即使在非intel的CPU上也能正常运行的。它是 Win32环境自身所带的执行体文件格式。其实不光是EXE文件是PE格式,其它的一些重要文件,例如动态链接库文件(DLL),驱动文件(SYS)等也是PE格式的,所以学好PE格式是非常重要的,以下我把这类文件统称为PE文件。学习PE文件结构不仅可以使我们知道可执行文件是怎样运行的,也可以使我们了解一下windows操作系统的一些工作机制,精通PE是成为计算机高手的必经之路。 其实说白了PE文件格式就是一种文件组织的方式,里面对一些重要信息的存放做了一些规定,比如文件要运行,我们就得先知道入口地址,可是我们从哪去得到入口地址呢,我们必须把保存有入口地址信息

PE文件各区段说明

PE文件各节所包含的内容 2008-11-13 13:34 关于 sections 的意义以及它如何定位,相信你已有个概念。现在我们要看看在EXE 和OBJ 档中的一些常见的 sections 。虽然我所列的并不是全部,但已经涵盖了你每天会接触到(但也许你自己并不知道)的 sections 。排列次序是根据其重要性以及遭遇它们的频繁度。 .text section .text 内含所有一般性的程序代码。由于 PE 文件在 32 位模式下跑,并且不受约束于 16 位元节区,所以没有理由把程序代码分开放到不同的 sections 中。联结器把所有来自 .OBJ的 .text 集合到一个大的 .text 中。如果你使用Borland C++,其编译器制作出来的 code section 名为 CODE 而不是 .text 。请看稍后「Borland CODE 以及 .icode sections 」一节。 我很惊讶地发现,在 .text 中除了编译器制作出来的码,以及runtime library 的码之外, 还有一些其它东西。在 PE 文件中,当你呼叫另一模块中的函数(例如 USER32.DLL 中 的 GetMessage ),编译器制造出来的 CALL 指令并不会把控制权直接传给 DLL 中的 函数,而是传给一个JMP DWORD PTR [XXXXXXXX]指令,后者也位于 .text 中。JMP 指 令跳到一个地址去,此地址储存在 .idata 的一个 DWORD 之中。这个DWORD 内含该 函数的真正地址(函数进入点),如图8-4 所示。

图8-4 一个 PE 档呼叫 imported function 。 沉思良久,我终于了解为什么 DLL 的呼叫需要以这种方式实现。把对同一个DLL 函数 的所有呼叫都集中到一处,加载器就不再需要修补每一个呼叫 DLL 的指令。PE 加载器 需要做的,就只是把 DLL 函数的真实地址放到 .idata 的那个 DWORD 之中,根本就 没有程序代码需要修补。这和 NE 档有极明显的差异。NE 档的每一个节区内含一串待修 正记录(fixup records ),如果某一节区呼叫同一个 DLL 函数 20 次,加载器就必须忙 碌 20 次,将函数地址拷贝到待修正记录之中。PE 档这种处理方式也有缺点:你不能够 以 DLL 函数的真正地址初始化一个变量。例如: FARPROC pfnGetMessage = GetMessage; 是把 GetMessage 函数地址放到 pfnGetMessage 变量中。在 Win16 这没问题,在 Win32 ,变量中放的其实将是稍早我说过的JMP DWORD PTR [XXXXXXXX]指令的地址。如 果你根据这个函数指针来呼叫函数,事情会如你所预期。但如果你要以此指针读取 GetMessage 的前数个字节,幸运之神不会站在你那边。稍后我将在「PE 文件的输出 (exports )」一节中再继续讨论这个主题。 在我写完本章的第一个版本之后,Visual C++ 2.0 推出了。它介绍另一种新的呼叫方式。 如果你看过 Visual C++ 2.0 的系统表头文件(例如 WINBASE.H ),你将看到和过去不同 的东西。在 Visual C++ 2.0 中,API 函数原型都有一个 __declspec(dllimport )作为原型 的一部份。当你呼叫一个这样的函数,编译器不会在模块的另一个地方产生JMP DWORD PTR [XXXXXXXX]指令,而是产生一个CALL DWORD PTR [XXXXXXXX]函数呼叫。XXXXXXXX 位 址位于 .idata 内,作用与原先在JMP DWORD PTR [XXXXXXXX]指令中的地址相同。就我 所知,Borland C++ 4.5 编译器并没有这样的性质。 Borland CODE 以及 .icode sections

PE文件结构详解

PE文件结构详解 1 摘要 Windows NT 3.1引入了一种名为PE文件格式的新可执行文件格式。PE文件格式的规范包含在了MSDN的CD中(Specs and Strategy, Specifications, Windows NT File Format Specifications),但是它非常之晦涩。 然而这一的文档并未提供足够的信息,所以开发者们无法很好地弄懂PE格式。本文旨在解决这一问题,它会对整个的PE文件格式作一个十分彻底的解释,另外,本文中还带有对所有必需结构的描述以及示范如何使用这些信息的源码示例。 为了获得PE文件中所包含的重要信息,我编写了一个名为PEFILE.DLL的动态链接库,本文中所有出现的源码示例亦均摘自于此。这个DLL和它的源代码都作为PEFile示例程序的一部分包含在了CD中(译注:示例程序请在MSDN中寻找,本站恕不提供),你可以在你自己的应用程序中使用这个DLL;同样,你亦可以依你所愿地使用并构建它的源码。在本文末尾,你会找到PEFILE.DLL的函数导出列表和一个如何使用它们的说明。我觉得你会发现这些函数会让你从容应付PE文件格式的。 2 介绍 Windows操作系统家族最近增加的Windows NT为开发环境和应用程序本身带来了很大的改变,这之中一个最为重大的当属PE文件格式了。新的PE文件格式主要来自于UNIX操作系统所通用的COFF规范,同时为了保证与旧版本MS-DOS及Windows操作系统的兼容,PE文件格式也保留了MS-DOS中那熟悉的MZ头部。 在本文之中,PE文件格式是以自顶而下的顺序解释的。在你从头开始研究文件内容的过程之中,本文会详细讨论PE文件的每一个组成部分。 很多解决PE文件格式的工作和直接观看数据有关。例如,要弄懂导入地址名称表是如何构成的,我就得同时查看.idata段头部、导入映像数据目录、可选头部以及当前的.idata段实体,而EXEVIEW.EXE就是查看这些信息的最佳示例。 在针对PE文件的有关编程中,你可能用到以下一些数据结构: IMAGE_DOS_HEADER IMAGE_IMPORT_DESCRIPTOR IMAGE_NT_HEADERS IMAGE_SECTION_HEADER IMAGE_OPTIONAL_HEADER IMAGE_DA TA_DIRECTORY IMAGE_FILE_HEADER 3 PE文件结构图

相关主题
相关文档
最新文档