第1章 绪论
讨论反编译技术自然离不开编译技术。反编译与编译是两个相对的过程,但它们实质上都是翻译的过程—对程序的翻译、对代码的翻译,在翻译的过程中会用到多种程序分析和变换技术,而这些技术是本书关注的重点。
反编译器处理的对象是编译器输出的目标程序,所以反编译的处理过程很大程度上基于编译技术和编译优化理论,并以新的方式应用于反编译。本章从对编译和编译过程的回顾出发,介绍反编译技术涉及的概念,对反编译过程和技术进行概括阐述,使读者对反编译技术有个总体认识。
1.1 编译与反编译
编译(Compile)是将源程序翻译成目标程序的过程。编译器就是完成这个翻译过程的程序或工具,如图1-1所示。编译器输入的源程序是与机器无关的高级语言源程序,输出的目标程序是与目标机器相关的二进制机器语言程序。
反编译(Decompile),又称为逆向编译或反向编译(Reverse Compile),是指将可执行文件翻译成高级语言程序的过程。反编译器是完成这个翻译过程的程序或工具,如图1-2所示。反编译器输入特定目标机器的二进制机器语言程序作为源程序,生成的高级语言程序作为其目标程序。
图1-1 编译器
图1-2 反编译器
反编译是编译的逆过程,反编译技术依赖于编译技术。为了更好地理解反编译的原理和技术,首先来简单地回顾编译技术。
从编译原理上看,编译程序(编译器)把一个源程序翻译成目标程序的工作过程从逻辑上可以分为六个阶段:词法分析、语法分析、语义分析、中间代码生成、代码优化和目标代码生成,如图1-3所示。
(1)词法分析。词法分析是编译的第一个阶段,以高级语言源程序作为输入,其任务是对由字符组成的单词进行处理,将源程序看作字符流,逐个字符地对源程序进行扫描,产生一个个的单词符号,把作为字符串的源程序改造成为单词符号串。
(2)语法分析。语法分析以单词符号作为输入,分析单词符号串是否形成符合语法规则的语法单位,如表达式、赋值、循环等,昀后观察是否构成一个符合要求的程序。
(3)语义分析。语义分析是审查源程序有无语义错误,为代码生成阶段收集类型信息。
图1-3 编译程序的逻辑结构
(4)中间代码生成。中间代码是源程序的一种内部表示,或称中间语言。中间代码的作用是可使编译程序的结构在逻辑上更为简单明确,特别是可使目标代码的优化比较容易实现。中间代码生成阶段将被编译代码转换为中间代码形式。
(5)代码优化。代码优化是指对程序进行多种等价变换,使得从变换后的程序出发,能生成更有效的目标代码。
(6)目标代码生成。目标代码生成是编译的昀后一个阶段。目标代码生成器把语法分析
后或优化后的中间代码变换成目标代码。
在编译过程中还会对生成的符号信息和出错信息进行记录与维护。
反编译要实现编译的逆过程。那么,是不是把上述编译过程的六个阶段倒置,就变成了反编译的过程呢?显然是不对的。对于反编译过程,可以这样去理解:源程序现在是二进制可执行文件或者汇编指令,目标程序是某种特定的高级语言。那么现在这个过程该如何转化呢?这其中的中间代码的生成是否和编译过程中的一样呢?
基于上述原理和问题,很容易会考虑到这种思路:将特定的机器代码,即反编译的源程序,先翻译为低级的中间代码,然后针对特定的高级语言将中间代码翻译为高级程序。没错,反编译的主要思想确实就是这样,但这期间,涉及各种程序的分析和变换技术,有些是基于编译技术的,有些是反编译所特有的,这些技术正是后面章节所要讨论的。
由于编译过程是利用编译程序从高级语言编写的源程序来产生目标程序,在现实应用的时候,很多情况下会把通过编译后的目标程序(可执行文件、字节码文件或某种专用格式的文件)得到源程序的过程,认为是反编译,例如,将反汇编称为反编译, Java字节码的反编译、 APK 的反编译、SWF 文件的反编译等。这是一种广义的观点,本书则从狭义的角度,重点讨论从二进制机器码到高级语言程序的过程。
反编译技术跟编译技术有着非常多的相似之处,其中的很多研究方法借鉴了编译技术。反编译器是软件逆向工程中昀高级别的工具,它与反汇编器和调试器等逆向分析工具相比,逆向结果更加直接。
反编译要逆向编译过程,难度显然要比编译大很多,即使是编译程序本身,一般也没有反编译能力。然而对反编译的需求,却大有市场,不仅仅是开发者,一些应用人员,由于某些特殊需要,也对反编译情有独钟。这些特殊的需求中,也包括不便明说的原因,道德的与不道德的,合法的与非法的,一起构成了反编译的动机和来源。工具与用途的关系,总是可以进行辩证和辩论的。反编译不仅是工具,也是技术,蕴含在其中的智慧和智力结晶,值得热爱技术的人们去钻研。逆向工程可能会被误认为是对知识产权的严重侵害,但在实际应用上,反而可能会保护知识产权所有者。例如,在集成电路领域,如果怀疑某公司侵犯知识产权,可以用逆向工程技术来寻找证据。反编译正是针对目标程序的逆向工程技术。
1.2 反编译技术的发展历史
第一代反编译器诞生于20世纪60年代早期,仅比编译器晚十年。第一代反编译器被大量地用来翻译科学程序。由于第三代计算机的产生,当时运行在第二代计算机的软件面临着即将报废的危险,而第三代计算机却面临着软件匮乏的问题。为了挽救这些当时正大量运行在各种即将报废的第二代计算机上的价值不菲的软件,同时也为加速开发第三代计算机上的软件,美国的一些公司开始研究针对特定软件的专门用途的反编译工具用来进行软件移植,以及其他的一些研究,如程序转换、交叉汇编、翻译器等。反编译技术出现后,人们同时也在做反编译理论的研究。在整个60年代,反编译方面的研究主要是开发专门用途的反编译器作为软件移植的工具。当时,知识产权尚未得到如今的重视,而且针对当时的高级语言和目标代码的反编译的效果也较理想,因此吸引了众多科研人员的加入。
但是,随着高级语言的发展,由于编程模型的发展和复杂类型的引入等因素,编译系统修改和掩盖的信息越来越多,使得反编译研究越来越困难,研究难度远远大于相应的编译过程;另外,知识产权问题也阻碍了反编译技术的发展,因此学术界公开的文献和成果不多。但在航天、军工等尖端领域中,反编译研究从未终止过。许多研究项目由于各种原因秘而不宣,所以只能通过公开而且具有影响的研究来管窥一斑,例如, IBM为 NASA的航天飞机研制的反编译器,澳大利亚电子研究实验室的针对 Pascal语言的反编译器,欧共体 ESPRIT计划中也有许多反编译研究(例如,英国的核工业委员会使用反编译技术验证大量的 Safety Critical软件,以提高软件的正确性)。
在20世纪80年代,随着改革开放,国内计算机技术飞速提高,关于反编译方面的研究也如火如荼地开展起来。首先,合肥工业大学微机所开展了用手工方法反编译 UNIX操作系统的研究。1984年在国家自然科学基金资助下,研究 DUAL 68000机器上的 C语言反编译系统,成功开发了68000C反编译系统,获国家机电工业部科技进步奖二等奖。1988年起该所开展了 IBM PC系列机的 C语言反编译系统的研究工作,成功开发了8086C反编译系统,并发表了题为 Research on Decompiling Technology的论文,该课题被列为“七五”国家科技攻关计划。1992年起,微机所利用自筹资金,在8086C反编译系统研究的基础上开展了商品化的 C语言反编译系统的研究工作,1995年底完成商品化系统 DECLER并投入使用。近年来,国内在反编译方面,关于反编译器的研究进展不大,虽然华中科技大学在反汇编系统上实现了静态库函数识别功能,能够给汇编代码增加一些库函数的信息,但由于其使用的方法仍旧基于 C语言反编译器的技术,因此只能适用于 C语言的静态库函数识别。
虽然近年来对完整的反编译器的研究不多,但反编译的相关理论和技术的研究确是方兴未艾,如程序分析变换、高级语义理解、控制流图恢复与分析、代码混淆、数字签名等理论和技术,应用领域也不断拓展,在代码理解、系统维护和网络安全等方面都有着很大的应用价值。
另外,近年来随着人工智能技术的蓬勃发展,人们开始研究和探索进行反编译的新途径,将机器学习、自然语言处理等思想应用于反编译处理,验证了利用人工智能技术实现反编译的可行性。
1.3 反编译所面临的问题
把二进制程序从各种各样的机器语言反编译为多种多样的高级语言,都要用到基本的反编译技术。反编译器的结构以编译器的结构为基础,采用与之相似的原理和技术来进行程序分析。在编写一个反编译器的时候,会面对一些理论上和实现上的问题,有些问题能够通过使用启发式方法解决,而另一些目前则不能完全解决。由于这些限制,反编译器对某些源程序能够进行全自动的程序翻译,而对其他一些源程序则只能进行半自动的程序翻译。这与编译器能对所有源程序都进行全自动程序翻译是不同的。本节主要讨论反编译所面临的一些理论和技术难题。
1.体系结构带来的问题
从世界上第一台计算机诞生以来,冯 诺依曼体系结构一直占据着主导地位。在冯 诺依曼机器中,内存里的数据和指令以同样的方式进行存储。这意味着,只有当一个给定字节从内存读出放入一个寄存器作为数据或指令使用的时候,才知道它是数据还是指令(或两者都是)。即使在分段的体系结构上,其中数据段里只有数据信息,代码段只有指令,数据仍然能够以表的形式储存在一个代码段中,如 Intel架构的 case表;指令也能够以数据的形式储存,然后通过解释这些指令而运行。所以指令和数据的区分,是反编译首先必须解决的问题,而这在现行体系结构和编译机制下是一个难题。
2.自修改代码
自修改代码指的是指令或者预置数据在程序运行期间被修改。一条指令的一个内存字节位置能够在程序运行期间被修改,表现成另一条指令或数据。这个方法曾经很长时间用于实现各种目的。在20世纪六七十年代,计算机内存很小,难以运行大程序。那时,计算机内存昀多只有32KB或64KB可用。由于空间有限制,所以必须尽量充分利用。其中一个方法就是在可运行程序中节省字节,重复使用数据位置作为指令,或反之亦可。这样,一个内存单元在某一时间持有指令,而在另一时间变成持有数据或另一指令。而且,在指令不被需要时被其他指令修改并替换为其他指令,因此程序下次执行那部分代码的时候就会执行不同的代码。
现在的计算机在内存方面的限制少了,因此自修改代码已经不再用于节省空间,而是更多地用于代码保护,防止被逆向分析或者用于恶意代码的隐藏以避免被查杀。
图1-4给出一个 Intel架构的简单自修改代码示例。 inst定义的数据字节在 mov指令执行后被修改。在 mov指令执行之前,inst地址的内容是9090,作为两条 nop指令来执行; inst指令动作执行后该内存位置内容被修改成 E920,现在是0E9h20h,即是一条跳转到偏移20h的无条件跳转指令。这个过程就实现了程序运行过程中对内存单元内容的修改,而采用静态的程序分析技术则无法发现和分析到这些变化,从而使反编译的结果不正确。
图1-4 自修改代码的示例
3.编译习语
编译习语(Idiom)也称为指令习语或成语,通常是二进制可执行程序中使用频繁的指令序列,是编译器在程序翻译过程中使用的被编译界普遍接受的一些翻译规则,利用固定的指令序列来等价替代高级语言中的一些特定语义和操作,从而提高可执行代码的执行效率和模拟目标体系结构所不支持的操作。指令习语主要包括子程序调用、加减法、乘除法、取模和逻辑运算等。指令习语的识别能大大增强反编译结果的可读性和准确性。
编译习语表现为一个指令序列,它形成一个逻辑实体,作为整体表示一个含义,而这个含义无法从各个组成指令的基本含义推导
展开