壳和病毒在某些方面类似,都需要比原程序代码更早地获取控制权。壳修改了原程序文件的组织结构,从而能够比原程序早获得控制权,而且不影响原程序的正常执行。
1、保存入口参数
加载程序在初始化时会保存各寄存器的值,待外壳执行完毕,再恢复各寄存器的内容,最后跳转到原程序执行。通常用pushad/popad、pushfd/popfd指令对来保存与恢复现场环境。
2、获取壳本身需要的API地址
在一般情况下,外壳的输入表中只有GetProcAddress、GetModuleHandle和LoadLibrary这三个API函数,甚至只有kernel32.dll和GetProcAddress。如果需要使用其他API函数,可以通过函数LoadLibraryA(W)或LoadLibraryExA(W)将DLL文件映像映射到调用进程的地址空间中,函数返回的HINSTANCE值用于标识文件影响所映射的虚拟内存地址。
LoadLibrary函数的原型如下。
1 | HINSTANCE LoadLibrary( |
如果DLL文件已经被映射到调用进程的地址空间中,可以调用GetModuleHandleA(W)函数获取DLL模块句柄。函数原型如下。
1 | HINSTANCE GetModuleHandleA(W)( |
一旦DLL模块被加载,线程就可以调用GetProcAddress函数获取输入函数的地址了。函数原型如下。
1 | FARPROC GetProcAddress( |
3、解密原程序各个区块的数据
出于保护原程序代码和数据的目的,壳一般会加密原程序文件的各个区块。在执行程序时,外壳将解密这些区块,从而使程序能够正常运行。
4、IAT初始化
IAT的填写本来由PE装载器实现,但由于在加壳时构造了一个自建输入表,并让PE文件头数据目录表中的输入表指针指向自建的输入表,PE装载器会对自建的输入表进行填写。程序的原始输入表被外壳变形后存储,IAT的填写会由外壳程序实现。外壳要做的就是将这个变形输入表的结构从头到尾扫描一遍,重新获取每一个DLL引入的所以函数的地址,并将其填写在IAT中。
5、重定位项的处理
因为Windows操作系统没办法保证在DLL每次运行时都提供相同的基地址,因此在壳中也要有用于“重定位”的代码,否则原程序中的代码无法正常运行。
6、Hook API
在程序文件中,输入表的作用是让Windows操作系统在程序运行时将API的实际地址提供给程序使用。壳大都在修改原程序文件的输入表后自己模仿Windows操作系统的工作流程,向输入表中填充相关的数据。在填充过程中,外壳可以填充Hook API代码的地址,从而间接获得程序的控制权。
7、跳转到程序原入口点
壳将控制权还给原程序。