第一部分:Java的安全基础——虚拟机和字节码安全
概论:安全问题对很多数人来说都非常重要。从其历史看,Java安全主要意味着虚拟机和字节码安全。然而这个看法忽略了两个重要方面—应用程序和网络安全。在下面一系列文章中,Todd Sundsted讲解了JAVA虚拟机安全,应用程序安全,网络安全,解释了应该采取什么样的措施来全面巩固你的Java安全。在这第一部分,他向我们解释了Java安全的基础:虚拟机和字节码安全。
“似乎还没有人曾因为写出了不安全的Java代码而遭解雇”。这句话是我对那句流行语“没人曾因购买了IBM而遭解雇”的修正版本。那些更关心网络速度和那些更有爱好为简历添加更多有价值项目的雇员经常犯下安全问题。
再来看看另一个令人担心的现象:在我同治理人员和工程技术人员谈论安全问题时,我经常发现他们对自己的行为存在一些误解,他们认为不必考虑安全问题,因为“Java本身就等于安全”。在这样错误观念的指引下,工程师们没有去考虑以下三个方面的安全问题:虚拟机安全,应用程序安全,网络安全。
在以下一系列文章中,我会尽力修正这种错误见解。接下来,我将就三方面的问题来对Java安全进行讨论,并举列说明一般安全问题是怎样窃入的。另外我也会介绍一些办法来创建安全的应用程序。
·三种安全问题:
在Java初次露面时,开发者,研究人员,新闻媒体界对其安全问题就反响剧烈。在早前的时候,Java安全就是意味着字节码安全和虚拟机安全。由于Java过去主要是作为下载到本地执行的小应用程序开发语言,下载下来的代码的安全性和执行环境就是异常重要的事情。这种情况下的安全意味着正确安装类装载器和安全治理器以及验证下载的代码。
在我以前开发C/C++程序的数年里,我从没担心过虚拟机安全问题—这个问题完全是随着Java而成了人们关注的中心。谈到安全问题,我担心的总是应用程序漏洞或是危及程序或系统安全的执行情况。在C++领域,应用程序上的安全包括限制“setuid”代码范围(在Unix环境,setuid代码是作为另外的用户进程来运行—典型的情况是超级用户)并力图避免缓冲溢出及其它类型的堆栈问题。
而分布式应用程序的引入则带来了另外一些方面的问题。正如其名字所示,分布式程序由多个部分组成,每个部分都驻留在它自己的机器上,并通过公共网络和其它部分通信。一个Web应用就是典型的列子。在网络意义上的安全则意味着签名,授权,应用程序组件,加密通信管道等。
许多开发人员并不清楚以上几方面的不同,并以为Java在虚拟机一层安全了,那么整个应用程序就安全了。我很希望改变这种观念。下面就开始来讨论Java的虚拟机安全。
·安全基础:虚拟机安全
虚拟机安全,长期以来一直是开发人员注重的焦点,几乎直到现在也还是没有结果。
我最初对讨论虚拟机安全感到有爱好是在转向应用程序和网络安全之前。我决定给予它同另两个部分同样公平的时间来讨论,这出于两个理由:首先,优秀的编程教材因该包含过去6年来发现过的大量漏洞,第二,很多安全问题跨越了我要讨论的三个方面。为了能透彻理解,你必须要全面熟悉三个方面,包括Java虚拟机安全。
假如你检查过去6年发现的各种安全问题(看http://www.javaworld.com/javaworld/jw-06-2001/jw-0615-howto.Html#resources的官方清单),你将发现它们被分成一系列目录。就所关注的虚拟机安全来讲,最重要的两种安全漏洞都是围绕着未被验证和可能非法的字节码以及Java类型系统破坏来展开。在实际开发中,这两者经常是关联在一起
·未验证代码探秘
在JVM通过网络从服务器上下载类代码时,它并没有办法知道这些字节码是否能安全执行。安全的字节码永不会指示虚拟机执行让Java运行时处于不懈调和无效的状态。
通常,Java编译器可以确保创建的类文件里的字节码是安全的。然而也可以手工写出Java编译器不答应的字节码。Java校验器以一系列极富想像力的方法检查所有这样的字节码并验证那些不合规范的代码。一旦校验完成,JVM便知道程序代码是安全的—只要校验器正常工作。
下面让我们来看看一个列子,以更好的理解校验器所扮演的角色,并看看一旦校验器失效会产生什么后果。
考虑一下下面这个类:
publicclass Test1
{
publicstaticvoidmain(String [] arstring)
{
Float a = new Float(56.78);
Integer b = new Integer(1234);
System.out.println(a.toString());
}
}
当你写完它并运行,程序将向屏幕打印出字符串“56.78”。这是个在类里分配的一个浮点型变量。我们即将修改一处代码,欺骗虚拟机在整型变量上激活toString()方法而不是浮点型变量(你可以从网址下载并修改源代http://www.javaworld.com/javaworld/jw-06-2001/jw-0615-howto.html#resources)。
再来看看这段经反编译后的代码的输出:
Method void main(java.lang.String[])
0 new #3
3 dup
4 ldc2_w #13
7 invokespecial #8
10 astore_1
11 new #4
14 dup
15 sipush 1234
18 invokespecial #9
21 astore_2
22 getstatic #10
25 aload_1
26 invokevirtual #12
29 invokevirtual #11
32 return
上面的代码包含了main()函数的反编译输出。在这个方法的地址偏移量25处,虚拟机载入于偏移0到10处创建的浮点型变量的一个引用。这就是我们要修改的地方。
下面就是经修改后的反编译代码:
Method void main(java.lang.String[])
0 new #3
3 dup
4 ldc2_w #13
7 invokespecial #8
10 astore_1
11 new #4
14 dup
15 sipush 1234
18 invokespecial #9
21 astore_2
22 getstatic #10
25 aload_2
26 invokevirtual #12
29 invokevirtual #11
32 return
这个类在偏移量25处的字节码是完全相同的,载入一个整型变量的引用。
注重看看,修改后的代码仍然是安全的,这非常重要,这意味着JVM仍然将执行代码而不会崩溃或是将错误代码隔离开。然而校验器仍然能分辨出这些变化。在我的系统里,在我运行这片代码时,出现错误:
Exception in thread "main" java.lang.VerifyError:
(class: Test1, method: main signature: ([Ljava/lang/String;)V)
Incompatible object argument for function call
假如你关掉校验器或是你找到一处虚拟机漏洞并非常规地通过了校验器的检查,那非法代码就要启动了。执行下面的命令,我接收到值:1234—整型变量值。
java -noverify Test1
这个列子并无多大害处,但潜在的危害确是巨大的。以上这样的技术假如同虚拟机漏洞联系起来,造成未被检查的代码得以执行,那么这将造成严重的类型混乱。
·类型混乱
类型的概念对java编程语言来说是浑然一体的。每个值都同一种类型相关联,JVM就是用值的类型来决定什么样的操作可以作用在什么样的值上。
程序的类型信息对于虚拟机安全是至关重要的。一个被恶意的,未经验证的代码启动的类型混淆攻击会试图让JVM相信伪装成为一个类实列的内存块确实是是另一个类的实列,以此进行攻击。假如攻击成功,程序就会以设计者意想不到的方式来操作类实列。这种攻击称为“类型混淆攻击”,因为虚拟机已经闹不清被修改的类的类型。
假如类经过了完全的验证,那么“类型混淆攻击”是不会发生的。在上面的第二个列表中,校验器捕捉了这个企图并抛出了异常。也就是说,只要校验器没有被关闭或是被绕过,那么安全就是能够保障的。
幸运的是,我所担心的Java字节码校验器最后的一个漏洞在1999年末被发现。基于这个事实,你可能会认为自己不会在陷入危险中,然而,这过于疏忽大意了。
虽然漏洞越来越少,但还是有充足的机会留给狡猾的代码混入程序之中。记住,你可以手工关闭校验器检验。在接下来的文章中中,我列举出三种主要的java程序,以向大家示列在怎样的环境下关掉校验器。其中一个程序有一个重要的RMI(远程方法调用)组件(如你以后将要学到的,RMI可以让类通过网络载入到程序中,并让你的程序失控)。假如你能避免这种情况发生,就不要关掉校验器验证。
JVM安全是java安全体系中非常重要的一方面。这些未验证代码和类型混淆方面的讨论将有助于你理解为什么。对于下载代码和类型系统来说,没有适当的校验保证,安全计算将成为一句空话。