大部分程序员都使用像C/C++或Java这样的高级编程语言来编写代码,因为这些语言更适合人类阅读。然而,计算机被设计成运行机器码,而机器码是用二进制表示的指令。
编译是将编程语言转化为机器码的过程。这意味着反编译就是将机器码重新转换回原始编程语言、恢复原始源代码的过程。在可以获取源代码的情况下,反编译是最简单的逆向工程方法,因为源代码是供人类阅读的。本书将主要关注无法进行反编译的常见情况。但对大家而言,重要的是要记住,如果可以反编译回源代码,那么可以考虑使用这个方法。
对于许多编程语言来说,完全反编译是不可能的。这些语言将代码直接构建成机器码,在这个过程中会丢失一些信息,比如变量名。虽然一些高级反编译器能够为这些语言构建伪代码,但是这个过程并不完美。
然而,一些编程语言会使用所谓的即时(Just-In-Time,JIT)编译技术。当使用JIT语言编写的程序被“构建”时,它们会被从源代码转化为一种中间语言(Intermediate Language,IL)而非机器码。JIT编译器在程序运行之前会将一份代码以这种中间语言形式存储起来,等到程序开始运行的时候,再将代码转换为机器码。使用JIT编译技术的语言包括Java、Dalvik(Android)以及.NET。
例如,Java因在很大程度上不受平台限制而闻名。背后的原因是它使用了中间语言Java字节码(Java bytecode)和Java虚拟机(Java Virtual Machine, JVM)。通过将程序代码分发为字节码并在运行时对其进行编译,JVM将Java中间语言转换为特定于运行它的机器的机器码。虽然这种方式可能会对文件大小和性能产生负面影响,但在可移植性上有所收获。
JIT编译还大大简化了这些应用程序的逆向工程。这些中间语言与原始源代码非常相似,可以被反编译或转换回源代码。源代码的设计初衷就是为了方便人们阅读,这使得理解应用程序的逻辑、识别软件保护措施或其他嵌入的秘密变得容易得多。
对于像.NET这样的即时编译语言(简称“JIT语言”),有几种免费的反编译器可供使用。一个被广泛使用的.NET反编译器是JetBrains dotPeek,它可以从https://www.jetbrains. com/decompiler/获得。图1.1展示了在JetBrains dotPeek中反编译.NET代码的例子。
如图1.1所示,由于中间语言在元数据中编码了大量信息,因此,反编译后的.NET代码可读性很强,可以更准确地重建源代码。代码中包含的任何敏感信息或商业秘密都能被逆向工程师轻易获取。
图1.1 .NET反编译器JetBrains dotPeek
与真正的机器码程序不同,即时编译程序往往可以被转换成源代码。这降低了逆向工程代码的门槛,使得我们在后续章节中讨论的许多x86逆向工程防御机制变得多余和过度。
对于可反编译的语言,常用的防止逆向工程的防御措施是采用混淆技术。图1.2展示了.NET应用程序在混淆前后的样子。
图1.2的上半部分展示的是代码被混淆之前的情况,其中的函数名、变量名和字符串都很容易阅读。这些变量名中的信息使得逆向工程师更容易理解每个函数的用途以及整个应用程序的工作方式。
图1.2的下半部分展示的是同一段代码的混淆版本。现在,函数名、变量名和字符串都被混乱地重命名,使得我们很难理解显示的函数的用途,更别说理解整个应用程序的功能了。
另一个重要的安全性最佳实践是避免用易于逆向工程的即时编译语言编写涉及安全性或隐私的关键代码。相反,应该用汇编语言(如C/C++)编写这些代码,对这种代码逆向工程要难得多。这些代码可以包含在动态链接库(Dynamic Link Library,DLL)中,这些库可被链接到包含用即时编译语言编写的非敏感代码的执行文件中。
图1.2 JetBrains dotPeek中的混淆