能帮到你的话,就给个赞吧
-----这篇文章虽然不是很厉害,但我写的时候也碰到了麻烦,主要是一开始工具都下载不下来,都要积分,这点让我很无奈,我的资料一直都是公开的,我希望看我这篇文章的人能够和我一样,秉着互联网原本的理念进行下去,正因有了互联网,我们才能更好的学习,我也是再看了许多文章之后才写的这篇文章,衷心感谢那些人,还有我用到的工具网站,哈哈。最后千万不要白嫖啊,记得点赞
------PE,是对Windows下所有可执行文件的统称,例如exe、dll等,一般都叫PE文件,同时也是指Windows下可执行文件的格式,文件正因有了PE格式,才能被Windows加载执行。
------众所周知,文件是用来存东西的,但是呢,不同的文件就要按不同的格式来存,例如你想存txt文件就按照txt格式来存,你想存png就按照png格式,那想存pe就要按照pe格式。打个比方,写作文,你想写散文就按照散文来写,想写日记也有日记的格式。当然,我这个比喻有不恰当处,因为文件它存在磁盘上都是二进制,你加载器按照pe来加载它就是pe,你按照png来加载它就是png,作文便不行了,哈哈。
------就是一个helloworld消息框,只不过是纯手写
链接:https://pan.baidu.com/s/1d_ahkgId8WuhP6rj5HQ_ww
提取码:8pkh
------链接:https://pan.baidu.com/s/1lZqgop9NYYuxajudULKcJQ
------提取码:iy4t
------链接:https://pan.baidu.com/s/1Xx8RYL9mf0-509Fj0AYrSA
------提取码:rnjz
------进制转换网:https://tool.oschina.net/hexconvert
------Ascii转换网:http://www.ab126.com/goju/1711.html
------磁盘以200H为一个单位,比方,假如PE的headers部分只有40H,但在磁盘上也要占200H。
------内存以1000H为一个单位,比方,假如PE的sections部分只有40H,但在内存上也要占1000H。
------PE文件分两部分:一部分是headers,包含了各种header;另一部分是sections,包含了各种section。所以呢,在写的时候也分两部分,一部分写headers,一部分写sections。
------但是需要注意的是,headers我们都一起写,所有的header只占一个,但sections我们要分开写,一个section占一个。
------举例
4个header3个section,那么在编写文件时,所有的header一起算等于355H,那么占文件400H,而section则一个占一个,也就是a占200H,b占400H,c占200H。同理,加载进内存也是如此,不过单位不一样,1000H。
------编写都是16进制的,其次就是写值的时候要倒过来,为什么呢,因为我们计算的时候是高位在前,而计算机则是低位在前,如图
文件中4D5A那两个字节的真实值其实是5A4D,所以我们算出值写的时候要倒着写。再举个例子吧,例如往四个字节写102A3240,那么应该是40322A10。
------Dos头是一个IMAGE_DOS_HEADER结构体变量(可在WinNT.h查找定义) ,占40H。
所有PE文件都以一个简单的DOS头开始。有了它,一旦程序在DOS下执行,DOS就能识别出这是有效的执行体,然后运行紧随之后的DOS程序,以此达到对Dos系统的兼容。如若不是DOS系统,则跳转到PE头
------Dos头第一个成员为WORD e_magic,占两个字节,被用于表示DOS兼容的可执行文件,它的值固定为0x5A4D。
最后一个成员为LONG e_lfanew,占四个字节,是一个指针,指向PE头,也就是PE文件标志。那么值就是40+70=B0,所以应该填B0000000。其他填充0如下
------接着是Dos程序
Dos程序是个有效的程序,但在不是Dos系统下,将被跳过,大多数情况下它由汇编编译器自动生成。通常,它简单调用中断21h,服务9来显示字符串"This program cannot run in DOS mode"。(在我们写的程序中,他不是必须的,可以不予以实现,但是要保留其大小,大小为70H,为了简洁,可以使用00来填充。)
如图:
------PE头是一个IMAGE_NT_HEADERS32结构体变量(可在WinNT.h查找定义) 。有三个成员{DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;}
------Signature也就是PE标志,代表着PE开始,是一个DWORD类型,占4个字节,Windows下这个值必须为0x00004550,所以编辑器中填写“50450000”。
如图
------接着是FileHeader,FileHeader是一个IMAGE_FILE_HEADER结构体变量(可在WinNT.h查找定义) 。
第一个成员WORD Machine,占两个字节,表示运行所要求的CPU,对于Intel来说该值是0x014C,所以编辑器中应该填写“4C01”
第二个成员WORD NumberOfSections,占两个字节,表示PE文件中段的总数,在我们这个程序中,计划写3个段,(.text(代码段)、.rdata(只读数据段)、.data(全局变量数据段))。所以此处值是0x0003,因此填写“0300”。
第六个成员WORD SizeOfOptionalHeader,占两个字节,表示“PE文件可选头 ”的大小,也就是0x00E0,因此填写“E000”。
第七个成员WORD Characteristics,占两个字节,表示关于PE文件的属性,比如文件是exe还是dll。这个值实际上是二进制位运算得到的值。各二进制位表示的意义如下:
Bit 0 :置1表示文件中没有重定向信息。每个段都有它们自己的重定向信息。这个标志在可执行文件中没有使用,在可执行文件中是用一个叫做基址重定向目录表来表示重定向信息的。
Bit 1 :置1表示该文件是可执行文件。
Bit 2 :置1表示没有行数信息;在可执行文件中没有使用。
Bit 3 :置1表示没有局部符号信息;在可执行文件中没有使用。
Bit 4 :未公开
Bit 7 :未公开
Bit 8 :表示希望机器为32位机。这个值永远为1。
Bit 9 :表示没有调试信息,在可执行文件中没有使用。
Bit 10:置1表示该程序不能运行于可移动介质中(如软驱或CD-ROM)。在这种情况下,OS必须把文件拷贝到交换文件中执行。
Bit 11:置1表示程序不能在网上运行。在这种情况下,OS必须把文件拷贝到交换文件中执行。
Bit 12:置1表示文件是一个系统文件例如驱动程序。在可执行文件中没有使用。
Bit 13:置1表示文件是一个动态链接库(DLL)。
Bit 14:表示文件被设计成不能运行于多处理器系统中。
Bit 15:表示文件的字节顺序如果不是机器所期望的,那么在读出之前要进行交换。在可执行文件中它们是不可信的(操作系统期望按正确的字节顺序执行程序)。
对于我们的程序,因为它是可执行程序,所以Bit 1必须置为1,其他位按照需要置位即可。在我们的程序中只需将第二位置1表示是可执行程序。也就得到二进制值0000000000000010(计算的时候高位在前),将其转换为十六进制形式为0x02,而该成员占两个字节,补齐一位00由此得到成员7的值为0x0002。因此在编辑器中填写“0200”。
其他填充0,如图
------最后是OptionalHeader,OptionalHeader是一个
IMAGE_OPTIONAL_HEADER32结构体变量(可在WinNT.h查找定义) 。
第一个成员WORD Magic,占两个字节,表示文件的格式,对于EXE文件值为0x010B,因此填写“0B01”。
第七个成员DWORD AddressOfEntryPoint,占四个字节,表示程序执行第一条指令的RVA地址(RVA就是VA-文件基址,也就是在内存中的偏移)。值为0x1000,因此填00100000。(因为headers占1000H)
第十个成员DWORD ImageBase,占四个字节,表示文件在内存中的起始地址。通常为0x00400000,因此填为00004000。
第十一个成员DWORD SectionAlignment,占四个字节,表示程序在内存的对齐大小,通常为1000H,因此填00100000。
第十二个成员DWORD FileAlignment,占四个字节,表示程序在磁盘上的对齐大小,通常是200H,因此填00020000。
第十七个成员WORD MajorSubsystemVersion,占两个字节,表示win32子系统,值为4H,因此填0400。
第二十个成员DWORD SizeOfImage,占四个字节,表示程序加载进内存的大小,那么headers占1000H,三个段各占1000H,因此填00400000。
第二十一个成员DWORD SizeOfHeaders,占四个字节,表示文件headers的大小,即40H+70H+4H+14H+E0H+3*28H=220H,但占磁盘400H,故填00040000。
第二十三个成员WORD Subsystem,占两个字节,表示程序所用系统,也就是win32,值为3H,故填0300。
第三十个成员DWORD NumberOfRvaAndSizes,占四个字节,指第31成员数组的元素个数,值为0x10,故填10000000。
第三十一个成员IMAGE_DATA_DIRECTORY
DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES],是一个IMAGE_DATA_DIRECTORY类型数组(可在WinNT.h查找定义,占8个字节),共有16个成员,占128个字节,其中我们要填写的是第二个成员,其指向导入目录,在这里我们准备把导入目录写在.rdata段。但由于还未写,故先填AAAAAAAAAAAAAAAA。(前四个是导入目录的RVA,后四个是导入目录的大小)
如图
------段头是一个IMAGE_SECTION_HEADER结构(可在WinNT.h查找定义),占40个字节,我们有三个段,故要写三个段头。
------text段头
第一个成员BYTE Name[IMAGE_SIZEOF_SHORT_NAME],占八个字节,值为.text,填2E74657874000000。
第二个成员DWORD VirtualSize,指text段在内存的大小,也就是1000H,填00100000。
第三个成员DWORD VirtualAddress,指text在内存的RVA,也就是1000H,填00100000。
第四个成员DWORD SizeOfRawData,指text在文件的大小,也就是200H,填00020000。
第五个成员DWORD PointerToRawData,指text在文件的位置,也就是400H(因为headers占了400H),填00040000。
第十个成员DWORD Characteristics,占4个字节,表示段的属性,也是按位表示,各二进制位表示的意义如下:
bit 5 (IMAGE_SCN_CNT_CODE),置1,节内包含可执行代码。
bit 6 (IMAGE_SCN_CNT_INITIALIZED_DATA)置1,节内包含的数据在执行前是确定的。
bit 7 (IMAGE_SCN_CNT_UNINITIALIZED_DATA) 置1,本节包含未初始化的数据,执行前即将被初始化为0。一般是BSS.
bit 9 (IMAGE_SCN_LNK_INFO) 置1,节内不包含映象数据除了注释,描述或者其他文档外,是一个目标文件的一部分,可能是针对链接器的信息。比如哪个库被需要。
bit 11 (IMAGE_SCN_LNK_REMOVE) 置1,在可执行文件链接后,作为文件一部分的数据被清除。
bit 12 (IMAGE_SCN_LNK_COMDAT) 置1,节包含公共块数据,是某个顺序的打包的函数。
bit 15 (IMAGE_SCN_MEM_FARDATA) 置1,不确定。
bit 17 (IMAGE_SCN_MEM_PURGEABLE) 置1,节的数据是可清除的。
bit 18 (IMAGE_SCN_MEM_LOCKED) 置1,节不可以在内存内移动。
bit 19 (IMAGE_SCN_MEM_PRELOAD)置1, 节必须在执行开始前调入。
bits 20 to 23指定对齐。一般是库文件的对象对齐。
bit 24 (IMAGE_SCN_LNK_NRELOC_OVFL) 置1, 节包含扩展的重定位。
bit 25 (IMAGE_SCN_MEM_DISCARDABLE) 置1,进程开始后节的数据不再需要。
bit 26 (IMAGE_SCN_MEM_NOT_CACHED) 置1,节的 数据不得缓存。
bit 27 (IMAGE_SCN_MEM_NOT_PAGED) 置1,节的 数据不得交换出去。
bit 28 (IMAGE_SCN_MEM_SHARED) 置1,节的数据在所有映象例程内共享,如DLL的初始化数据。
bit 29 (IMAGE_SCN_MEM_EXECUTE) 置1,进程得到“执行”访问节内存。
bit 30 (IMAGE_SCN_MEM_READ) 置1,进程得到“读出”访问节内存。
bit 31 (IMAGE_SCN_MEM_WRITE)置1,进程得到“写入”访问节内存。
因为这是代码段,所以bit 5 (IMAGE_SCN_CNT_CODE)位要置1,一般代码段都含有初始化数据,那么bit 6 (IMAGE_SCN_CNT_INITIALIZED_DATA)位要置1,又因为代码段的代码可以执行的,所以bit 29 (IMAGE_SCN_MEM_EXECUTE) 位要置1,那么这3个二进制位进行或运算最终得到的二进制值为00100000000000000000000001100000(低位在前),将其转换为十六进制值为0x20000060,所以此处应该填写60000020。
到此整个.text头编写完毕,按照上面的方法,分别填写.rdata段头和.data段头(原谅我,写这个实在太累了~)。因为要文件对齐,所以后面的代码用零补齐,直到3ffh。
如图:
------我们先写rdata段和data段,先预留200H字节。
当制作完之后开始写text段.text段代码如下
push 0 ; MessageBoxA的第四个参数,即消息框的风格,这里传入0。
push 0x403000 ;第三个参数,消息框的标题字符串所在的地址。
push 0x403007 ;第二个参数,消息框的内容字符串所在的地址。
push 0 ;第一个参数,消息框所属窗口句柄,这里填0。
call 40101A ;调用MessageBoxA,实际是跳转到该函数的跳转指令所在地址。
push 0 ;ExitProcess函数的参数,程序退出码,传入0.
call 401020 ;调用ExitProcess,实际是跳转到该函数的跳转指令所在地址。
jmp dword ptr [0x402080] ;跳转到MessageBoxA的真正地址处。
jmp dword ptr [0x402088] ;跳转到ExitProcess的真正地址处。
40101A和401020这两个地址如何而来呢,
push 0 ;指令长度为2。
push 0x403000 ;指令长度为5。
push 0x403007 ;指令长度为5。
push 0 ;指令长度为2。
call ???? ;指令长度为5。
push 0 ;指令长度为2。
call ???? ;指令长度为5。
总长度为2+5+5+2+5+2+5=26。转换为十六进制为1A,那么紧随其后的两条jmp指令的地址偏移应该为0x101A和0x1020。加上基址后得到其绝对地址分别为0x401041和0x401020。
将这些指令翻译成机器码后如下:
6A00680030400068073040006A00E8070000006A00E806000000FF2580204000FF2588204000
如图
------rdata段要存导入目录,导入目录是一个IMAGE_IMPORT_DESCRIPTOR(占20个字节,可在WinNT.h查找定义) 结构数组,有几个Dll就有几个元素,同时以一个空元素结尾。例如我们这个程序将从2个DLL库中导入函数,那么这个数组就有2个元素,同时还有一个空元素。因此整个导入表的大小应该为(2+1)*20=60byte,转换成十六进制也就是0x3C。
------IMAGE_IMPORT_DESCRIPTOR
第一个成员DWORD OriginalFirstThunk,占四个字节,是指向IMAGE_THUNK_DATA 结构(占四个字节,可在WinNT.h查)的RVA,这个结构也是一个指针,名叫nameTrunk,指向本Dll的函数名称。
我们先构造函数名称,为了紧凑,将所有的函数名和动态库名写在一起并放在导入表之后。首先输入MessageBoxA字符串,这里有一点要注意,一个PE程序在导入函数的时候可以按函数名来导入,也可以按照函数序号导入。函数序号在各个动态库的导出表中可以查询到。该序号是一个WORD类型的值。无论是否我们以序号方式导入都要保留其位置,也就是在“MessageBoxA”字符串前应该预留一个WORD值的位置。因此先填写0x0000。然后紧接其后写入“MessageBoxA”字符串,注意字符串要以一个字节的0x00结尾。如果还导入了其他函数,那么依次输入那些被导入的函数名AscII值即可。最后输入导入库名的AscII值,即“user32.dll”,这样我们完成了一个导入库的名称表。紧随其后以相同方式完成另一个导入库,即“ExitProcess”和“kernel32.dll”。导入表后的内容如下:
“00004D657373616765426F7841007573657233322E646C6C0000004578697450726F63657373006B65726E656C33322E646C6C00”。
接着我们在函数名称写两个nameTrunk,nameTrunk指向函数名称,并且以0x00000000结尾。故填3C20000000000000,5520000000000000(RVA)。(不要急,一会有总图)
第四个成员DWORD Name,占四个字节,指向库名(RVA)。
第五个成员DWORD FirstThunk,占四个字节,同第一个成员一样,也是指向IMAGE_THUNK_DATA。不过这次不是函数名称,而是函数地址,叫AddressTrunk。其指向函数地址。但我们无法获知函数到底加载到哪,而是由PE加载器获得函数的真实地址来填充。同样为了紧凑,我们将两个AddressTrunk安排在nameTrunk之后。注意
虽然函数调用地址最终由PE加载器来填充,我们只需指定AddressTrunk的位置。但是由于AddressTrunk以全零的DWORD值作为结束标记。所以如果我们开始也填充全零将使PE加载器填充失败,所以我们需要随便填入一个非零值,我们填入AAAAAAAA。
如图
那么之前的8个字节A填00200000,3C000000。
------data段,这个段非常简单,就是MessageBoxA所需的参数,消息框的标题和内容:即“LSKDSG”、“Hello, World !”两个字符串的AscII值。其余部分用00填充,直到0xA00处。如图:
http://www.360doc.cn/article/6844233_256165643.html
https://blog.csdn.net/Apollon_krj/article/details/77069342
https://blog.csdn.net/god00/article/details/6217367
https://www.cnblogs.com/night-ride-depart/p/5773451.html