现代加密技术使用巨大的运算量换取安全性的提高。而破解的分析大多还依靠人脑。由于计算机领域的累积性,加解密领域具有排斥性,门槛变高

第 0 章 准备知识

  • 新创建的进程首先执行 LdrInitializeThunk,它由一个断点异常回调,此时只加载了 ntdll。主线程的入口则是在 ntdll.RtlUserThreadStart
  • 通过观察 dll 的加载理解 pe 的加载。LdrLoadDll 调用 NtOpenSection 然后 NtMapViewOfSection。RtlImageDirectoryEntryToData 获取初始化用的 DataDirectory 数据目录

第 1 章 软件保护技术

  • 反调试、反分析、反修改
  • 反调试
    • 函数,IsDebuggerPresent
    • 数据,查 peb 的 BeingDebugged 位
    • 使用了驱动的调试器通讯符号
    • 调试器窗口名称
    • 搜所有进程中调试器的特征码
    • 行为检测,如代码的运行速度下降,线程数量,中断等
    • 断点检测
    • 破坏调试相关功能。NtSetInfomationThread 设置 ThreadHideFromDebugger
    • 行为占用,如占用调试器的坑位,即双进程保护
  • 反虚拟机
    • Bios 检测。GetSystemFirmwareTable
  • 数据校验。代码段校验。因为重定位可能会修改代码,所以可以不事先计算,而是计算两次比较是否相同
  • 导入表加密
    • 调用方式有:Call(FF 15)、Jmp(FF 25)、Mov reg
    • 在导入函数前先跳到自己的内存,干扰调试器的解析
    • 一种是解出真实导入函数地址后再跳转过去,另一种是直接将入口代码变形/虚拟化后执行,没有了跳转到入口的过程,而且这样保护加密表的同时也能保护前后的相关代码
    • 建立自己的运行时函数,不再调用系统的(IAT函数模拟)
  • 模块拷贝移位。复制模块代码以反代码 hook(第七章),需要处理重定位并修改代码调用地址
  • 资源加密。基本限于对资源的压缩和位移,同时 hook 资源函数以便动态解密
  • 代码加密
    • 指令变形,将简单代码复杂化,同时也会导致指令数大幅膨胀,壳里有用到
    • 花指令
    • 代码乱序,用 jmp 跟猴子一样东跳西跳
    • 多分支,会插入形式不同的等价代码,并加入条件跳转产生大量等价的分支
    • Call 链。产生嵌套的调用,并在嵌套中的某个调用里通过直接分析栈来转入真正有效的子函数中
  • 代码虚拟化
    • “堆机”,开辟新的栈空间,容易实现但也容易绕过,实现上有明显的栈切换
    • “栈机”,使用原有的栈空间,实现麻烦但是效果好,与原有数据一起干扰分析,因为对栈的使用有限制,因此难执行复杂指令,也难用高级语言编译器编写虚拟机
    • “状态机”,另一种高级形式的虚拟机,此处暂未介绍。
    • 表达方法:一一对应的 opcode,现在大多数都采用这种方法。另一种是将代码直接转为新的代码,类似代码变形,直接将代码的意义体现在虚拟机中,难度较大,目前没有虚拟机产品使用
  • 脚本引擎。变相获得了虚拟机的能力,通过对脚本引擎的修改也能获得不错的效果
  • 网络加密。从服务器下载代码或者在服务器上执行核心代码
  • 硬件加密。加密狗等,和网络加密的形式类似
  • 代码签名。校验签名后再运行代码。windows 用得少,移动端用得多。

第 2 章 软件保护系统(壳)

  • 提供了两个方面的服务:注册、试用、黑名单、打包等功能;代码的加密
  • 保护功能:试用控制、授权控制、扩展特定功能等
  • WinLicense & Themida:傻瓜化的设计,利用内嵌汇编标签实现保护代码的定位,这样只能在函数内部做保护,无法保护整个函数框架
  • VMProtect

