手写PE文件

发布时间 2023-06-20 13:39:15作者: Auion

构造PE文件需要将所需的结构逐一构建出来,即需要将IMAGE_DOS_HEADER、IMAGE_FILE_HEADER、IMAGE_OPTIONAL_HEADER、IMAGE_SECTION_HEADER、IMAGE_IMPORT_DESCRIPTOR和数据节构造好,进而完成整个PE文件的代码。

1、构造IMAGE_DOS_HEADER结构

在PE文件格式中以IMAGE_DOS_HEADER结构开始,根据对其结构题进行分析可知其大小为40h字节(十进制中的64字节)。此时可以都打开C32Asm编辑器,选择“文件”、“新建十六进制文件”,在所弹出的“新建文件”对话框中设置“新文件大小”为“64”,即如下所示:

 

 

设置好“新文件大小”后,单击“确定”按钮,在C32Asm中即可插入有64字节的文件,即如下所示:

通过上述步骤可创建一个全0的64字节的数据,根据IMAGE_DOS_HEADER结构体对该64字节数据进行修改。在IMAGE_DOS_HEADER结构体中,关键字段只有两个,分别为e_magic和e_lfanew,其取值如下表所示:

 

字段

取值

备注

e_magic

4D 5A

MZ头标识

e_lfanew

40 00 00 00

指向PE标识符的偏移

 

在IMAGE_DOS_HEADER中,只须为这两个字段赋值,其余字段值均为0,赋值后的数据情况如下图所示:

 

 

在IMAGE_DOS_HEADER结构体中,最后4字节指向PE标识符的偏移,由于手写PE文件不会像编译器一样插入DOS Stub所以在DOS头后紧跟着PE标识符,此处所给出的值为0x00000040。

 

2、构造PE标识符

 

构造完DOS头后则需要构建PE标识符,PE标识符占4字节,因此,C32Asm中需要新增4字节的数据。在C32Asm中选择“编辑”、“插入数据”选项,在所弹出的“插入数据”对话框中设置“插入数据大小”为“4”点击“确定”即可,此时所插入的4字节十六进制数据均为“0”。已知PE标识符对应的十六进制数据为“50 45 00 00”,因此,需要将插入的4字节数据的前两字节修改为“50 45”,修改后的数据情况如下图所示:

 

 

3、构造IMAGE_FILE_HEADER结构体

 

构造好PE标识符后则需要构造IMAGE_FILE_HEADER结构体,该结构体的大小为14h字节(十进制的20字节),同样需要在C32Asm中插入20字节的全0数据,并根据需求进行相应的数据修改,其中IMAGE_FILE_HEADER结构体的关键字段取值如下表所示:

 

字段

取值

备注

Machine

4C 01

表示i 386类型的CPU

NumberOfSections

03 00

该PE文件共计3字节

SizeOfOptionalHeader

E0 00

在Win32环境下可选头的大小

Characteristics

03 01

没有重定位信息的32位平台的可执行文件

 

填充数据时,SizeOfOptionalHeader字段值大小与32位平台的IMAGE_OPTIONAL_HEADER结构体的大小相同,Characteristics字段的值由多个值组合而成。填充的数据均为word类型,其余没有填充的字段值均为0,填充数据后的情况如下图所示:

 

 

4、构造IMAGE_OPTIONAL_HEADER结构体

 

IMAGE_OPTIONAL_HEADER结构体是PE文件中最为重要且体量较大的结构体之一,该结构体划分为32位和64位两个版本,此处以32位版本为例。该结构体大小为0E0h字节,即十进制224字节,因此,需要在C32Asm中填充224字节的全0十六进制数。

 

由于没有填充数据目录,所以根据IMAGE_OPTIONAL_HEADER结构体中字段的取值可先将结构体中的字段填充一半,填充数据是将0的部分同时在表格中进行展示,其具体填充数据情况如下图所示:

 

 

填充完IMAGE_OPTIONAL_HEADER结构体的基础数据部分后,话需要填充其数据目录部分。由于此处为手写一个EXE文件,所以数据目录只须存在两项,分别为第1个数据目录项和第13个数据目录项。其中数据目录的第1项是导入表,第13项是导入地址表

 

根据对PE文件的规划可在0x00003000起始处存放导入表的相关内容,在0x00003000中先存放导入地址表,即数据目录的第13项,再在导入地址表后存放导入表,即数据目录的第1项。导入地址表占用16字节,即从0x00003000处起始,在0x0000300f处结束。又导入表从0x00003010处起始。按照该布局在数据目录中输入导入表和导入地址表的RVA。此时IMAGE_OPTIONAL_HEADER数据目录部分的填充数据情况如下所示:

 

 

