壳的加载过程

记录加密解密一书中对壳的加载过程的描述,第 16 章:16.1;

眼过千遍,不如手过一遍;

壳的加载过程

加壳软件通过修改原程序执行文件的组织结构,从而使壳比原程序代码更早获得控制权,且不影响原程序的正常运行,以下是壳的常见加载过程:

  1. 保存入口参数

    加壳程序在初始化时会保存各寄存器的值,待外壳执行完毕,再恢复各寄存器的内容,最后跳到原程序执行,通常使用 PUSHAD/PUSHFD、POPAD/POPFD 指令来保存与恢复现场;

  2. 获取壳本身需要使用的 API 地址

    在一般情况下,外壳的输入表中只有 GetProcAddress、GetModuleHandle 和 LoadLibrary 这 3 个 API 函数,甚至只有 Kernel32.dll 及 GetProcAddress;

    如果需要使用其它 API 函数,可以通过函数 LoadLibraryA(W)或 LoadLibraryExA(W)将 DLL 文件映像映射到调用进程的地址空间中,函数返回的 HINSTANCE 值用于标识文件映像所映射的虚拟内存地址;

    LoadLibrary 函数原型:

    1
    2
    3
    HINSTANCE LoadLibrary(
    LPCTSTR lpLibFileName // DLL 文件名地址
    );

    返回值:成功则返回模块的句柄,失败返回 NULL;

    如果 DLL 文件已经被映射到调用进程的地址空间中,可以调用 GetModuleHandleA(W)函数获取 DLL 模块的句柄;

    1
    2
    3
    HMODULE GetModuleHandle(
    LPCTSTR lpModuleName // DLL 文件名地址
    );

    返回值:成功则返回模块的句柄,失败返回 NULL;

    一旦 DLL 模块被加载,线程就可以调用 GetProcAddress 函数获取输入函数的地址了;

    1
    2
    3
    4
    FARPROC GetProcAddress(
    HMODULE hModule, // DLl 模块句柄
    LPCSTR lpProcName // 函数名
    );

    参数 hModule 是 DLL 模块的句柄,是调用 LoadLibrary(Ex) 或 GetModuleHandle 函数的返回值;

    参数 lpProcName 是函数名或函数序数值,如果此参数是序数值,则必须使用低位字;高位必须为零;

    返回值:成功则返回函数地址,失败返回 NULL;

    在外壳中使用的其他函数都是由这 3 个函数调用的;

    有些壳为了提高强度,连系统提供的 GetProcAddress 函数都不使用,而是自己实现一个相同功能的函数来代替 GetProcAddress,从而增加函数调用的隐蔽性;

  3. 解密原程序各区块的数据

    出于保护原程序代码和数据的目的,壳一般会加密原程序文件的各个区段;

    在程序执行时,外壳将解密这些区段数据,从而使程序能够正常运行;

    壳一般是按区块加密的,所以解密时也按区块解密,并把解密的区块数据按照区块的定义放入内存中合适的位置;

  4. IAT 的初始化

    IAT 的填写本来应该由 PE 装载器实现,但由于在加壳时构造了一个自建输入表,并让 PE 文件头数据目录表中的输入表指针指向自建输入表,PE 装载器会对自建输入表进行填写;

    程序的原始输入表会被外壳变形后存储,IAT 的填写会由外壳程序实现;

    外壳程序要做的就是将这个变形输入表的结构从头到尾扫描一遍,重新获取每个 DLL 引入的所有函数的地址,并将其填写在 IAT 中;

  5. 重定位项的处理

    文件执行时将被映射到指定内存地址中,这个初始内存地址称为基址,当然,这只是程序文件中声明的,当程序运行时,操作系统一定会满足其要求吗?

    对 EXE 文件来说,操作系统会尽量满足其要求,如:程序声明的基址是 400000,操作系统提供的基址也是 400000,在这种情况下就不需要基址重定位了;

    由于不需要对 EXE 文件进行重定位,加壳软件会删除原程序文件中用于保存重定位信息的区块,使加壳后的程序更小巧;

    对 DLL 的动态链接库文件来说,操作系统无法保证 DLL 每次运行时都提供相同的基址,所以重定位就很重要了;

    此时,壳中也要有用于重定位的代码,否则原程序将无法正常运行;

    所以,加壳的 DLL 比加壳的 EXE 在修正时多了一个重定位表;

  6. Hook API
    在程序文件中,输入表的作用是让 Windows 操作系统在程序运行时将 API 的实际地址提供给程序使用,在程序的第 1 行代码执行前,Windows 操作系统就完成了这项工作;

    壳大都在修改原程序的输入表后,自己模仿 Windows 操作系统的工作流程,向输入表中填充相关的数据,在填充过程中,外壳可以填充 Hook API 代码的地址,从而间接获得程序的控制权;

  7. 跳转到程序原始入口点(OEP)

    从这个时候起,壳就把控制权还给原程序了,一般的壳在这里会有一条明显的分界线;

    现在越来越多的加密壳先将 OEP 代码段搬到外壳的地址空间里,再将这段代码清除(这种技术称为“Stolen Bytes”),这样,OEP 与外壳之间那条明显的分界线就消失了,脱壳的难度也增加了;

脱壳机

针对特定的壳开发出来的脱壳软件称为“脱壳机”;

脱壳就是将加壳后的程序恢复到原来的状态,脱壳成功的标志是文件能正常运行;

由于脱壳时可能没有将壳本身的代码去除,脱壳后程序的体积通常会比原程序的体积大;

脱壳机一般分为专用脱壳机和通用脱壳机;

专用脱壳机是针对某种壳专门编写的,只能脱特定的壳,虽然使用范围小,但效果好;

通用脱壳机具有通用性,可以脱多种不同类型的壳(主要是压缩壳);

在分析一个软件前,可以使用 PEiD 确定壳的种类,再选择合适的脱壳机;

手动脱壳

对一些加密壳或修改的壳,没有脱壳机,因此必须要分析外壳并手动脱壳;

手动脱壳过程一般分为 3 步:

  1. 查找真正的程序入口点(OEP);
  2. 抓取内存映像文件;
  3. 重建 PE 文件;

当程序执行时,外壳代码首先获得控制权,模拟 Windows 加载器,将原程序恢复到内存中,这时,内存中的数据就是加壳前的映像文件了,适时将其抓取并修改,即可还原到加壳前的状态;