技巧:防范代码的 finalizer() 漏洞 一种预防创建无效类的模式
2011-10-13 13:08
471 查看
本文原作者:Neil D. Masson, Java 支持工程师, IBM (本人在拜读过程中添加了自己的一些理解。)
原文地址:http://www.ibm.com/developerworks/cn/java/j-fv/index.html?ca=drs-
简介: 您的 Java 代码有可能会因终结操作带来的漏洞而易受到攻击,了解这一漏洞是如何起作用的,并学习如何通过修改代码来防止此类攻击。
在使用终结器 (finalizer) 来创建对象时,其可能会给 Java 代码带来漏洞。该漏洞是使用终结器来恢复对象的著名技术的一种变体。当包含
终结器的理念是允许 Java 方法释放任何需要返回到操作系统的本机资源。遗憾的是,任何 Java 代码都可以在终结器中运行,可以使用类似清单 1 的代码:
清单 1. 一个可恢复的类
运行结果:
null
Zombie@18a992f
在调用
此代码的一种存在更大隐患的版本甚至允许恢复部分构造的对象。即使对象在初始化过程中不能通过正确性检查,其仍能够被终结器创建出来,如清单 2 所示:
清单2 :创建一个非法的类
运行结果:
1
-1
当然,可能没有人会编写类似 清单 2 这样的代码。但如果类被继承了的话,则可能出现漏洞,如清单 3 所示:
清单 3 中的
清单 4. 破坏
运行测试用例会得到以下结果:
为什么
这种类型的攻击甚至可用于绕过显式安全检查。例如,如果在
清单 5.
清单 5 中的
清单 6. 攻击
在
如何避免攻击
在 Java SE 6 中实现第三版 Java 语言规范 (JLS) 之前,避免攻击的仅有方式是使用
使用
一种避免攻击的方式是使用
预防子类化
您可以将所创建的类声明为
创建一个
可以为所创建的类创建一个终结器并将它声明为
一种新的、更好的方式
为了更容易避免此类攻击,而无需引入额外的代码或限制,Java 设计人员修改了 JLS(参见 参考资料),声明如果在构造
但是如何在构造
要理解如何在构造
当创建对象时,JVM:
为对象分配空间。
将对象中所有的实例变量设置为它们的默认值。这包括对象超类中的实例变量。
分配对象的参数变量。
处理任何显式或隐式构造函数调用(在构造函数中调用
初始化类中的变量。
执行构造函数的剩余部分。
重要的是构造函数的参数在处理构造函数内的任何代码之前被处理。这意味着,如果在处理参数时执行验证,可以通过抛出异常预防类被终结。
这带来了 清单 3 的
清单 7.
在 清单 7 中,
清单 8 中的代码尝试攻击
清单 8. 尝试破坏
使用 Java 5(针对较旧 JLS 版本而编写),创建了一个
在 Java SE 6(自 Oracle 的 JVM 的通用版本和 IBM 的 JVM 的 SR9 开始)及以后的规范中,不会创建该对象
结束语
终结器是 Java 语言的一种不太幸运的功能。尽管垃圾收集器可自动回收 Java 对象不再使用的任何内存,但不存在回收本机内存、文件描述符或套接字等本机资源的机制。Java 提供了与这些本机资源交互的标准库通常有一个
对于其他对象,通常最好避免终结器。无法保证终结器将在何时运行,或者甚至它是否会运行。终结器的存在意味着在终结器运行之前,不会对无法访问的对象执行垃圾收集,而且此对象可能使更多对象存活。这导致活动对象数量增加,进而导致 Java 对象的堆使用率增加。
终结器恢复即将被垃圾收集的能力无疑是终结机制工作方式的一种意外后果。较新的 JVM 实现现在保护代码免遭此类安全隐患。
原文地址:http://www.ibm.com/developerworks/cn/java/j-fv/index.html?ca=drs-
简介: 您的 Java 代码有可能会因终结操作带来的漏洞而易受到攻击,了解这一漏洞是如何起作用的,并学习如何通过修改代码来防止此类攻击。
在使用终结器 (finalizer) 来创建对象时,其可能会给 Java 代码带来漏洞。该漏洞是使用终结器来恢复对象的著名技术的一种变体。当包含
finalize()方法的对象变得无法访问时,它会被放入一个将在以后某个时刻处理的队列上。本文解释此类攻击的工作原理,介绍如何保护代码免遭此类攻击。所有代码示例都可供 下载。
终结器的理念是允许 Java 方法释放任何需要返回到操作系统的本机资源。遗憾的是,任何 Java 代码都可以在终结器中运行,可以使用类似清单 1 的代码:
清单 1. 一个可恢复的类
public class Zombie { static Zombie zombie; public void finalize() { zombie = this; } public static void main(String[] args) { Zombie demo = new Zombie(); System.out.println(demo.zombie); demo.finalize(); System.out.println(demo.zombie); } }
运行结果:
null
Zombie@18a992f
在调用
Zombie终结器时,它获取被终结的对象(由
this引用)并将它存储在静态
zombie变量中。现在该对象又是可访问的,其不能被垃圾收集。
此代码的一种存在更大隐患的版本甚至允许恢复部分构造的对象。即使对象在初始化过程中不能通过正确性检查,其仍能够被终结器创建出来,如清单 2 所示:
清单2 :创建一个非法的类
public class Zombie2 { static Zombie2 zombie; int value; public Zombie2(int value) { if (value < 0) { throw new IllegalArgumentException("Negative Zombie2 value"); } this.value = value; } public void finalize() { zombie = this; } public static void main(String[] args) { Zombie2 demo=new Zombie2(1); System.out.println(demo.value); demo.value=-1; demo.finalize(); System.out.println(demo.value); } }
运行结果:
1
-1
finalize()方法的存在使对
value参数的检查变得无效
当然,可能没有人会编写类似 清单 2 这样的代码。但如果类被继承了的话,则可能出现漏洞,如清单 3 所示:
class Vulnerable { Integer value = 0; Vulnerable(int value) { if(value <= 0) { throw new IllegalArgumentException("Vulnerable value must be positive"); } this.value = value; } @Override public String toString() { return(value.toString()); } }
清单 3 中的
Vulnerable类用于预防设置非正的
value值。此意图被
AttackVulnerable()方法破坏,如清单 4 所示:
清单 4. 破坏
Vulnerable类的类
class AttackVulnerable extends Vulnerable { static Vulnerable vulnerable; public AttackVulnerable(int value) { super(value); } public void finalize() { vulnerable = this; } public static void main(String[] args) { try { new AttackVulnerable(-1); } catch(Exception e) { System.out.println(e); } System.gc(); System.runFinalization(); if(vulnerable != null) { System.out.println("Vulnerable object " + vulnerable + " created!"); } } }
AttackVulnerable类的
main()方法试图创建一个新的
AttackVulnerable对象实例。因为
value的值超出了范围,所以抛出了一个被
catch块捕获的异常。
System.gc()和
System.runFinalization()的调用促使 VM 运行一个垃圾回收周期并运行一些终结器。这些调用不是成功攻击的必要条件,但它们可用来说明攻击的最终结果,那就是创建了一个包含无效值的
Vulnerable对象。
运行测试用例会得到以下结果:
java.lang.IllegalArgumentException: Vulnerable value must be positive Vulnerable object 0 created!
为什么
Vulnerable的值是 0,而不是 -1?请注意,在 清单 3 中的
Vulnerable构造函数中,在执行参数检查后才会给
value赋值。所以
value拥有自己的初始值,在本例中为 0。
这种类型的攻击甚至可用于绕过显式安全检查。例如,如果在
SecurityManager下运行并且调用方没有权限向当前目录写入数据,就可以使用清单 5 中的
Insecure类来抛出
SecurityException:
清单 5.
Insecure类
import java.io.FilePermission; public class Insecure { Integer value = 0; public Insecure(int value) { SecurityManager sm = System.getSecurityManager(); if(sm != null) { FilePermission fp = new FilePermission("index", "write"); sm.checkPermission(fp); } this.value = value; } @Override public String toString() { return(value.toString()); } }
清单 5 中的
Insecure类可通过与前面相同的方式攻击,如清单 6 中的
AttackInsecure类所示:
清单 6. 攻击
Insecure类
public class AttackInsecure extends Insecure { static Insecure insecure; public AttackInsecure(int value) { super(value); } public void finalize() { insecure = this; } public static void main(String[] args) { try { new AttackInsecure(-1); } catch(Exception e) { System.out.println(e); } System.gc(); System.runFinalization(); if(insecure != null) { System.out.println("Insecure object " + insecure + " created!"); } }
在
SecurityManager下运行 清单 6 中的代码会得到以下输出:
java -Djava.security.manager AttackInsecure java.security.AccessControlException: Access denied (java.io.FilePermission index write) Insecure object 0 created!
如何避免攻击
在 Java SE 6 中实现第三版 Java 语言规范 (JLS) 之前,避免攻击的仅有方式是使用
initialized标志、禁止子类化或创建
final终结器)并不是令人满意的解决方案。
使用
initialized标志
一种避免攻击的方式是使用
initialized标志,在正确创建对象之后它被设置为
true。该类中的每个方法首先检查是否设置了
initialized,如果没有设置则抛出一个异常。这样的代码编写起来很无趣,很容易被意外省略,也无法阻止攻击者子类化该方法。
预防子类化
您可以将所创建的类声明为
final。这意味着没有人可创建该类的子类,这会阻止攻击生效。但是,此技术降低了灵活性,无法扩展该类来特殊化它或增加额外的功能。
创建一个
final终结器
可以为所创建的类创建一个终结器并将它声明为
final。这意味着该类的任何子类都无法声明终结器。此方法的缺点是,终结器的存在意味着对象的存活期比其他情况下更长。
一种新的、更好的方式
为了更容易避免此类攻击,而无需引入额外的代码或限制,Java 设计人员修改了 JLS(参见 参考资料),声明如果在构造
java.lang.Object之前在构造函数中抛出了一个异常,该方法的
finalize()方法将不会执行。
但是如何在构造
java.lang.Object之前抛出异常呢?毕竟,任何构造函数中的第一行都必须是对
this()或
super()的调用。如果构造函数没有包含这样的显式调用,将隐式添加对
super()的调用。所以在创建对象之前,必须构造相同类或其超类的另一个对象。这最终导致了对
java.lang.Object本身的构造,然后在执行所构造方法的任何代码之前,构造所有子类。
要理解如何在构造
java.lang.Object之前抛出异常,需要理解准确的对象构造顺序。JLS 明确给出了这一顺序。
当创建对象时,JVM:
为对象分配空间。
将对象中所有的实例变量设置为它们的默认值。这包括对象超类中的实例变量。
分配对象的参数变量。
处理任何显式或隐式构造函数调用(在构造函数中调用
this()或
super())。
初始化类中的变量。
执行构造函数的剩余部分。
重要的是构造函数的参数在处理构造函数内的任何代码之前被处理。这意味着,如果在处理参数时执行验证,可以通过抛出异常预防类被终结。
这带来了 清单 3 的
Vulnerable类的一个新版本,如清单 7 所示:
清单 7.
Invulnerable类
class Invulnerable { int value = 0; Invulnerable(int value) { this(checkValues(value)); this.value = value; } private Invulnerable(Void checkValues) {} static Void checkValues(int value) { if(value <= 0) { throw new IllegalArgumentException("Invulnerable value must be positive"); } return null; } @Override public String toString() { return(Integer.toString(value)); } }
在 清单 7 中,
Invulnerable的公共构造函数调用一个私有构造函数,而后者调用
checkValues方法来创建其参数。此方法在构造函数执行调用来构造其超类之前调用,该构造函数是
Object的构造函数。所以如果
checkValues中抛出了一个异常,那么将不会终结
Invulnerable对象。
清单 8 中的代码尝试攻击
Invulnerable:
清单 8. 尝试破坏
Invulnerable类
class AttackInvulnerable extends Invulnerable { static Invulnerable vulnerable; public AttackInvulnerable(int value) { super(value); } public void finalize() { vulnerable = this; } public static void main(String[] args) { try { new AttackInvulnerable(-1); } catch(Exception e) { System.out.println(e); } System.gc(); System.runFinalization(); if(vulnerable != null) { System.out.println("Invulnerable object " + vulnerable + " created!"); } else { System.out.println("Attack failed"); } } } with the addition of } else { System.out.println("Attack failed");
使用 Java 5(针对较旧 JLS 版本而编写),创建了一个
Invulnerable对象:
java.lang.IllegalArgumentException: Invulnerable value must be positive Invulnerable object 0 created!
在 Java SE 6(自 Oracle 的 JVM 的通用版本和 IBM 的 JVM 的 SR9 开始)及以后的规范中,不会创建该对象
java.lang.IllegalArgumentException: Invulnerable value must be positive Attack failed
结束语
终结器是 Java 语言的一种不太幸运的功能。尽管垃圾收集器可自动回收 Java 对象不再使用的任何内存,但不存在回收本机内存、文件描述符或套接字等本机资源的机制。Java 提供了与这些本机资源交互的标准库通常有一个
close()方法,允许执行恰当的清理,但它们也使用了终结器来确保在对象错误关闭时,没有资源泄漏。
对于其他对象,通常最好避免终结器。无法保证终结器将在何时运行,或者甚至它是否会运行。终结器的存在意味着在终结器运行之前,不会对无法访问的对象执行垃圾收集,而且此对象可能使更多对象存活。这导致活动对象数量增加,进而导致 Java 对象的堆使用率增加。
终结器恢复即将被垃圾收集的能力无疑是终结机制工作方式的一种意外后果。较新的 JVM 实现现在保护代码免遭此类安全隐患。
相关文章推荐
- 如何防范PowerShell代码注入漏洞绕过受限语言模式
- 防范java代码的finalizer()漏洞
- 如何防范代码的 finalizer() 漏洞?
- PHP代码网站如何防范SQL注入漏洞攻击建议分享
- 创建后台任务的两种代码模式
- PHP代码网站如何防范SQL注入漏洞攻击建议
- 推荐:Php代码不开源下的一种漏洞检测思路
- 关于Entity Framework采用DB First模式创建后的实体批量修改相关属性技巧
- iOS开发技巧:使用Objective-C创建UUID的代码
- ACE 对象生命周期管理者:一种用于控制对象创建和销毁的补充模式
- PHP代码网站如何防范SQL注入漏洞攻击建议分享
- 转:代码管理技巧——两步创建本地SVN服务器图文教程
- 设计技巧11:静态创建方法(非设计模式中的工厂方法) 利用一个静态的方法封装构建器
- [转]代码管理技巧——两步创建本地SVN服务器图文教程
- Visual Studio使用技巧,创建自己的代码片段
- 线程:创建线程有两种方式,一种是继承Thread类,另一种是实现Runnable接口。代码如下:
- 以人为本的机器学习:谷歌人工智能产品设计概述 By 机器之心2017年7月17日 12:13 取代了手动编程,机器学习(ML)是一种帮助计算机发现数据中的模式和关系的科学。对于创建个人的和动态的经历
- 出售时间的网站(跟王利芬创建的优米网其中一种模式一样)
- Visual Studio使用技巧,创建自己的代码片段
- 【怎样写代码】复杂对象的组装与创建 -- 建造者模式(三):建造者模式