PE文件格式之win32应用程序初探

来源:互联网 发布:2016网络彩票代理加盟 编辑:程序博客网 时间:2024/05/19 17:23

   PE文件格式之win32应用程序初探

       汉字最好--2008.1

        看雪论坛首发

   几年前曾打算学习PE文件格式,但满篇的英语和c语言令人声畏。一直到上个月因为需要才硬着头皮学下来。于是参考了几篇1.9版译文,同时对数个文件反编译、修改,最后自己组装了一个小exe,总算对PE文件有了一个初步的认识。

   本文仅仅研究的是win32GUI程序中重要的内容,对dll,驱动,控制台没有涉及,适合最初学的、不懂c语言和英语的学习者学习

   工具:ollice,超级工具,本文用来PE文件在内存中的情况。

         Winhex,二进制编辑工具,本文用来查看、编辑、创建PE文件。

         Restool,资源工具,本文用来验证资源数据。

以上工具都有前辈的汉化修正版,对不懂鸟语学习者来说是天大的福音。译者易也,我能理解翻译者的难处,给计算机英语取个好中文名有点难度。是他们给我节约了学习一门外语的时间,让我不懂英语一样也学到了知识。

本文中不正确的地方望指正!

 PE文件格式之win32应用程序初探 附件.rar

 

         序言

PE文件是微软弄出来的可执行文件格式。Pe文件中的数据是一节节整齐的排列下去的,所以我们把一节数据称为‘’。PE文件的第一个节叫头节,但为了与其他节区别开,我把头节还是叫文件头。文件头依次包括dospe选项头节头表四个部分。

执行pe文件实际上是系统的pe载入器把PE文件的各节映射到一个虚拟的进程空间后再执行代码的。这个虚拟的进程空间在32位下有$FFFFFFFF($表示十六进制,相当于c0x,汇编的H),4GB大小,即所谓的虚拟内存,里面的地址也就是虚拟地址.这个pe文件在虚拟空间里称为映象文件,映象文件开始的地方叫映象文件基址.而本文后面提到的虚拟地址都是相对虚拟地址,即相对于映象文件基址的偏移量.pe文件中的偏移量称为物理地址.

本章给出了很多重要定义,有一些和1.9版译文不一样.其实起什么名不重要,重要的是能不能理解.另外,本文中称的’**除特别说明外指**的数组.:节头表,就是节头的数组.

下面用winhex打开附件中的st.exe对照学习.

 

 

      dos

Pe文件的开始是文件头,文件头的开始是dos,dos头的开始是’MZ’.如下:

物理地址

占用字节

含义

常用值

在文件中形式

0

2

Dos头开始标志

‘MZ’

40 5A

2

58

Dos下的东西

Windows下都置0

$3C

4

PE头的物理地址

如果后面没有dos根就取$40,否则还要加上dos根大小.下文将这项取值用ppe表示.

(数值和字符串在文件中的形式不用我提了吧?)

下面一般有一个dos根结构,是一小段dos程序,对我们初学者没有用,本文就不提了.

 

 

 

         PE

PE头是本文的第二部分,$18字节.

物理地址

占用字节

含义

常用值

在文件中形式

Ppe+0

4

PE头开始的标志

‘PE’

50 45 00 00

Ppe+4

2

Cpu要求

$14C(386以上)

4C 01

Ppe+6

2

节数目

后面节头表声明了几个就是几

Ppe+8

4

日期时间

多半是乱填

 

Ppe+$C

8

没什么用

都置0

 

Ppe+$14

2

选项头大小

$E0

E0 00

Ppe+$16

2

PE属性

$10F$818E,互改了都能运行

0F 01

PE头中最重要的是节数目,其他值都用常用值就可以.

 

 

        选项头

  选项头是文件头的第三部分.选项头中的许多选项是告知系统如何在虚拟内存中执行的数据.$E0字节

物理地址

占用字节

含义

常用值

在文件中形式

Ppe+$18

2

选项头标志

$10B

