在平时的剖析当中,常常会碰到 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 构造都有了更粗浅的理解了。
了解原理,方可变动
欢送关注公众号:信安本原(sec-source)
本文由博客一文多发平台 OpenWrite 公布!