第 3 章 软件保护强化

  • 设计优化:避免直接记录状态数据,而是用一些函数进行适当的计算;使用壳的 sdk(应对比加密前后代码,防止有明显漏洞,如前面说到的无法保护整个函数;代码运行时适当地验证保护系统,防止被脱壳等使壳失效;
  • 加壳优化:壳的功能上侧重选代码加密;选择授权相关的重要代码进行保护,避开循环、通用代码;充分测试

第 4 章 软件破解工具第 5 章 软件破解技术

  • 破解技术的难题在于计算机处理能力的巨大提升和我们自身承受数据量能力之间的差距,破解技术已经滞后于保护技术
  • 静态分析
    • 基本信息分析: pe 头,区分其开发语言,使用的壳等
    • 代码静态分析。.NET 可用 ildasm IL 或 .NET Reflector。
  • 调试。类型一般有本地/远程调试。调试原理有 windows 调试原理(又称一般调试原理)和虚拟机调试技术
    • 一般调试原理。
      • 利用 cpu 内置的调试功能,这里特指在 windows 系统中的体现。
      • 后面介绍了 windows 的调试 API。
      • 调试事件的处理本质上是异常的处理。
      • windows 的异常处理机制中程序可以处理自身异常,此时可以修改如 dr 系列 ring3 下不能修改的寄存器值。
      • 内存断点通过修改内存页属性来触发。
    • 伪调试技术。因为一般调试原理是系统的组成部分,被调试时程序的状态总是有变化的,也能够被侦测到,更何况现在游戏保护措施都加入了内核技术来检测。伪调试可以避开这些检测手段。
      • 其基本原理是异常分发发现不存在调试器时会进入 KiUserExceptionDispatcher 将异常发送到程序进程,注入代码将其捕获后再转至外部的调试器处理,模拟了系统转交调试事件的过程。
      • 下断点则可以改用 hook 方式进行
      • 需要自己实现一套调试 API,管理异常和断点,处理线程调度等问题
    • 本地调试
    • 远程调试。windbg 的 cdb
    • 虚拟机调试。VMware
  • 反反调试(只介绍了一个 hook NtSetInfomationThread 函数的例子)
    • 参考《Windows下反(反)调试技术汇总 – FreeBuf专栏·天融信阿尔法实验室》
  • HOOK
    • 用代码移位应对代码 hook 中遇到的还原代码、重新 hook 问题。即将原有代码片段转移,并在后面附加跳转到原函数体的指令,而原位置放入hook跳转,最后在hook出口跳到原有代码片段执行。类似的,手动调试中下 int3 断点也可以这么处理。问题在于这种方式需要代码的反汇编引擎确定指令长度。代码hook中原有逻辑总是会运行的,想要修改原有逻辑还需要其他方法
    • 函数hook。基于函数的代码特征和函数的完整性,我们(仅)可以在函数入口处hook
    • 模块hook。插入自己仿制的模块来糊弄系统和程序在进程模块判定过程,如 GetModuleHandle 获取模块基址。通过在 peb 的 LoaderData 的 hashTableList 里插入伪装的模块信息项,从而使系统遍历时先取到伪装模块的信息。
    • 导出表hook。类似于函数hook的效果,使得模块的所有函数都是受控的。有一点要注意的是,需要将自己的伪装函数/模块放在地址高于原模块的位置,以免导出表的RVA计算为负数。
  • 代码注入
    • 以暂停方式创建的进程,系统会把主模块入口放在线程的 eax 中,修改它即可修改入口地址
    • 代码要能自己重定位(p.98 重定位代码),自行解析模块函数地址(一种是直接通过ntdll的LdrLoadDll,因为ntdll在同一会话的所有进程里的基址是一样的。另一种是teb->peb->模块列表->模块的导出表,一般都是先确保 GetModuleHandle 和 GetProcAddress )
    • 复制整个 pe:可以利用重定位区段的信息
  • 补丁。冷补丁、热补丁、SMC(针对压缩数据或保护系统,要分析压缩/加密的算法)、虚拟化补丁(通过硬件或软件虚拟技术将代码运行时执行和读写的代码页分离,再修改执行页中的数据。一般用在修改代码相当困难的情况下,一般较少用)
  • 模块重定位。和模块 hook 类似,但实现原理不同。windows 中除 ntdll.dll 外都能重定位,不过对 kernel32.dll 也做了特殊处理,文中的方法还需改进。
    • 篡改 ZwOpenSection,对特定模块的第一次调用返回 0xc0000034(找不到模块)
    • 篡改 ZwMapViewOfSection,为特定模块指定加载基址。
  • 沙箱技术。说到底还是函数 hook。安全技术中的沙箱是驱动层在内核通过对内核函数的 hook 来过滤程序访问;破解技术中则可以在进程内加入隔离层。一般用来对付具有试用或者授权功能的软件,拦截它的注册表/文件访问。可以通过修改 KiFastSystemCall 或 fs:[0C0] 来实现,要点在于在调用内核函数前一刻拦截下来。
  • 虚拟化。不同于前面提到的代码虚拟机,此处的虚拟化是指虚拟一个 cpu 去执行目标代码,这样可以彻底的控制流程,并且不被目标代码感知,同时可以控制内存访问,执行环境,可以记录快照。
  • 代码虚拟机。介绍代码虚拟机的分析相关技术。
    • OP 分支探测。因为 OPCODE 的结构大多加密或者非常复杂,关注它的加密解密是不明智的,且每次加密的代码都不一样。应当研究它的 OP 分支
      • 有些 op table 特征比较明显,有类似虚表的函数地址项,甚至能直接定位出 op 分支数量(很可能是 xor 加密过的地址)
      • 代码追踪侦测技术。通过分析记录下的指令执行顺序和跳转分支,可以推测出执行次数较高的 op 分支语句
    • OP 调试。在实现了 op 分支探测和块执行技术后便可对 op 分支的意图调试分析
  • 自动化技术。面对复杂、膨胀的代码,我们需要降低理解代码的成本,因此需要想办法忽略垃圾代码。
    • 代码追踪。一种是类似OD的,利用调试器单步功能实现,一种则使用前面提到的虚拟化技术,可以记录每条指令执行后对环境的影响,移除干扰的jmp等
    • 预执行。虚拟执行代码追踪中,可以(根据当前条件)模拟代码执行,预测结果,在当前状态难重复时可以节约大量时间。
    • 代码简化。如何判断出垃圾指令。通过分析简化类似在栈上申请又释放空间的垃圾指令,在每个栈平衡区间中进行分析查找(垃圾指令一定会保证栈的平衡)。(当然也要基于虚拟化执行)
      • 直接读新申请空间的脏数据->垃圾
      • 只写不读就释放了->垃圾
      • 释放前写入又读取->有效
      • (利用栈空间的内存着色技术,下面)
    • 代码重建。因为重建时只有预测过的分支结果,所以还要考虑其他分支。需要保证代码完整性。追踪时不能删除条件跳转,而是要预测每个分支,再对结果合并。
    • 块执行。将一部分指令块视为一条指令,便于调试和理解。借助分支探测和管理
    • 多分支剔除。追踪到条件跳转时开辟两个全新的模拟cpu,分别执行完后再合并结果,同时移除这个实际为“多分支”的条件跳转,继续原先的追踪。
  • 动态分析
    • 代码着色。通过着色给出复杂指令中数据转移的记录,简化代码。通过虚拟化技术实现运行过程中的追踪
    • 黑盒测试。测试代码组合了解其功能,避免进行具体分析。例如 vmp 的 not_not_and,后面会介绍。
  • 功能模拟。脱壳容易把壳自带的功能也脱掉,需要进行修补。
    • 授权模拟。自行实现注册函数,简单的如直接返回已注册等
    • 网络模拟。分析网络包,模拟一个对端出来。比如某些游戏私服。工作量大成本高。
    • 加密狗模拟。模拟保护系统的高级接口,而不是直接模拟加密狗硬件。类似授权模拟
  • 脱壳
    • 导入表重建。现成工具如Scylla。第一步是找出壳处理后的地址转为真实函数入口地址的办法。有些壳只是隐藏入口,可以通过代码追踪还原。有些壳会对函数入口处的代码变形。
      • 函数通用追踪。自动化跟踪壳处理的外部模块函数的程序。对类似vmp单纯隐藏入口的方式可以直接追踪得到真实入口。对WinLicense变形入口代码的,可以利用模块hook设计指令让WinLicense只虚拟化我们的代码(因为壳是启动时动态处理的),然后在我们的代码中再将真实入口暴露给追踪程序。
      • 调用代码修复。追踪到函数地址后需要修复调用处代码,还要识别堆栈平衡。1) 一般 call 或 jmp 指令的长度为6字节,壳只能用它完成跳转,选择不多,所以要先找出所有跳转形式的指令。2) 从前面结果里剔除非外部函数的调用:这个函数里面调用了同一代码段的多个其他函数,则它不是外部函数,调用它的也就不是外部函数调用;利用代码追踪,如果刚进代码就有异常、运行很久还没跳出调用函数所在区段、没有平衡栈就跳走了,说明这个调用很可能也不是。这两步之后再用虚拟化对每一项进行模拟追踪,得到真实地址。
      • 模拟函数识别。即自己实现一份外部函数,好在由于兼容性跨平台等,这类函数不多。可以通过1)对比参数数量、调用方式2)黑盒测试对比参数、结果、异常(和1撞车)3)特征判断,就是具体分析代码自己判断了。
    • 资源重建。运行时资源往往会解压到同一个地方,大多数情况下不需要重建,但有些壳会重定位资源节并扰乱资源的目录结构,或者加密资源。
      • 获取资源数据。直接dump,或从被hook的系统资源函数的参数和结果中获取大部分资源,然后通过这些信息模拟调用这些被hook的函数,从而获取剩下的资源。(详见第8章)
      • 重建资源节。通过自动化重组资源结构。(详见第3部分)
    • 区段重建。
    • OEP定位。
      • ESP定律。在壳的入口esp下硬件访问断点,因为栈平衡oep处也会访问到。
      • 特征定位。cry 代码代码特点(ret, call xxx, jmp xxx)
      • 函数定位。利用入口处的常见系统api
      • OEP要被抽了就要其他办法,如进程快照、代码回溯等。
    • PE头修复。修复OEP、SizeOfImage、Characteristics、ImageBase
    • 重定位修复。在rva=1000处放断点等待解码,解码后在待重定位处下断等待重定位,随后监视此处即可得到所有需要重定位的地址。或通过对比两次不同基址载入后的差异,计算筛选出重定位的地址。
    • PE重建。压缩体积和简化区段
    • 补区段。壳会申请一段随机空间,然后放入代码,再破坏代码原本的地址空间。一般分两步,一是控制壳的内存分配,让它分配到便于附加区段的地址(自动化技术)。再将dump内存数据转储到pe中。
  • 进程快照技术。在OEP[附近]dump下进程数据,还有栈等信息和建立系统api的中间层,以便在新进程中重新运行。监视系统函数的调用和栈平衡点找到oep附近。控制 NtAllocVirtualMemory 来使内存集中分配在模块地址之后(也要接管默认堆,仅使用ntdll函数,因为其他模块未加载),可以自己首先分配一大块内存,后面的内存分配就可以自己控制了。保存栈和寄存器有效数据,并在新进程中恢复,再重新计算esp和ebp。将使用导出表hook在系统调用中插入自己的中间层,在新进程中就可以跳转到正确的地址上。如有多线程校验可能还要teb数据。对快照文件,可以用pe工具设置各个内存段为不可写不可读来测试是否是必要内存段,从而删除垃圾数据。
  • 代码回溯技术。便于我们回顾原先代码的执行情况。可用函数定位来实现,即通过代码执行过程中调用的函数来识别它。首先用函数追踪大致定位区间,然后暂停进程并转入虚拟机中执行并记录执行过程。

