首页
社区
课程
招聘
[原创]学写压缩壳心得系列之二 掌握PE结构 ,曲径通幽
发表于: 2012-2-5 17:41 16689

[原创]学写压缩壳心得系列之二 掌握PE结构 ,曲径通幽

2012-2-5 17:41
16689
1.数据与指令,以及加载前期相关概念(已完成)
2.pe文件的结构解析(已完成)
3.pe文件load的过程(已完成)
4.壳的处理(已完成)

PE文件中的结构众多,但是其实,这些结构一多半是告诉loader怎么去加载自身PE的。正常的PE文件总是很严格去填充自身的内部结构,但是也有一小部分变形PE文件没有那么的中规中矩,所以PE结构中有的信息到了loader可能最终也只是起到了一个校验作用。在纵观PE整个大结构的时候,不应该去拘泥于每一个数据结构的特征,应该从大体上去把握它。本文试图通过loader加载方式的类比和举例的角度,介绍了loader是如何讲PE结构中相关重要数据进行加载的。因为介绍PE的优秀文章有很多加上作者水平很有限,如果有错误,请各位指正。

下面,就开始逐一的介绍相关重要的结构:
1.各种表头
  1.1 Dos表头
  1.2 pe文件头
  1.3 区块表头
2.导入表
3.导出表
4.重定位表
5.资源
6.附加数据


1.表头

一共包含了有三个类型的表头,一个是Dos表头,目的是为了兼容以前的平台。一个是pe文件头,包含了pe自身加载的各个配置信息,还有一个就是区块表头,包含pe文件各个区块的信息。整个表头,就给loader提供了一个这样的信息:“这个PE是什么属性,整个PE如何在内存分布的,又会被加载到哪里去“。

1.1 Dos表头
为了兼容以前的DOS平台,MS在设计的PE格式的时候考虑到了MS-DOS头部。其中有一个结构是MZ值,也就是相对偏移00处。在壳程序判断是否是PE文件的时候,这个是条件之一。另一个条件就是距离偏移0X3Ch处。这里保存了一个地址,这个地址指向了(PE header)PE文件头。这个才是装载器开始装载真正意义上的PE开始的地方。在相对于PE header偏移0h的地方,保存着一个“PE”标识。

根据上面两个信息,我们就可以判断被加壳的文件是否为PE文件了。

1.2 pe文件头
PE标识的下面一个偏移,就是FILE_HEADER了,它被称之为PE头,其中相对FILE_HEADER偏移0x06h的NumberOfSections表示的是文件的块数目。0x14h偏移处的SizeOfOptionalHeader表示的是可选头的大小,这个值可以用来定位节表可选头OptionalHeader的大小。

PE头0x18h偏移处就是OptionalHeader了。OptionalHeader是PE中相对重要的结构。虽然译做可选头,但是里面的很多选项都是必选的。

其中以下的结构是比较重要的:

AddressOfEntryPoint
指出文件载入内存后,代码开始执行的地方,也就是我们常说的OEP。例如我们对文件进行加壳之后,原来的入口点保存后,修改其中的值,指向外壳代码,使加壳的后PE能够先进行解压,再跳到原始的入口点继续运行。

ImageBase
指出文件的优先装入地址。Loader优先将文件装入到由ImageBase字段指定的地址中,若指定地址装载失效,才被装入到其他地址中。
EXE文件通常即指定,即装载,所以EXE总是能够按照这个地址装入。因为ms为每一个exe文件设定了单独的4GB进程空间,对于装载来说,exe的地位是优先的,所以总能够保证其装载ImageBase成功。
而DLL文件来说,不能保证优先装入地址没有被其他的DLL使用,因为一个exe可以装载不同的dll到其地址空间去,所以DLL通常含有重定位信息来进行二次定位,即该地址若被占用,调用重定位表的偏移,重新定位。

SectionAlignmentFileAlignment
这里即是内存对齐粒度和文件对齐粒度,参见第一篇的阐述。

DataDirectory
数据表它由16个相同的IMAGE_DATA_DIRECTORY结构组成,因为PE文件中有很多的表块,例如导入表,导出表,重定位表等等,这样就需要有一个结构,保存着这些表的相关信息,来告诉Loader去加载哪里并且如何加载这些表。IMAGE_DATA_DIRECTORY结构指出了某种数据表的位置和长度。

