windows的PE详解,教你手写,可执行文件

伊始

  现如今windows可执行程序是基于PE结构的。如果我们了解了其中构造的方法,我们就可以实现手动打造可执行程序。也可以更好的防范DLL注入。
  本文是DLL注入与安全延申。

主体

  首先本文所用到的工具如下:

序号 软件名称
1 PE查看器
2 16j进制编辑器

  那么我们开始吧!
  在这里插入图片描述
首先我们要了解PE的组成。直接上图片!
在这里插入图片描述
整个exe由下面列表构成(重定位.reloc没有加入,.rsrc资源文件也没有加入。因为,对我们这篇文章影响不大)本程序,没有用那么多,不必要的已经删除,目的最小空间,完成我们的目标。

序号 名称
1 IMAGE_DOS_HEAD     DOS头部
2 MS-DOS Stub Program    汇编残余程序,省略了
3 IMAGE_NT_HEADERS    NT(PE)头
4 Signature    签名
5 IMAGE_FILE_HEADER    文件头
6 IMAGE_OPTIONAL_HEADER    可选头
7 IMAGE_SECTION_HEADER .text    text节头

先我们先打开16进制编辑器
在这里插入图片描述

(注:一些头部可在winnt.h文件中看到)

  1. IMAGE_DOS_HEAD     DOS头部 ,大小64与MS-DOS Stub Program大小,0-任意。
    在这里插入图片描述
      PS:数据类型 WORD 占 0x0000,也就是16进制编辑器图片中,2个00 的大小 LONG 占4个00的位置。根据系统采用大端或小端结构填充的数据也不同。我们这个环境是,低地址对应16进制低位,高地址对应16进制高位
      对于这个IMAGE_DOS_HEAD我只介绍重要的部分,不重要的部分均用0来填充,如果想知道其中含义及其使用方法,请自行百度。
      在这个结构体中,最重要的是第一个变量e_magic和最后一个变量e_lfanew。第一个变量是固定值,0x5A4D 对应的ASCII 是MZ,为什么是这个是由历史意义的,请自行百度。对一个机器码填写4D5A.
    在这里插入图片描述
      最后一个变量是指向IMAGE_NT_HEADERS,下一个头的位置。所以这个位置与你sizeof(IMAGE_DOS_HEAD)+sizeof(MS-DOS Stub Program)的总和大小。
      因为我们是win32程序,我们就不写残余程序了,直接sizeof(MS-DOS Stub Program)=0;所以就是sizeof(IMAGE_DOS_HEAD)=64,对应16进制0x00000040。所以IMAGE_NT_HEADERS起始点位地址00000040.
      因为占了4位且由低位到高位,所以填写40 00 00 00.其他变量填写00,所以由如下结果。在这里插入图片描述
      上面就把IMAGE_DOS_HEAD头,与MS-DOS Stub Program构成完毕了。
  2. MAGE_NT_HEADERS包含3部分:
    在这里插入图片描述
      1) Signature:4位固定值:0x00004550 ASCII为PE,结果如下:
    在这里插入图片描述
      2) IMAGE_FILE_HEADER,大小sizeof(MAGE_NT_HEADERS)=20
    在这里插入图片描述
       Machine 所要运行的cpu,intel值是0x014C。所以
    在这里插入图片描述
       NumberOfSections后面节的数量,由于我们只建立一个代码段所以只有一个节点。
    在这里插入图片描述
       TimeDateStamp和PointerToSymbolTable和NumberOfSymbols,代表文件创建时间和与符号表有关的,我们这里不用全部填00.
    在这里插入图片描述
       SizeOfOptionalHeader后面可选头大小,这个需要建立可选头才知道,PE可选头部分大小正常情况下是224 byte,固定值0x00e0,但我们这里去除了空位。我们使用仅仅使用了0x70大小。后面在列出计算公式。
    在这里插入图片描述
       Characteristics描述的是文件信息,比如exe还是dll。这个是按位定义为,具体位代表什么请百度,我们填写的是0x0020。是可执行文件。
    在这里插入图片描述
    3)IMAGE_OPTIONAL_HEADER头部,PE可选头部分大小正常情况下是224 byte,固定值0x00e0,可减少。

在这里插入图片描述
   Magic固定值0x0b代表可执行程序,至于其他值,请百度。
在这里插入图片描述
  MajorLinkerVersion和MinorLinkerVersion和SizeOfCode和SizeOfInitializedData和SizeOfUninitializedData表示版本号,代码长度,初始化等,不影响我们的程序为即可
在这里插入图片描述
  AddressOfEntryPoint重要的,代表可执行代码的入口!!!这个是代码段的入口后面我们才能填写,我们暂时先全部填写C来记录位置.
在这里插入图片描述
  BaseOfCode和BaseOfData,表示可执行代码起始位置和初始化数据的起始位置,对程序无影响,填0.
在这里插入图片描述
  ImageBase,代表该PE文件加载到内存的保护模式下的虚拟地址。一般为0x00400000
