最小PE文件的制作
战前准备
要制作最小PE文件,那么需要知道哪些字段可以删,哪些字段不能删,而哪些字段需要修改
首先,需要知道哪些字段可以删除/修改,即对这个字段进行任何操作都不会影响程序的运行
下图将所有可删的字段全部改为了1,由于010editor的特性,这里将会显示为橙色
当然,要缩小PE文件绝不止是将这些橙色部分进行修改,下面的数据节、引入函数节等都需要修改
值得注意的是,由于修改PE文件会涉及RVA和FA的修改,如果每一步都进行这些修改,虽然会使得更改过程更加清晰,但是效率将会大大降低,所以作为马后炮,我在这里先将RVA和FA的修改按下不表,将大致过程结束之后在进行RVA和FA的统一更改
第一步
删除DOS桩和MZ签名部分
MZ签名最重要的就是前两个字节,其余都可以删除,但是需要注意的是,003CH处用于定位NT映像头开始的位置,当NT映像头的位置改变时,这个地方也需要做出变化
如下图所示,是删掉上图所示一些部分的结果(注意观察左边的地址)
为什么不删除MZ签名后的两个字节呢?这就需要注意观察刚才所说的003CH位置了,删去部分MZ文件头和DOS桩后,003CH的位置将会被NT映像头中的可选映像头中的SectionAlignment(节的对齐粒度)替代,而在给到的模板中,这个对齐粒度为4字节,而这个数转换为地址则为00000004H,恰好是删除操作之后的NT映像头的准确位置,因此,MZ签名后的两个字节并不删除,方便寻址与粒度的配合
第二步
删除可选映像头的Directory域部分(这里放没有删除DOS-MZ文件头+DOS桩的图以方便理解)
这里分为两部分,一个是NumberOfRvaAndSizes,一个是Directory域
蓝色部分的第一个四字节为NumberOfRvaAndSizes,指明了数据目录有多少个项,此处的值为00000010H,即16个项,由于Directory域中只有输入表是必须的,而输入表在数据目录的第二项(如图所示)
因此只需要留下两项,即把NumberOfRvaAndSizes改为00000002H
后面的更改便是把Directory域中的除了输出表和输入表的部分给删除就行了,由于一个项在域中占8个字节,因此在NumberOfRvaAndSizes后面的16个字节不删除,其余的都可以删
这里放删除DOS-MZ文件头+DOS桩之后需要删的部分(蓝色部分)
删除后
记得改NumberOfRvaAndSizes
第三步
删除.rdata节在节表中的位置
让我们从模板来看这个位置
如图所示,这个地方就是节表,紧跟在Directory域后面,节表中的每一个项都占40字节,即28H
节表最重要的是代码节,即.text节的部分,其余都可以进行删除
此处,我们需要删除.rdata节的部分,即下图蓝色部分
而在删除之后,需要更改NumberOfSections这一项为1
(吐槽一下,就算不删.rdata部分,直接改这个为01程序也能继续运行)
而除了.rdata项之外,.text项是否也有可以删除的地方呢
答案是肯定的
节表中的.text项后面的16个字节是可以被删除的
这个可以被删除的部分为“本节属性”,可能被后面的东西取代了,但不造成影响
于是,在上一步的结果后面,我们继续做减法
这一段可以删除
然后记得把NumberOfSections这一项改一改
在这一步我们可以得到这个结果
第四步
由于要制造最小PE文件,所以后面汇编有得你写的需要重写,因此原先代码节的部分就可以删除了
根据可选映像头中的AddressOfEntryPoint可以定位代码节的开始
那怎么定位结束呢?
这就需要知道IAT表的位置了,由于.text节和.rdata节紧密相连,而.rdata节中的第一个部分就是IAT,因此找到IAT的开始就找到了.text节的结束了
那么如何找到IAT的位置呢?
这就需要看我们的Directory域了(虽然上面删掉了,但是也可以看模板找字节进行搜寻对吧?),Directory域中有IAT这一项,这一项和其他项一样,有RVA和大小,有了RVA就可以定位了
下图为模板中Directory域中IAT的RVA
对应的位置
根据OEP和IAT的RVA,我们就可以确定要删除的代码段了
于是这一步我们可以得到这个结果
第五步
见缝插针
在这一步,我们需要把.rdata中需要的函数名和dll文件的名字放在上面那些可以被更改但不会影响程序运行的地方
首先明确,在模板给出的函数中,实际真的被用到的只有MessageBoxA函数和其对应的dll,即use32.dll
因此我们需要把这两个放到适当的位置
此处略过对比,直接上结果
把user32放到这里
把MessageBoxA放到这里
这样下面那些名字就可以删掉了
但是有个小问题,应该删到哪呢?
这里我们需要知道.rdata节的最后一部分,即IMPORT Hints/Names&DLL Names的结构了,在一个部分里面,分为两个小部分,两个小部分通过一个字节的00隔开,部分与部分之间用两个字节00隔开
下面简要说明这两个小部分:
一个是IMAGE_IMPORT_BY_NAME,还有一个是DLL names字符串,第一个是一个结构由两项组成:Hint和Name,Hint占两个字节,Name为一个字符串
通过这个,我们可以推断,如果要知道删到哪,那么需要知道整个部分的第一个函数,然后往前找两个字节就可以了
于是我们可以知道要删的就是下图这个部分(以上一步的结果为例)
那么我们就删除了后面的IMPORT Hints/Names&DLL部分了
第六步
这一步,我们需要重构原先的IAT和IDT
可能有人会问,那INT(IMPORT Name Table)呢?
这个可以直接删掉的,只要让IDT指向IAT或DLL文件名,IAT再指向函数名就好了,INT?真不熟!
为此,我们先删除整个.rdata节,然后根据IAT和IDT的结构进行复原
删去之后,我们需要明确,由于我们设置的粒度为4,所以最后的结果必须是4字节的整数倍,在这个地方,我们删除.rdata节的所有数据后,可以在文件末尾补两个字节的0,后面就不用再补了
现在,让我们开始复原IAT和IDT吧
首先我们先认识IAT的结构
1 | IAT是一个指针数组 |
然后再让我们看看IDT的结构
1 | IDT由一系列的 IMAGE_IMPORT_DESCRIPTOR结构组成 |
下面重点介绍一下IMAGE_IMPORT_DESCRIPTOR 的数据结构
1 | typedef struct _IMAGE_IMPORT_DESCRIPTOR { |
由结构可知最重要的是OriginalFirstThunk、Name和FirstThunk这三个部分,由于我们已经删除了INT表,因此需要让OriginalFirstThunk和FirstThunk的值都指向IAT表
对于IAT表,我们需要让它指向MessageBoxA这个函数,因此可以重构成这样
1 | 1C 00 00 00 00 00 00 00 |
对于IDT表,我们可以重构成这样
1 | E8 00 00 00 |
最后结果如下(注意IAT指向的地址和IDT指向的地址)
第七步
让我们最后整点汇编吧
首先把能进行修改的字节标1(橙色)
OD,启动!
我们以模板的汇编代码为准进行修改
由于上面已经分析过代码段的位置,这里不再赘述,在OD中直接找到这个位置
先结合模板分析一下
(模板中的代码段)
(模板中的数据)
这里需要一点汇编知识
1 | push 0x1040 |
由上文所述,如果需要程序正常运行,那么需要将数据段的数据准确压入栈中,为此我们需要在地址中下一点功夫
根据数据段的位置,以及可以被修改的字节位置,我们可以写出以下汇编代码
1 | push 0x1040 |
然后就可以插入了
这里重点提一下中间的call后面地址的计算
1 | X=真正要跳转的地址-(E8这条指令的地址+5) |
根据在OD中对模板文件的查看,容易知道E8跳转的地址实际上就是汇编代码最后一段的地址(FF25),因此我们可以得出
1 | X=00400070H-(00400060H+5H)=0BH |
这样我们的汇编部分就结束了
第八步
最后做一点小修改
首先是SizeOfOptionalHeader
这个是整个可选映像头的大小,鉴于我们对可选映像头的修改只有删除Directory域,因此这里做个简单的加减法(E0-70=70)就可以得出结果了
然后是OEP
鉴于我们把汇编代码乱放,因此我们需要找到第一条指令的执行位置并修改OEP
接下来是SizeOfHeaders
别看这有个Size,实际上指向的应该是第一个节开始的位置,这里放OEP就好
之后是Directory域的修改
即输入表的RVA和大小的修改,不多赘述
最后是代码节的修改
将地址改为数据的起点即可
重点要更改的是节表的VirtualAddress和PointerToRawData这两项
当然,不能忘记改自己的名字和学号,这个就不放了(GBK编码)
至此,所有工作已经结束,我们已经获得了一个Win11下可以正常运行的最小PE文件(以模板给的字符串是268B,如果删掉数据节一点东西的话应该能更小)