第 6 章 软件分析技巧

  • 精确代码范围。根据函数定位、根据代码特征定位,比如某些地方的代码被加密混淆了等
  • 多用对比参考。用同版本的壳加密别的程序,与未加密的程序进行对比分析,相似性高的情况下可以大胆猜测。也可根据特征判断是否使用了某些成熟的库。
  • 逆向思考。分析中要从更高的层次考虑:开发语言、开发目的、某些功能可能的实现方式等,来理解程序的流程和意图。
  • 多利用自动化优势。利用自动化分析提高效率
  • 利用环境优势。有些壳在不同系统下的强度会不一样,如 win7 和 xp,32 位和 64 位等。虚拟机的快照优势等。
  • 避免算法分析。这些算法往往只在特定场合下有用。
  • 够用原则。不要追求完美。“软件都是由人设计的,再复杂的软件也是可以被分析的。我们不可能从完美的分析或者还原中学到高深的知识,有时间还不如多看书来得实在”。

第 7 章 打造函数监视器

  1. 监视进程所有模块导出函数调用情况
  2. 过滤能力
    1. 根据模块名过滤
    2. 根据函数名过滤
    3. 根据调用代码信息过滤
  3. 特殊监视功能
    1. 根据模块名设定监视函数
    2. 根据函数名设定监视函数
  4. 监视时机设定功能
  5. 辅助调试
    1. 设定触发调试断点
    2. 设定暂停进程运行
  6. 函数调用信息统计

第 8 章 打造资源重建工具

  1. 获取资源数据
    1. 根据资源目录获取
    2. 监控资源函数获取
    3. 强制搜索内存穷举获取
  2. 重建资源区段

第 9 章 打造重定位修复工具

  1. 抓取内存快照获取映像数据
  2. 通过映像内存快照对比收集重定位地址
  3. 重建标准重定位区段

第 10 章 打造进程拍照机

  1. 先期注入模块。通过 WriteProcessMemeory 直接覆盖 LdrInitializeThunk 入口进行 hook(如果事先附加调试器会导致 LdrInitializeThunk 代码的重写,hook 代码会被覆盖掉),之后获取并调用 ntdll 的导出函数
  2. 接管进程内存管理。通过 NtAllocateVirtualMemory 分配大内存,再将其 hook 后管理其他的内存分配,防止进程堆位置乱跑
  3. 建立函数调用中间层。和函数监视器的中间层一样,只要再加入重新定位函数地址的功能即可
  4. 建立场景载入功能。实际上就是获取 api 地址,设置一些标志等
  5. 转储并修正映像及相关数据。保存线程上下文、当前 esp 到 oep 时 esp 之间的栈数据、关闭堆 hook、dump PE(复制内存,PE 头里修正 SizeOfImage)
  • 增加 TIB 转储。无法在 PE 恢复时设定线程的 TIB 空间,不过同时也表明这个空间地址位置的重要性不高,保护系统也不会依赖它的地址。hook 线程创建函数,然后将 fs:[018] 指向我们的地址

第 11 章 打造函数通用追踪器

  1. 插件式程序框架。od 插件(OllyDbg PDK)
  2. 分层式虚拟机。内存读写:从缓存读取->从目标进程读取->仅写回缓存
  3. 调用代码查找识别功能。通过在od内存窗口植入搜索菜单触发范围查找,在od中创建查找结果窗口,最后搜索满足条件的调用指令(call xxx / jmp xxx / call [xxx] / jmp [xxx] / push xxx,ret / mem addr ref),call 和 jmp 的目标地址是相对偏移,需要转换。搜索完后再剔除掉明显错误的结果。
  4. 函数追踪功能。使用虚拟CPU执行目标代码并记录栈变化
  5. 导入表重建功能。通过追踪得到正确函数结果集的集合后再创建一个新的区段作为导入表
  6. 调用代码修复功能。直接调用系数函数而不是使用加密的入口。实际应用中可能需要多次重建导入表和修复调用代码
    1. 内存式修复:用于调试和测试,根据调用指令类型分别进行处理
    2. 文件式修复:不能硬编码地址,要配合重建的导入表信息填入地址

第 12 章 打造预执行调试器

  1. 预执行调试。用函数通用追踪程序的分层虚拟机
  2. 代码追踪记录。考虑到准确度、技术限制和效率,由普通的代码虚拟机完成
  3. 代码回溯。通过在合适时机执行代码追踪,再从调试器里加载分析和利用
  4. 代码对比分析。代码追踪记录的结果与正在执行的代码或与多次结果之间的对比
  5. 块执行。基于代码追踪结果,在结果上实现代码块的识别并和调试器交互同步
  6. OP记录调试。在块执行的基础上针对代码虚拟机这样的代码块复用率高的代码调试。将很多代码块视为一个OP操作

第 13 章 打造伪调试器

  1. 设计自己的调试函数来模拟系统的调试函数,并通过管道通讯。比如 int 3 的替代指令和替代异常,单步的替代指令等,读写内存则是通过管道让进程自己读写后返回给调试端

第 14 章 VMProtect 虚拟机分析

  1. 用壳加密自己编写的程序,借以分析壳/虚拟机的行为特征
  2. 栈机
  3. 使用 push XXXX, jmp YYYY 进入虚拟机
  4. 利用前面的 OP 分支预测,代码预执行等工具分析,推测 op 操作,跳转指令等
  5. 代码膨胀问题,还是需要自动化还原来解决

第 15 章 WinLicense 虚拟机分析

  1. 同栈机
  2. jmp 进入虚拟机,也会保存 key
  3. 平栈虚拟机:未在栈上申请额外的空间,导致复杂的 op 被跳过,可以直接分析栈数据来推测 op 意图
  4. 不要花精力在被层层加密的 opcode 运算和解密上,而是理解相应 op 的意图

第 16 章 VMProtect 脱壳

  1. 函数监视器到 kernel32!GetSystemTimeAsFileTime 断住
  2. 进程拍照机拍照(可以正常运行,但资源异常)
  3. 在第一步的断点处获取并修复资源
  4. 通用追踪工具载入快照,程序停在 tls 处,tls 数据可以安全的清除,使用追踪程序追踪代码区域的调用 call
  5. 识别系统调用,重建 IAT
  6. 修复 ThunkRVA
  7. 有些自动修复也会有问题,要手动修补(如 call reg,后面还会再次 call reg,自动修复不一定会识别出)
  8. 压缩体积,设置整个区段为不可访问来排除

第 17 章 WinLicense 脱壳

  1. 函数断点、快照
  2. 资源不用修复
  3. 函数查找,在区段范围内查找函数调用代码,数量比 vmp 多很多,所以不少是错误的
  4. winlincense 没有抛弃 IAT,这样可供利用。尝试追踪导入表确定是否是函数入口(大部分都成功了)
  5. WinLicense 会检查导入表判断是否被脱壳,可以暂时先用旧的导入表最后再修复,或者程序载入后手动修改指向旧导入表
  6. 内存地址的引用类型不用修复(因为 IAT 没有变动),主要关注 jmp 和 call,同时 call [XXX] 和 jmp [XXX] 都被成功还原了。虽然 jmp 类型取不到返回地址,但好在壳也不能随意使用,一般就是转换为其他类型
  7. 壳会将一些代码放到虚拟机里执行,比如压入虚拟机入口地址然后 jmp 到系统函数,直接返回到虚拟机中继续执行。修复办法是还原 call,再 jmp 到虚拟机入口
  8. 修复被抽取的代码。根据函数追踪失败的结果来修复,因为代码被抽取所以才无法追踪。函数不重要可以不修复
  9. 压缩体积减肥