0B 01

Ppe+$1A

2

链接器版本号

可随便写

 

Ppe+$1C

4

代码段长度

用不上

 

Ppe+$20

4

初始化数据长度

用不上

 

Ppe+$24

4

未初始化数据长度

可能有用吧

 

Ppe+$28

4

程序入口(OEP),程序开始执行的地方.这是虚拟地址

如果加了壳,就是壳的入口

Ppe+$2C

4

代码段基址

好像用不上

Ppe+$30

4

数据段基址

Ppe+$34

4

映象文件基址.改成$200000没影响.改成$100000就与栈冲突了.如果载入这个基址出了问题,系统就会从基址重定位表中找.

$400000

00 00 40 00

Ppe+$38

4

节对齐.节被映射到虚拟内存后,占用节对齐的整数倍.:节有$2400字节,在虚拟内存中占$3000.

$1000也就是4KB

00 10 00 00

Ppe+$3C

4

文件对齐.节在文件中占用文件对齐的整数倍,不足的补0,包括文件头(头节)

$200也就是512字节

00 02 00 00

Ppe+$40

4

这三项是操作系统及子系统版本号

4

04 00 00 00

Ppe+$44

4

4

04 00 00 00

Ppe+$48

4

4

04 00 00 00

Ppe+$4C

4

没用的

0

 

Ppe+$50

4

映象文件大小.是所有节映射到虚拟内存后的大小.别忘了计算文件头和未初始化数据节.文件头一般占一个节对齐,节映射到虚拟内存后是节对齐的整数倍,所以映象文件大小也是节对齐的整数倍.

 

 

Ppe+$54

4

文件头大小.随便写也没问题

$400

00 04 00 00

Ppe+$58

4

校验和.可能用得上.

0

 

Ppe+$5C

2

NT子系统(控制台选3)

2

02 00

2

Dll状态

0

00 00

Ppe+$60

4

保留栈

1MB

00 00 10 00

Ppe+$64

4

初始栈

4KB

00 10 00 00

Ppe+$68

4

保留堆

1MB

00 00 10 00

Ppe+$6C

4

初始堆

4KB

00 10 00 00

Ppe+$70

4

载入风格

0

 

Ppe+$74

4

数据索引表的索引数

16

10 00 00 00

上面共占用$60字节.下面紧跟一个数据索引表,占用$80字节.

数据索引表由16索引组成,索引的前4字节是某种数据的虚拟地址,4字节是数据的大小.数据的类型由索引在表中的位置决定,各位置的意义如下:

0:输出表(DLL必用)

1:输入表

2:资源数据

3:异常

4:安全

5:基址重定位表

6:调试

7:描述文字

8;机器值

9:线程存储地址(TLS)

10:载入配置

11:绑定输入表

12:输入地址表

13-15未见定义

St.exe定义了第1,2,12,没有用到的都置0,

选项头到这里就结束了.其实也只有程序入口,映象文件基址,数据索引表让你斟酌一下,其他都有默认值.

 

 

      节头表

节头表由一串节头组成.每个节头都声明如何把一个节映射到虚拟内存中去.一个节头有$28字节,结构如下:

占用字节

含义

说明

8

节的名称

Asni字符.可不写

4

物理地址或大小

既然‘或,那随便吧

4

节的虚拟地址

别定义到已经定义了地方.$300,显然已经被文件头占有.

4

节数据大小

注意:肯定是文件对齐的整数倍,即使你的一个节实际使用2个字节也要占一个文件对齐.未初始化数据是文件对齐0

4

节的物理地址

 

12

无用的,都置0

 

4

节属性

是个32位数据,具体查1.9版译文

常见节属性:

1:代码$00000002

2:已初始化数据$00000004

3:未初始化数据$00000008

4:可共享$10000000

5:可执行$20000000

6:可读$40000000

7:可写$80000000

 

 

$60000020   1 5 6 (算术或运算)  通常是代码段

$C0000040   2 6 7              通常是输入表

