手工打造小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文件
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课