• 加壳器目的:隐藏恶意代码的存在,缩小恶意代码可执行文件的大小,阻碍对加壳程序的探测与分析
  • 基础静态分析技术不能分析加壳后的程序,要先进行脱壳
  • 原理
    • 将一个可执行文件转换成一个新的可执行文件,被转换的文件在这个新的可执行文件中作为数据存储
    • 新的可执行文件还包括一个供操作系统调用的 脱壳存根(stub)

18.1 剖析加壳

  • 加壳器:将一个可执行文件作为输入,输出一个新的可执行文件。被加壳的可执行文件经过压缩、加密或者其他转换,目的是使它们难以被识别和逆向分析
  • 多数加壳器用压缩算法压缩原始文件
  • 要保持原程序的功能,加壳程序需要存储程序中的导入函数表信息,脱壳后重构导入表是必要的
  • 脱壳存根
    • 被加壳程序中的脱壳存根也是由操作系统加载,然后它负责加载原始程序
    • 可执行程序的入口点指向脱壳存根,而不 是原始代码。原始程序通常存储在加壳程序的一个或多个附加的节中
    • 脱壳存根执行的操作
      • 将原始程序脱壳到内存中(加载可执行文件)
      • 解析原始可执行文件的所有导入函数
      • 将可执行程序转移到原始的程序入口点(尾部跳转)
  • 加载可执行文件
    • 当加载一个标准的可执行文件时,加载器会首先读取硬盘上可执行文件的PE头部信 息,然后根据PE头部信息为可执行文件的 各个节分配内存。
    • 加壳后的可执行文件会组成PE头部,让加 载器为它的节分配空间,它的节要么来自原始程序,要么是脱壳存根创建的节
    • 脱壳存根会脱壳每个节的代码,并将它们复制到分配的内存空间中
  • 解析导入函数表
    • windows加载起不能读取被加壳可执行文件的导入函数表,对于加壳过的可执行文件,脱壳存根负责解析导入函数表
    • 方法:
      • 脱壳存根仅导入LoadLibrary和GetProcAddress两个函数,脱壳出原始可执行文件后,再加载DLL到内存并获得每个函数内存地址,填好导入表
      • 保持原始导入函数表的完整,让Windows加载器能够加载所有DLL及导入函数,缺点是缺乏隐蔽性,压缩性也不明显
      • 为原始导入表中的每个dll保留一个导入函数,分析时,这种方法只能看到每个导入库中的一个函数,比第二种方法的隐蔽性略高
      • 最后一种方法是不导入任何函数(包括 LoadLibrary和GetProcAddress),加壳程序在不用函数的前提下,自己从库中查找所有需要的函数,或者加壳程序首先找到LoadLibrary和GetProcAddress函数,用它们定位其他库,隐蔽性高,但是脱壳存根编写复杂
  • 尾部跳转
    • 脱壳存根完成脱壳后转到OEP运行,这个指令叫做尾部跳转指令
      • jmp指令是最简单且最流行的转移运行指令
      • 多数恶意的加壳程序试图使用ret或者call指令来隐藏这种行为
      • 使用操作系统转移控制的函数来掩盖尾部跳转,比如使用函数NtContinue或者ZwContinue
  • 图示脱壳过程
    18.1.jpg
    18.2.jpg

18.2 识别加壳程序

  • 加壳程序的标识
    • 程序中导入函数很少,导入函数仅有LoadLibrary和 GetProcAddress时,应该引起特别注意
    • 当使用IDA Pro打开程序时,通过自动分析,只有少量代码被识别
    • 当使用OllyDbg打开程序时,会有程序可能被加壳的警告。
    • 程序拥有不正常的节大小,例如.text节的原始数据大小为0,但虚拟大小非零
    • 程序的节名中包含某款加壳器的标识(如UPX0)
    • 工具探测
  • 熵计算
    • 熵值是一种测量系统或者程序中混乱程度的技术
    • 原理:压缩或者加密数据更接近于随机数据,因此它们有一个较高的熵值

18.3 脱壳选项

  • 三种方法:自动静态脱壳、自动动态脱壳、手动动态脱壳
  • 目的:分析恶意代码行为而不是重建原始恶意代码

18.4 自动脱壳

  • 自动静态脱壳程序
    • 最快并且也是最好的脱壳方法
    • 仅仅针对某个壳,并且对那些用来阻碍分析的 壳无效
    • 例如:PE Explorer,提供一些默认安装的静态脱壳插件,支持NSPack、UPack及UPX
  • 自动动态脱壳程序
    • 运行可执行文件,并让脱壳存根脱出原始的可 执行文件,要在安全的环境中进行脱壳,一旦被脱壳它将被写入赢哦按,脱壳程序重构原始的导入表