在这里插入图片描述
  SectionAlignment文件内,表示文件加载到内存中,的对齐,一般为4K=4096也就是1000h
在这里插入图片描述
  FileAlignment,文件内对齐的大小,一般为512,200h.
在这里插入图片描述
  MajorOperatingSystemVersion和MinorOperatingSystemVersion和MajorImageVersion和MinorImageVersion与系统版本号有关,我们为0即可。
在这里插入图片描述
  MajorSubsystemVersion子系统版本号,这个需要为0x04即可.
在这里插入图片描述
  MinorSubsystemVersion和Win32VersionValue不影响程序,填0即可
在这里插入图片描述
  SizeOfImage表示程序载入内存后的大小(别忘记了内存对齐)此处大小,后买你写完代码我们才知道,因此先用CC填充
在这里插入图片描述
  SizeOfHeaders表示所有头文件大小。这个头大小等我们设计完头就知道了,我们先用BB填充。
在这里插入图片描述
  CheckSum对于我们程序影响不打,填0
在这里插入图片描述
  Subsystem表示NT子系统,为0x03即可
在这里插入图片描述
  DllCharacteristics和SizeOfStackReserve和SizeOfStackCommit和SizeOfHeapReserve和SizeOfHeapCommit和我们程序关系不大,填0即可。
在这里插入图片描述
  NumberOfRvaAndSizes表示后面数据目录成员的个数,实际上我们永改了个所以为0x02。
在这里插入图片描述
标准化整个长度为224,后面都是一个数据目录表,也就是一个IMAGE_DATA_DIRECTORY[16],中每个元素有两个变量
在这里插入图片描述
  一个是RVA,也就是当本文件加载到内存后,相对于内存中本文件的偏移。长度4位,一个是大小,4位,总长度0x8h。因为我们程序只用了2个成员,所以标准16个成员-2=14个成员没用,所以我们可选头大小为224-14*8=112,也就是0x70.所以我们可选头大小位0x70
在这里插入图片描述
  由于我们没用到导出表,所以都填0在这里插入图片描述
在这里插入图片描述
  导入表 是我们导入函数的表,等我们后面设计的时候在填写,目前填BB吧
在这里插入图片描述  4) 紧接着是,节点头了.节.text结构为MAGE_SECTION_HEADER 大小40.
在这里插入图片描述
  Name节点名字,我们用ASCII表示2E74657874000000来表示.text也可以其他名字。
在这里插入图片描述
  VirtualSize映射到内存大大小。我们的写的代码编程机器码后大小为0x26,
在这里插入图片描述
  VirtualAddress表示映射到内存的.text节数据的地址。目前我们还不知道,先填AA
在这里插入图片描述
  SizeOfRawData表示文件内大小,虽然我们的汇编代码仅仅26h,因但由于文件对齐是200h,所以我们可以填200h
在这里插入图片描述
  PointerToRawData…text段在文件中的起始地址,这里我们不知道先填写AA
在这里插入图片描述
  PointerToRelocations和PointerToLinenumbers和NumberOfRelocations和NumberOfLinenumbers,对我们程序没有影响,所以均为0
在这里插入图片描述

  Characteristics,比较重要,是给.text,可执行权限的,具体有哪些权限请,百度。我们这里为0x20000000可执行权限
在这里插入图片描述

  至此我们头就都完成了由于文件内200h对齐,我们把0填充到0x1ff。
在这里插入图片描述
  5)现在我们写,我们重要的一个节数据,.text可执行程序的部分。我们要做的是用MESSAGEBOX显示hello word。

我们用编译器写出hello word程序,然后,看机器码。我这里就直接把机器码写出来了。下面这一段机器码代表hello word。我们把他们写道我们的文件中。

6A 00 68 A0 01 40 00 68 A7 01 40 00 6A 00 E8 07 00 00 00 6A 00 E8 06 00 00 00 FF 25 80 11 40 00 FF 25 88 11 40 00 00

在这里插入图片描述
我们来分析下,其中有什么。上面机器码对应下面这个汇编语言,
(注:messagebox有4个参数)
push  0   push 机器码为6A,整体长度为2,消息框的风格,我们为0
push   004001A0  push 机器码为6A,整体长度为5,消息框的标题字符串所在的地址,需要我们填写,这里我把数据放到文件的004001A7地址处了。
push  004001A7   push 机器码为6A,整体长度为5,消息框的内容字符串所在的地址,需要我们填写,这里我把数据放到文件的004001A7地址处了。
push  0   push 机器码为6A,窗口句柄,整体长度为2,我们为0也就是NULL
call   00000007  CALL 整体长度为5,机器码为E8,汇编代表偏移,调用MESSAGEBOX,与call距离5+2=7所以偏移为7
push  0   push 机器码为6A,程序退出码为0
call  00000006  CALL 机器码为E8,整体长度为5,汇编代表偏移,与call距离6所以偏移为6
jmp  00401180  JMP 机器码为FF 25,整体长度为6,转到messagebox,这个函数是user32.dll的地址,所以我们要导入。
jmp  00401188  JMP 机器码为FF 25,整体长度为6,转到退出函数,这个函数是user32.dll的地址,所以我们要导入。

  下面是消息框标题和内容放到的位置。在这里插入图片描述
  现在我们就差两个jump跳转的真实函数地址了。我把它放到了