IMAGE_OPTIONAL_HEADER结构体的字段相对较多,可将其划分为普通字段信息和数据两个部分进行数据填充。

 

5、构造IMAGE_SECTION_HEADER结构

 

构造完IMAGE_OPTIONAL_HEADER结构体后则需要构造节表,节表中一共包含3个节表项,也就是需要构造3个IMAGE_SECTION_HEADER结构,每个IMAGE_SECTION_HEADER结构体的大小为40字节,由于需要构造3个节表项,因此,节表大小为120字节,节表中字段的填充情况如下表所示:

 

Name

.text

.data

.idata

VirtualSize

0x00001000

0x00001000

0x00001000

VirtualAddress

0x00001000

0x00002000

0x00003000

SizeOfRawData

0x00001000

0x00001000

0x00001000

PointerToRawData

0x00001000

0x00002000

0x00003000

Characteristics

0x60000020

0xC0000040

0xC0000040

 

在C32Asm中填充120字节的全0数据并根据上表填充,填充完成后的数据情况如下所示:

 

6、0数据的填充

 

PE文件格式的头部到此填充完毕,在IMAGE_OPTIONAL_HEADER结构体结构体中,SizeOfHeader字段的值是0x00001000。因此,为了对齐粒度需要将头部的大小用0字节补足。目前已填充的PE文件格式头部的大小为432字节,在C32Asm中,将光标移动到最后一字节处,可以看到C32Asm右下角“光标”的值为“000001B0”,“文件长度”为432 bytes,即如下图所示:

 

由于需要按照0x00001000长度进行对齐,因此用0x1000-0x01B0=0x0E50,即十进制数的3664。在C32Asm中插入“3664”个“0”字符将PE文件头部按照IMAGE_OPTIONAL的SizeOfHeader对齐。插入3664个0字符后,文件的结束偏移地址是0x00000FFF。在填充PE文件头部后,需要继续填充0x00001000字节的0字符,该0x00001000字节的数据用来存放.text节的内容,即代码节内容。继续使用C32Asm插入4096个0字符。

 

7、填充.data节的数据

 

.data节用来保存程序运行时弹出的提示对话框中显示的字符串。提示对话框使用MessageBox函数来实现。MessageBox函数的第二个参数和第三个参数分别是两个字符串,第二个参数lpText是提示对话框中用于显示的字符串,第三个参数lpCaption是提示对话框中标题显示的字符串。

 

lpText显示的字符串是“Hello,PE 文件”,lpCaption显示的字符串是“Binary Diy”。在C32Asm中先插入4096个0字符后,再在0x00002000地址处写入lpText的值,在0x00002020地址处写入lpCaption的值,即如下所示:

 

 

8、填充.idata节的数据

 

.idata节用于保存PE文件中重要的两个部分,即导入表和导入地址表。在填充.idata节的数据之前,先来对.idata节的数据进行分析。

 

导入表和导入地址表地址分别由数据目录给出,在数据目录中,导入表的RVA为0x00003010,导入地址表的RVA为0x00003000。由于此所构造的PE文件的RVA与FOA地址相同,故不需要进行转换,RVA即为FOA。因此,导入地址表的偏移地址为0x00003000,而导入表的偏移地址为0x00003010。

 

在本例中需要导入两个DLL文件,因此需构造3个IMAGE_IMPORT_DESCRIPTOR,因为导入表需要由一个全0的IMAGE_IMPORT_DESCRIPTOR来结束。因为导入表中的字段大部分是具体的RVA值,所以先来构造一个占位用的导入表,导入表中字段的填充表如下所示:

 

OriginalFirstThunk

AA AA AA AA

BB BB BB BB

CC CC CC CC

TimeDateStamp

00 00 00 00

00 00 00 00

CC CC CC CC

ForwarderChain

00 00 00 00

00 00 00 00

CC CC CC CC

Name

AA AA AA AA

BB BB BB BB

CC CC CC CC

FirstThunk

AA AA AA AA

BB BB BB BB

CC CC CC CC

 

按照上表可以在文件偏移地址0x00003010处构造占位用的导入表,即如下图所示:

 

 

在本例中导入了两个DLL文件,分别是user32.dll和kernel32.dll。在user32.dll中调用了MessageBoxA函数,在kernel32.dll中调用了ExitProcess函数。先构造user32.dll的导入信息,按照IMAGE_IMPORT_DESCRIPTOR结构体来进行构造。

 

在0x00003050地址处构造导入表的Name字段的值“user32.dll”。

 