1.3 区块表头
紧跟在IMAGE_NT_HEADERS后面的是区块表。在未载入这个块表信息之前Loader只知道根据前面提供的NumberOfSections,知道有多少个块,可是块的大小,块的各个偏移和名称却不知道,所以有一个结构来告诉Loader。这样loader就可以按照载入的规则将所以的块信息放入内存了。

注意:在进行区块装载的时候,有可能被装载的PE是个变形文件,所以,当其结构提供的数据代码和loader装载规律不一致的时候,就要按照loader对齐粒度来进行装载了。

那么如何读取了,首先要定位到各个头的起始位置(参见前面介绍的判断是否是PE文件中的方法定位),因为偏移是固定的,只需移动的固定的偏移读取即可。

至此,头表的重要结构都介绍完毕了,我们发现,这些结构都是按照固定偏移寻址读取的,告诉loader怎么去装载PE文件的,从宏观上,大致的表述了一个PE文件粗枝大叶的部分。可以说,这就如同之前所形容的那样,loader只是将这些作为数据来对待,进行读取,然后和自身的装载规则作比较。

下面就来介绍PE中比较重要的几个区块:
2.导入表
  Exe要用到外部DLL提供函数,在装载的时候,loader需要将各个函数的地址分配过去。在生成exe的时候,连接器为了统一一种形式将所有的调用函数统一成call [xxxxxxxx]的形式,而这个地址一开始会被PE文件预留,只有当要被加载的时候,loader进行填充真正的函数地址到xxxxxxxx地址处。

例如某一处的代码:

00401000  CALL  [[COLOR="Red"]00404000[/COLOR]]


00404000这个地方在文件自身未被加载时候,会被预留(通常会填充和OriginalFirstThunk一样值),直到被loader装载的时候,才会写出正在的要调用的函数地址。

每一个DLL都对应一个IMAGE_IMPORT_DESCRIPTOR,其重要结构如下:
 Dword OriginalFirstThunk //输入名称表结构 IMAGE_THUNK_DATA ->指向 IMAGE_IMPORT_BY_NAME
  Dword TimeDateStamp 
  Dword ForwarderChain 
  Dword Name1             //DLL名字指针
  Dword FirstThunk        //输入地址表结构 IMAGE_THUNK_DATA ->指向 IMAGE_IMPORT_BY_NAME


一共是三个结构,IMAGE_THUNK_DATA其实就是一个Dword大小的联合结构,当最高值为1,低31位就是表示的一个函数序号,当最高值为0,就表示一个指向IMAGE_IMPORT_BY_NAME的RVA。而IMAGE_IMPORT_BY_NAME表示的是一个带Hint值(可以理解为序号值)和带一个可变大小的byte结构,指向输入函数的函数名。这就好像我们上学报到的时候,除开自己的名字以外,我们还会被赋予一个属于自己的学号。

对照着图,就可以很清晰的看到了,在Loader未载入PE之前,都是IMAGE_THUNK_DATA结构的OriginalFirstThunk其实和FirstThunk指向的是同一个结构IMAGE_IMPORT_BY_NAME,loader通过它们的RVA指引定位到IMAGE_IMPORT_BY_NAME,从而知道了输入函数名称,一切就是这么简单:)

当Loader载入PE之后,OriginalFirstThunk依旧是指向了IMAGE_IMPORT_BY_NAME,但是FirstThunk此时被指向了之前指向输入函数在系统的真实地址。

一个EXE有很多的DLL,Loader要装载这么多的数据,那么它又是怎么判断一个结构的结束和另一个结构的开始呢?其实,在c语言表示的字符串中,我们知道,一个字符串的结束,使用‘0’来表示的,同样,一个结构的结束,也可以用同结构大小一样的0来表示结束:)。

我们可以这样来理解导入表的工作。假设学生组织考试(对应装载PE文件),在给学生要排位置,我们通过学生的学号或者姓名,来给出座位号(通过OriginalFirstThunk 以及 FirstThunk 寻找到MAGE_IMPORT_BY_NAME)。当学生坐到位置上考试的时候,我们才开始派发试卷,让学生考试(载入PE文件的时候,修改FirstThunk指向的数据。)。这样,排座位给座位号只是考试的一个手段,最终的目的是为了让学生考试(对应装载PE文件)。

3.导出表
Dll的一个重要功能就是函数封装,那么告知内部函数给loader的这个任务,就交给了导出表。作为导出表,要告诉loader自己提供了什么样的函数,一个是告知自己的导出序号,一个就是告知函数名。同样就好像老师在点名,可以点学号,也可以点名字。

导出表的重要结构如下:

Name	                 DLL名称
Base	                 基数,加上序数就是函数地址的索引值了。
NumberOfFunctions	 DLL导出的函数总数。
NumberOfNames	         通过名字引出的函数数目 
AddressOfFunctions	 所有函数的RVA地址 
AddressOfNames	         导出函数名地址
AddressOfNameOrdinals	 通过名字导出序号数的地址


我们可以这么去理解一个导出表是如何进行工作的。假设是在课堂上,老师要点名,首先要知道的就是这个班(对应总DLL_Name)学生的总人数(对应NumberOfFunctions)。再一个,老师要知道第一个学生的学号是多少(对应Base),这样,只要依此往下递增,即可点出所有的学生来(对应AddressOfFunctions)。有的学生中途退学,那么名字就会被注销,可是为了保证学号的唯一和一致性,学号会保留,他后面的同学学号通常是不会变的。有的学生勤奋好学(对应NumberOfNames),上课积极回答问题,给老师留下很深的印象,老师和他很熟,可以直接喊出他的名字。有的不熟悉,就只能去点学号(对应NumberOfFunctions - NumberOfNames)。老师点名的时候只要保证学号不越界,(即保证所点的学号都在这个班级内)那么可以不按照顺序来点(AddressOfNameOrdinals的顺序不是按照AddressOfFunctions排列顺序来的,内部排序号不同)。我们要明确的一点:如果有确定的导出函数,那么导出函数一定至少是有序号的,名字是可选的。



导出表函数扫面的算法表现:序号(AddressOfFunctions的顺序 + Base) + 名字(若序号存在对应的AddressOfNames的序号中) +地址(AddressOfFunctions)

关于导出函数,有两点要值得注意:

1.AddressOfNameOrdinals在AddressOfFunctions和AddressOfNames之间起到了纽带作用。因为AddressOfNames的顺序对应AddressOfFunctions的顺序,这个顺序在AddressOfNameOrdinals取值,其值+Base得出导出函数序号。可见,AddressOfNameOrdinals是AddressOfFunctions和AddressOfNames的寻址根据,也是导入表内部排序的索引号,也是通过这个值来求出序号的。
2.若某一个导出函数是按序号导出,序号的计算方法是AddressOfFunctions的顺序 + Base,其结果带入到AddressOfNameOrdinals后,是不会有对应的序号。另外若导出函数的AddressOfFunctions指向的值为0,则表示该序号的导出函数是不存在的。

4.重定位表
  当某个dll要加载的时候,预计加载地址已被占用,此时重定位表会告诉loade一系列的数据,loade会根据所给的数据对加载地址进行修正。这就是重定位表的任务。

例如在DLL中有如下指令:

0010720B   A1 00104000     mov eax,dword ptr ds:[0x0040100]

试图将0x401000指向的数据拷贝到eax中,可是当DLL真正加载的时候,loader发现这时候指针被移到了0x0050100h的地方,有0x100000h的偏差,怎么办呢?没错,loader这个时候就会根据重定位表提供的信息修改代码为:

0010720B   A1 00105000     mov eax,dword ptr ds:[0x0050100]

很明显,试图加载地址在PE可选头已经设定好,实际加载地址在loader载入动态获取,唯一需要告知loader的就是需要重定位代码的位置了。

重定位表应该在PE格式中是比较好理解的了,它的结构就是告诉loader怎么找到自己,然后给出一系列的数据(其实就是要重定位代码的地址),经过某种一致的方式来修正成新的加载地址。就如同你去参加图书馆还书时(要加载DLL),发现二楼还书要排长队(加载地址被占用需要重定位),这时候你查了一下书的登记序列号搜索其他可以还书地点(搜寻重定位代码位置,选择其他加载地址),发现四楼也可以还这本书且不要排队,于是你就跑到四楼把书还了(加载更新后的代码)。

