《软件加密与解密》试读:第一章:什么是隐蔽软件

在本章中,将讨论一些用于保护软件中秘密的基本技巧,即代码混淆、数字水印、防篡改和软件“胎记”。这些技术有很多有意思的应用,例如使用代码混淆和防篡改技术来保护数字版权管理系统。这些技术应该能够激发你学习它们的热情。我这样认为的原因是:代码混淆以及其他3种技术能够“解决”传统计算机安全和加密所不能解决的问题。用引号括起“解决”两字,是指到目前为止还没有一种算法能够永远提供100%的保护,我们能指望的只是去尽量延长黑客破解我们的防护所要花费的时间。你也许会想,这也算是解决问题了?你是正确的,但至少我们还有一些漂亮的办法能让黑客的日子过得更不爽些,有总比没有强吧。 1.1 概述 当你听到计算机安全这个词时,脑海里可能马上浮现出这样一个场景:一台电脑(由善意的用户所使用,我们称她为Alice)正在受到一个黑客(我们叫他Bob)的攻击,或正受到由Bob编写的病毒、蠕虫、木马、rootkit以及键盘记录器的攻击。计算机安全研究的目的就是防止Bob控制Alice的计算机或当Bob试图这样做的时候,向Alice发出报警。这些技术的基本设计思想都是,限制Bob在Alice计算机上的权限,同时又不过度妨碍Alice的正常工作。例如,网络防火墙允许Alice访问网络上的其他电脑,但限制了Bob对Alice的电脑的访问。入侵检测系统则会分析Alice访问网络的模式,并在当Bob似乎在做一些异乎寻常或可疑的事情时,向Alice发出警报。病毒扫描程序将拒绝运行Bob的程序,除非这个程序能够证明其本身不含恶意代码 。换言之,Alice给她的电脑增加了一个保护层——防止有人进入她的电脑,检测是否有人进入她的电脑,或者阻止已经进入她的电脑的人损害她的电脑,如图1-1所示。 图 1-1 如果我们把情景颠倒一下,又会发生什么呢?如果不是Bob向Alice发送一个恶意程序,而是一个名叫Doris的软件开发人员给了或者更准确地说是卖了一个正常的程序给Axel 呢?为了让事情变得更加有趣些,我们假设Doris的程序包含了一些秘密S,而Axel为了某些经济利益,瞒着Doris提取出或改变了S,如图1-2所示。 图 1-2 这个秘密可能是任何东西。它可能是一个绝妙的算法,使得Doris编写的程序运行起来比Axel的快许多,所以Axel很想把它弄到手;它可能是Doris程序的整体架构,而这个架构对Axel建立他自己程序的整体架构无疑是非常具有参考价值的;它还可能会是加密密钥,能够用来解开被数字版权管理系统保护的一些音像资料;又或是程序中让Axel不去注册就只能用试用版的软件注册算法。那么,Doris可以做些什么来保护这一秘密呢? 乍一看,你也许认为加密技术可以解决这一问题,毕竟加密技术就用来保护数据的保密性的。具体来说,加密系统就把明文S搅乱成了一篇密文EK(S ),如果没密钥K,就休想读出明文S,如图1-3所示。 图 1-3 那么为什么Doris不在出售程序之前就使用加密的方法来保护她程序中的秘密呢?很遗憾,因为这个办法是不会奏效的——Axel必须能够正常执行程序,因此在程序运行过程中的某些时候,Doris的秘密肯定会被解密成明文! 软件保护与标准计算机安全和加密技术有很大的不同,这两者的区别表现在,一旦Axel得到了Doris的程序,那么他就能为所欲为地对Doris的程序进行分析了:他可以研究程序代码(可能首先对程序进行反汇编或者反编译),执行程序来研究其行为(可能使用调试器),或者修改代码使其运作方式有违作者本意(例如跳过检查程序是否已经注册这一步骤)。 在软件保护术语中,一次典型的对Doris的程序P的攻击是由3部分组成的,即分析、篡改和盗版,如图1-4所示。 图 1-4 Axel首先通过分析程序P搞懂了程序的核心算法与设计思想,并且找到了加密密钥或者发现了用以负责软件注册的代码的位置等秘密。接下来,他会修改Doris的代码(例如,他可能会把用以检查软件是否已经注册的代码给删掉)或者将有关算法纳入自己的程序。最终,Axel发布了由此产生的程序(即盗版),从而侵犯了Doris的知识产权。 当然这一过程有很多变种。Axel可能只是破解某个软件,却没有把程序的破解版发布出来,而仅仅是自己享受由此带来的乐趣。他可以把已知的注册码连同程序一起转手卖掉,而不必非得篡改代码。另外,他还可以反编译和分析程序以验证其安全性(比如,确定程序中肯定不包含病毒或木马,或者分析某个投票软件,以保证这个软件在统计选票时肯定不会作弊),而不会使用由此获得的信息来改进自己的程序。不过,尽管实际情况是如此千变万化,但它们都回避不了一个基本的事实:一旦作者发布了程序,其中的任何秘密就会立即变成裸露在狼群中的羔羊。 在研究的场景中,Axel通常是出于一些经济上的原因才去提取或修改该程序中的信息的。而Doris一般也只是要求在某个时段内保持这些秘密。所以软件保护的目标基本上也只是让被保护软件能在这段时间里撑过攻击就行了。比如,由于游戏销售的大部分收入产生于它发行后的很短的一段时间内,所以如果能让新游戏在上市之后的最初几周内不出现盗版的话,游戏的开发商就很高兴了。 在大多数的软件保护案例中,Doris通常都会混淆她的代码,使Axel更难以分析,在代码里增加防篡改保护以防止Axel修改它,最终她还要为自己的代码添加水印(例如,她嵌入对软件的版权声明或者Axel特有的标识),以保护她的知识产权,如图1-5所示。 图 1-5 本书会讨论5种软件保护技术来帮助Doris加固其软件:用来防止代码被他人分析的代码混淆技术、软件水印技术、指纹技术、利用软件“胎记”发现和追踪代码剽窃的技术以及基于软件和硬件的防篡改技术。 虽然保护程序中的秘密是开发软件保护技术的主要动机,但是这些技术也被应用于在保护电子出版物(数字版权管理)的分销链、防病毒、隐蔽地传递消息以及网游防外挂。我们还将展示这些技术是如何被用于制造免杀的电脑病毒和在电子投票系统中作弊的。 软件保护这门学科与计算机安全和密码学有很多共通之处,但是它与隐写术的亲缘关系最为密切。隐写术是密码学的一个分支,专门研究如何隐蔽地传输秘密信息。通常使用下面这个“囚徒问题”来说明隐写术的研究内容。在这个问题里,Alice和Bob正通过看守Wendy传递纸条来计划越狱,如图1-6所示。 图 1-6 当然,如果Wendy发现自称是情书的便条中提到越狱的话,她将立即停止为他们传递纸条,并分别监禁Alice和Bob。那么这两个家伙应该怎么做呢?他们不能使用密码,因为只要Wendy一看到难以理解的信息就会感到可疑,进而不再为他们传递消息了。相反,他们必须通过发送看上去平淡无奇但能隐藏秘密的信息来暗中沟通。例如,Alice和Bob可以商定一个方案:将隐含的信息(有效载荷)隐藏在掩护消息的每个句子的第一个字母上,如图1-7所示。 图 1-7 这种办法被称为“藏头诗” ,当然还有很多其他方法能帮助这对囚犯。比如,Alice可以传给Bob一幅prison猫的照片,并且用这个图片文件中所有像素的最低有效位来存储她要传递的信息 。或者她也可以传给Bob一首自己喜欢的歌的mp3文件,她在音乐中嵌入一些人耳不易察觉的回声——用相对较短的回声表示0,相对较长的回声表示1。再或者她也可以调整PDF文件的行距,用12磅行距表示0,12.1磅行距表示1。或者更绝一点,Alice可以让Wendy给Bob带一个她自己编写的俄罗斯方块游戏,供他消磨时间。但是可怜的Wendy并不知道,这个俄罗斯方块程序并不光是用来玩的,在这个程序的某些控制结构或者数据结构中 还藏有Alice和Bob的越狱计划。在本书中,我们要讨论的恰恰就是这些技术。我们把这种能保护软件中含有的秘密免受攻击的技术称为软件保护技术,相应的软件则称为隐蔽软件(surreptitious software)。 1.2 攻击和防御 所谓攻击模型就是事先假定的对手所拥有的能力以及攻击系统的方法。不熟知它,就没有资格进行计算机安全领域的研究或者从事计算机安全相关的工作。比如在密码学研究中,我们一般都会假设“攻击者是拿不到密钥的”、“攻击者在因式分解的计算能力上并不比我强 ”或者“攻击者破不开这种智能卡”。一旦确定了攻击模型,你就可以由此出发,着手设计一个在此模型下安全的系统。在实际操作中,攻击者会马上试图寻找你忽略掉的东西,并以此绕过你设置的防线!攻击加密系统最好的办法并不是花上10万美元去制造专门的硬件去破解密钥,而是用5万美元去贿赂那个拥有密钥的人。同样破解智能卡的最佳方法也不是硬把卡撬开(那样肯定会触发一些专门针对这类攻击而设计安全装置 ),而是通过改变输入智能卡的电压,把智能卡放在X光灯下面照等方法探测智能卡设计上的缺陷。总之,就是要从被设计者忽略掉的地方着手。 在隐蔽软件的研究中,情况也是一样。研究者也会根据自己的研究方向,而不是所有破解者可能使用的攻击方法,去设定攻击模型 。在我们自己的研究工作中,就经常假设“攻击者会使用静态分析的方法攻击软件”。这是因为我们两个都有编译器设计的背景,这是我们擅长的领域。而有的研究者可能会给出这样一种攻击模型:“攻击者会把整个软件看成一个图,并试图从图中找出一个与图的其他部分联系最不紧密的子图,而秘密信息很可能就隐藏在这个子图中。因为程序中所含的秘密并不是程序自身的一部分,而是后来被添加进程序的,所以它与程序之间的联系一定会比程序自身内部的联系弱一些”。啊哈,我一眼就看出,这位一定拥有深厚的图论功底。有些研究者可能低估攻击者的能力(比如他会假设“攻击者不会运行这个程序”——哪个傻瓜会接受这样的规则),又有些研究者会高估攻击者的本领:“攻击者拥有或者会编写一个很复杂的针对被攻击程序的测试输入集,所以即使程序不是攻击者编写的,甚至在攻击者连程序的源码或者文档也没有的情况下,他仍然能对程序进行批量修改”。 遗憾的是,现在已发表的很多关于隐蔽软件的论文中根本就不曾提及针对的是何种攻击模型。我们希望本书能有助于纠正这种不良习气,因此每当我们在书中介绍一种算法时,都会告诉读者这一算法是针对现有的或者将来可能出现的哪种攻击方法而设计的。 在第2章中,我们还会讨论防御模型——好人们是如何抵御坏蛋攻击的。我们将使用动植物的自卫手段和人类的防御方法作为例子来说明软件防御模型。此外,我们还会使用这一模型对文献中提及的各种攻击方法进行分类。 1.3 程序分析的方法 对程序的攻击一般分为两个步骤:分析阶段——从程序中收集相关信息,转换阶段——根据收集到的信息对程序进行修改。而分析程序则有两种基本的方法:只是分析程序代码本身(静态分析)和运行程序搜集信息(动态分析)。 静态分析只要一个输入——程序自身P,如图1-8所示。 图 1-8 在过去的这些年里,人们已经开发了很多程序静态分析方法。它们大多是由软件工程研究人员为了减少程序中可能出现的漏洞,或者编译器设计者为了优化程序而研发的,但也有一些破解高手为了去除程序中的软件保护代码而自己开发了一些程序静态分析方法。静态分析收集到的信息是保守的。说它是“保守的”是因为:只有那些确定一定以及肯定的信息才会被收集到,即使只有略微一点点疑问的信息都不会被静态分析工具收集。例如,如果一个静态分析工具告诉你“在程序执行到第45行时,变量x的值一定是42”,那你就可以确定这一事实必定会发生 。有时,使用这种保守方法收集到的信息可能会让你错过一些实际上会确实发生的事实 。但是它至少不会欺骗,不会告诉你一个真实的谎言。 动态分析是通过运行程序,并给程序输入一定的数据的方式来收集信息的,如图1-9所示。 图 1-9 而动态分析收集到的信息的精确性则取决于输入数据的完备性。所以对由动态分析收集到的信息只能这么说:“在第45行,变量x的值貌似一直是42啊!好吧,至少对已经试过的数据来说是这样”。只有根据使用静态分析获得的信息进行的代码转换才是安全的,我们可不想把程序改得到处是漏洞(当然在这里我们必须要假设所进行的代码转换是保持语义不变的,即完全不改变程序的原意)。换言之,根据动态分析所收集到的信息进行的代码转换是不安全的——如果输入的数据集不完备,那么所做的修改很可能就把程序给改错了。 攻击者是根据自己的意图选用不同的代码分析与转换方法的。比如,如果他只想破解某个软件,那他只需要一些最基本的静态和动态分析工具就够了——他可以在调试器里运行要被破解的程序,直到弹出一个“本程序尚未注册”的对话框来,接着他只要找到程序中弹出这一对话框的代码所在的位置,反汇编它,从中找出if today's date > license date then...这类语句,再用一个编辑器把相关的代码删掉即可。但他如果是想搞清楚一个大型程序中所使用的某个算法的话,那一个能把可执行文件完全反编译回源码状态的反编译器对他就会比较有用。 逆向工程的例子 为了更形象地说明这一问题,我们来看下面这个逆向工程的例子。假设你的老板给了下列的这些字符串,要求你对它进行分析。 老板同时还告诉你,这些十六进制码是从我们竞争对手的Java程序中“抠”出来的,其中包含有一个很重要的秘密算法。这些十六进制码代表的实际上就是下面这个程序,当然作为一个逆向工程师你现在还不知道这一点。 既然任务就是搞清楚这个“天大的”秘密,所以你最好是把这些字节码转换成清晰易读的Java源代码。而且,如果在这些代码中使用了一些用来愚弄你的代码混淆手段或者其他什么抗逆向分析技术,你最好捎带着把它们也一起干掉。 首先,应该反汇编这些十六进制码,即把这堆难看的字节串转换成带符号的汇编代码。对于Java字节码来说,有很多工具(比如jasmin或者javap)能帮你完成这一任务(如下所示),所以你可以很轻松地迈出这一步。 我把Java源码和反汇编结果中对应的部分一一标上了同样深浅的阴影和虚线框,方便你把它们对照起来。在反汇编结果里,每一行中一对方括号括起来的部分是汇编指令对应的字节码。 Java字节码的设计使我们反汇编它很容易。因为在Java字节码中包含了足够多的信息,能让我们能轻松恢复出各个变量的数据类型以及函数的控制流。但对于x86或者其他处理器的机器码来说,我们的日子就要难过许多。因为在这些机器码中很容易被插入一些用来愚弄反汇编器的代码或数据。在第3章中将详细讨论这一点。 把字节码变成汇编代码之后就要进行控制流分析了。我们会在这一步恢复代码中各条指令执行顺序,结果就得到了控制流图(Control Flow Graph,CFG)。图中的各个结点都是由一些顺序执行的指令组成的,多数结点的最后一条指令是一个跳转指令。而如果程序在执行时可能从这个结点跳转到另一个结点执行,那么这两个结点之间就有一条边 。如图1-10所示。 图 1-10 控制流图中的结点被称为基本块。在许多编译器或者逆向工程工具中,控制流图是必须构造的核心数据结构 。在控制流图(图1-10)上我也标上了阴影和虚线框,方便你把控制流图和Java源码、反汇编结果对应起来。 接下来,我们就可以使用各种分析方法从代码中收集所需要的信息了。使用这些信息可以进一步简化代码,使之更易于理解。它们甚至也有助于去掉代码中可能使用的反逆向分析技术。一种被广泛使用在编译器和逆向分析工具中的常用方法叫做数据流分析,将在3.1.2节中详细介绍。在这里,我们只是对变量x进行一次常量传播分析 。分析结果将告诉你,在代码的一些位置上某个变量的值是个常量。如图1-11所示。 图1-11中,左边那幅表示分析的初始状态——假定初始时,我们并不知道变量x在各个基本块的入口点和出口点上的值。然后,我们分别分析各个基本块,以获取一些信息。比如说,如果某个基本块中执行了语句x=3,那么显然在这个基本块的出口点x的值就一定是3 ,由此我们就得到了中间的那幅图。此外,如果某个基本块中不包含对变量x的赋值语句,那么变量x在这个基本块的出口点上的值就应该是它在这个基本块的入口点上的值。如果控制流从一个基本块出来之后流向了两个不同的基本块,我们也可以说在这条边上x的值是一样的。而且,我们也可以更进一步地肯定:在那两个基本块的入口点上,x的值是一样的。在逐步地考虑上述因素之后,控制流图就变成了图1-12中左图这个样子。 图 1-11 图 1-12 现在可以对这张图进行转换了。首先我们已经确定变量x是一个常量3,所以不管x在哪里被使用,我们都可以用常量3来代替它。因此表达式x<=3就一定为真,这个结果使我们可以执行一次“消除死代码”的操作,即把那些不可能被执行到的基本块从控制流图中删掉。此外还可以删除冗余语句。结果,控制流图就被转换成了右边那张图。 请看!是不是比原先那张干净多了?更夸张的是,这张图还可以继续简化下去!我们注意到图1-12中的那个循环实际上就等价于赋值语句i=4,而且这个函数也没有返回值,所以我们甚至可以干脆把整个函数都删掉!在这个例子中,完成这么一次分析似乎还是挺容易的。但是实际情况会比这复杂得多。分析结果在很大程度上是由分析强度和程序复杂度共同决定的。 但目前为止还不能确定的是,刚才消除掉的这段代码究竟是代码混淆器为了增加程序混乱程度而插入的程序,还是仅仅由一个优化性能不怎么样的编译器生成的。唯一可以确定的是,你已经把老板交给你的一堆十六进制数转换成比较简单且结构化的形式,而且其中不相关的部分也已经被去掉了。 逆向工程的最后一步是要把控制流图反编译成源代码形式。如果控制流图的结构很清晰,这个过程对你来说简直就是举手之劳。 显然,源码也可以写成其他的形式,比如可以用一个for循环来代替while语句。 我们已经把原始的字节码转换成大家都能读懂的源代码了,这一步对于逆向工程师来说是很有用的。但是故事还没结束(不出你所料)。接下来有人可能要去深入分析这个源码所表达的算法,也有人可能要去修改源码以跳过程序注册验证步骤……而这些则通常都是手工完成的。 在第2章中,我们将向你揭示逆向工程师是如何找出软件中的秘密,或者让程序做他想做的事。在第3章中,我们将更加深入地介绍和讨论破解者们使用的各种工具及分析技巧。我们不光介绍一些市面上能买到的现成工具,还会教你如何使用现有技术定制自己的工具。这样的讲法太简单了:“现有的反编译器只能对付那些静态链接的代码,而我们已经使用x86的动态链接实现软件保护技术了,所以我们的软件保护方法是安全的。”而更有意思的表述可能是这样的:“无论是过去还是将来,使用我们的保护技术保护的软件,通过反编译器反编译生成的代码量会以几何级数增长”。 1.4 代码混淆 下面我们要介绍第一种保护技术——代码混淆。这一技术的特别之处在于,它是一把双刃剑:它即能被坏蛋们在恶意代码上使用,让病毒木马难以被发现(下一小节中将会看到这一点),又能被好人们用来防止自己的软件被破解,但同样在盗版/反盗版的斗争中,坏蛋们又会用它来抹除好人们嵌在软件中的水印。 多数情况下,混淆程序就意味着把程序转换成一种更为难读、更难修改的形式。在这个定义中“更为难读、更难修改”中 “难”字的含义是很模糊的。但就一般而言,我们所认为的“难”意味着分析混淆后的程序要比处理未经混淆的程序需要更多的人工和金钱,或是更高的计算强度。从这个角度上说,我们发布软件的二进制可执行文件而不是程序的源码就是一种代码混淆,因为二进制机器码要比高级语言源码难读得多。同样,使用编译器优化一个程序也可以被认为是对这个程序进行混淆。因为不论是手工分析还是用使用工具(比如反汇编器或者反编译器),优化后的代码总是比没有优化的代码难处理得多。 不过我们书中介绍的工具可要比编译器或者代码优化工具强得多!因为代码优化工具对代码进行转换只是为了生成运行速度更快、个头更小的代码,而我们混淆代码的目的就是为了使代码变得更难分析。当然代码混淆也有些副作用,比如它会生成体积比较大,或者(有时是“并且”)运行速度更慢的程序。所以软件开发人员在使用代码混淆技术时要权衡一下利弊,决定一下程序的哪些部分需要保护,哪些部分只好放弃保护。 代码混淆常会被误会成“不公开,即安全”,“不公开,即安全”(通常是一个贬义词)是密码学或者安全学一个“分支”中的术语,专指“如果算法不被公开,其中包含的秘密就不会泄露”这一想法。这种想法一直为学术主流所唾弃,大部分研究者都认为所有的加密算法都应该是公开的,你只要管好加密的密钥就行了。当然,密码学研究人员主张公开加密算法并不是为了方便破解,而是因为他们认为:公开算法有利于让更多的人参与到寻找算法中可能存在的漏洞的行动中,而如果这么多人都找不出算法中的漏洞的话,那算法无疑就应该是安全的。可是普通消费者却往往不买专家的账,你往往会在一些网站上看见出售“军用机密”加密算法的广告,广告词通常都是“我们加密算法的细节别人是不知道的,所以破解起来更难!”微软等一些软件厂商也持有类似的观点“由于任何人都能随意查看开源软件的源码,因而也就使黑客能更方便地找出其中的漏洞并加以利用。所以开源软件的安全性不如闭源软件。”但是从实际效果上看,这种观点是有问题的。不开放源码并没能阻止黑客发掘出闭源软件中的漏洞,而即使是那些完全不公开的算法也经常(这是不可避免的)会被发现含有严重的、可资利用的设计缺陷。 根据本书中对代码混淆的定义,它并不是“不公开,即安全”。相反,和密码学研究者的观点一样,我们认为对代码进行混淆处理的算法就像密码学中的加密算法一样应该是公开的,而对应于密码学中的需要保密的密钥,在代码混淆中防御者需要保密的则是:这些算法是在哪里以及是如何被使用的。 在介绍代码混淆技术的应用领域之前,我们先来看看被混淆后代码会变成什么样子。我们来看一下代码清单1-1,这些代码是使用SandMark Java代码混淆器生成的。先别看提示 ,你先试试完全搞懂这段代码需要花上多少时间。好吧,你再想想,这段代码才20行!“正常的”软件通常都有成千上万行代码,完成这样一个“常规”破解又要花上多长时间呢?直觉是否告诉你,被分析代码的大小以及其中被混淆部分的比例与完成破解所需的时间应该成正比关系呢?此外,会不会有一些混淆器比其他的混淆器更厉害些?会不会有些混淆器会产生更难被修改的代码呢(比起其他混淆器)?如果确实存在这些终极混淆器的话,它们到底有多厉害?是不是它们产生的代码就不可能被修改?很遗憾,对此我们一无所知。因为直到现在为止,没有一个理论模型能告诉我们要花多长时间才能破开使用某个特定算法混淆的代码,或者使用这种混淆技术处理后的代码至少会增加多大的开销(虽然搞清楚这一点应该比较容易)。很多代码混淆理论的研究者确实正在试图建立这类模型[289],但是到目前为止,还没有一个能够完善得可以在实践中使用。 代码清单1-1 代码混淆的例子。混淆前的源码可以在本书官方网站上找到 1.4.1 代码混淆的应用 下面来看看代码混淆技术能用在软件保护的哪些领域中。 1.(防止)软件被恶意逆向分析 这是第一种情况,Doris开发了一个含有某个极具价值的商业秘密(这个秘密可能是一个精妙的算法,也可能是一个高超的设计)的程序。而Axel则是Doris的商业竞争对手,他想把这个秘密搞到手,并把它集成到自己的软件中去,重新包装后再卖给他的客户Carol。如图1-13所示。 图 1-13 当谈论代码混淆技术时,大多数人首先想到的可能就是这种情况。但是,正如下面将会讲到的,代码混淆技术的应用远不止于此。我们假定(虽然我们不会去严格地证明这一假定),只要有足够的时间和资源,Axel总能成功地逆向分析出任何一个程序。换而言之,没有哪个隐含在程序中的秘密能够永远不被发现。Doris的目标只是使用代码混淆技术尽量延长Axel破解所需的时间,同时又不使由此增加的性能开销大到自己承受不了的地步。一般而言,只要代码被混淆得足够复杂, Axel就会放弃攻击,说“算了,我还是自己去研究一下这个该死的算法吧!这样大概还比较快些。”而Drois在混淆代码时一般也会考虑效率问题,即Doris要选择正确的混淆算法(或者混淆算法的组合),对最重要的代码进行混淆,以保证混淆后的代码不会慢到(或大到)客户无法忍受的地步。 2. 数字版权保护 在这种情况下,Doris是个开发多媒体播放器的软件工程师。这个播放器只能播放一种被称为“cryptolope”的加密多媒体(图片或者音视频)文件。由于多媒体文件是以加密形式存在的,所以在播放它之前必须有一个密钥把它解密出来。如图1-14所示。 图 1-14 由于客户肯定希望能“随时随地”观赏到他购买的图片或者音视频文件——播放器很可能会在一些没有网络的地方(比如在飞机上)被使用,所以Doris只能把密钥存储在客户的计算机上,更准确地说是藏在播放器中。当然除了密钥之外,解密的算法也一定要在播放器中实现的。此外,播放器中还需要有一个解码器,它的作用是把解密后的多媒体文件解码成你能听见或者看见的模拟信号 。这样一个播放器的整个解码过程如图1-15所示。 图 1-15 你大概已经注意到,这一过程中Axel有3个攻击点。他可以去寻找密钥(如果这个密钥是个通用密钥的话,Axel甚至可以解密所有用于这种播放器的多媒体文件,而且如果解密后的多媒体文件中不再含有专门用以识别Axel的签名,Axel还可以在网上把它们卖掉),也可以去获取解密后的数字多媒体信号,甚至他也可以截取解码后输出的效果并不理想的模拟信号。这类软件中可能存在很多薄弱环节。首先,Doris可能毫无理由地会相信Axel不是很熟悉程序中使用的解密算法。所以,除非实现解密算法的代码已然经过了混淆处理,否则攻击者就能通过模式匹配的方法 ,一下子找到用来实施解密的代码以及解密时所用的密钥。Axel也可以使用动态分析的方法进行攻击。比如,加解密算法中一般都含有一些比较特别的操作模式(比如不断地执行含有大量异或操作的循环),要是不进行代码混淆的话,攻击者可以很容易地通过trace发现解密算法。密钥本身也是一个薄弱环节。密钥通常都是一个很长的非常随机的字节串,这些东西放在正常的程序里显得很扎眼。所以Axel只要对整个程序进行一次搜索,通过寻找那些看上去随机得离谱的东西就能把密钥找出来。而那些用到这些字节串的代码很可能就是解密代码。一旦Axel找到了解密代码,他就能很快找出解密后的多媒体文件是在哪里生成的,以及它们是如何被传递给解码器的。接下来,Axel只要往程序中插入一小段代码,把解密的结果写到他指定的文件里去就大功告成了。从这个案例中,Doris所得到的教训是:她必须要对自己的代码进行混淆处理,这样Axel就不能仅仅依靠模式匹配这种小伎俩轻易地找出解密器和解码器的位置以及两者之间传递数据的通道了。另外Doris还必须在代码中使用防篡改技术,以防止Axel在程序中加入新的代码。最后,Doris除了要混淆静态的代码之外,还必须对程序的动态行为及数据(比如密钥)进行混淆。干完了这一切之后,她还是必须要明白,这些保护措施都只不过是权宜之计,只要有足够的时间,Axel还是能攻破她设置的防线。所以她还必须事先想好软件被破解之后该如何应对。 3. 网上自动询价机器人 Doris往互联网上发送了一个网上自动询价机器人(Mobile Agent),这个机器人会依次向所有网上商城询问某个CD的价格,然后告诉Doris哪个店开的价最便宜 。当然,坏蛋Axel现在也在网上开了家小店。不用问,他现在想的就是如何擦掉询价机器人中的记录,让自己的价格(当然这个价格是比较贵的)变成网上最低价。如图1-16所示。 图 1-16 但对于Axel来说,光做到这一点还不够 ,要是在机器人在离开他的站点之后(当然这时Axel的报价已经被改成网上最低价了),再去访问的其他站点中还有比他更低的报价的话,他还是白忙一场!所以Axel还要修改机器人的代码,使其离开他的站点之后就再也不会去修改网上最低价了。只有这样他才能保证在Doris那里,最低价一定是他的。 在这里我们当然也要建议Doris对机器人进行混淆处理(就像在许多其他情况中一样)[165],以缓解攻击。通常在这种情况下Axel可能没有足够的资源(毕竟他可能要同时处理多个并发的请求)来对机器人进行逆向分析并修改它的代码。而且,如果机器人在Axel的站点中逗留时间过长的话 ,仅此一点也足以引起Doris的怀疑。此外,Doris还可以用不同的混淆技术分别对不同的机器人进行处理,这样即使Axel之前分析过Doris的机器人,也只能再次从头开始,而不能利用之前的经验加速分析/修改这一过程。 4. 网格计算 Doris要运行一个程序P,但是她的计算能力不够,所以购买了Axel的超级计算机上一些CPU时间以运行P。这一过程中,她必须把程序P以及为了运行P必须输入的数据发送到Axel这边,然后再从Axel这里读取计算结果,如图1-17所示。现在问题来了:如果P、输入的数据以及计算结果中的一个或者多个需要保密,Doris该怎么办? 图 1-17 Doris要担心的还不光是这些,她还要担心Axel是否会修改她的程序。如果不能确认P在Axel的机器上的完整性,Doris也就无法确认Axel返回给她的结果是否有效。 要想保证输入输出数据不被偷窥,Doris可以加密输入和输出,还要把P改造成能直接处理加密输入,产生加密输出的形式。这基本上都是非对称式加解密所研究的问题,但是这种方法实现起来效率实在太低,基本无法在实践运用。 加密这条道走不通的话,Doris还可以使用本书中介绍的方法——对代码进行混淆以保证算法不被剽窃,在程序中使用防篡改技术以保证其完整性。而要保持输入和输出的机密性,只要使用类似在数字版权管理系统中使用的方法就可以了(在这类应用中,代码混淆和防篡改技术是用来保护实现加解密操作的代码的)。 网格计算中实现软件保护相对而言是比较困难的。因为除了要考虑算法和数据的保密性及代码的完整性之外,最重要的是必须要考虑添加保护技术之后程序的性能!毕竟Doris把P交给Axel运行的初衷就是利用Axel的超级硬件使程序跑得更快些。如果应用保护技术会引起程序性能的大幅下降,我想Doris是无论如何都高兴不起来的。 5. 系统变形 代码混淆技术也能被用于操作系统防病毒这个方向[74,75]。基本思路是:Doris可以使用代码混淆技术随机化她的代码,使得每一段代码都不一样,这样恶意软件就无法定位或者利用已知的安全漏洞来攻击Drois的系统了。比如在移动设备上,对于同一个系统(如图1-18中的P),我们可以把它处理成多个发布版本(如图1-18中的P1、P2、P3):要是每个版本的代码都被混淆成完全不同的样子的话,那么除非Axel的病毒变得极其聪明,否则它就休想感染所有的系统。 图 1-18 这就是所谓的“系统变形”。当然,病毒的作者们也使用这一技术来实现病毒免杀,而且还取得了不小的成功。下一节将讨论这一点。 1.4.2 混淆技术概述 只要愿意,你当然可以手工把自己的代码转换成一种难以被对手理解并修改的形式。但在实践中,这种手工代码混淆执行起来太枯燥,也太容易出错 。所以更好的办法是编写一个混淆工具,自动地把原本设计良好、易于理解和修改的代码转换得难以读懂且基本上不能修改,就像一团浆糊。混淆器实际上就类似于一个编译器,但是编写它的目的并不是为了生成紧凑高效的代码,而是为了产生你的对手读不懂的东西。 从概念上说,代码混淆器要接受4个参数:被混淆程序P,用户指定的混淆级别,所能接受的最大开销和P中需要混淆的代码的位置,如图1-19所示。 图 1-19 从内部结构上来看,混淆器中含有一组代码混淆算法、为了实施各种混淆转换而必须拥有的程序分析集,以及一种能够挨个把各种用户选择的混淆转换应用到程序P中去的循环。这里使用的程序分析集与编译器、逆向分析工具中所使用的程序分析工具是一样的。而混淆循环则会一直持续到达到你所指定的混淆级别或者程序因为混淆而引起的开销超过了你所能接受的范围为止。混淆器的输出P'在行为上与P是一致的,但是它们的内部结构则大不相同。在实践中,有些混淆器会比上面这个模型更为简化,比如,有的混淆器只需要输入要混淆代码的位置以及混淆级别就可以了。 混淆技术可以分为4大类。结构混淆——程序员把程序组织成类、模块和函数。这类混淆技术就是用来破坏程序的这类总体结构的。数据混淆——混淆程序中所使用的数据。控制流混淆——隐藏程序执行时的控制流结构(比如if或者while语句)。动态混淆——混淆器将往程序中插入一个解释器T,T的存在将使得程序在运行时会不断地改变自身的代码。所以经这类混淆技术混淆过的程序结构应如图1-20所示。 图 1-20 我们将用3章内容来讨论代码混淆的内容。在第4章中,你可以看到许多控制流、数据和结构混淆的算法。我们将讨论这些算法所能带来的混淆程度、破解掉这些保护需要花费多大代价(强度)以及使用这些混淆方法可能给程序带来多大的性能开销(副作用)。在第6章中,我们讨论一些动态混淆技术以及这些技术所能增加的混淆级别、强度和副作用。第5章将介绍一些代码混淆的基础理论。我们特别关心的是确定什么能够被混淆而什么不能。 为了使你对代码混淆有个初步的了解,让我们用下面这个小程序来演示一下相关技术。下面给出的是一段C程序的源代码。我们将依次应用结构混淆、数据混淆、控制流混淆以及动态混淆来处理这段代码。 首先要隐藏的是这一事实:这个程序中的主要业务逻辑是分别在两个函数里完成的。编写这个程序的程序员一定是在某种设计思想的指导下把整个程序分解成3个部分main、foo和bar的。这个结构应该就是这个程序在编写它的程序员心中的模型,所以对于破解软件的黑客一定也很有用。因此要通过把foo和bar函数合并成一个函数foobar来打破程序的这个结构。新函数有3个参数,其中两个是bar函数原有的参数,x和z,foo函数只需要一个参数,所以包含在bar函数的两个参数里就可以了。还有一个参数是用来确定到底应该执行foo函数的代码还是应该执行bar函数的代码的s。下面给出的就是结构混淆后的程序代码。 你看,现在main函数好像是调用了同一个函数2次,但是实际上,它调用的是两个不同的函数。 但是在许多程序里,我们需要保护的秘密是程序中的数据,而不是代码的设计思想。比如在数字版权保护系统中,我们不能让黑客拿到明文的多媒体数据。一般在这种系统中,数据绝不会以明文形式出现,而是被加密成对于攻击者来说难以理解的形式,而且这些数据在程序中也总是以这种加密的形式被操作的。假设在下面这个示例程序中,我们要保护程序中所有整型(int)数的值不会被攻击者获得,而攻击者则主要通过在调试器中运行程序的方式来窥探这些数据。 在这个程序中,对数据只有3种操作:赋值、乘法运算和比较两个值是否相等,我们运气不错!为什么这么说呢?因为我们手头就有一种现成的,可以说就是针对这一情况设计的混淆算法——RSA算法!我们把这一算法的原理和细节放在第4章中详细讨论,现在只给出它的一个实现。令: 再对程序进行一下混淆转换。你看现在程序中已经没有明文的int型数据了吧。 事实上,明文6已经被加密成了12,42已经被加密成了36,7也已经被加密成了37!除了最后解码的时候(在这个演示程序中就是要把明文用printf函数打印出来),程序中一直是处理这些数的密文的。注意,在原来的程序中有一个语句x*7,这句语句中的7在混淆后的代码中已经直接被替换成了37,即E (7) 了。 在结构化编程中,总是用适当的嵌套式的条件语句以及循环语句来组织你的程序,这也使得程序易于理解和修改。而控制流混淆的目的就是打乱这些东西——扁平化控制流,通过重写函数,把结构化的语句变成一团浆糊。下面给出的就是经过了控制流混淆处理后的foobar函数,原先优雅的控制结构已经全部变成丑陋的goto语句了。 看一下代码清单1-2,我们又把函数foobar中的代码拆分成两个函数:A和B。光这一点并不能算是很有效的代码混淆方法,但这段代码漂亮就漂亮在:foobar函数每被调用一次,函数A和B就会互相交换一次位置,如图1-21所示。 图 1-21 攻击者不论是用静态方法还是动态技术分析这个程序都很困难。如果他使用静态方法进行分析(不运行程序),结构混淆、数据混淆和控制流混淆已经把程序的组织结构、数据明文以及控制流结构去除了。而如果他试图运行程序进行分析的话,动态混淆已经改变了他关于程序的一个基本假设,即每当程序运行到内存的同一个位置时,该地址上的代码应该是不变的。 代码清单1-2 动态混淆的结果,在运行时,函数A和函数B的位置会不断地互换。代码中的swap引 自代码清单6-3 1.4.3 被黑客们使用的代码混淆技术 长久以来,代码混淆技术一直都被认为是不能登大雅之堂的奇巧淫技,没有哪个学者会拿正眼瞧它一眼。国际C语言混乱代码大赛(International Obfuscated C Code Contest,IOCCC)[177,227]就是一个很好的例子——尽管每年都有不少参赛选手写出令人啧啧称奇的代码,但是大家对此的评价却总是:“啊!这个很有意思,不过对实际工作有什么用处吗?”当时C语言还被认为是写这类程序的最佳选择 。人们那时以为:代码混淆技术并没有什么真正的价值,使用这些技术的人脑子都坏掉了,想靠这种把戏愚弄攻击者简直就是痴心妄想。但是突然Java横空出世了。Java在带给人们可以在任何平台上运行的便利的同时,由于Java字节码设计的特殊性,使得传统的针对机器码的安全保护技巧一夜之间变得毫无用处。这时人们才想起了代码混淆技术,因为当时在Java平台上,这似乎是唯一有效的代码保护 技术。 不幸的是,代码混淆技术被应用的最成功的领域是在黑客的手上。大家对此应该已经见怪不怪了。好像坏蛋们总能比好人们更快地适应新技术——尽管这些新技术都是好人们设计出来的。比方说密码学吧,它既可以保护执法部门之间的通信,也能够用来保护罪犯之间的联系。又比如隐写术,国家安全部门可以用它保护国家机密信息,但犯罪分子也能利用它以不被发现。目前已知的最早破解基于智能卡的防护技术的人是破解电视机机顶盒的黑客,但是这些黑客转手又使用相关防护技术来保护他们自己的产品免受机顶盒厂家的破解。 Axel对他编写的病毒进行混淆处理,使之免于被Doris发现——这是黑客们手中代码混淆技术的用途之一。如图1-22所示。 图 1-22 病毒由两个部分组成:载荷(payload)和混淆部件(obfuscator),载荷是用来做坏事的代码,而混淆部件则是病毒用来保护自身免于被查杀的。Axel首先要感染程序P,使之携带病毒v,这样得到一个带病毒的程序P ′。然后Axel开始传播P ′。如果Doris在她的机器上运行了P ′,那么病毒就会感染Doris系统中的另一个程序Q。在病毒感染Q之前,会先使用代码混淆技术对自身进行修改,生成自身的另一个完全不同的版本v',然后再去感染Q,产生带毒程序Q ′……Axel这样做的目的在于:如果每一代病毒都是完全不一样的话,Doris的杀毒软件就很难完全查杀这些不同版本病毒。这和之前讨论的系统变形的情形是类似的,只不过攻守的双方互换了下位置罢了。 1. 故意出错——不露痕迹地进行代码混淆 如果你见过IOCCC的参赛代码,就会发现它们与普通代码迥然不同。机器生成的、经过混淆的或者被优化过的代码看上去也是这个德行——与人工编写的代码之间存在巨大的差异。比如说,看看代码清单1-1,你一下子就能断定这些代码肯定不是人写的。所以当Axel分析Doris的程序时,这段代码首先就会引起他的怀疑——密码不是很可能就藏在这种被混淆的代码中吗?在本书的许多案例中,你都可以看到不露痕迹地使用保护技术是多么的重要——不能给对手以提示:我们对这些代码使用了软件保护技术,或者我们按着那样的一个顺序使用了这几种软件保护措施。 如果坏人们使用了这种技术,我们分析嫌疑程序时就很头疼了。黑客们通过故意在程序中插入错误的方式来达到隐蔽地使用代码混淆技术的目的。我们来看一下代码清单1-3,这个程序是用来对“美国偶像”的投票结果进行记录和统计的。该程序从系统的标准输入中读取投票数据,然后经过统计输出投票结果。下面给出的就是该程序的一次运行实例。 代码清单1-3 经过混淆的投票统计代码 这个程序的对投票进行统计的算法正确吗?其中是否含有作弊代码?在阅读提示之前 ,请先花点时间,看看从这个只有58行代码的程序中找出错误需要多少时间?好吧,现在你又认为在现实中,从一个拥有成千上百行代码的投票系统中找出类似的错误要花你多少时间呢?如果代码清单1-3中所示的技术与代码清单1-1中所示的技术被结合起来使用的话,又会有怎样的结果呢?这样是否就能使作弊程序隐蔽地潜伏到投票系统的代码中,而不被发觉呢?也许下一期的“美国偶像”早就已经被内定了。(或者,要是这个作弊程序被嵌入在美国总统选举的计票系统中呢……) 2. 混淆病毒 正如你所见,今天,对于一些很重要的实际问题,代码混淆技术可能是唯一可行的解决方案。但不幸的是,代码混淆技术最辉煌的成就却是在黑客们手中实现的——用于保护病毒、蠕虫、木马和rootkit免遭查杀。观察黑客们研发并在实践中已经证明了其有效性的技术,以及安全研究者们能否再次利用同样技术确实是件很意思的事情。病毒的作者和杀毒软件厂商已经陷入了一场猫捉老鼠的游戏中:每当杀毒软件厂商开发出一种新的病毒检测技术时,病毒的作者们就会使用一种更为巧妙的代码混淆技术来抵御这种检测技术,而这又促使杀毒软件厂商研发更为强大的病毒检测技术……到目前为止,好像还是黑客们在这场游戏中略占上风。根据最新的报道,世界上有25%的计算机曾经被botnet[368]入侵和控制。当然,这一“成就”也不能光归功于恶意软件中使用的代码混淆技术,许多人不及时给操作系统打补丁、不经常升级杀毒软件也是一个很重要的原因。 病毒逃避杀毒软件检测的主要方法是使杀毒软件“看不见” 病毒体中的代码。由于杀毒软件只能占用计算机中极小一部分资源 ,所以它不可能完整地分析硬盘中每个文件。而且即使它能做到这一点,理论研究的结果[74]也表明仍然会有病毒可能漏网。因此大多数杀毒软件都是使用病毒的特征码来识别病毒的。从某种角度上说,如果病毒体的代码在病毒每次感染文件时都不会发生变化的话,特征码确实是一个不错的检测方法。它就类似于软件“胎记”(这一技术将在第10章中详细讨论)。 那么病毒的作者又是使用哪些技术让杀毒软件“看不见”病毒体中的代码呢?我们来看一下代码清单1-4中给出的这个Java程序。当然,在实际生活中几乎不会有人会用Java去编写病毒,我们编写这个例子只是为了帮助你理解病毒是怎样使用代码混淆技术保护自身的。所以也请你大人有大量,原谅我们用Java编写它吧。 在这个Java病毒中有几个做法值得引起我们的注意。首先,我们注意到病毒好像是把自己的整个源码都当成是一个字符串,并把它放在了程序中。为了不至于不断地重复自身导致死循环,编写这个程序的程序员使用了一些代码复制的技巧 。这个技巧在一种源自基内斯 的搞怪表演中也常被使用——在这种表演中,程序运行之后将会输出自己的源码[1]。 源码是被放在一个名为self的变量中的。最终它会被写到一个文件(m.java)里去,并由被感染计算机上安装的Java编译器编译执行。最近出现的Slapper蠕虫[320]及其变种也使用编译器来尽量减少自身对于平台的依赖性。使用编译器对黑客有利的另一个副作用是——使用不同的编译器编译同一份源码时可能会产生略微不同的可执行文件(尽管在语义上这些可执行文件是完全等价的),这就使得我们提取病毒特征码时还要考虑不同编译器之间的差异,进而增加了特征码提取的难度。 代码清单1-4中的代码更进了一步。在把自身复制到m.java文件中之前,它先要把自身用morph函数处理一下。morph函数的作用就是在程序的各行之间都插入一句i++语句。这个语句并不会影响程序的行为或者输出结果,但是这个小花招的确使每个新的病毒样本都与其前一代不一样!这也就是说,只使用了简单的特征码技术的杀毒软件是无法发现这个病毒的。 代码清单1-4 一个Java变形病毒 你可以想象一下:如果这里,病毒的作者没有使用这个简陋的morph函数,而是使用了那些下文详细介绍的、更为狡诈的代码混淆技术,你认为杀毒软件要用到哪些分析技术才能发现这个病毒呢? 实践中,病毒会使用代码混淆技术尽可能地确保每次感染后产生的新一代病毒的许多属性都变得完全不同。我们把这种在每次感染之后都用代码混淆技术把整个病毒体的代码全部改得面目全非的病毒称为变形病毒。而多态病毒可以认为是变形病毒的更进一步优化的版本。因为它不像变形病毒对整个病毒的代码都进行混淆处理 ,而是用不同密钥把整个病毒的代码全都加密起来。当然,为了保证能正常运行,病毒还要自带一个解密模块,多态病毒中使用代码混淆技术处理的就只有这个解密模块。总的来说,在多态病毒中,代码混淆技术只是保护了解密模块免于被杀毒软件发现,而病毒剩下的部分则全部是由加密技术进行保护的。 1.5 防篡改技术 对代码进行混淆处理的目的之一就是要让代码变得足够复杂,从而使攻击者放弃分析程序中的算法或者修改程序的尝试。但是万一Axel成功地突破了Doris的混淆保护,我们又该怎么办呢?Drois除了进行代码混淆之外,还可以对她的代码进行防篡改处理 。这也就是说,当Axel试图去修改Doris的程序时,程序会产生一些出乎Axel意料的行为:比如被破解的程序会拒绝运行,或者会随机地崩溃掉,更有甚者它会删掉Axel计算机中所有的文件,或者再绝一点,它会给Doris家里打个电话告诉她Axel正在破解她的程序…… 通常,防篡改算法要完成两个基本任务。第一个任务是,它要能检查程序是否被修改。要做到这一点,一个常用的方法是计算代码的校验和,然后把这个检验和和正确的值相比较。另一个常用方法是通过检查一些变量的值去确认程序的状态是否处于一个正常范围之内 。 一旦发现代码被修改,防篡改算法就要去完成第二个任务,执行相应的反制措施。比如它可以让程序马上退出。不过这一点并不像它看上去那样容易做到——因为我们还要使这些代码不容易被黑客找到并删掉。比如说不能用下面这种代码: 这段代码实在是不堪一击,因为黑客很容易就能找到使程序退出的代码(就是调用abort()函数的这句),接着黑客马上能从abort()函数出发找到检查代码是否被修改的函数。所以黑客很容易就能把它干掉!一个好的防篡改系统一定是把检测程序是否被修改的代码和反制代码放在程序的不同位置上,而且不会让它们像上面给出的代码那样顺序执行。 1.5.1 防篡改技术的应用 防篡改技术在数字版权保护系统中扮演了一个极为重要的角色。因为在这一系统中,对代码的任何修改都可能使Axel能免费欣赏被保护的音乐或电影。下面这段代码模拟的就是数字版权保护系统的大致结构。 注意,在代码中Axel已经把检查用户是否已经付费的代码(浅灰色)注释掉了。当然,他也可以在深灰色标出的位置上插入他自己的代码,用以获取解密密钥,以及解密出来的数字/模拟信号。大体上讲,防篡改技术就是要保证程序中的代码不被删掉,不被插入新的代码,原有的代码不被修改。因为这些操作都会给程序带来毁灭性的影响。 防篡改技术的另一个应用是保护软件的使用许可。Doris在她的程序中会加入一些软件使用许可代码,以防止Axel超过试用期限或者试用次数之后继续使用她的软件或者它也可以用来限制Axel的公司里同时最多只能有几个人使用Doris的软件。而Axel则会千方百计地去找到软件使用许可代码,删掉它,让自己能随意使用Doris的程序。Doris可以对她的代码进行混淆处理(增加Axel的破解难度),也可以使用防篡改技术保护软件使用许可代码,这样如果Axel找到并且删掉了软件使用许可代码,程序就不会正常工作了。如图1-23所示。 图 1-23 可是有时只能做到让程序在被修改之后就拒绝运行还是不够的。在某些情况下,我们可能还需要警告软件的开发商:“有人正在试图对软件进行破解”。比如在网络游戏中,可能是出于网络数据传输效率比较低的考虑,游戏的客户端程序往往会把一些不能让玩家看见的信息(比如整张地图)缓存在玩家本地的计算机上。同时肯定会有某些玩家会希望能看到这些信息,使自己在游戏时能更加得心应手。但是对于游戏的发行商来说,如果游戏中作弊盛行的话,肯定会使正直的玩家感到失望而不再玩它。所以,网游管理员一个重要的任务就是,一旦发现有人作弊(比如修改了客户端软件)就立刻把他踢出去。我们把这类问题统称为远程防篡改技术。目的是确保运行在不可信任的主机上的是一个正确且没有被修改过的程序,而且还是运行在一个“安全的”环境中的。这里所讲的“安全的”是指程序运行在正常的硬件(而不是黑客的模拟器)上的,操作系统也处于一个正常的状态,即系统中所有的环境变量的值都是正常,程序也没有被附加调试器……如果客户端程序确认自己是运行在一个安全的环境下,而且自身也没有被修改的话,它就要隐蔽地向服务器发送一个“平安无事啰~~~” 的消息。如图1-24所示。 图 1-24 在图1-24中,客户端程序S使用隐写术中的一些技巧,把表示“平安无事”的位串嵌入在TCP/IP包的首部里发送给Doris。 在一些极端的情况下,甚至都不用去惩罚作弊的玩家,只要监视被保护的程序,看它是如何被破解的就很有用了。光是我就知道至少一家以软件保护技术为主营业务的公司在他们的代码中嵌入了这类代码,使他们能够实时地监视软件被破解的整个过程。他们会记录黑客在破解过程都使用了哪些技术,以及其中的哪些在破解中起了作用。以此为基础,他们还会进一步评估哪些保护措施是有效的,而哪些没有取得预期的效果。而且我们认为,在其他软件保护公司中也会有类似的活动,但他们大都会选择回避这类问题,因为这里面涉及个人隐私问题。 1.5.2 防篡改技术的例子 如果曾用过经过签名的Java applet或者安装过微软签名认证的程序,那么你实际上就已经拥有使用防篡改技术的软件的经验了。经过签名认证的程序都带有一个软件作者签发的证书,通过这个证书我们就能检测程序中的某些部分是不是已经被修改过了。通常这类检测都不会由程序自己来完成。例如,安装一个微软签名认证的程序,那么操作系统就会去检查程序的签名是不是微软认证的以及程序在被签名之后是不是被修改过了。可能有人想更进一步——让程序能自己检查自己是否被修改,可惜,这类尝试大都以失败而告终。比如下面给出的代码清单1-5,这个Java程序的作用是华氏温度转换为摄氏温度。其中的函数bad_check_for_tampering就试图在程序中检查自身代码的校验和是否正确,以判定程序是否被修改,并且让程序在发现被修改之后马上退出。 代码清单1-5 一个进行自我检查的Java程序 编写这样的程序时可能遇到什么问题吗?我们将要面对的一个难题是:程序的校验和是嵌入在程序之中的,它的值也会改变程序的校验和!所以我们必须要在程序写完之前就知道程序校验和的值,但是计算程序的校验和又必须等到程序写完之后!好一个“先有鸡还是先有蛋”的问题! 另一个问题是:程序的结构过于简明,一旦黑客认出这个函数是用来防篡改的,就能轻易地把它删掉。 函数better_check_for_tampering的情况稍微好一点。它并不只是检查程序校验和是否正确,而是把检验和融入到了程序的计算结果中。如果程序被修改了,使用该程序转换出来的摄氏温度就不正确了。 尽管如此,函数better_check_for_tampering还是要面对一个和函数bad_check_for_tam- pering 一样的问题——这两个函数都很容易被发现,因为几乎没有一个正常的程序会要去读自己!而一旦被黑客发现,他就会直接把它的返回值换成一个常量 。之后,黑客就能为所欲为地修改程序了。 尽管存在这样那样的问题,检查代码校验和仍是实践中许多防篡改检测技术的基础。在第7章里还会多次见到这位“老朋友”。 1.6 软件水印 在很多种情况下,你都需要在自己的东西上做上标记以表明你对它拥有某种权利。其中,大家最熟悉的莫过于政府在纸币上做的水印了。这些水印往往是嵌入在纸币中的,因此也很难被破坏或者复制。比如,要是别人找给你一张破旧的纸币,你往往会把它举起来,对着光亮处检查纸币中的水印,以此来确认纸币的真伪。同样,如果有人想要使用复印机印制伪钞的话,那它制作出来的伪钞就肯定没有水印,很容易被人识破。 数字水印也是很有用的。我们通常需要在电子出版物(比如图片、文本、视频或音频)中嵌入一个唯一的标识。比方说,Doris是个网上音像店的老板,当Axel去她的店里买一首歌时,Doris会在卖出去的这份副本中嵌入两个标记,一个是版权声明A(在Doris卖出去的每一份副本中都有这个标记),它表明Doris拥有这份副本的版权,另一个是客户标识B(这个是专门针对Axel),它是用来防止Axel非法复制这首歌,并把非法复制品转手再卖出去的。如图1-25所示。 图 1-25 要是Doris发现了一份这首歌的非法副本,她就可以通过从非法副本中提取到的客户标识B(B有时也被称为指纹)找到Axel并把他告上法庭。要是Axel还嘴硬:“老子才是原创!”。Doris就可以继续提取出版权声明A,驳倒Axel。 数字水印算法一般都要利用人类感知系统的局限性。假如要在一段音乐中嵌入水印,我们就要在这段音乐中加入一些对于人耳来说难以察觉的短促回声。可以用相对而言更短些的回声表示0,而用相对而言较长的回声来表示1。又比如在PDF文件中,我们可以通过调整文本中各行的行距来嵌入水印,如用12磅行距来表示0,用12.1磅行距来表示1。而对于一个图片来说,我们可以通过调整(一组)像素的亮度方法来嵌入水印。从上面几个例子中可以看出:要加入水印,就必须对原始文件进行必要的修改。一般会使用一个伪随机数发生器从原始文件中挑选出一系列位置,用以承载水印。而用来产生伪随机数的种子就是提取水印的密钥,没有它是不可能从文件中提取出水印的。所以典型的水印系统都至少由两个函数组成:嵌入函数和提取函数。如图1-26所示。 图 1-26 这两个函数都要输入密钥,嵌入函数还要把原始对象[又称为载体(cover object)]和水印[又称载荷(payload)]作为输入,最终生成一个添加了水印的对象[又称为隐秘对象(stego object) ]输出。而提取函数,顾名思义,则是以隐秘对象和密钥作为输入(用来将水印提取出来的函数)。上面介绍的就是一个基本的数字水印系统,其他更复杂的数字水印系统将在第8章中讨论。 根据以往的经验,我们也绝不能低估Axel的本事。Axel盗版Doris的电子出版物的时候必须要确认他已经干掉了Doris嵌入的所有水印。更准确地来说,他必须要以某种方式干扰数字水印的提取过程,使得Doris即使输入了正确的密钥也休想从中提取出水印来。在这类攻击中,Axel通常都会往隐秘对象中掺入大量干扰信息,这些干扰虽然还是很小,令人无法察觉(否则就不会有人去买Axel制作的盗版了),但是已经足以使Doris再也无法提取水印了。比如Axel可以随机地调整PDF文件中各行的行距,在音频中混入大量人耳不易察觉的回声,或者把图片中所有像素的最低有效位全部设为0。数字水印技术的研究就像是好人和坏蛋之间的一场游戏,好人们想方设法地研发出稳定、健壮的水印算法,而坏蛋们则千方百计地要破坏这些水印。同时,不论是好人还是坏蛋都必须尽力使他们的行为不至于影响到大众对电子出版物的阅读。 当然,本书中我们关心的是软件水印而非数字水印。但是它们之间许多原理是一致的。比如对于给定的程序P,水印w和密钥k。将它们输入到嵌入函数中去之后,产生一个新的程序Pw。我们希望Pw在功能上与等价P(拥有相同的输入/输出行为),只是比P的个头稍微大点,运行起来略微慢点而已。当然P中还要包含水印w,即把Pw和密钥输入到提取函数中去之后,能够返回水印w。 1.6.1 软件水印的例子 在下面给出的代码清单1-6中,你能找出几个水印"Bob"?我这样问你确实不公平。因为前面已经讲过,假设Doris所使用的算法是公开的,而唯一需要保密的是她输入程序的密钥。尽管如此,我还是要请你认真地读一下下面这段程序。首先,有一个东西很扎眼——有个字符串就叫"fingerprint"!有人现在可能已经在想了:藏的不咋地啊。不过这样实现的话,嵌入和提取水印倒是挺方便的,而且要是没别的用处的话,把它当成一个麻痹Axel的烟幕弹,使他因此就停下继续深入分析其他水印的脚步也不错。 代码清单1-6 软件水印的例子 再看看还有什么吗?我们来看code方法。Doris可以令a = 0, b = 1, …, o = 14, …, z = 25,这样就能把字符串"Bob"当成一个二十六进制数了,因为bob26 = 1• 262 + 14• 261 + 1 = 104110 ,所以二十六进制数"Bob"就等于十进制数1041。然后Doris再把1041变成整数集合<0, 1, 2, 3, 4, 5, 6>的某个排列顺序: <0, 6, 5, 4, 2, 1, 3> 接下来,Doris就可以用这个排列顺序来调整code方法里switch语句的各个case分支出现的先后顺序了。如果需要提取水印,Doris只要把上述步骤反过来操作一遍就可以了。首先她把嵌有水印的这个方法找出来 (输入提取函数的密钥应该就能找出这个方法),然后从switch语句中把这个排列顺序抄出来,并把它映射成十进制数1041,最后把1041解码成字符串"bob"。有很多种使用类似想法的水印嵌入技术,它们都是通过对程序中的某个方面进行重新排序的方法来完成水印的嵌入的。比如,最早公开的水印算法[104, 263](微软的一个专利)就是通过重排某个函数控制流图中的各个基本块在代码中出现的先后顺序来嵌入水印的。我们将在8.4节详细讨论这一算法。 最后让我们看看x=1-(3%(1-x)),这个语句到底是个什么玩意?实际上,Doris是使用了下面给出的这张转换表(图1-27),它把英文字母转换成了二进制的操作数/码对。 图 1-27 因此,由3个字母组成的字符串"bob"就变成了3个操作数—操作码对:1-、3%、1-,把它们连在一起就成了x=1-(3%(1-x))。这一方法与Monden等人提出的一个算法[252,263]颇有神似之处,我们将在8.7.1节详细讨论这一算法。 到现在为止,我们所讨论的3种水印嵌入算法都是静态的——我们直接把水印嵌入在程序的代码中。在上例中就是把水印嵌入程序的源代码里,但也可以把水印嵌入任何一种我们想要的程序表示形式(比如二进制可执行文件、Java的字节码或者任何一种在编译器中使用的程序中间代码)中去。第8章中将更加深入地讨论静态水印算法。 在这段代码中,最终发挥作用的是一种动态水印。为什么说它是动态的呢?因为这个水印只有在程序运行时,当程序接收到一个特定的输入时水印才会显示出来。在这个例子中,只有当Doris执行这个程序,并且输入密钥42时,语句 才会被执行,显示出嵌入的水印,如图1-28所示。 图 1-28 在第9章中,我们将会详细讨论这类水印。在实际运用中,动态水印当然不会像这个例子中这么简单:对于Axel来说找到弹出带有字符串窗口的代码简直太轻而易举了。相反,水印是隐藏在程序执行时的某个特定状态(寄存器、栈、堆等)中的。而程序的这一状态只有在程序获得了一个指定的而且是秘密的输入时才会被构造出来。我们只有使用调试器或者专用的识别器才能去检查程序的状态并从中提取出水印来。 1.6.2 攻击水印系统 和在其他各类安全场景中一样,你必须把各种可能用来攻击水印系统的方法都考虑到。Doris现在理所当然要假设Axel在销售盗版软件之前一定会尽力去抹掉软件中嵌入的水印。而且很不幸的是,有一种百试百灵的攻击方法,总能抹掉软件中的水印。你能想到它是什么吗?为了确保盗版软件中不包含水印,Axel只要重写一遍这个程序就可以了! 我们把这称为重写攻击。如图1-29所示。 图 1-29 Axel也可以把自己的水印加入程序中,我们称这一方法为掺假攻击(additive attack),如图1-30所示。 图 1-30 掺假攻击可能会糊弄Doris的识别器。但更重要的是,由于程序中有多个水印,那怎么在法庭上证明确实是Doris而不是其他什么人拥有软件的版权呢? 扭曲攻击则是对程序中的代码进行保持语义不变的转换(比如对程序进行代码优化,代码混淆等),以此搞晕Doris的识别器。如图1-31所示。 图 1-31 最后,Axel还可以使用一种专门针对客户指纹的比对攻击 。即Axel分别购买同一个软件的两份副本,然后把这两份副本进行比对,找出其中不同的部分,这些不同的部分就很可能是嵌入客户指纹的位置。如图1-32所示。 图 1-32 针对这种攻击方法,Doris可以对同一个程序的各个副本使用不同的代码混淆技术,以此确保即使比对使同一程序的不同副本也得不到什么有用的信息。 还有一种并不直接攻击水印本身的巧妙方法。程序还是Doris加了水印的程序P’,但是Axel可以假装说程序中包含有他的水印。比如Axel可以自己写一个识别器,让这个识别器识别出程序P’中包含有Axel的水印。如果Axel成功地写出了这样一个识别器的话,我们就不能分辨哪个识别器是真的,哪个是假的。而Doris也就拿不出软件版权属于她的证据来了。 在第8章和第9章中,将会介绍很多水印算法。这些算法中,有些必须水印整个应用程序,而有的则可以只水印程序的某个部分。有的适用于二进制可执行文件,而有的则专为字节码而设计。有的能嵌入隐秘的水印,而有的则能嵌入大量相互冗余的水印。有的嵌入的水印很难被擦除,而有的会使因嵌入水印导致的程序性能开销很小。但就是没有一种算法能够满足用户的所有需求。因为(设计一种能满足所有各种不同需求的水印算法,)这对于软件水印的研究者们来说实在是个不小的挑战。 1.7 软件相似性比对 还有一类并不使用基于代码转换算法的软件保护问题,我们将其统称为软件相似性分析。从概念上说,这些问题的共性是它们都依赖于你能否判定两个程序是不是非常相似的,或者其中的一个是不是(部分)包含于另一个中的。我们用下面这两个函数来描述这一过程,相似度(similarity)和包含度(containment),如图1-33所示。 图 1-33 1.7.1 代码剽窃 我们通常把学术领域里的偷盗活动称为剽窃——学生抄袭别人的作业、研究员冒用他人的研究成果……这一现象在每个人类的创造性工作领域中都会发生,总是会有人想“走走捷径”、“借鉴”一下别人的创造性工作的成果。在艺术领域中就是复制。比如引用一段别人谱写的乐曲,照搬别人家具的样式,或者模仿他人的设计风格……迄今为止,已经有不少著名的作家、艺术家、音乐人因在其作品中含有太多其他同行的东西而被告上法庭。这句话中“太多”的准确含义就是离法律规定的标准实在是太远了。其中一个著名的案例是关于John Fogerty的。在这个案例中,由于他出品的新专辑听起来太像她自己以前的,已经申请了版权保护的作品了,而被指控抄袭了自己的作品(即自我抄袭)。 本书中,我们关心的是代码剽窃。这一现象经常发生在计算机科学技术领域。比如很多学生都觉得“参考”一下同学的作业比自己独立完成作业要方便得多——坏学生Axel会把Doris同学的编程作业Q中的某一段复制下来,粘帖到他自己的作业P中,然后把P交给老师。如图1-34所示。 图 1-34 如果学生很多,计算机课老师想要靠人工的方式逐一比较所有学生的作业,基本上是不现实的。但他可以写一个工具(作为一个程序员,他也习惯于把事情尽量交给计算机处理),自动地把学生的作业逐一配对比较,比方说这个工具可以输出图1-35所示的结果,把比较的结果按相似度从高(最有可能是抄袭的)到低(最不可能抄袭)的顺序排列出来。 图 1-35 自动比对是批量排除哪些基本不可能是抄袭的配对的最好方法,老师只要手工分析剩下的少数几个极有可能是抄袭—被抄袭的作业对就可以了。 有些学生可能已经知道老师会用这种方法来对付他们 ,于是他们也会想办法逃避检测。他们可以进行一些简单的代码转换——比如改变某些变量的变量名,或者重新排列函数的调用顺序都是一些常见的伎俩。因此对于一个软件相似性比对工具来说,正确识别这类变换的能力是很重要的。 1.7.2 软件作者鉴别 软件作者鉴别(software forensics)就是要回答这样一个问题:“在这些程序员中,是谁编写了程序S?”为了回答这个问题,你必须先拿到所有可能编写程序S的程序员的代码样本,如图1-36所示。 图 1-36 从这些样本出发,你可以提取一些你认为可以唯一的标识每个程序员的特征,然后把这些特征和从程序S中提取出来的特征相比较,以图1-36为例,辨认的结果很可能就是图1-37中给出的这个样子,即程序S的作者最有可能是Axel,而不是Doris或者Carol。 图 1-37 图1-37中f 是用来从程序中提取各类特征集的函数。实践中,特征集中一般都含有每行代码的长度、函数的大小或者花括号的位置等信息(尽管对这些特征能否准确地指明作者的身份这一点还有激烈的争论)。 尽管现在大多数的软件鉴别算法都是针对高级语言的源代码的,但研究针对二进制可执行文件的鉴别方法也是很有前途的。例如,FBI抓到了一名编写恶意软件的疑犯,他们可能想要把这名疑犯的编程风格和所有已知的病毒相比较,以便尽可能多地找到这家伙编写的病毒,进而在法庭上提供更多对他不利的证据。 1.7.3 软件“胎记” 在之前讨论的案例中已经涉及了一些代码剽窃的内容,所谓“代码剽窃”就是你的竞争对手把你程序中的一个模块M复制到他自己的程序Q中,如图1-38所示。 图 1-38 代码混淆和水印技术都能使实施这类攻击变得很困难。攻击者很难从经过混淆技术处理的P中找到M。即使找到了,也很难从P中把它“干净的”剥离出来——你可以把M和程序中的其他模块混在一起,使得任何自动代码提取工具在提取M的代码时都要不得不拖泥带水地带上大量与M不相干的代码。 你也可以在M中嵌入一个水印或者指纹。比方说,M是一个由第三方(你)开发的图形显示加速模块。你把它卖给了Doris,允许她在其开发的游戏中使用它。要是哪天你在Axel的游戏中发现了Doris的指纹,你就可以断定Axel剽窃了你的代码,而且这些代码是复制自你卖给Doris的产品的。接下来你可以以代码剽窃的罪名把Axel送上法庭,也可以起诉Doris,因为她没有践行软件使用许可协议,使用有效的保护措施防止你的代码被他人剽窃。 可是,出于种种原因,你可能选择不对你的代码进行混淆处理,也不把水印嵌在它里面。比如,代码的执行效率是一个至关重要的指标,又或者使用混淆技术会让你调试程序或对软件进行质量评估变得非常困难。再或者你只是想知道一些很久之前编写的 老代码是否有人剽窃。除了混淆和水印技术之外,你还可以在攻击者的程序Q中搜索一下,看看你的模块M是否包含在Q里面。如图1-39所示。 图 1-39 这一招很管用,除非你的对手已经预计到你可能会使用这一方法,并且针锋相对的使用了一些代码转换技术——比如对M(或者Q中的所有代码)进行混淆处理,使你难以在Q中找到M。如图1-40所示。 图 1-40 经过一番复杂的代码转换之后,只是简单地搜索Q就不管用了。如图1-41所示。 图 1-41 于是利用软件“胎记”进行检测的方法就应运而生了。这一方法的基本思路是分别从Q和M中提取一些“特征”,然后把M的特征和Q的特征相比较,而不只是简单地在Q中寻找M。如图1-42所示。 图 1-42 图1-42中的f 是从程序或者模块中提取特征的函数,我把这些特征称为软件“胎记”。之所以把它们称为软件“胎记”,是因为这些特征是软件与生俱来的,即使软件经过了一些常见的代码转换(比如代码优化或者混淆)也能保持不变的东西。 仅就我所知就至少有一个利用软件“胎记”成功地证明代码剽窃的案例。20世纪80年代,IBM控告其竞争对手剽窃了PC-AT ROM上的代码[128]。IBM的人员作证道:对手程序往寄存器里存入和读取数据的顺序 以及有关指令所使用的寄存器与IBM开发的程序使用寄存器的方法是一致的。而读写寄存器的顺序以及所使用的寄存器实际上就是程序的“胎记”之一。同时他们也证明如果指令序列push R1; push R2; add 和 push R2; push R1; add在语法上是等价的,那么如果一个程序使用了push R1; push R2; add这个序列的话,另一个程序也使用push R1; push R2; add这个序列的概率p就只有50% 。如果这个指令序列越长,使用的通用寄存器越多,那么等价的指令序列就会越多,进而概率p的值就会进一步降低。要是指令序列足够长,使用的通用寄存器足够多,p就会降低到一个几乎不可能发生的地步。 1.7.4 软件“胎记”的案例 为了保证“胎记”的有效性,有关算法必须从哪些不易被攻击者篡改的程序的本质特征着手提取“胎记”。一种很常用的方法是把程序对标准库函数或者系统调用的使用情况当成软件“胎记”予以提取。这一方法将在第10章中详细讨论,这里只是用一个简单的例子演示一下。基本思路是由于标准库函数或者系统调用所实现的功能很难被攻击者用自己的函数所代替,所以软件对这些函数的使用情况可以被当成它的一个“胎记”。比如在Unix系统中写一个文件最终都是要通过调用write函数来实现,所以程序对write函数的使用情况就不会被任何保持语义不变的代码转换算法所修改。 我们来看下面这个C函数,这个函数从一个文件中读出两个字符串,并把它们都转换成int型整数,最后输出这两个数的乘积。 有好几种可行的“胎记”提取方法。比如说,可以把“胎记”定义为程序调用各个标准库函数的次序,予以提取,如下。 不过考虑到有些库函数之间是没有依赖关系的,攻击者可以调整这些库函数调用的顺序这一事实,我们也可以忽略掉库函数调用的顺序,如下。 再或者你也可以认为软件“胎记”是它所使用的系统调用的集合以及各个系统调用被调用的频率。据此,你也可以再把各个库函数被调用的次数加上来,如下。 攻击者拿到这个函数之后,会把它复制到他自己的程序P中,然后进行一些代码转换,妄图糊弄我们。在如下所示的这个例子中,他把函数名改成了f,在函数中增加了对gettimeofday和getpag- esize这两个函数的调用(使用这两个函数不会影响到这个函数的功能和运行结果),改变了fclose 和atoi函数的调用顺序,最后还额外多调用了一次fopen 和fclose函数。 在上面的代码中,攻击者增加的库函数的调用用深灰色标出。假设P的其余部分(函数y和z)中没有调用标准库函数,那么就得到了程序P的“胎记”: 要确定程序P中是否含有函数x,只要计算一下containment(bmi(x), bmi(P))就可以了,这个containment函数返回一个0.0到1.0之间的值,它表示P中含有x的几率。在这个示例中,我们看到尽管攻击者用尽了各种方法去掩盖其踪迹:他修改函数被调用的顺序,增加被调用函数的种类,改变各个函数被调用的频率,但是他的剽窃行为还是被我们利用软件“胎记”成功地识别了出来。 1.8 基于硬件的保护技术 设计一个牢固的软件保护技术的困难之处在于,我们是在流沙之上修筑城堡!软件保护和密码学之间的不同之处在于,在密码学中,有一个基本假设:攻击者永远都拿不到密钥。所谓“成也萧何败萧何”,在所有密码学成功/失败的案例中都有坚持/违反这一基本假设的因素。但在软件保护中,代码混淆也好,水印也好,防篡改也好,它们终归会被攻击者击破,究其原因就是你的程序必须要在攻击者(他甚至可以是你的客户)那里运行,所以他最终还是能够获得你的代码的。 基于硬件的保护技术则试图改变这一情况。通过给数据、代码或可执行文件提供一个安全的环境,可以使被保护的软件能像你之前看到的那样:免于被逆向分析,不被篡改,或阻止代码剽窃事件的发生。所不同的是,现在你可以使用一些可信硬件,并由此出发,构建一个坚实的软件保护方案。 1.8.1 把硬件加密锁和软件一起发售 导致软件盗版猖獗的根本原因在于,数字产品是非常容易被复制的。在现实中还没有哪样东西是这么容易被复制的。当然,你不能把上海的襄阳路市场算在里面。可是不管怎么说,要仿制衣服、古董之类的商品总要有一定的技术和设备吧!但在虚拟世界里,你只要会用copy命令就行了。 有一些软件反盗版技术是围绕着“程序运行之前,用户必须先证明自己拥有某个硬件设备,否则程序就会拒绝运行”这一思路展开的。因此,你可以把程序分成两个部分:很容易被复制的二进制代码和一个很难被复制而且程序执行时必须使用的硬件设备。在程序运行过程中,这个硬件设备必须一直以某种方式连接在计算机上,以便程序能随时抽查用户是否拥有指定的硬件设备。如图1-43所示。 图 1-43 为了保证硬件设备的不被仿造,你必须用专门的特殊工具去制造它。常见的这类硬件设备有两种:软件狗和程序的存储介质(软盘、CD或者DVD)。软件狗一般是通过某个接口(现在大多数的软件狗使用的都是USB接口)与计算机相连的。而程序的存储介质,比如CD则是程序运行时必须要放入CD-ROM中的,一般这种光盘都是使用特殊工艺制作的 ,使人很难把其中的信息完全复制 出来。 但是这类防盗版技术除了在一些价格非常昂贵的程序上使用之外,很少有人问津。造成这一现象的原因是多种多样的。首先,这个技术会让正版软件的用户感到很不爽——因为这使他们在使用自己花钱买来的软件时很麻烦!特别是如果谁的USB接口/光盘坏掉了 ,或者忘了把软件狗/CD放在哪里了,他就不能使用这个软件了。其次,这个技术还让软件的开发商恼火:因为不但制造软件狗或者专门的防复制CD需要增加额外的成本,他们还要有专人来处理那些丢了软件狗/CD的客户的投诉。而且,这一技术也使他们必须借助物流或者在世界各地设置销售网点才能销售他们的软件。 1.8.2 把程序和CPU绑定在一起 如果制造CPU的厂商在他们制造的每一块CPU上都带有一个唯一的标识的话,我们就能用这些唯一的标识来处理我们的程序,使卖出去的每个程序都只能在指定的CPU上运行,从而解决软件盗版的问题。在实践中,一种具有代表性的做法是,在CPU中添加一个硬件的解密部件,在这里面含有一个被用作解密的私钥的唯一标识。而Doris卖软件给Axel时则要用Axel的公钥对程序进行加密。如图1-44所示。 图 1-44 这样做的结果是,即使Axel把软件偷偷地卖给了他的朋友,但是由于朋友CPU上解密的私钥与Axel CPU上的不同,他的朋友还是运行不了Doris的程序。 当然,这个办法也并非完美无缺。首先,要在每块CPU上都带上一个唯一标识,将使CPU的生产过程变得极为复杂。当然我们可以用“在CPU上预置一个PROM ,等CPU生产完毕之后再把每块CPU的唯一标识写入各自的PROM”这一方式解决这个问题。其次,由于这种办法制作出来的每份软件都不一样,对于现在已经习惯从网上购买并从网上下载软件的用户来说,这类软件的购买方式过程也实在是复杂了一点。最后,要是用户升级他的机器,买了一块新的CPU怎么办?由于CPU里的私钥已经变掉了,他是不是要把他之前买过的所有软件都换成新的CPU才能解密的版本?这也就意味着他必须还要能以某种方式通知软件的开发商:原来那块老CPU已经作废,我已经开始使用一块新的CPU了。 1.8.3 确保软件在安全的环境中执行 在本书提及的例子里,Doris假定Axel的计算机不是一个安全的环境,但是她的程序却必须在这上面运行。说它不是一个“安全的”环境是指在Axel的计算机上可能存在一些分析工具(比如调试器、十六进制编辑器、模拟器或者虚拟光驱工具),使用这些工具Axel就可以制作软件的盗版,修改软件或者对Doris的软件进行逆向分析。 要是Doris能判定Axel的计算机是否安全就好了。这样,她就能放心地让合法用户执行她的程序而不必担心她的程序是不是会被黑掉。但是想要靠纯软件来实现这一点是比较困难的,因为这个用来检查Axel计算机是否安全的程序本身就有可能被黑掉!所以我们必须在Axel的计算机里装上一个小小的可信硬件,用它把Axel计算机里运行或安装的所有软件的列表发送给Doris。如图1-45所示。 图 1-45 首先,Axel会发送一个请求给Doris,要她把程序P传过来。对于这个请求,Doris的回答是:“先别忙,先证明一下你的运行环境是安全的!”于是在可信硬件的帮助下,Axel的计算机把自己安装的所有安全相关的程序/固件的制成清单发送给Doris。Doris则对这份清单进行审查,只有当她确认Axel的计算机是一个安全的执行环境之后,她才会把P发送给Axel。 在第11章中将继续讨论这一技术的相关细节。但仅凭上述这点信息已经足够找出这一方案的一些本质缺陷了。首先,任何要运行Doris软件的计算机上都必须安装额外的硬件,这显然是要花钱的!其次,Doris必须要维护一份白名单——所有可以信任的程序的清单,或者一份黑名单——所有不能信任的程序的清单。各种不同的硬件平台、操作系统及其相关配置环境,以及在各种平台上运行的程序的数量是如此之多,光要做到这一点,就会变成Doris的一场恶梦。最后,我们还要考虑隐私的问题——并不是每一个用户都愿意让Doris知道自己是在哪种环境下使用她的软件的。 还有一个可能令许多人心神不宁的额外问题,这个方案在数字版权保护系统系统中表现得可能过于完美。假设P是一个实现了数字版权保护系统的播放器,而上述方案可能使Doris能够确认Axel不可能通过破解P拿到解密后的多媒体信息。这就使Doris自信心大增,继而允许Axel下载所有加密后的多媒体文件。这样,一旦系统被破解(理论上这是不可避免的)损失可就大了。 1.8.4 加密可执行文件 软件保护的终极解决方案就是把程序加密起来。只要程序一直保持加密状态,攻击者就无法修改它、分析其中使用的算法或制作盗版软件,因为这个程序根本就不能运行。但是你终究是要让你的客户(和潜在的攻击者)运行程序的。这就意味着他们必须要能拿到解密的密钥,把程序变成明文。但是这样,他们就又能随心所欲地攻击你的软件了。Game over! 所以,为了使用加密技术保护软件,软件必须在整个生命周期中一直保持加密状态,只有在被CPU执行的一瞬间才被解密出来。如果CPU中带有一个加密程序的密钥就好了。此外这个密钥还必须被封装在CPU内部,拿不出来,否则黑客还是能拿到程序的明文。理论上的这样一个加密处理器应该是如图1-46所示的这个样子。 图 1-46 程序(和其他敏感信息)在内存中是以密文的形式存放的。在处理器的内部有一个加/解密单元分别连接内存和处理器内部的缓存。在CPU从内存中读取指令/数据时,这个加/解密单元负责把它们解密出来,再放到处理器内部的缓存中。当CPU往内存写数据时,这个加/解密单元又负责把数据加密起来,再写入内存。 就像你将在第11章里看到的那样,要想让这个方案正确地运行起来,任何离开CPU的东西都必须被加密起来。CPU需要访问的内存地址在地址总线上出现的顺序很可能泄露一定的信息,而被加密的数据通过数据总线的情况也会泄露一些信息。如果这些信息不加以处理的话,除非你能在每次读取数据时都进行检查,确保其没有被破解者修改过,否则攻击者就有可能通过修改加密后的程序来完成破解(详见11.3节)。 所有这些额外的机制显然都要你付出代价——增加性能开销!比如现代处理器是根据计算机程序的“访问局部性原理”进行设计和优化的。如果你为了隐藏程序访问内存的模式而搅乱相关数据在内存中的位置,缓存和通过调整数据存储位置而进行的优化工作就全部白费了。另外还有一个问题是:你准备怎么说服客户为了保护你的代码的安全而花钱把他们的处理器“升级”成运行速度反而更慢的加密处理器呢?或者即使客户这样做了,我想过不了多久,他们还是会把他们的机器换成普通PC机的。事实上,加密处理器的最重要的应用领域是在金融行业中,比如ATM机上。 1.8.5 增添物理防护 任何一种使用加密处理器的软件保护系统都假设:黑客永远拿不到藏在CPU里的加密密钥。但攻击者们绝不会就此认输,他们一定会千方百计地把密钥搞到手!比方说智能卡吧。自从智能卡被用作储值卡、公交电子车票并在付费电视系统中广泛使用之后,由于破解它们能带来很大的经济利益,迄今为止,已经有许多针对智能卡的攻击方法被开发了出来。 攻击加密处理器的方法可以分为侵入式攻击和非侵入式攻击两种。所谓“侵入式攻击”就是把CPU上面的塑料封膜撕掉,直接操作CPU的电路板,进入到CPU的内部。这样嵌入在CPU内部的所有秘密(比如密钥)或算法都会被黑客搞到手。而所谓“非侵入式攻击”则不会去破坏CPU的包装,而是给CPU输入大量的代码或数据,或者让CPU在设计者没有考虑到的环境和条件下运行,所有这些的目的只有一个:分析CPU中的隐含的秘密。比如,常见的一些非侵入式攻击手法会去改变输入CPU的电压,给CPU输入一个错误的时钟信号,或者把CPU放在X光下照射。这些举动都会使CPU以一种设计者始料未及的方式运行,这样攻击者就能分析出隐藏在CPU内部的代码或数据了。 因此加密处理器还应该自带物理保护设备,抵御这类攻击,如图1-47所示。 图 1-47 如图1-47所示,如果温度、电压、时钟信号或者周边辐射量不正常时,感应器就会向CPU发出警告。如果CPU能够确认这是攻击,它将销毁掉所有秘密信息(包括密钥),关机甚至于自毁。为了防止暴力攻击,处理器也会自带一个牢固的封壳,使他人很难把它拆开,露出电路板来。这些封壳也有感应器,能警告CPU它正在被探查。 可惜,增添的这些物理防护也会影响CPU的性能!通常CPU的时钟频率越快,它的发热量也就越大。而厚厚的物理防护层又会直接影响CPU的散热性能。于是这种加有物理防护装置的CPU的主频就不得不比正常的CPU要低很多! 1.9 小结 读到这里,你已经看到了很多使用软件保护技术保护代码的理由,当然你也应该看到不少不这样做的理由了。在最后,你必须要权衡一下利弊,决定是不是要在你的项目中使用软件保护技术。 1.9.1 使用软件保护技术的理由 正方观点:在很多情况下,即使只是稍微延缓一下对手的破解速度也会给你带来不少的好处。比如现在的计算机游戏产业,一般在游戏上市后几天或者几周之后就会出现盗版,要是公司有内鬼的话,盗版甚至会比正版更早上市!但是游戏的玩家们关心的是能不能玩到最新最好的游戏,这就使游戏的大部分销售收入是在它上市之后的最初几周里获得的。所以,尽管使用保护技术后,盗版仍会在新游戏上市之后很短的一段时间之内出现,但游戏的开发商们却仍然很愿意在他们的产品中使用软件保护技术。 又比如,你编写的程序价值很高。这时你也需要使用软件保护技术。要是你的软件一套就要卖100 000美元而且一年也只卖出去几十套的话,只要其中有一套被人拿去做盗版,你就受不了了。在这种情况下,一般你会把一个防盗版的特殊硬件(比如软件狗)和你的软件一块儿卖,而且还会对软件中检测软件狗的代码进行混淆处理,防止别人破解你的软件。 在有些情况下,你需要使用软件保护技术防止你的软件被一些破解技术的爱好者破开。由于法律手段对这些破解技术爱好者基本上没什么用——因为他们要么是在国外,你抓不着,要么就是个穷光蛋,告倒他对你也没什么好处。所以你需要用一些软件保护技术来防止你的软件被破解。在另一些情况下,你可能又需要在软件保护技术的帮助下去告某些人。就是说,公司盗版带来的损失可比个人盗版带来的损失要大得多!有些公司明明只买了10套软件,却想让50个人能同时使用你的软件,这怎么行!这些公司都是拥有雄厚的资金的,要是你能阻止他们破解,规规矩矩地使用你的软件的话,你就能让他们买更多的软件,从他们手中赚到更多的钱。从另一方面来说,个人盗版的方式要么是直接破解程序,要么是直接从网上下载软件的破解版,但就是不花钱去买正版软件。不过因为大量个人盗版行为而损失的金钱可能并不像媒体报道的那么大,全力去捉拿这些对手显得有点得不偿失。 1.9.2 不使用软件保护技术的理由 反方观点:使用软件保护技术需要付出很多代价——性能降低、软件开发周期变得更复杂,最后但对你影响不算是最小的——惹恼你的客户。 使用软件保护技术将使开发和销售环节成本大幅上升。在开发环节,你可能需要购买一个软件保护工具,开发自己的保护工具或者干脆自己手工在程序中应用保护技术。但不管使用哪种方法,都要在开发环节里增加一个额外的步骤,使软件开发周期变得更复杂。本书中介绍的一些技术(比如指纹或者代码加密)将使你卖出去的每一份软件都不一样。这样就意味着你还不得不在软件发布、质量评估和处理bug等各个环节面对更多令人头痛的问题。 此外,还有很多保护技术将会对程序的性能产生重大的影响。这里我们所说的“重大的”影响可是实实在在地指程序的性能会以极大的幅度下降!我们还不能确定是否越高级的保护措施就会使程序的性能下降的越厉害,但是事实貌似就是这样的。最终你将不得不面对一个艰难的抉择:要么使用强度较低的保护技术,这样对程序的性能影响可能比较小;要么使用强度较大的保护技术,但为了尽量不影响程序的性能,你只能对一小部分关键代码进行保护,而放弃其余部分;要么也可以使用高强度保护技术保护整个程序,但是程序的性能会将受到极大地影响,于是你只能把程序卖给那些拥有足够计算资源的客户。 最后,一些软件被破解的原因并不是攻击者真的想要制作盗版软件或者想要对它进行逆向分析,攻击者只是想保障自己应得的利益——比如他可能想要让程序也能在他的笔记本里正常运行,就像在他的台式机里那样,或者他只想对软件做一个备份,又或者他只是想把旧机器里的程序转移到刚买的新机器里运行,再或者他只是不想在每次运行程序时都要输入一个恼人的口令或者插入软件狗/光盘之类的……不管作为软件开发者的你是怎么想的,也不管软件的使用许可中是否允许他这么做,只要把客户惹急了,他照样会动手破解你的软件。 对了,我还没提bug的事呢!在本书中提到的很多技术都是基于代码转换的。从这个角度来说,这些工具就类似于一些优秀编译器中的代码优化器。但是和代码优化器一样,它们也是滋生bug的温床。结果你本来已经编写的很完美的代码,一旦应用软件保护技术之后就会出现一些飘忽不定的怪异行为。更有甚者,有些保护工具本身就有bug,应用了保护措施之后,你的程序中也会增加一些潜在bug。 1.9.3 那我该怎么办呢 好吧,假设现在你已经写好了一个漂亮的程序,而且出于某种方面的考虑,你已经决定对你的软件进行一些保护。于是你买了我这本书 ,并且反复阅读其中的内容,希望能从中找出一个正确的算法应用于你的程序中。可是很遗憾,如果你的目的仅限于此的话,我恐怕你要失望而归了。 研究软件保护技术要面对的问题是多种多样的,但是它们都有一个核心问题需要回答:你该如何评估某个算法的有效性?可惜的是这方面资料极少!道理上,本书的结尾应该给出一张大表,表中详细罗列出每一种算法的应用效率、防破解效率、防平行攻击效率以及它将带来的性能开销。 不过我想你已经猜到了——最终这张表也没能画出来。 如果不知道你的软件会在几个小时?几天?几个星期?还是几个月?里被破掉,你如何判断保护技术是否起了作用呢?不对两个算法进行比较,你怎么才能判定哪个算法更好些呢?又怎么才能选择正确的算法呢?要是所有的刊物的编辑都只对新的算法感兴趣,而拒绝录用对旧有算法进行改进的文章的话,这一研究领域又如何才会进步呢? 这一切最终都是源自各种防护手段之间的相互干扰 !在实际应用中,软件保护技术是以一种“叠罗汉”的方式来保证安全的——在性能开销所能容忍的范围内,不断地给软件加上一层又一层的防护措施,直到你感到“足够”安全为止。如果还有闲钱,你还能请一个专业的破解小组进行测试,看看破解这些防护措施需要多长时间,使你心里对你的软件在现实世界中能挺多长时间大概有个数。但是你最终还是不得不接受这样一个事实:你已经陷入了一场“猫捉老鼠”的游戏了——只要攻击者惦记上了你的软件,那么最终他们都是会成功的。这实在是让人很郁闷,可生活中这样的事还少吗?最后,你还得时刻关注破解者们的任何一点进步,同时时刻准备着不断改进你的防护方法。 1.10 一些说明 在本书中,我们将按照某种规则给每种软件保护算法起一个名字,而且尽量使这些名字不要重复。每种算法的名字前面都会有一个表示该算法应用领域的前缀(比如WM代表水印技术,OBF代表代码混淆技术,TP表示防篡改技术,SS表示软件相似性比对技术)后面跟上提出该算法的各位专家姓氏的第一个字母组成的字符串。对于那些攻击者使用的方法,我们会给它加上一个RE前缀 。如果提出算法的专家只有一位,为了避免重复,除了他姓的第一个字母之外,还会再加上他名的第一个字母。有些研究团队可能会提出多个算法,那么为了区分这些算法,我们将给这些算法的名字后面加上一个下标 。要是提出两个算法的研究者们的姓的第一个字母组成的集合恰好相同的话,我们将按论文中作者的排序,排列算法名称中各个研究者的姓的第一个字母。要是这也不能奏效的话,我们还将在算法的名字中加入他们的名的第一个字母以及相关论文的第一个字母。
1人

>软件加密与解密

软件加密与解密
作者: [美] Christian Collberg, [美] Jasvir Nagra
原作名: Surreptitious Software:Obfuscation,
isbn: 7115270759
书名: 软件加密与解密
页数: 601
译者: 崔孝晨
定价: 99.00元
出版社: 人民邮电出版社
装帧: 平装
出版年: 2012-5-3