在0x00003060地址处构造导入表的OriginalFirstThunk字段的值“0x00003070”。OriginalFirstThunk指向一个IMAGE_THUNK_DATA,而IMAGE_THUNK_DATA在高位不为1的情况下指向一个IMAGE_IMPORT_BY_NAME结构体。

 

在0x00003070地址处根据IMAGE_IMPORT_BY_NAME结构体导入函数的名称。

 

0x00003000地址处是导入地址表,该值由FirstThunk来指向,当磁盘中时,该值与OriginalFirstThunk相同。因此,在文件偏移地址0x00003000处输入0x00003070。按照构造user32.dll的方式构造kernel32.dll的导入信息,导入表信息的填充情况如下所示:

 

 

随后则需要根据如下的导入表进行数据填充,下表如下所示:

 

OriginalFirstThunk

60 30 00 00

90 30 00 00

00 00 00 00

TimeDateStamp

00 00 00 00

00 00 00 00

00 00 00 00

ForwarderChain

00 00 00 00

00 00 00 00

00 00 00 00

Name

50 30 00 00

80 30 00 00

00 00 00 00

FirstThunk

00 30 00 00

08 30 00 00

00 00 00 00

 

填充后的导入表情况如下所示:

 

 

在LordPE.EXE中打开该EXE文件查看器导入表信息,其步骤为单击右侧列中的“PE Editor”按钮,找到所需要打开的文件(PE.exe文件),在弹出的对话框中单击“Directories”,在接下来所弹出的对话框中找到“ImportTable”,点击其RVA、Size后的“…”按键打开其输出表,其具体步骤如下图所示:

 

此时则可以查看器导入表的相关信息,具体情况如下所示:

 

9、填充.text节数据

 

手写PE文件的最后一步是填充PE文件.text节的内容,即可执行程序的的代码。将按照前面步骤所构造的PE文件使用OD打开,在反汇编窗口,OD会自动定位到0x00401000地址处,即如下图所示:

 

由上图可知OD反汇编窗口中“HEX数据”列(第二列)显示的全是0字符,这是因为在构造PE文件是并未对.text节填充任何内容。

 

随后再查看OD的数据窗口,数据窗口是从0x00402000处开始显示的,即如下所示:

 

如上图可知OD数据窗口中显示字符串“Hello,PE File!!!”和“Binary Diy”,这是在.data节中填充的数据。

 

在0D数据窗口中通过快捷键“Ctrl+G”跳转至地址0x00403000处,查看导入表信息,即如下图所示:

 

由上图可知导入地址的信息已经与构造PE文件时有所差别,这是因为导入地址表在载入内存后其中的值会发生变化,其会被填充为实际的导入地址。在OD数据窗口右键单击,在所弹出的快捷菜单栏中选择“Long”、“Address”选项则可以直观地查看导入地址表信息,即如下所示:

 

通过上图中“数值”列(第二列)显示的“7709CB80”和“76307F40”即是被填充后的函数地址。

 

在OD中查看手写PE文件后还需要再次查看手写PE文件,这是因为编写代码时会使用到.data节和.idata节的内容,而此时内存中与磁盘中的地址会发生些许改变,因此需要在OD中再次查看构造的数据。

 

在OD中查看再次查看构造的数据可以得到如下结果:

 

①.text节的位置是从0x00401000处起始。

 

②“Hello,PE File!!!”字符串的地址为0x0040200。

 

③“Binary Diy”字符串的地址为0x403000。

 

④“MessageBoxA”函数的导入地址为0x403000。

 

⑤“ExitProcess”函数的导入地址为0x403008。

 

综上所述,需要在OD反汇编窗口的0x00401000地址起始依次将9个地址的反汇编代码修改成以下反汇编代码:

 

push 0

 

push 00402020

 

push 00402000

 

push 0

 

call 0040101A

 

push 0

 

call 00401020

 

jmp [00403000]

 

jmp [00403008]

 

首先,双击0x00401000地址处的“反汇编”列(第三列),即可修改其反汇编代码,依次将上述反汇编代码进行填充修改,最终修改结果如下所示:

 

 

将上述反汇编代码写入后选中并右键单击执行快捷菜单栏,选中“Copy to executable”执行“Selection”命令即可进入到文件编辑窗口,在空白处单击右键执行快捷菜单中的“Save File”命令,即可将修改保存到文件之中将其并命名为“ple.exe”。至此,手写可执行文件任务完成。

 

此时双击刚修改后并保存的“pel.exe”文件,此时即可查看到其运行结果如下图所示: