Java学习者论坛

 找回密码
 立即注册

QQ登录

只需一步,快速开始

手机号码,快捷登录

恭喜Java学习者论坛(https://www.javaxxz.com)已经为数万Java学习者服务超过8年了!积累会员资料超过10000G+
成为本站VIP会员,下载本站10000G+会员资源,购买链接:点击进入购买VIP会员
JAVA高级面试进阶视频教程Java架构师系统进阶VIP课程

分布式高可用全栈开发微服务教程

Go语言视频零基础入门到精通

Java架构师3期(课件+源码)

Java开发全终端实战租房项目视频教程

SpringBoot2.X入门到高级使用教程

大数据培训第六期全套视频教程

深度学习(CNN RNN GAN)算法原理

Java亿级流量电商系统视频教程

互联网架构师视频教程

年薪50万Spark2.0从入门到精通

年薪50万!人工智能学习路线教程

年薪50万!大数据从入门到精通学习路线年薪50万!机器学习入门到精通视频教程
仿小米商城类app和小程序视频教程深度学习数据分析基础到实战最新黑马javaEE2.1就业课程从 0到JVM实战高手教程 MySQL入门到精通教程
查看: 386|回复: 0

[Java基础知识]防范java代码的finalizer()漏洞

[复制链接]
  • TA的每日心情
    开心
    2021-3-12 23:18
  • 签到天数: 2 天

    [LV.1]初来乍到

    发表于 2014-9-30 17:46:47 | 显示全部楼层 |阅读模式
    在使用终结器 (finalizer) 来创建对象时,其可能会给 java 代码带来漏洞。该漏洞是使用终结器来恢复对象的著名技术的一种变体。当包含
    1. finalize()
    复制代码
    方法的对象变得无法访问时,它会被放入一个将在以后某个时刻处理的队列上。本文解释此类攻击的工作原理,介绍如何保护代码免遭此类攻击。所有代码示例都可供下载。
    终结器的理念是允许 Java 方法释放任何需要返回到操作系统的本机资源。遗憾的是,任何 Java 代码都可以在终结器中运行,可以使用类似清单 1 的代码:  


    清单 1. 一个可恢复的类


      
       
       
                                       
    public class Zombie {
      static Zombie zombie;
      public void finalize() {
        zombie = this;
      }
    }
                      [/code]
       
       
      


    在调用
    1. Zombie
    复制代码
    终结器时,它获取被终结的对象(由
    1. this
    复制代码
    引用)并将它存储在静态
    1. 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;
      }
    }
                      [/code]
       
       
      


    在 清单 2 中,
    1. finalize()
    复制代码
    方法的存在使对
    1. value
    复制代码
    参数的检查变得无效。  
    该攻击的工作原理
    当然,可能没有人会编写类似 清单 2 这样的代码。但如果类被继承了的话,则可能出现漏洞,如清单 3 所示:  


    清单 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());
      }
    }
    [/code]
       
       
      


    清单 3 中的
    1. Vulnerable
    复制代码
    类用于预防设置非正的
    1. value
    复制代码
    值。此意图被
    1. AttackVulnerable()
    复制代码
    方法破坏,如清单 4 所示:


    清单 4. 破坏
    1. 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!");
        }
      }
    }
    [/code]
       
       
      


    1. AttackVulnerable
    复制代码
    类的
    1. main()
    复制代码
    方法试图创建一个新的
    1. AttackVulnerable
    复制代码
    对象实例。因为
    1. value
    复制代码
    的值超出了范围,所以抛出了一个被
    1. catch
    复制代码
    块捕获的异常。
    1. System.gc()
    复制代码
    1. System.runFinalization()
    复制代码
    的调用促使 VM 运行一个垃圾回收周期并运行一些终结器。这些调用不是成功攻击的必要条件,但它们可用来说明攻击的最终结果,那就是创建了一个包含无效值的
    1. Vulnerable
    复制代码
    对象。  
    运行测试用例会得到以下结果:  
      
       
       
        java.lang.IllegalArgumentException: Vulnerable value must be positive
    Vulnerable object 0 created!
    [/code]
       
       
      


    为什么
    1. Vulnerable
    复制代码
    的值是 0,而不是 -1?请注意,在 清单 3 中的
    1. Vulnerable
    复制代码
    构造函数中,在执行参数检查后才会给
    1. value
    复制代码
    赋值。所以
    1. value
    复制代码
    拥有自己的初始值,在本例中为 0。  
    这种类型的攻击甚至可用于绕过显式安全检查。例如,如果在
    1. SecurityManager
    复制代码
    下运行并且调用方没有权限向当前目录写入数据,就可以使用清单 5 中的
    1. Insecure
    复制代码
    类来抛出
    1. SecurityException
    复制代码



    清单 5.
    1. 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());
      }
    }
    [/code]
       
       
      


    清单 5 中的
    1. Insecure
    复制代码
    类可通过与前面相同的方式攻击,如清单 6 中的
    1. AttackInsecure
    复制代码
    类所示:


    清单 6. 攻击
    1. 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!");
        }
      }
    }
    [/code]
       
       
      


    1. SecurityManager
    复制代码
    下运行 清单 6 中的代码会得到以下输出:
      
       
       
        java -Djava.security.manager AttackInsecure
    java.security.AccessControlException: Access denied (java.io.FilePermission index write)
    Insecure object 0 created!
    [/code]
       
       
      


    如何避免攻击
    在 Java SE 6 中实现第三版 Java 语言规范 (JLS) 之前,避免攻击的仅有方式是使用
    1. initialized
    复制代码
    标志、禁止子类化或创建
    1. final
    复制代码
    终结器)并不是令人满意的解决方案。
    使用
    1. initialized
    复制代码
    标志
    一种避免攻击的方式是使用
    1. initialized
    复制代码
    标志,在正确创建对象之后它被设置为
    1. true
    复制代码
    。该类中的每个方法首先检查是否设置了
    1. initialized
    复制代码
    ,如果没有设置则抛出一个异常。这样的代码编写起来很无趣,很容易被意外省略,也无法阻止攻击者子类化该方法。  
    预防子类化
    您可以将所创建的类声明为
    1. final
    复制代码
    。这意味着没有人可创建该类的子类,这会阻止攻击生效。但是,此技术降低了灵活性,无法扩展该类来特殊化它或增加额外的功能。  
    创建一个
    1. final
    复制代码
    终结器
    可以为所创建的类创建一个终结器并将它声明为
    1. final
    复制代码
    。这意味着该类的任何子类都无法声明终结器。此方法的缺点是,终结器的存在意味着对象的存活期比其他情况下更长。  
    一种新的、更好的方式
    为了更容易避免此类攻击,而无需引入额外的代码或限制,Java 设计人员修改了 JLS,声明如果在构造
    1. java.lang.Object
    复制代码
    之前在构造函数中抛出了一个异常,该方法的
    1. finalize()
    复制代码
    方法将不会执行。
    但是如何在构造
    1. java.lang.Object
    复制代码
    之前抛出异常呢?毕竟,任何构造函数中的第一行都必须是对
    1. this()
    复制代码
    1. super()
    复制代码
    的调用。如果构造函数没有包含这样的显式调用,将隐式添加对
    1. super()
    复制代码
    的调用。所以在创建对象之前,必须构造相同类或其超类的另一个对象。这最终导致了对
    1. java.lang.Object
    复制代码
    本身的构造,然后在执行所构造方法的任何代码之前,构造所有子类。  
    要理解如何在构造
    1. java.lang.Object
    复制代码
    之前抛出异常,需要理解准确的对象构造顺序。JLS 明确给出了这一顺序。  
    当创建对象时,JVM:  
      
      为对象分配空间。
      将对象中所有的实例变量设置为它们的默认值。这包括对象超类中的实例变量。
      分配对象的参数变量。
      处理任何显式或隐式构造函数调用(在构造函数中调用
    1. this()
    复制代码
    1. super()
    复制代码
    )。
      初始化类中的变量。
      执行构造函数的剩余部分。  
      
    重要的是构造函数的参数在处理构造函数内的任何代码之前被处理。这意味着,如果在处理参数时执行验证,可以通过抛出异常预防类被终结。
    这带来了 清单 3 的
    1. Vulnerable
    复制代码
    类的一个新版本,如清单 7 所示:  


    清单 7.
    1. 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));
      }
    }
    [/code]
       
       
      


    清单 7 中,
    1. Invulnerable
    复制代码
    的公共构造函数调用一个私有构造函数,而后者调用
    1. checkValues
    复制代码
    方法来创建其参数。此方法在构造函数执行调用来构造其超类之前调用,该构造函数是
    1. Object
    复制代码
    的构造函数。所以如果
    1. checkValues
    复制代码
    中抛出了一个异常,那么将不会终结
    1. Invulnerable
    复制代码
    对象。  
    清单 8 中的代码尝试攻击
    1. Invulnerable
    复制代码



    清单 8. 尝试破坏
    1. 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");
    [/code]
       
       
      


    使用 Java 5(针对较旧 JLS 版本而编写),创建了一个
    1. Invulnerable
    复制代码
    对象:
      
       
       
        java.lang.IllegalArgumentException: Invulnerable value must be positive
    Invulnerable object 0 created![/code]
       
       
      


    在 Java SE 6(自 Oracle 的 JVM 的通用版本和 IBM 的 JVM 的 SR9 开始)及以后的规范中,不会创建该对象:
      
       
       
        java.lang.IllegalArgumentException: Invulnerable value must be positive
    Attack failed
    [/code]
       
       
      


      
    结束语
    终结器是 Java 语言的一种不太幸运的功能。尽管垃圾收集器可自动回收 Java 对象不再使用的任何内存,但不存在回收本机内存、文件描述符或套接字等本机资源的机制。Java 提供了与这些本机资源交互的标准库通常有一个
    1. close()
    复制代码
    方法,允许执行恰当的清理,但它们也使用了终结器来确保在对象错误关闭时,没有资源泄漏。  
    对于其他对象,通常最好避免终结器。无法保证终结器将在何时运行,或者甚至它是否会运行。终结器的存在意味着在终结器运行之前,不会对无法访问的对象执行垃圾收集,而且此对象可能使更多对象存活。这导致活动对象数量增加,进而导致 Java 对象的堆使用率增加。  
    终结器恢复即将被垃圾收集的能力无疑是终结机制工作方式的一种意外后果。较新的 JVM 实现现在保护代码免遭此类安全隐患。  
      
      
      


      
      

      

      
    关于作者
      
       
       Neil Masson 已从事 Java 语言开发和支持工作多年。目前他主要研究如何改进 Java 版本的质量和安全。
       
      


      
      
       
       

         
       

         
       
      
    复制代码
    回复

    使用道具 举报

    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    QQ|手机版|Java学习者论坛 ( 声明:本站资料整理自互联网,用于Java学习者交流学习使用,对资料版权不负任何法律责任,若有侵权请及时联系客服屏蔽删除 )

    GMT+8, 2024-5-10 03:18 , Processed in 0.437693 second(s), 48 queries .

    Powered by Discuz! X3.4

    © 2001-2017 Comsenz Inc.

    快速回复 返回顶部 返回列表