18.5 手动脱壳

  • 手动脱壳方法:
    • 找到加壳的算法,然后编写一个程序逆向运行 它(效率低)
    • 运行脱壳程序,让脱壳存根从内存中转储出程 序,然后再手动修正PE头部,保持程序的完整(常用方法)
  • 应用Ollydump脱壳的过程
    • 确定OEP:OllyDump插件Find OEP By Section Hop, 记下OEP
    • 使用Ollydump插件转储可执行程序:OllyDump插件Dump Debugged Process,自动重构导入表和修正PE入口点,使入口点指向OEP
  • 使用导入重构器重构导入表
    • 使用导入重构器 (ImpRec)重构导入表,修复加壳程序的导入表
  • 查找OEP
  1. 使用自动工具查找
    • 在Ollydump中通过Section Hop调用查找OEP
    • 通常,脱壳存根在一个节里,而可执行程序被 打包在另一节中
    • 当程序从一个节跳转到另一个节运行时, OllyDbg可以探测到这种转移,并且在那里进行 中断
    • Call指令被用来运行其他节中的代码,如果一个 call函数没有返回,OllyDbg将会定位到OEP
    • 结合使用step-over和step-into。
  2. 手动查找OEP
    • 手动查找尾部跳转指令,尾部跳转指令的特征:
      • 一条Jmp(或ret)指令,一串无效字节指令前的最后一条有效指令,后面没有跟返回指令
      • 跳转地址的大小,正常跳转地址在几百个字节以内,长跳转不正常
        18.3.jpg
      • IDA pro会将不确定跳转位置的跳转标注为红色
    • 在栈上设置读断点(使用硬件断点或内存断点实现)
      • 在栈中记录第一个入栈的内存地址(push开头),然后在这个栈位置设置一个读断点
      • 当脱壳存根最后执行pop获取那个地址时就会命中断点,在pop前后找尾部跳转
    • 在代码中每个循环后面设置断点
    • 在GetProcAddress函数设置断点
      • 脱壳起会使用GetProcAddress函数解析原始函数导入表
    • 在被原始程序调用且继续向后工作的函数上 (如GetCommandlineA、getModuleHandleA)设置断点
    • 调用GetModuleHandleA和GetVersion函数的开始位置就是OEP
    • 使用OllyDbg的Run Trace选项,能够在较大范围的内存地址上设置断点。OEP总是位于原始文件的.text节中,通常是这个节中第一条被调用的指令,可以使用Run Trace设置这样一个断点。
    • 堆栈平衡/ESP定律
      • 调用子程序时先保存堆栈信息(某些堆栈相关的寄存器),待子程序返回后将堆栈恢复到调用前的状态(堆栈用于保存局部变量、函数参数等重要信息),以保证程序能够继续正确运行,称为堆栈平衡。
    • 使用ESP定律脱壳
      • 程序启动时会将各寄存器的初始值压栈,对相应栈地址下硬件访问断点,当壳解密程序运行完毕恢复现场时将会中断(直接在初始的栈针指向的地址下断点)
  • 手动修复导入表(两个表)
    • 一个表存函数名称或序号列表
    • 一个表存导入函数的地址列表
    • 如果名字列表被移除,需要手动重构这个表,动静态结合,逐个导入函数分析,一旦在IDA中发现加载程序地址范围之外的地址,就去OD中看对应地址处的内容

18.6 常见壳的技巧与窍门

  1. UPX
    • 具有很高的压缩速度,较小的空间占用
    • 脱壳容易,找到尾部跳转后使用OllyDump转储即可
  2. PECompact
    • 脱壳比较困难,包含反调试异常与混淆代码
    • 需要设置OD将异常传回程序
  3. ASPack
    • 使用了自我修改代码,让设置断点和分析变得困难
    • 设置硬件断点脱壳
    • POPAD附近找OEP跳转
  4. Petite
    • 反调试机制
    • 单步异常(传回程序解决)
    • 保持从原始导入表的每个库中至少导出一个函数
  5. Winpack
    • 拥有GUI中端,为了优化压缩设计
    • 有自动化脱壳器
  6. Themida
    • 反调试反逆向,难脱壳和分析

18.7 不完全脱壳情况下的分析

  • 并不总是需要创建一个完全脱壳后可运行的可执行文件
    • 如果不能完全修复导入表和PE头部
      • 可以使用IDA Pro来分析它
    • 一些脱壳器在原始程序运行之前,并不真正完 全脱壳原始程序。相反,它们会脱壳原始程序的一部分,并且运行这部分
      • 把重点放到动态分析技术上

18.8 加壳DLL

  • 加壳DLL要保证DLL的导出表正确,确保DLL
    正常运行
  • DLL中的OEP是DllMain原始函数的开始地址
  • OD可以运行dll,为了用在exe上的脱壳方法分析dll,可以将IMAGE_FILE_HEADER节的特征标志域0x2000处的比特位从1改为0,找到OEP后再改回来,确保这个程序转储后当作DLL对待