在这里插入图片描述
  目的是减少空间的浪费。
由于JMP指令FF25是绝对跳转,也就是在内存中的地址,所以,根据我们,在可选头中IMAGE_OPTIONAL_HEADER中ImageBase变量为0x00400000.而在文件中我们的地址是把函数的真实地址放到了上图的0x00000380处,所以加在一起是,0x00400380,为什么不是0x00401180?
  这是因为,在内存中我们的对齐是1000h,而文件中我们的对齐是200h,我们要把380h转化为内存中的位置。因此有
  文件中0-200h对应内存中0-1000h。200h-400h,对应内存中1000h-2000h。所以380h=200h+180h,内存中1000h+180h=1180h。再根据基址为0x00400000+1180=0x00401180

jmp  00401180  

jmp  00401188  

同理MESSAGEBOX的数据地址004001A0和helloword 的地址004001A7,会计算了吧
在这里插入图片描述
  那么函数的真实地址是怎么来的?这就涉及到user32.dll,关于dll导入表的设定了。

  我们在可选头的数据目录中,有一个import table,就是下面的这个。
在这里插入图片描述
我们还没有完善,我们前面把它放在了文件的0x00000300处,大小为3ch(至于为什么是0x3c后面有介绍哦),文件对齐200h,转换成内存中是0x1100,大小0x3c为所以我们完成这项.在这里插入图片描述

  我们的代码段,在可选头还没有填写,我们来填上吧。
(注意,这里的数值是,在内存中,相对的偏移地址哦,也就是文件中0x00000200处,在内存中是1000h)
在这里插入图片描述
  还有.text头内容哦,一个是内存中偏移1000h,一个是文件中偏移200h。
在这里插入图片描述在这里插入图片描述

  接下来我们就来介绍,导入表吧,大小为20
在这里插入图片描述
  OriginalFirstThunk函数名称地址表,也就是指向函数名称地址的指针首地址。
  TimeDateStamp和ForwarderChain我们用不到为0即可。
  Name,所导入dll名字的地址。
  FirstThunk,导入函数的真实地址。
  因此,我们需要构造这个表,还有其中的内容我们也需要构造。

  由于我们引用了2个dll,一个user32.dll和kernel32.dll,所以,我么要建立上面这个结构2个,2x20=40,但是导入表需要以全0结尾来结束的所以有,3x20=60也就是3ch。
在这里插入图片描述
  我们先构造调用的函数名称。由于dll既可以按序号,也可以按名称。所以我们流出序号的位置,因为我们用函数名,所以序号为0就可以了。
在这里插入图片描述
  所以下面是函数名+dll名称,因此我们函数名地址和dll名地址都有了。
在这里插入图片描述
  因此导入表中Name;可以填写了在文件中是0x0000034A和0x00000363,转换为内存偏移是0x114A,和0x1163。

在这里插入图片描述
在这里插入图片描述
  OriginalFirstThunk接下来我们完成这个,由于这个里面存储的是一个指向函数名的地址,所以我们在下面构造一个,函数名所在文件地址分别为,0x0000033c和0x00000355转换为内存偏移为0x113c和0x1155。
因此在文件中的地址分别为0x00000370和0x00000375,转换为内存中偏移为,0x1170和0x1178为什么是这两个数值,因为OriginalFirstThunk指向的函数名地址表也需要4位的0结尾,一个函数名地址+1个4位0结尾,所以就占8位呀。
在这里插入图片描述
在这里插入图片描述
  好啦我们就差真实函数地址了FirstThunk,这个地址是,系统自动加载的,因此我们留好位置就好了。因此,我们放到函数名称地址表后面就好了,也是以4位的0结尾。但是为什么我写11开头了,因此,系统是按全0识别的,所以我们的位置不能位0,你可以填写任意不为0的数值。所以FirstThunk我们这里直接写结果啦,0x1180和0x1188。
在这里插入图片描述

  到了现在我们就差,下面了,SizeOfImage和SizeOfHeaders,分别表示,整个文件载入内存的大小和整个头的大小。我们知道文件对齐是200h所以,看最下面的图片,整个文件大小0x400,内存对齐为2000h。而头大小为200h.

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
  至此,我们整个exe完成。
保存运行结果如下:
在这里插入图片描述

结束语

  累死我了!终于写完了,这就是手工打造exe了。我们越了解它,我们越能做好安全防控,所以小伙伴么,不要随便下载exe文件哦,有可能被,恶意插码哦!
(关于资源目录,以后再介绍把。)

发布了7 篇原创文章 · 获赞 13 · 访问量 152

猜你喜欢

转载自blog.csdn.net/itsaoght/article/details/105451676
今日推荐