重定位表的结构比较的简单,如下所示:

   
Dword VirtualAddress //重定位的起始地址
    Dword SizeOfBlock    //当前重定位结构大小
    Word  TypeOffset     //数组结构,高4位表示重定位类似(在X86系统中,该值一直是IMAGE_REL_BASED_HIGHLOW),低12位表示重定位地址,其值+VirtualAddress可以定位到要修改地址。
整体的结构如下图所示:



当一个重定位表项结束的时候,会以一个类型IMAGE_REL_BASED_ABSOLUTE的重定位结束,这个重定位什么都不做,只用于填充,以便下一个重定位标项结构是以四字节分界线来对其。所有的重定位结构以VirtualAddress为0的重定位表结束。

5.资源段
  资源可以说是PE中,结构相对来说比较复杂,而且种类也比较复杂。windows中的各种界面,包括声音,图像,甚至是一段代码stub,都可以作为资源来保存。
  资源分为了16个类型,每一个类型下面又有不同的项目名称,每一个项目都会包含不一样的数据,那么资源表就是依据资源这样归类的属性,来安排其结构的。如,常见的资源树:

  一个PE的资源表通常至多是包含三层的,第一层一般是表示资源的类型,第二层表示同一资源类型下的各个资源项,第三层表示每一个资源项属性,这其中有三个重要的数据结构来分别的实现这些功能:
1. IMAGE_RESOURCE_DIRECTORY

只需要关心其中的最后两项结构:NumberOfNamedEntries和 NumberOfIdEntries,前者是表示使用名字的资源数目个数,后者表示使用ID数字资源条目的个数,加起来就是资源目录中,目录项的总和。

2. IMAGE_RESOURCE_DIRECTORY_ENTRY

紧随IMAGE_RESOURCE_DIRECTORY其后就是该结构,包含两个成员:name和OffsetToData,前者指向目录项名称或者ID号,后者表示资源数据或者子目录的偏移的地址。根据所在目录位置的不同有不同的含义

name
1.位于第一层目录的时候,表示资源的类型
2.位于第二层目录的时候,表示资源ID或者是名字
3.位于第三次目录的时候,当最高位为0,表示其值作ID,当最高位为1,低位作指针,指向一个unicode编码结构,该结构表示一个以unicode编码的字符串。

offset
这个就是一个之中,它本身的最高位来决定自身的意义
1.如果最高位为1,低位数据指向下一层IMAGE_RESOURCE_DIRECTORY地址
2.如果最高位为0,则指向IMAGE_RESOURCE_DATA_ENTRY结构

值得注意的是,name和offset做指针的时候,都是从资源区段开始的地方算,而不是之前文件头开始算起。

3. IMAGE_RESOURCE_DATA_ENTRY
这个结构其实就是定位到资源数据最关键的结构,不管前面的目录有多少层,laoder遍历前面的结构,做的所有的前期准备都是为了找到资源的地址。该结构有四个成员:
  
OffsetToData //资源数据的RVA
  Size        //资源数据的长度
  CodePage    //代码页,通常置0
  Reserved    //预留值段


其中,RESOURCE DATA的OffsetToData和Size就告诉loader从哪里去加载和加载的大小。



一旦loader定位到了资源段开始的时候,剩下的结构,只需要按照事先定义的格式,按部就班的来了。举个例子,0XE17H这个字段,数据是0x80h,其实就是告诉loader,第二层还有一个IMAGE_RESOURCE_DIRECTORY的目录,loader继续遍历,接着0XE2Fh也告诉loader,第三层还有一个IMAGE_RESOURCE_DIRECTORY的目录,loader继续遍历,直到0xE48h,读取到了RESOURCE_DATA结构,这才找到真正资源的位置。loader就可以将资源进行加载了。

6.附加数据
    附加,顾名思义,就是附属上去的数据,为什么有一个这样的称呼?因为,loader判断一个PE文件的大小(即需要载入的文件大小),为该文件最有一个节磁盘的磁盘偏移+最后一个节的文件大小。那么,超出这个大小的数据,是不被Loader载入的,即不会被映射到内存空间去。就好像你考试时候的附加分一样,虽然有,但是不会计入总分:)。附加数据可以包含很多东西,例如程序的特殊的数据调用和校验,运行时候的配置信息。有的病毒就是将正常感染的程序放在附加数据等等。

处理附加数据比较的简单,只需定位到最后一个节末尾,附加的大小等于取文件大小减去节末尾偏移。
举一个例子:

我们看到,最后一个节偏移+大小是0x1000h,也就是说,最后一个节末尾偏移应该是0x9ffh,之后的数据就成为了附加数据,不会被loader载入。


熟练的掌握pe结构是操作pe文件的基础,上文介绍了PE文件中最基本常用的结构,希望通过这篇文章的介绍,能能对初学者有一定的帮助,一起共同的进步:)但水平有限,还请大家多多指正。

[课程]Linux pwn 探索篇!

上传的附件:
收藏
免费 6
支持
分享
最新回复 (28)
雪    币: 71
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
这么详细
太好了
2012-2-5 17:51
0
雪    币: 1844
活跃值: (35)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
3
呵呵呵,看标题吸引进来的。。顶
2012-2-5 18:08
0
雪    币: 195
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
请问,sys文件可以压缩的更小吗?
2012-2-5 18:14
0
雪    币: 1163
活跃值: (137)
能力值: ( LV12,RANK:230 )
在线值:
发帖
回帖
粉丝
5
期待楼主后续文章
2012-2-5 19:11
0
雪    币: 967
活跃值: (1138)
能力值: ( LV6,RANK:90 )
在线值:
发帖
回帖
粉丝
6
个人觉得这样搞没有任何意义
(你可以说和我没有关系)
真想学好
哪怕慢点
但是不得不说 写壳吧
哪怕壳很简单
2012-2-5 22:06
0
雪    币: 223
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
7
顶 顶 顶 顶 顶 顶
2012-2-5 22:15
0
雪    币: 408
活跃值: (156)
能力值: ( LV7,RANK:110 )
在线值:
发帖
回帖
粉丝
8
呵呵,这是我学习PE格式的一些心得体会和记录,所以发上来了:)
没有明白前辈的意思,看前辈以前的发的帖子,原来也对写壳颇有心得,可否指点一下小菜呀。
2012-2-5 23:29
0
雪    币: 408
活跃值: (156)
能力值: ( LV7,RANK:110 )
在线值:
发帖
回帖
粉丝
9
SyS文件也是属于PE格式的范畴,记得论坛上有写可以压缩sys的帖子:)
2012-2-5 23:38
0
雪    币: 71
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
10
图文并茂,很不错
可能是因为基础的原因吧,还是不怎么懂

能不能弄个视频教程啊

谢谢啦
2012-2-6 00:36
0
雪    币: 76
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
11
本来我自认为对pe结构很熟悉的,结果读了这篇帖子却糊涂了
2012-2-6 09:41
0
雪    币: 408
活跃值: (156)
能力值: ( LV7,RANK:110 )
在线值:
发帖
回帖
粉丝
12
我也是一个菜鸟,水平有限,不足之处希望和你多多交流 :)
2012-2-6 10:03
0
雪    币: 220
活跃值: (15)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
13
嗯 挺细 顶一下
2012-2-7 15:15
0
雪    币: 1708
活跃值: (586)
能力值: ( LV15,RANK:670 )
在线值:
发帖
回帖
粉丝
14
学习....
2012-2-7 15:20
0
雪    币: 231
活跃值: (12)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
15
学习了,讲解的很详细。
2012-2-7 15:22
0
雪    币: 321
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
16
很好的文章 谢谢
2012-2-7 16:36
0
雪    币: 1737
活跃值: (110)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
17
又有写壳的啊~~ 佩服~~
2012-2-8 09:32
0
雪    币: 184
活跃值: (56)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
18
马克~~~谢谢楼主分享
2012-2-8 21:41
0
雪    币: 243
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
19
不错,特别是曲径通幽四个字很有点别的意思。。。
2012-2-9 01:02
0
雪    币: 240
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
20
有的程序的附加数据是正常运行时需要的,loader不载入运行会报错
2012-2-11 09:56
0
雪    币: 130
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
21
mark一下
2012-2-13 13:11
0
雪    币: 442
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
22
好文章..........
2012-2-13 13:13
0
雪    币: 210
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
23
非常好,这么详细实用,顶!
2012-2-15 17:39
0
雪    币: 34
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
24
确实看的不怎么懂,也许火候不到.
2012-3-5 15:52
0
雪    币: 210
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
25
写得非常好,值得我们学习
2012-3-18 20:02
0
游客
登录 | 注册 方可回帖
返回
//