161bytes的PE文件是如何炼成的~


                                 161bytes的PE文件是如何炼成的~


本文视你对PE文件的熟悉程度,初学者,大概会花掉你两个小时左右的时间完全理解。

最近在网上看到有大神做了个161bytes的hello world,正巧毕业前忧郁闲着,就花几个小时的时间把这个程序结构厘清了一遍,算是重新更深入地学习一次PE文件,写下本文和大家交流。

预备工具:一个xp系统,win7下它跑不动。Winhex,记得弄个破解版。其它的一些你习惯的PE查看工具(我试了几个都不能很好解析这个PE,其实PE解析工具就是在模仿windows解析PE文件,如果程序员本身都对PE文件没有一个深入的理解,写出来的工具模仿windows加载器不够像,就不足够应付畸形的却能够正常运行的PE)

下面先附上十六进制。
4D5A0000504500004C010100E97700000000007869616F00280002000B0148656C6C6F20576F726C642100000C0000005553455233320000000040000400000004000000300000009D000000040000000C000000300000000C00000000000000020000000000000000000000000000000000000000000000020000000000000000000000380000006A006813004000681E0040006A00FF159D004000C3DD010080

Copy到winhex里,粘贴选择ASCII Hex,保存即可。

正式进入正题:
缩小PE,一个惯用手段就是把各种PE结构重叠到一起,这样的PE看起来很晦涩,一个数据项往往扮演着多个数据结构的角色。

---------------------------------------------------------------
1、重叠NT文件头到MZ-DOS头。
161bytes的PE文件是如何炼成的~_第1张图片

如图,把原MZ-DOS头的最后一个数据offset to new exe header设为04,就把NT文件头移到了第四个字节,NT头的数据把MZ DOS头完全覆盖。
此时MZ-DOS的offset to new header同时是NT头的可选头中,section alignment,节对齐,好在4是2的幂次,是可以使用的数值。
这样就省下了不少字节。
---------------------------------------------------------------
2、重叠引入表和可选头。
这里先回顾一下引入表。由于EXE中需要引入别的DLL中的函数,所以在PE中就要有所说明,即引入表,其中要说明包含引入的DLL信息和引入的函数的信息。
具体说来,可选头尾部的数据目录项的第二项(data directory)-->IMAGE_IMPORT_DESCRIPTOR(引入的DLL的信息)-->IAT(引入函数的信息,这里仅讨论IAT,关于INT和IAT不作解释)
由于引入的DLL可以有很多个,引入的函数也可以有很多个,所以上面所说的IMAGE_IMPORT_DESCRIPTOR其实会有许多个,是一串数组。IAT也是个数组。
OK,预热完毕,还不懂的自行百度有许多资料。

附上
数据目录项结构:
IMAGE_DATA_DIRECTORY STRUCT 
  VirtualAddress dd ? 
  isize dd ? 
IMAGE_DATA_DIRECTORY ENDS
DLL描述符结构:
IMAGE_IMPORT_DESCRIPTOR STRUCT 
  union 
    Characteristics dd ? 
    OriginalFirstThunk dd ? 
  ends 
  TimeDateStamp dd ? 
  ForwarderChain dd ? 
  Name1 dd ? 
  FirstThunk dd ? 
IMAGE_IMPORT_DESCRIPTOR ENDS


可选头末尾指出有两个数据目录项,第一个全为0,第二个即是框住的,引入表。引入表指向的DLL描述符,“30”处指向的即是“user32”的ASCII码。
紧随其后的“9D”即指向IAT表,看到图中9D的偏移,“DD 01”即1DD,即是MessageBoxA在user32.dll导出表中的序号。
附IAT项的结构
IMAGE_IMPORT_BY_NAME STRUCT 
  Hint dw ? 
  Name1 db ? 
IMAGE_IMPORT_BY_NAME ENDS
---------------------------------------------------------------
3、重叠节表到可选头
这里需要指出的是NT头中的SizeOfOptionalHeader项中的值,并不是指可选头的大小,更准确来说,应该是,节表开始的位置,相对可选头开始的位置的偏移。
简单地说,其实这个东西标识的是节表的开始,在这个例子里,作者给它写上了0x28,也就是说节表在0x44处。

161bytes的PE文件是如何炼成的~_第2张图片

第二个框是可选头开始的幻数“0B01”,下面那一大串框起来的就是节表中的第一个节的信息。
附上节表结构
struct  IMAGE_SECTION_HEADER
{
BYTE Name[8];
union
{
DWORD PhysicalAddress;//物理地址
DWORD VirtualSize;//真实长度,这两个值是一个联合结构,可以使用其中的任何一个,
//一般是节的数据大小
} Misc;
DWORD VirtualAddress;//RVA
DWORD SizeOfRawData;//物理长度
DWORD PointerToRawData;//节基于文件的偏移量
DWORD PointerToRelocations;//重定位的偏移
DWORD PointerToLinenumbers;//行号表的偏移
WORD NumberOfRelocations;//重定位项数目
WORD NumberOfLinenumbers;//行号表的数目
DWORD Characteristics;//节属性 如可读,可写,可执行等
};

可以看出,真正有用的信息从0x4c开始,意思就是说这个节实际只有4个字节大小,文件中的地址是0xC,对齐后真正需要装到内存的有0x34个字节,装入后的RVA还是0xC。

这里插个问题,为什么对齐后真正需要装到内存的是0x34个字节?(其实0x2d个字节就可以)
这个我也不是特别肯定,我觉得应该是与引入表需要的字符串资源有关,如果小于0x2d,按照对齐只能是0x29,则没有完全包括“user32”这个字串。会导致windows想要加载相应模块时出错。



---------------------------------------------------------------
4、重叠汇编代码和data directory。
如你们所见,在引入表处,作者就已经近不及待地在不影响程序正常运行的引入表的大小处直接插入了代码,随后汇编代码覆盖的是其余的data directory项,并不会造成什么别的影响。
---------------------------------------------------------------
最后说一下程序运行起来的过程。
首先windows就像我们之前分析的那样,把所有东西准备好,把IAT表指向的位置换成MessageBoxA的真实地址(0x9d)。
随后windows到可选头中查找程序入口地址,0xC,然后跳到0xC处开始执行。0xC处是一个跳转语句直接跳到下面的正常代码。
这个插个问题,为什么要这么拐弯抹角跳一次,直接指向功能代码不行吗?
因为之前节表限制了,这个节大小只能有4个字节,这个“4”,即是节表的大小,还是MajorSubsystemVersion,这个值必需得是4。4个字节能做什么?能放下一个跳转,跳到任何地方,做任何事情(长跳也可以,分两次跳),于是就有了一个跳转。

---------------------------------------------------------------
总结:
黑客精神就是这样,投机取巧,无孔不入,在剑尖上带着脚镣跳舞,最终完成一个个奇迹。 

你可能感兴趣的:(161bytes的PE文件是如何炼成的~)