$40000040   2 6                   通常是资源数据

我们一起看st.exe,红色标记虚拟地址:

00000060   00 00 00 00 00 00 00 00  30 10 00 00 00 00 00 00   ........0.......

00000070   00 00 00 00 00 00 40 00  00 10 00 00 00 02 00 00   ......@.........

$6C处是入口=$1030,$70处是映象文件基址=$400000.

00000130                         2E 74 65 78 74 00 00 00           .text...

00000140   00 01 00 00 00 10 00 00  00 02 00 00 00 02 00 00   ................

00000150   00 00 00 00 00 00 00 00  00 00 00 00 20 00 00 60   ............ ..`

00000160   2E 72 64 61 74 61 00 00  6A 01 00 00 00 20 00 00   .rdata..j.... ..

00000170   00 02 00 00 00 04 00 00  00 00 00 00 00 00 00 00   ................

00000180   00 00 00 00 40 00 00 40  2E 64 61 74 61 00 00 00   ....@..@.data...

00000190   04 00 00 00 00 30 00 00  00 00 00 00 00 00 00 00   .....0..........

000001A0   00 00 00 00 00 00 00 00  00 00 00 00 40 00 00 C0   ............@..?

000001B0   2E 72 73 72 63 00 00 00  60 02 00 00 00 40 00 00   .rsrc...`....@..

000001C0   00 04 00 00 00 06 00 00  00 00 00 00 00 00 00 00   ................

000001D0   00 00 00 00 40 00 00 40                            ....@..@

上面就是节头表,

名称

虚拟地址

大小

物理地址

属性

.text

$1000

$200

$200

$60000020

.rdata

$2000

$200

$400

$40000040

.data

$3000

$0

0

$400000C0

.rsrc

$4000

$400

$600

$40000040

看这些节都没有超过节对齐,所以都只占一个节对齐..data只是预定了空间,适合变量用.

虚拟地址转物理地址:

我们看程序入口正好落到了.text节的+$30($1030-$1000),来到$200+$30:

Offset      0  1  2  3  4  5  6  7   8  9  A  B  C  D  E  F

00000230   6A 00 E8 35 01 00 00 A3  00 30 40 00 E8 4F 01 00   j.?...?0@...

00000240   00 6A 00 68 5E 10 40 00  6A 00 6A 65 FF 35 00 30   .j.h^.@.j.je5.0

可在OLLICE中查看此处代码含义($00401030.

 

在节头表部分,每个节头的虚拟地址,大小,物理地址,属性都要仔细设定.

文件头到这里就结束了,不足文件对齐整数倍的要补足0.文件后面紧跟节头表定义的各种节.

当然后面也可能只有一个节.

 

 

             输入表

   输入表在虚拟内存中的地址和大小由选项头的数据索引表中第二条索引指定.输入表有点回调函数的味道,是给系统调用的.通过输入表,系统了解程序需要哪些动态库的函数,在加载PE文件时把这些动态库也映射到进程虚拟空间,并把这些函数的地址写到程序指定的地方供调用.

输入表实际上是一个‘输入说明结构的数组.该数组的最后一个成员置0以表结束.

一个输入说明结构由5个双字组成,占用20字节,对应一个动态库.如下:

 

占用字节

名称

含义

1

4

地址一

指向函数索引地址表的地址

2

4

时间戳

用于验证dll或绑定输入

3

4

中转链

 

4

4

动态库名

指向动态库名的指针(Pcharchar*),动态库名是标准字符串

5

4

地址二

指向地址表的指针.这个地址表可能与其他输入说明结构指向的地址表组成一个连续的输入地址表.(见数据索引表)

标准字符串指以空字符结束的ansi字符串.

   上述结构中第2,3项没有找到实例所以跳过,呵呵.

   动态库名也好理解,关键是第1,5.地址一是个指针,指向函数索引地址表.这个表里都是函数索引的地址,以空地址(0)表示结束.函数索引由一个2个字节的索引号和进跟其后的一个标准字符串(函数名)组成.

   系统的工作就是首先获得动态库名,然后顺着地址一找到函数索引地址表,再一条条地读函数索引,将找到的函数的地址依次写到地址二指定的地址表中,直到读到空地址,再读取下一条输入说明结构,直到为0.

   有点难于理解?再次用winhexollice打开st.exe:

000000C0   30 20 00 00 50 00 00 00  00 40 00 00 60 02 00 00   0 ..P....@..`...

来到$C0,输入表的虚拟地址是$2030,大小$50.换成物理地址在$430,.rdata节中.

来到$430,可以读倒条输入说明结构:

00000430   88 20 00 00 00 00 00 00  00 00 00 00 F2 20 00 00   ?..........?..

00000440   08 20 00 00 9C 20 00 00  00 00 00 00 00 00 00 00   . ..?..........

00000450   3A 21 00 00 1C 20 00 00  80 20 00 00 00 00 00 00   :!... ..€ ......

00000460   00 00 00 00 5C 21 00 00  00 20 00 00 00 00 00 00   ..../!... ......

00000470   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................

上面都是虚拟地址,转成物理地址时-$2000+$400.

以第一条为例,地址一为$2088->$488,地址二为$2008->$408,动态库名为$20F2->$4F2.

000004F0   00 00 6B 65 72 6E 65 6C  33 32 2E 64 6C 6C 00 00   ..kernel32.dll..

$4F2原来是kernel32.dll.

$488:

00000480                          E2 20 00 00 CE 20 00 00      ?..?..

00000490   BE 20 00 00 B0 20 00 00  00 00 00 00               ?..?......

有四个地址,也就是说有四个函数.来到$4E2($2012):

000004E0   00 00 3C 02 53 65 74 4C  61 73 74 45 72 72 6F 72   ..<.SetLastError

000004F0   00 00                                              ..

找到了一个函数,其它的就这么找.

在地址二指向的$408好像看不到什么,OLLICE,’M’图标,,在列表中双击

输入表项(Memory map, 条目 21),在弹出的窗口中看$400000+$2008处已经写入了函数地址,红色字体表示是载入时写入的.

多数情况下程序并不是直接CALL这个地址,而是先CALL一个跳转表,然后JMP到这里:

00401030 >/$  6A 00         push    0                           ; /pModule = NULL

00401032  |.  E8 35010000   call    <jmp.&kernel32.GetModuleHandleA> ; /GetModuleHandleA

0040116C   $- FF25 0C204000 jmp     dword ptr [<&kernel32.GetModuleH>;  kernel32.GetModuleHandleA

00401172   $- FF25 08204000 jmp     dword ptr [<&kernel32.SetLastErr>;  ntdll.RtlSetLastWin32Error

一些编译器将地址一都置0,这时系统用地址二代替地址一,此时,地址二指向的地址表要符合函数索引地址表的规范,即以空地址表结束.Project1.exedelphi编写的)为例:

输入表

00002C00   00 00 00 00 00 00 00 00  00 00 00 00 08 61 00 00   .............a..

00002C10   78 60 00 00 00 00 00 00  00 00 00 00 00 00 00 00   x`..............

00002C20   90 62 00 00 D0 60 00 00  00 00 00 00 00 00 00 00   ............

00002C30   00 00 00 00 BC 62 00 00  DC 60 00 00 00 00 00 00   ............

00002C40   00 00 00 00 00 00 00 00  FC 62 00 00 EC 60 00 00   ............

00002C50   00 00 00 00 00 00 00 00  00 00 00 00 48 63 00 00   ............Hc..

00002C60   00 61 00 00 00 00 00 00  00 00 00 00 00 00 00 00   .a..............

00002C70   00 00 00 00 00 00 00 00

  地址二指向的地址表

00002CD0   9C 62 00 00 AE 62 00 00  00 00 00 00 CA 62 00 00   ..........

00002CE0   DE 62 00 00 EE 62 00 00  00 00 00 00 0A 63 00 00   .........c..

00002CF0   18 63 00 00 26 63 00 00  34 63 00 00 00 00 00 00   .c..&c..4c......

00002D00   54 63 00 00 00 00 00 00                            Tc......

再看看ollice中相应的地方,明白了吗?

多打开几个文件看看就会加深映象。

 

 

              资源数据

   其实有些程序除了程序图标并不使用其他的标准资源数据,标准的资源数据在链接到PE文件中前是一个单独的资源文件,通常以.res为后缀.

   资源数据的虚拟地址和大小由数据索引表第三项指定.

   标准的资源数据是一个树状结构,共分5:

              一层

         _______|_______________

         |       |               |

              二层

    _____________|__________________

    |        |           |           |

              三层

    _____________|__________________

    |        |           |           |

               四层

    _____________|__________________

    |        |           |           |

               五层

一、二、三层是相似的结构,四层是资源数据的描叙,五层是具体的资源数据(如光标、位图).

   一、二、三层都由一个或几个资源干组成.当然一层只能有一个资源干(是这个资源树的主干嘛).一个资源干由16字节的项目干和数目不等的项目组成,项目干决定项目的数目.另外,标准资源中的字符都是unicode字符.

资源干=项目干+项目1+……+项目n.

项目干结构如下:

4字节

特征

4字节

时间

4字节

版本

2字节

已命名项目数.使用名称标识资源的项目数目

2字节

ID项目数.使用ID(数字编号)标识资源的项目数目

项目的结构如下:

 

占用字节

最高位

含义

132位数据

4

1

剩下31位是资源名称的偏移量

0

ID

232位数据

4

1

剩下31位是下层某资源干的偏移量

0

四层某资源描叙的偏移量

下面的内容很重要:

项目中的偏移量指相对于资源数据起始位置(一层的开头)的偏移.

项目中的ID在一层指资源类型,二层指具体资源的ID,在三层指语言ID(04 09是美国英语).

一层中资源类型ID如下:

 1: 光标 2: 位图3: 图标 4: 菜单5: 对话框6: 字串表 7: 字体目录8: 字体 9: 快捷键
  10: 
未格式化资源数据11: 信息表12: 组光标 14: 组图标 16: 版本信息

ID10时可以用来导入任何文件

程序可以利用二层的ID或资源名称来调用相应的资源。如例程中st.exe用函数调用了ID号为$65的对话框资源.($00401052调用dialogboxparama)

三层的项目给出了四层某资源描叙的偏移量.资源描叙的结构如下:

占用字节

含义

4

具体资源的虚拟地址(当然是相对映象文件基址的偏移量)

4

具体资源的大小

4

代码页(不知道有什么用)

4

未用.

具体资源就是五层了.具体资源的格式就不在本文讨论范围内了.太阳的,俺连对话框格式还没有完全弄明白呢.本来还想说说数据索引表指定的其他几种数据(如重定位,TLS),可实在找不到有价值的资料,遗憾.

还是以st.exe为例:

$C8处指定资源数据开始于$4000,$1B0.rsrc节也开始于$4000,所以这一节就是资源.

来到其物理地址$600,读资源干,原来有两个项目一个是对话框(5),一个是版本($10).下面我用颜色标识对话框这条分支.

00000600   00 00 00 00 00 00 00 00  00 00 00 00 00 00 02 00   ................

00000610   05 00 00 00 20 00 00 80  10 00 00 00 38 00 00 80   .... ..€....8..€

00000620   00 00 00 00 00 00 00 00  00 00 00 00 00 00 01 00   ................

00000630   65 00 00 00 50 00 00 80  00 00 00 00 00 00 00 00   e...P..€........

00000640   00 00 00 00 00 00 01 00  01 00 00 00 68 00 00 80   ............h..€

00000650   00 00 00 00 00 00 00 00  00 00 00 00 00 00 01 00   ................

00000660   09 04 00 00 80 00 00 00  00 00 00 00 00 00 00 00   ....€...........

00000670   00 00 00 00 00 00 01 00  04 08 00 00 90 00 00 00   ............?..

00000680   A0 40 00 00 82 00 00 00  00 00 00 00 00 00 00 00   ..?..........

00000690   28 41 00 00 38 01 00 00  00 00 00 00 00 00 00 00   (A..8...........

上面红色标出ID($65就是上面提到的资源ID),紫色标出偏移量(注意($80就是最高位为1 ),蓝色标出具体资源的虚拟地址,换成物理地址就是$6A0.有兴趣到$6A0处研究下对话框格式.

 

       组装PE文件

学习到这里就告一段落了,这章我们一起来组装一个PE文件.不知道为什么1.9版译文中组装的控制台程序在我的xp下总报错.重新弄个例子吧.

winhex新建一个文件,大小位$600字节,命名为测试1’,开工:

偏移

写入值

0

4D 5A//’MZ’

$3C

40 00//PE头偏移

$40

50 45//’PE’

$44

4C 01//cpu

$46

02 00//节数目

$54

E0 00//选项头大小

$56

0F 01//PE属性

$58

0B 01//标志

$68

00 10 00 00//入口

$74

00 00 04 00//映象文件基址

$78

00 10//节对齐

$7C

00 02//文件对齐

$80

04 00

$84

04 00

$88

04 00//版本号

$90

00 30//映象文件大小(两个节加一文件头,各占一个节对齐)

$94

00 02//文件头大小

$9C

02 00//NT子系统

$A4

00 00 10 00//

$A8

00 10

$AC

00 00 10 00//

$B0

00 10

$B4

10 00//数据索引数

数据索引表只有输入表索引(这里把输入表放到第二节)

$C0

00 20 00 00//输入表虚拟地址

$C4

28 00 00 00//输入表大小

下面定义第一个节,包含代码和两条字符串

$138

63 6F 64 65//节名

$144

00 10 00 00//虚拟地址

$148

00 02 00 00//节大小

$14C

00 02//物理地址

$15C

20 00 00 60//节属性,

下面定义第二个节,是输入表

$160

64 61 74 61//节名

$16C

00 20 00 00//虚拟地址

$170

00 02//大小

$174

00 04//物理地址

$184

40 00 00 C0//属性

文件头写完了.先完成第二个节的具体数据:$460

00000460   75 73 65 72 33 32 2E 64  6C 6C 00 00 00 00 4D 65   user32.dll....Me

00000470   73 73 61 67 65 42 6F 78  41 00                       ssageBoxA

只输入一个函数,索引号没有写.记下库名和函数名的地址,换成虚拟地址分别是$2060$206C.现在可以填函数地址索引表了,只有一个,$430处写入6C 20($206C).然后填输入表了,也只有一条输入说明结构:$400处写地址一30 20($2030->$430),$40C处写库文件名60 20($2060),$410处写地址二30 20(呵呵跟地址一一样哦).载入后messageboxa的地址就会写到地址二,我们在代码中就可以调用这个函数了.

下面完成第一个节,先写两条字符:

00000220   B3 AC D0 A1 B3 CC D0 F2  00 00 00 00 00 00 00 00   超小程序........

00000230   50 45 CE C4 BC FE D1 A7  CF B0 B3 C9 B9 FB 00 00   PE文件学习成果..

记下它们的虚拟地址$1020$1030.

$200处开始写代码:

55              push    ebp

8BEC           mov     ebp, esp

6A 00           push    0

68 20104000     push    00401020

68 30104000     push    00401030

6A 00           push    0

FF15 30204000   call    dword ptr [$00402030]   ; user32.MessageBoxA

61              popad

保存收工.运行看看.不对头?对照附件中的测试1.exe看看哪里写错了.

有兴趣看看测试2.exe,代码和输入表写到一个节里,节属性要改,用的是跳转方式.

完了,请斧正!

参考资料: PE文件格式”1.9 完整译文(附注释)

感谢俺老婆打了一半的文字

 
原创粉丝点击