在平时的分析当中,经常会碰到PE结构的文件,虽然 010 Editor 等工具会提供一个模板,把各个部分都详细的标记出来,但是在调试的时候,经常会需要在 VS 等程序框中进行调试,所以,就需要对PE结构有一定的了解,才能够快速定位到自己想要的地方。
为了更好的了解PE结构中的每一位的作用,最好的办法就是自己手写一个PE文件,这样对每一个部分的理解,都会清晰很多。
目录
0x00 准备工作
0x01 构造DOS头
0x02 构造File头
0x03 构造Optional头
0x04 构造节表
0x05 构造导入表
0x06 执行代码
0x00 准备工作
在开始之前,有一些细节是需要提前思考好的,这些细节对于整个PE结构来说是非常重要的。
因为只需要完成一个弹窗的效果,代码量是非常少的,所以在程序的设计上,一个节表就完全足够了,同时,我们希望保证文件尽可能小,所以将文件对齐设置为200,将内存对齐设置为1000。
再加上头部和对齐的考虑,文件就需要占用400个字节了,到内存展开以后就占用2000字节。
注:PE格式中,所有可以被覆盖掉,而不影响程序运行的位置,我都会用CC来填充,这些位置可以写入字节的shellcode等。
0x01 构造DOS头
DOS头部在编辑器中占用了4行,其中的多数数据都是在16位的DOS环境下运行时所必备的,在现在看来,已经是可以占用的内容,只有两个参数是必须的:e_magic和e_lfanew。
e_magic
这个位是识别性的头部(MZ),这个位置是会被作为一个合法PE文件的检测位。
e_lfanew
用来指向一个新的结构,这个也就是我们现在来说,最重要的结构,所有的参数信息都是在这个结构中定义的。
在DOS头部后面还有一个Stub数据区,是16位程序的残留数据,是可以去掉的,所以就直接将e_lfanew指向了0x40,在这个位置开始新的结构。
0x02 构造File头
PE标识、File头以及Optional头统称为NT头,这里就不提NT的概念了,PE标识有4字节。
File头有1.4行,有4个重要的参数。
Machine
这是一组宏,表示在什么硬件下运行,一定要根据实际的情况来进行更改,当前是Intel386,所以填0x014c。
NumberOfSections
描述节表的个数,在前面规划的时候提到了,使用一个节表就足够了。
SizeOfOptionalHeader
描述Optional头的长度,在自己写程序进行分析的时候,一定要注意这一点,原版的OD直接使用sizeof来获得了,忽视了这个值是变长的,我们可以自己来更改,达到反调试的目的,这里填写0xE0。
Characteristics
描述可执行文件的属性,具体参照下面这张图。
最终填写如下
0x03 构造Optional头
Optional头涉及到的参数就比较多了,也是最重要的一个部分
Magic
类型识别,可以来判断到底是32位还是64位,再或者是其他的,我们使用32位的,为0x10b
AddressOfEntryPoint
程序入口点,因为头部对齐后为200字节,所以程序就从文件的200位置开始写,对应到内存中就是1000,所以填写0x1000
ImageBase
建议装载地址,这个地址并不是一定能占住的,如果没有到这个位置的话,会根据重定位表来修正程序中的地址,因为要写一个exe文件,一般默认是0x400000
SectionAlignment
内存对齐,填写0x1000
FileAlignment
文件对齐,填写0x200
MajorSubsystemVersion
这个版本号是不能进行修改的,目前系统一般都是NT4的,填写0x4
SizeOfImage
内存中的文件大小,在前面规划的时候也提到过了,这里填0x2000
SizeOfHeaders
头部的大小,这里都是要考虑对齐后大小的,所以填0x200
CheckSum
校验和,在3环程序中,是不会检测这个位置的,在0环中才会进行校验,但是计算这个值的算法是公开的,所以可以自己计算并填写,检测的意义不大,我们把这个位填0。
Subsystem
这个位置是程序运行在什么情况下,填3,是命令行下,2是图形化界面下,1是内核文件中,根据实际情况填写。
DllCharacteristics
是否是基于WDM的驱动程序,填0就可以了,如果是的话,填0x2000
SizeOfStackReserve
准备保留多大的栈空间,自己填写,合理即可
SizeOfStackCommit
程序运行的时候,占用多大的栈空间,自己填写,合理即可
SizeOfHeapReserve
准备保留多大的堆空间,自己填写,合理即可
SizeOfHeapCommit
程序运行的时候,占用多大的堆空间,自己填写,合理即可
NumberOfRvaAndSizes
数据目录的长度,默认是16个,填写0x10
紧接着后面就是数据目录的描述了,一个描述占用8个字节,4个字节的RVA,4个字节的长度,只有导出表和重定位表的长度是会被使用的,其他的数据目录的长度都是可以覆盖掉的,他们通过一个全零结构来判断结尾。
最终填写如下
0x04 构造节表
在数据目录的描述结束以后就是节表描述了,一个节表的描述是两行半
Name
节表名字的长度是固定的,而且是可以随便写的,并不是说.data就一定是数据段,一定不能通过名字来判断其中的内容。
VirtualSize
这是一个共用体,一般我们使用的都是VirtualSize位,节表在内存中的长度,有效字节的长度,这个是对齐前的长度,这里可以填0。
VirtualAddress
节表的开始位置在内存中的RVA,按照前面的设想,这里应该填0x1000
SizeOfRawData
节表在文件中的长度,这个是对齐后的大小,按照前面的设想,这里应该填0x200
PointerToRawData
节表的起始位置,按照前面的设想,这里应该填0x200
Characteristics
节属性,描述这个节是可读的,可写的还是可执行的。
对于上面的那四个参数,可以描述为,从文件中起始地址为PointerToRawData的地方,复制SizeOfRawData的数据,粘贴到内存中RVA为VirtualAddress的位置,实际字节为VirtualSize的地方。
最终填写如下,还需要对齐
0x05 构造导入表
在完成了这些内容以后,就需要开始构造导入表了,因为我们需要调用MessageBoxA函数来实现弹窗的功能。
导入表也是整个PE结构中最复杂的地方,占用1.4行,在程序执行前和执行后,导入表的结构是不一样的。
OriginalFirstThunk
导入名称表,这里是一个RVA,它指向了一个结构,说它是一个数组更为合适,里面存储的也是一个RVA,指向了_IMAGE_THUNK_DATA结构,这个结构也是一个共用体,可以填一个序号,也可以填一个函数名称,因为导出表有按名字导出和按序号导出两种形式。
这里我们使用按名字导出的方式,这样就又涉及到了一个结构_IMAGE_IMPORT_BY_NAME
在这个结构中,Hint属于废弃的状态,所以只需要写上函数的名字就可以了,这个名字是一个字节的,因为不知道函数名字的长短,也就没法使用定长的方式,为了避免空间的浪费,所以它只记录了名字的起始位置,通过00来判断结尾
Name
动态链接库的名称,也是一个RAV,它指向了名字
FirstThunk
导入地址表,这里也是一个RVA,一样指向了一个数组,与导入名称表是对应的,在运行前,与导入名称表一样,都指向了_IMAGE_THUNK_DATA结构,结构图下
当程序执行以后,导入地址表就会按照名称进行搜索,得到函数的地址,然后把地址填入到对应的位置中,结构图就变成了下面这个样子
在执行的时候,也就是间接调用的函数地址表
我们先把导入表的结构写出来,地址的位置先用0来补充,这里将导入表也到250的位置
文件偏移是250,对应的内存偏移是1050,所以在数据目录的第二项,也就是导入表的位置,写上导入表的RVA,长度是可以随便写的
因为导入表是依靠一个全零结构来判断结尾的,我们需要给它留下足够的空间,我们将 dll 名称写到 2C0 的位置,最后的 .dll 是可以不用写的,操作系统不依靠后缀名来判断
文件偏移2C0对应的内存偏移是10C0,写到导入表中对应的位置
然后布置函数名称MessageBoxA,开头的两个字节是可以随意填写的,我们将它放到 2E0 的位置,最后以00来结尾
文件偏移2E0对应的内存偏移是10E0,这个先记住,等一下再进行填写
然后是导入名称表和导入地址表,在执行前,这两个的内容是一样的,都指向了函数名称,也就是上面的10E0,因为这里我们只用一个函数,所以导入名称表和导入地址表都只有一项,前面也说过了,它们相当于是一个数组,是需要一个00来结尾的,所以每一个都需要占用8个字节,刚好是一行,所以,我们把导入名称表放到2D0的位置,将导入地址表放到2D8的位置。
文件偏移2D0对应的内存偏移是10D0,文件偏移2D8对应的内存偏移是10D8,然后将它们填到导入表中对应的位置。
到这里为止,导入表的编写也就完成了。
0x06 执行代码
最后就是代码的编写了,我们先设置一下弹窗的标题和内容,我们将标题放到2F0的位置,对应的内存偏移是10F0,因为ImageBase是400000,所以我们需要push的地址是4010F0,然后把弹窗的内容放到2F4的位置,对应的内存偏移是10F4,需要push的地址是4010F4
前面已经提到过了,在代码执行的时候,我们需要调用的函数地址在导入地址表中,所以需要调用的地址是4010D8
这样,所需要调用的内容也就都有了,接下来就是硬编码的事情了,如果对硬编码不熟悉的话,我们可以通过在OD中写汇编,然后把硬编码扣下来
然后把这段代码写到200的位置
这样就大功告成了,然后保存运行,看看效果
成功弹窗,也就完成了手写PE结构的任务,虽然还有导出表,重定位表等都没有涉及到,但是通过这样的一次小的练习,也就对整个PE结构都有了更深刻的了解了。