反汇编器、调试器这些工具,原本都是用来提高查找 bug的效率的,然而因为它们能够在机器语言层面对软件进行分析,因此也被用来破解软件。二进制分析技术能够帮助发现设计时所没有想到的问题,另一方面也能够用来进行软件破解。 “破解”(cracking)这个词的涵义十分宽泛,在游戏中作弊也可以算作破解,例如在网络游戏中修改二进制数据(修改内存)使自己无敌,或者复制稀有道具等。作为游戏的运营方,也会采取对策防止这些行为的发生,例如对通信进行加密,以及尽量将数据存储在服务器上等。 在此基础上,本章我们将学习如何保护软件不被破解。 2.1 解读内存转储 2.1.1 射击游戏的规则 首先我们来看一个示例游戏。请大家运行 chap02shooting中的 shooting.exe。 射击游戏 ▲ 这个游戏的规则如下。 ●空格键:射击 ●←键:向左移动 ●→键:向右移动 ●↑键:填充能量(以当前得分为上限) ●↓键:时间停止(消费能量) 用左右键移动,用空格键射击,这些操作和一般的射击游戏一模 一样。通过按上下键可以使用能够让时间停止的特殊能力。其中↑键用来填充能量,↓键则用来消费能量并让时间停止。能量的上限是当前得分,因此随着游戏的进行,能够填充的能量也 会增加。击中敌人可以增加得分,被敌人击中则减少得分。得分越高,敌人越强,子弹的追踪性能也会提高。 首先请大家先玩玩看,一般来说能玩到 500~ 1000分。不过,当超过 1000分之后,游戏难度就会大大增加,要想达到 2000分可以说是相当难的。 2.1.2 修改 4个字节就能得高分 得 2000分虽然难,但其实我们只要修改内存中 4个字节的数据就可以轻松实现了。为了修改内存数据,我们在这里要用到有万能进程内存编辑器之称的工具“兔耳旋风”A。 ●兔耳旋风 http://hp.vector.co.jp/authors/VA028184/#TOOL http://www.vector.co.jp/soft/win95/prog/se375830.html 在运行射击游戏的同时,打开兔耳旋风,然后从进程列表中选择 shooting.exe。 A原名为“うさみみハリケーン”,这又是一个只出现在日本的软件,它也只有日文界面,而且菜单在中文系统下会乱码。——译者注 ▲ 万能进程内存编辑器——兔耳旋风 我们先来确认一下栈地址,也就是看一下主线程的 ESP寄存器值。 点击菜单中的“デバッグ”(调试)→“スレッド別レジスタ表示”(按线程显示寄存器值)。 ▲ 按线程显示寄存器值 选择类型为“通常”、优先度为“8”的线程,然后查看 ESP的值。 ESP的值在每个环境上都不一样,因此请大家按自己环境的值来进行后续操作。笔者的环境中 ESP的值为 0012FC74,这就是栈地址的起点。 由于当前得分为 29,因此我们先搜索一下栈数据中“ 29”这个值存放在哪里。 使用下面两种方式中的任意一种,都可以打开范围检索窗口。 ● 点击菜单中的“検索”(检索)→“メモリ範囲を指定して、検索”(指定内存范围检索) ●按 Alt+F键 范围检索窗口 ▲ 在对话框中进行如下设置。 ●“検査•比較単位”(检索、比较单位):4字节 ●“数値”(数值):29 ●“開始アドレス”(起始地址):0012FC74 ●“範囲”(范围):先填 1000就好 然后按下“通常検索実行”(执行通常检索)按钮开始搜索。这时,右边的列表中显示出 0012FD88这一地址,看来 29这个数值的确被保存在内存中的某个地方。顺便说一句,内存中可能会搜索出不止一个 29。如果出现这种情况,可以改变一下得分的值,然后再试一次。 双击这个地址,主窗口就会跳转到这一地址的位置,这时可以关闭范围检索窗口,然后将光标移动到 0012FD88地址上的“ 1D”这一数值上。 我们可以把 1D改成其他什么数字试试看,比如 2D(45)。 将0x1D(29)加上 0x10改成 0x2D ▲ 然后我们回到游戏画面……哇,得分果然变成了 45分!这里写什么数字都是可以的,比如也可以输入一个很大的数。 ▲ 可以任意修改得分 这种一般情况下绝对达不到的得分也可以很容易修改出来。 在这里我们使用了“兔耳旋风”这一工具来进行讲解,实际上一般的调试器也完全可以实现这样的功能。当然,我们不仅可以修改得分,也可以修改能量,有兴趣的话可以试试看。 2.1.3 获取内存转储 刚才我们修改了一个示例游戏的内存数据,除此之外我们还可以将内存数据保存成文件,这被称为“内存转储”(memory dump)。随着程序(游戏)的运行,内存中的数据会不断实时变化,如果要保存某个时间点的状态(快照),我们就需要内存转储。生成内存转储非常简单。 在 Windows Vista及以上版本中生成内存转储 如果你使用 Windows Vista或更高版本,可以按以下步骤来操作。 ▲ 1. 按 Ctrl+Alt+Del打开任务管理器 2. 右键点击目标进程名称 3. 选择“创建转储文件”这样,系统就会生成一个扩展名为 .DMP的文件。 生成转储文件 1 ▲ 生成转储文件 2 ▲ 尽管操作系统会按照可执行文件中的内容将程序加载到内存中,但内存中的数据与可执行文件中的数据并不完全相同,大家可以用二进制编辑器来确认一下。 在 Windows XP及以下版本的系统中生成内存转储 Windows XP及更低版本的系统中自带了一个叫作 Dr. Watson的内存转储工具,不过遗憾的是,在 Windows Vista及以上版本中,这一工具已经被去掉了。准确地说,这是一个当进程异常终止时将内存数据和简单日志保存到文件中的工具,在没有安装调试器或开发工具的环境中可以帮助快速查找程序崩溃的原因,因此曾经受到很多开发者的青睐。 如果你手上有 Windows XP,可按照以下步骤来生成内存转储。如果没有 Windows XP,你也可以通过下面的描述大致理解内存转储的作用。 要启动 Dr. Watson,首先点击开始菜单→附件→系统工具→系统信息,这时桌面上显示出系统信息窗口。 从系统信息的菜单中选择工具→ Dr. Watson,或者也可以在开始→运行中输入 drwtsn32。 ▲ 启动 Dr. Watson 1 ▲ 启动 Dr. Watson 2 在 Dr. Watson的窗口中可以对日志和转储文件的保存路径、指令数量、要保存的错误数量等进行设置。 Dr. Watson会在进程异常终止时完成自己的工作。在我们的示例程序 guitest.exe中,从菜单打开帮助→关于对话框,在关闭该对话框时程序就会异常终止。至于其他程序,只要遇到程序崩溃的情况也都可以。我们可以准备一段简单的程序来引发崩溃(源代码包含在 chap02guitest guitest.cpp中)。 #include <stdio.h> int main() { char *p = NULL; *p = 'A'; return 0; } 接下来,我们在命令行窗口中运行 drwtsn32,并加上 -i选项。这样做的理由我们在后面会讲到,简单来说,就是将 Dr. Watson注册为实时调试器(just-in-time debugger)。 运行示例 C:>drwtsn32 -i 这样准备工作就完成了。下面让我们来运行一下会引发崩溃的程序。 ▲ 进程异常终止 ▲ Dr. Watson生成的文件 ▲ 我们可以看到,在我们刚刚设置的输出路径中, Dr. Watson生成了以下文件。 ● user.dmp ● drwtsn32.log user.dmp就是该进程的内存转储,此外还有一个 drwtsn32.log文件,在这个日志文件中,对错误的原因进行了简单的描述。 2.1.4 从进程异常终止瞬间的状态查找崩溃的原因 我们来看一下 drwtsn32.log的内容。 drwtsn32.log ▲ 首先,日志中记载了崩溃的应用程序名称、崩溃发生的时间、用户名、操作系统版本以及其他正在运行的进程列表等全局信息。从错误代码来看,程序崩溃的原因是对内存进行了非法访问。 系统信息中记载了操作系统、 CPU、用户名等简单信息,任务列表中记载了运行中的所有进程,这些都是与崩溃相关的环境信息。 接下来的模块清单中,记载了崩溃时进程所加载的模块,从中可以确认每个模块各自所映射的内存地址。 下面是最重要的部分——“线程 ID 0xde0 的状态转储”,这里面记载了导致崩溃的指令以及崩溃时的寄存器状态。 我们可以看到,在地址 004012bf的 mov指令旁边写着一个“错误”字样, mov [ecx],dx这条指令的功能是将 dx的值写入 ecx所代表的内存地址中。 再看一下寄存器,我们发现 ecx的值为 00000000,将数据写入 00000000这个地址当然会出错,这就是导致崩溃的原因。 像这样,通过获取进程异常终止时的状态,对于找到问题的原因是非常有帮助的。 2.1.5 有效运用实时调试 很遗憾,Windows Vista及更高版本中已经不再提供 Dr. Watson,但 Windows本身具备实时调试功能。具体来说,就是在进程遇到无法继续运行的错误而被强制关闭时,调试器会自动打开,并自动挂载到出错的进程上。 ●实时调试 https://msdn.microsoft.com/zh-cn/library/5hs4b7a6.aspx 在前面讲到 Dr. Watson的时候,我们在命令行窗口运行了 drwtsn32 -i,这就表示将 Dr. Watson注册为系统的默认实时调试器。 我们可以在注册表中启用或禁用实时调试。请用 regedit打开下面的注册表项。 实时调试的设置变更点 上面两个地方是实时调试的设置,下面两个地方是 64位系统中针对 32位程序的设置。在这里填入调试器的路径,就可以使用任意调试器来进行实时调试了。 当然,我们在第 1章中介绍过的 OllyDbg也可以作为实时调试器来使用。 在 OllyDbg的菜单中点击 Options → Just-in-time debugging,会弹出一个设置对话框。点击“ Make OllyDbg just-in-time debugger”按钮, OllyDbg就会将自己的信息配置到上述注册表项目中。 ▲ OllyDbg的实时调试设置 1 ▲ OllyDbg的实时调试设置 2 我们可以试试看在将 OllyDbg设置为实时调试器之后,再一次运行前面那个会崩溃的程序。这次程序崩溃后 OllyDbg会自动打开,然后挂载到崩溃的进程上。 ▲ OllyDbg挂载到崩溃的应用程序上 实时调试对于处理一些难以重现的 bug非常有效,当你已经能够熟 练使用调试器之后,不妨积极尝试一下。 2.1.6 通过转储文件寻找出错原因 当程序崩溃时,最好能够第一时间启动调试器,但有些情况下无法做到这一点。不过,即便在这样的情况下,只要我们留下了转储文件,也能够通过它来找到出错的原因。 转储文件可以使用 WinDbg来分析。 下面我们来分析一下 chap02guitest2中的 guitest2.exe的转储文件(user.dmp),顺便学习一下 WinDbg的使用方法。 首先打开 WinDbg,然后按 Ctrl+D或者点击菜单中的 File → Open Crash Dump,打开转储文件。 用 WinDbg打开转储文件 1 ▲ ▲ 用 WinDbg打开转储文件 2 WinDbg虽然看起来有图形界面,但实际上却更像是一个命令行工具,因为它基本上是通过命令交互来进行调试的。因此和 OllyDbg相比,对于初学者来说更难上手。然而,有一些情况只能使用 WinDbg来进行调试,例如 64位程序以及运行在内核领域的程序等,因此大家最好还是学一学。 第一次启动之后只有一个 Command窗口,从 View菜单中可以显示更多的窗口。 我们先来显示另外两个窗口,按以下步骤操作。 ●Alt+6:显示 CallStack(调用栈)窗口 ●Alt+7:显示 Disassembly(反汇编)窗口 Call Stack窗口 ▲ Disassembly窗口 ▲ ▲ ▲ 我们先来看一下 Disassembly窗口。 这里本来应该显示出反汇编之后的代码,但由于 EIP的值为 00000000,因此现在只显示一堆问号,这就表示“出于某些原因,程序跳转到了 00000000这个地址”。 下面我们来追溯一下函数调用的过程。从 Call Stack窗口中我们可以看到这样一行。 运行结果 0012f8f0 77cf8734 000b0144 00000111 00000001 guitest2+0x12d0 双击这一行,再看一下 Disassembly窗口,这时会显示出 guitest2+ 0x12d0地址的内容。 运行结果 004012b7 6844214000 push offset guitest2+0x2144 (00402144) 004012bc ff1500204000 call dword ptr [guitest2+0x2000 (00402000)] 004012c2 6860214000 push offset guitest2+0x2160 (00402160) 004012c7 50 push eax 004012c8 ff1504204000 call dword ptr [guitest2+0x2004 (00402004)] 当前显示的地址是 004012d0,我们看一下前一条指令 call eax,按 Alt+4可以查看寄存器的值。 eax寄存器的值果然是 00000000。也就是说, 004012ce的这条 call eax指令调用了 00000000这个地址,看来这就是引发崩溃的原因。 地址 004012c8处也执行了一条 call指令,由于返回值会存放在 eax ▲ 中,因此我们可以推测,eax的 00000000是从这里来的。 那么,这里调用的又是什么函数呢?按 Alt+5打开 Memory(内存)窗口,在显示 Virtual的地方输入“00402004”. 运行示例 Virtual: 00402004 地址 00402004的值为 04 24 00 00(=00002404)。 ▲ 这里显示的值是相对于基地址的偏移量,因此我们再输入 00400000+2404=00402404,这时会显示出调用的函数名称,即 GetProcAddress。 运行示例 ▲ 按相同的思路,我们还可以看一下地址 004012bc所 call的函数是什么。 运行示例 Virtual: 00402000 00402000 f4 23 00 00 04 24 00 00 00 28 00 00 ea 27 00 00 da 27 ▲ 运行示例 接下来我们在 Memory窗口中确认一下调用这些函数所传递的参数,现在我们可以将刚才的反汇编代码改写成更易懂的形式。 现在看起来更容易懂了吧,而且我们也已经发现了 bug的原因。 LoadLibraryW函数的参数为 kernel31.dll,但实际上系统中没有 kernel31.dll这个 DLL文件,因此 LoadLibraryW函数会调用失败。 到这里程序还没有崩溃,但后面的 GetProcAddress函数也会调用失败。 随后,失败的 GetProcAddress函数返回了 00000000,于是 call eax时进程就异常终止了。 可能有人会吐槽这个程序居然没有对 LoadLibraryW的返回值做容错处理,而且居然有人会犯 kernel31.dll这种低级错误。这个程序只是演示用的,所以请大家别太较真。 像上面这样,通过分析转储文件,我们可以找到一些导致意外错误的原因并进行修正。 专栏:除了个人电脑,在其他计算机设备上运行的程序也可以进行分析吗 像 Windows、Linux、Mac.OS.X等一般的主流操作系统中都具备内存转储和调试等帮助进行软件分析的功能。这些功能本来是为了提高软件开发的效率,当然,利用这些功能也能够实现其他一些或好或坏的目的。 那么,除了个人电脑以外,其他计算机设备上的情况又如何呢?比如说智能手机和游戏机上能不能进行软件分析呢?当然,在这些设备上开发软件时也会使用到调试器,厂商也会 提供专用的开发设备,换句话说,这些设备和个人电脑没有本质的区别。只不过,由于这些设备的技术并不像个人电脑一样公开,因此 “一般人对游戏机和家电产品进行分析”这种事好像挺少见的。实际上,这些设备在上市时往往会关闭调试功能,或者不公开具体的调试方法,因此用起来并不像个人电脑一样随心所欲。然而,这些设备中也装有处理器(CPU),这些处理器自然也能够执行汇编指令,因此如果有办法对其进行反汇编并获取内存转储,那么就可以进行分析了。 有一个叫作 devkitPro的网站上有很多喜欢分析各种游戏机的爱好者,他们发布了一些非官方的开发环境。这个网站是英语的,有兴趣的话可以上去看一看。 ● devkitPro http://devkitpro.org/ 专栏:分析 Java编写的应用程序 Java的开发理念是“Write.once,.run.anywhere”,即不依赖操作系统和硬件,编写一次代码就可以在各种平台上运行。为了实现这一理念,Java采用了下面的技术。 ●在编译时,源代码会被编译成字节码(一种抽象的中间语言) ●为各种环境分别安装能够解释和执行字节码的虚拟机 只要有 Java虚拟机,程序就可以在任何环境下运行——这就是Java的最大特点。 因此,对 Java编写的程序进行分析,实际上就相当于对 Java的字节码进行分析。 有一些工具能够将字节码还原成源代码,这些工具称为反编译器(decompiler)。 ●Java反编译器 http://java.decompiler.free.fr/ 相比 x86汇编语言来说,Java字节码更容易还原成源代码,相应的反汇编器也出现已久。Eclipse也有反汇编插件,除了软件分析以外,还有很多场合都会用到它。 class文件的反汇编结果 ▲