在熟悉了PE结构之后,我们还有知道系统从磁盘加载静态文件映射到内存,作了哪一些的改变。
PE结构,就好像一张指南,告之系统自己的各个属性,哪些地方保存着哪些数据,随后又加载到哪里去。加壳的PE文件始终是也是一个文件,同原始的PE文件比较,只不过是两种数据结构相同,却数据内容不同罢了。
下面我们模拟一次系统loader的过程,探讨一下PE文件是如何从磁盘的静态数据加载的内存中去的。
首先,loader会判断目标文件是否问正常的PE文件
IMAGE_DOS_HEADER结构的MZ标志
IMAGE_NT_HEADER结构的PE/0/0标志
如果出错,就会弹出一个错误提示。\
如果是正确的PE格式那么loader会读取IMAGE_OPTION_HEADER中ImageBase作为加载基址,随后读取IMAGE_OPTION_HEADER中的SizeofImage的值,来获得PE载入空间的大小。然后根据所开空间的大小和IMAGE_FILE_HEADER的NumberOfSections中读取节头部的数量,按照各IMAGE_SECTION_HEADER保存的信息将文件中各节的实体数据映射到内存之中。
我们看到,测试文件一共有三个区段,例如:
.text会被预计加载到ImageBase+10000h处的地方,在磁盘中的位置是0x400h处所占的内存映射空间是1000h,真实的大小是0x1c2h。我们来对照看一看:
可以看到,在内存RVA0x1000h处和offset0x400h处,数据是一样的。
在完成映射之后,读取IMAGE_DATA_DIRECTORY数组的第2项,即导入表的RVA,然后获取FirstThunk所指向的数据,修改成指向导入函数的实际地址。我们来看看未载入内存时FirstThunk所指向的值
再来看看未载入内存时OriginalFirstThunk的值\
我们发现,两个值是一样的,那么我们再来看看已经载入到内存时候FirstThunk所指向的数据
OD已经注释了,这里的数据全部都替换成了导入函数实际的地址了。
处理完导入表之后,读取IMAGE_DATA_DIRECTORY数组的第6项IMAGE_DIRECTORY_ENTRY_BASERELOC项,看是否存在重定位的数据。我们知道,如果是DLL文件,IMAGE_OPTION_HEADER中ImageBase不一定是正常的加载地址,这时候,需要重新定位。例如
我们看到重定位表中:
VirtualAddress 是0x1000h,第一个重定位数据是0x300Eh,高位的30中的3表示类型,低位0X0Eh表示偏移,所以对于内存映射偏移地址0x100Eh,转到内存中去看:
我们可以看到,ImageBase+偏移等于0x0010100Eh,这正好是一个指针,取得机器码是28420010,如果这时候,载入的ImageBase不等于0x100000,那么就会用实际地址-预设地址算出一个偏移,再加上机器码。至此,loader就基本将磁盘上的PE文件整体映射到内存中了,然后读取到IMAGE_OPTION_HEADER中的AddressOfEntryPoint,加上ImageBase得到文件的入口点。
可以参考linxer版主之前写过一篇关于模拟PE加载的代码:
一段仿真PE加载器行为的程序
那么,通过上面的介绍,我们要知道,为了模拟好系统loader,压缩壳的loader要做的步骤:
1.保存原始PE文件MAGE_OPTION_HEADER中的ImageBase和AddressOfEntryPoint以及表头其他相关重要的结构
2.解压完前后各个数据段的定位地址,方便准确找到各个结构
3.原始PE文件的导入表信息,重定位信息和导出表信息,方便还原后修改和填充
4.解压代码以及外壳代码
综上,模拟了一遍loader的流程,我们知道,加壳文件也是正常的PE文件,与正常文件最不同的一点,就是在变回正常文件之前,模拟了一次系统loaer对PE文件的处理,将加壳文件转换成了正常文件。我们可以近似地将壳的shell处理被压缩的文件是一次模拟的系统loader。我们在写壳的shell的时候,就要用到上述的流程,在内存中模拟完loader解压之后,写回到磁盘中,整个的解压过程就完成了:)写得有点冗长,感谢您耐心的看完:)但水平有限,还请大家多多指正。