手工打造小PE

    我很荣幸通过了入学考试并进入了科锐学习,目前我正在第三阶段学习PE知识点,在这里自己作一个小小总结,以备自己以后查看. 本文PE头指的是nt headers  与 section headers;
  所需要环境:Redasm 2.2.1.9, Winhex 即可!
  
  首先源代码如下:
  .386
  .model flat,stdcall
  option casemap:none
  
  include    windows.inc
  include    user32.inc
  includelib  user32.lib
  
  .code
       szClassName      db  'CR10',0
      start:                
      invoke MessageBox,NULL, offset szClassName, NULL, MB_OK
      ret
  end  start
  
  Redasm --工程---工程选项
  5,O,$B\LINK.EXE /SUBSYSTEM:WINDOWS /RELEASE /VERSION:4.0 /LIBPATH:"$L" /OUT:"$5",3
  换成
  5,O,$B\LINK.EXE /SUBSYSTEM:WINDOWS /RELEASE /VERSION:4.0 /LIBPATH:"$L" /OUT:"$5" /merge:".rdata"=".text" /align:4  ,3
  
  其实多加了两个链接选项,即/merge:".rdata"=".text" /align:4
  默认情况下,如果不改链接选项的话,链接器产生两个节区,.rdata,与.text.,为了减少PE节表,所以利用merge将.rdata节合并到.text节. 而align选项是将文件对齐与内存对齐设为4, 这个值对我们节区数据开始位置起到至关重要的一个值.因为文件默认对齐值是200,那么我们节区必须模200地址开始,对减少PE大小干扰是可想可知的.
  
  编译链接程序成功后,用winhex查看如下:图如下1:
  
       在DOS头中,其实有用的字段只有e_lfanew:位于3C处,指向PE头偏移 和0偏移的e_magic:DOS标志 .其它大部份成员均没用到,那么是否可以将PE头到DOS头里来呢放到这里来,放在什么位置呢. 
  看下图2:

  将PE头移动到DOS头里偏移4处,为什么是这里呢,因为对于DOS头来说,位于3c位置是e_lfanew字段,非常重要,指明了PE头偏移. 但此时的3C位置,对于PE头来说是是 optional header的SectionAlienment字段,这是我们内存中对齐值.恰恰 也是4. 
  
  
  在此图,也标明了大部份不重要字段,都用了CC填充,这为后面数据重合起到不可忽视的引导作用.
  PE头还有一处能减少,就是数据目录项,将默认的16项,改成了3项,影响optinal headers 大小,即字段fileheaders.SizeOfOptinal.,字段:optinal headers.NumberOfRvaAndSizes 数据目录有关,即数据目录项数,经测试这个值也不太重要,只设的不离谱.图如下3:

  
  
  把PE数据目录项减少后,重要两点:text节区数据,与输入表数据就要前移了,
  图如下4:

  为了方便RVA到FA转变,将节区内映射到RVA :0处,对应文件偏 移0处.
  
  位于84处,是输入表项,设为e0为我们输入表数据.在这个表中 填上相对应的位置就可以了.
  
  然后,我们看到,有大量的CC字段末使用,所以我们可以将后面的数据移到前面 有CC地方 去.
  
  B4到 BB 七个byte可利用.
  B4,B5:text.Relocation Number  不重要
  B6,B7 :text.Linenumbers Number  不重要
  B8-BB :则是text的节区属性, 只有最高四位比较重要.F表示可读写执行,共享.
  所以我们可以把这个七个字节存导入的dll名称,即user32.dll .这样当然我们放不下.所以导入dll名称,可以减短为user32即可,这个想法,源自于我记的调用loadlibrary函数时,不需要扩展名.相应的改变 输入表字段name(7c处)值为:B4 指向导入的dll名称;

  同样的道理,在PE头中C-17 都是不要重字段,这里正好可以放下我们导入函数名MessageboxA.
  所以我们可以把输入表FirstThunk 指向的 IMAGE_IMPORT_BY_NAME项移到这里来, 因为在IMAGE_IMPORT_BY_NAM中,他导入的ID是不重要的,即他的ID可以与位于A处:file head.NumberOfSections节区个数重叠. 随后C-17正好存放导入函数名MessageboxA; 当然相应的改变输入表项FirstThunk 指向的IMAGE_THNUK_DATA 指向导入的函数名.
  
  同样,可以把Messagebox的参数字符串,可以放到4e-53处.
  当把这些工作做完后,由于我们改动了输入表IAT的位置,改动了Messagebox参数的字符串的位置,这些位置在指令都是硬性编译好的,如果直接运行,程序肯定会崩的,所以我们还需要改动指令的地址,有点类似给指令重新定位吧.这里就截个图吧.图如下5:
  
  
  这样我们就把所有的数据压到PE头里了,对BC后面的数据,可以全部删除掉了.我们的PE也就只有188字节大小了.
  还记的前面我说的file head中字段SizeofOptionalHeader.吗?它决定了option header头大小,同时,它还有一个隐藏属性,它还决定了节区表的开始位置. 这一点对我们现在还能把PE压小,是至关重要的信息. 从上图中可以看到,如果我们能把.text节表再上移到上面大片的CC空间中,那么就能把PE打造的更小些.
  那么.text节表前移到什么位置呢?
  file head中字段SizeofOptionalHeader = 18,也就是.text节表起位置在34处. 为什么选择这个位置,原因有以下几个:
  1.text节名处34-3B,不重要,而38-3B正是optional header头中的ImageBase字段,非常重要.所以它们重叠是合理的.
  2.3C处是option header中SectionAlignment内存对齐为4,3C对于.text节表中的Virtual Size,对于它来说不重要.所
     以它们能重叠.
  对于40来说是option header中FileAlignment文件对齐为4,此对于.text节表来说正是起始映射的地址virtual addr
      ess,对于它来说不重要.所以它们能重叠.
  经过测试.text节表中文件大小(raw size),也可以全填充cc.
  同时为调整 "CR10~"参数,与导入dll名位置.
  .将"CR10~"字符串调整到62-67位置.正好命中了optional header中DllCharacteristics,SizeOfstackReserve字段.这两个    字段恰好重叠,过了PE检查,实属运气成份!
  同理导入库名"user32"一样的道理,调整到68-6E处.
  这样我们还能把导入表前提到70.
  对于导入表来说重要的两个字段分别是
  Name 位于7C处.对于optional header 来说是导出表目录,可随便值,他们可重叠.
  FirstThunk 位于80处.于optional header 来说是导出表大小,一样可随便值,所以能重叠.

  当然移动了字符串,导入库名,这些都要在指令跟导入表做相应的指针变动.下面就是最重要的把指令打散分别嵌入离散的空间中去.(这里最主要的就是利用短跳jmp指令,将这些代码连接起来)
  代码:
  6A 00 6A 00 68 4E 00 40 00 6A 00 FF 15 7C 00 40 00 C3
  反汇编如下:
  
  00400065 >push    0
  00400067  push    0
  00400069  push    0040004E                              ; ASCII "CR10!"
  0040006E  push    0
  00400070  call    dword ptr [<&USER32.MessageBoxA>]     ; user32.MessageBoxA
  00400076  retn
  图如下6:

  
  
  
  还有一个重点:
  对于导入表数据目录来说,只要留一个导入表起始位置字段即可,其它的均可删除,默认为0嘛.至此PE文件节约到133字节了!
  终于勉强算是自己对PE字段一个肤浅的总结.
   2011-11-29 19:29 
   科锐十期学生
   米汤

手工打造小PE样例.rar  附件中含从大到小的 exe各个大小版本.附带winhex pos文件