random和seurerandom随机数安全

前言:

        最近代码审计经常看到代码存在random生成随机数,扫描器提示应该使用seurerandom来生成随机数,一直没深入研究过random到底不安全在哪,今天有时间研究了下,下面把分析的记录下来。

原理:

Random:

        首先看看生成随机数的源码:

         如果没有设定种子,默认Random生成种子代码是seedUniquifier() ^ System.nanoTime(),种子的生成取决与两个值,一个是AtomicLong类型的seedUniquifier另外一个就是时间,这里seedUniquifier初始化的值是固定的,生成的结果是可以预测到的,那边真正又影响的就是后面的System.nanoTime(),这里System.nanoTime()生成的是一个毫秒十六位时间戳,所以Random的种子主要是根据时间生成的,是可以被预测。

        下面看一下有了种子是如何生成随机数的,当我们执行nextInt的时候,看到内部代码为:

         根据源代码可以看到multiplier,addend和mask三个值均为硬编码,真正会对随机数生成产生影响的就是seed,当再使用random的时候,理论上说只要我们知道了原始的种子,我们就可以使用(oldseed * multiplier + addend) & mask; 即可知道最新的种子和对应类型的随机数值。

SecureRandom:

        在了解SecureRandom是如何生成以前要了解什么是真随机数,什么是伪随机数:

        真随机数通常来源于硬件随机数生成器,每次生成的随机数都是真正的随机数,但是因为物理因素,生成的时间较慢。

        伪随机数通常来源于某个生成算法,每次生成的随机数虽然是随机的,但还是遵循生成算法的规则,优点是生成速度较快。

        我们将产生真随机数的模块称为真随机数生成器(TRNG),将产生伪随机数的模块称为伪随机数发生器(PRNG)(也叫做确定性随机数生成器,DRBG),其中伪随机数所使用的种子称为熵源(熵池)。

伪随机数生成算法,NIST SP 800-90A规范中描述了三种产生伪随机数的算法:

  • Hash_DRBG:使用单向散列算法作为伪随机数生成的基础算法;
  • HMAC_DRBG:使用消息认证码算法作为随机数生成的基础算法;
  • CRT_DRBG:使用分组密码算法的计数器模式作为随机数生成的基础算法

        SecureRandom使用何种算法,有三种,NativePRNG,DRBG和SHA1PRNG,代码如下:

        默认是使用DRBG,NativePRNG要指定才可以使用,这个时候要注意:

  • /dev/random 设备会返回小于熵池噪声总数的随机字节。/dev/random 可生成高随机性的公钥或一次性密码本。若熵池空了,对/dev/random的读操作将会被阻塞,它就一直等,这迫使JVM等待,会造成线上问题,这里要注意。
  • /dev/urandom (“unlocked”,非阻塞的随机数发生器)即NativePRNG,它会重复使用熵池中的数据以产生伪随机数据。这表示对/dev/urandom的读取操作不会产生阻塞,但其输出的熵可能小于 /dev/random 的。

        服务器中缺乏键盘和鼠标输入以及磁盘活动可以产生所需的随机性或熵,所以使用/dev/random很有可能导致系统出现问题,所以可以采用-Djava.security.egd=file:/dev/./urandom (这个文件名多个u)强制使用/dev/urandom这个文件。

      另外在windows中没有"file:/dev/random" or "file:/dev/urandom",所以会使用MS CryptoAPI来生成随机数:

        下面来看看在windows下SecureRandom是如何生成种子的,当我们调用nextInt的时候首先进入的是Random的nextInt,但是种子的生成会进入SecureRandom中:

        然后判断熵是否存在,第一调用时会生成种子,而后将instantiated置为True,后续nextint将不会再生成新的种子。

         种子生成通过MessageDigest提供信息摘要算法的功能,输出固定长度的哈希值,其中采用系统的配置和获取到的系统temp文件中的文件名等信息作为随机值作为字符串进行哈希运算来作为种子,这样来保证每台机器生成的值和系统有很强的关联性,进而保证种子的唯一和不可猜测。

        由此可见SecureRandom生成种子的方式主要通过系统信息和trmp文件中的文件名等信息作为熵,然后生成种子,由于系统信息的不可猜测和temp文件内容的不可控来确保攻击者无法预测种子的生成规律自然没有办法进行攻击。但是如果用户使用setSeed自己设置了种子就不会进入上述的生成种子的流程而是采用用户设定的种子来生产随机数,这样安全性就大大降低。

测试:

搭建spring环境:

        其实也不用搭建spring环境来测试,只是自己想模拟一个可能存在的真实情况的情况更有代入感,但是分析到后面在发现其实也不用搭建,但是既然搭建了也把搭建过程记录下,我使用的是springmvc方法搭建,springboot搭建更简单点,但是不小心用了springmvc搭建,这个部分就是记录下搭建过程,避免自己忘了,可以直接跳过。

        首先创建一个使用Maven的项目,然后添加web,选择file->Project Structure,这里记得确定下webapp目录和web,xml位置是否正确:

        然后再pom.xml里添加所需要的第三方jar包,另外要注意<packaging>war</packaging>这里设置打包类型为war:

<packaging>war</packaging>

    <dependencies>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>4.0.1</version>
            <!-- 作用在打包时确保servlet不会打包进去 -->
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>javax.servlet.jsp</groupId>
            <artifactId>jsp-api</artifactId>
            <version>2.2.1-b03</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>5.1.3.RELEASE</version>
        </dependency>

    </dependencies>

    <!-- 插件 -->
    <build>
        <plugins>
            <!-- 编码和编译和JDK版本 -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <!-- 根据自己电脑中的jdk版本选择maven的版本,如果不匹配可能报错 -->
                <version>3.8.1</version>
                <configuration>
                    <!-- 自己电脑中的jdk版本 -->
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>
            <!--tomcat插件-->
            <plugin>
                <groupId>org.apache.tomcat.maven</groupId>
                <artifactId>tomcat7-maven-plugin</artifactId>
                <version>2.2</version>
                <configuration>
                    <path>/</path>
                    <!-- 可自定义访问端口 -->
                    <port>1234</port>
                </configuration>
            </plugin>
        </plugins>
    </build>

    然后就要创建spring的配置文件,首先创建applicationContext.xml,为spring容器添加配置和约束:

        创建成功后写入下面内容,其中新加了xmlns:context和xsi:schemaLocation: 

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
            http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

    <!-- Spring配置文件:除了控制器之外的bean对象都在这被扫描 -->
    <context:component-scan base-package="org.example.dao"/>
</beans>

        然后再创建一个springmvc的配置文件 ,写入下面内容:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd
        ">
    <!-- SpringMVC配置文件:控制器的bean对象在这被扫描 -->
    <context:component-scan base-package="org.example.controller"/>
    <!--    启动mvc的注解-->
    <mvc:annotation-driven/>
    <!--    配置视图解析器的配置-->                                  
    <!--    调用视图解析器的方法:InternalResourceViewResolver-->
    <bean id="internalResourceViewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <!-- 前缀 默认访问路径是webapp根路径下的,如果webapp下还有其他文件夹就写:/webapp/文件夹名-->
        <property name="prefix" value="/"/>
        <!-- 后缀 如果是index.html文件,就写html -->
        <property name="suffix" value=".jsp"/>
    </bean>

</beans>

        然后修改web.xml,创建监听器加载我们的applicationContext.xml并加载servlet读取springmvc的配置文件springmvc.xml:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">

    <!-- Spring配置-->

    <!-- 1、让监听器知道spring的配置文件的位置-->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <!-- spring配置文件的文件名 -->
        <param-value>classpath:applicationContext.xml</param-value>
    </context-param>
    <!-- 2.创建监听器-->
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
    <!-- springmvc的 核心\前端\中央 控制器-->

    <!-- servlet的封装-->

    <servlet>
        <servlet-name>dispatcherServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <!-- servlet读取springmvc的配置文件-->
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <!-- springmvc配置文件的文件名 -->
            <param-value>classpath:springmvc.xml</param-value>
        </init-param>
        <!-- 在容器创建servlet对象的优先级.数字越小越先创建 -->
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>dispatcherServlet</servlet-name>
        <!-- 设置访问路径后必须加.do才能进行访问 -->
        <url-pattern>*.do</url-pattern>
    </servlet-mapping>

</web-app>

         然后代码中创建文件夹,可以按照这个模板创建,也不固定:

controller文件夹 :一般是放web控制器的服务类(就是根据前端发来的请求进行数据的处理,然后返回给前端)
dao文件夹 :一般是放数据库的操作类(数据库相关操作数据访问等)
pojo文件夹 :一般是放实体类(用来数据库映射对象)
service文件夹 :一般是做相应的业务逻辑处理的服务类

       然后就要编写我们的代码了,首先再service中创建ChangePasswordImpl,代码如下:

package org.example.service;

import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Random;

public class ChangePasswordImpl{
    public void hello(){
        System.out.println("TeamService---------hello");
    }
    
    public static String SecurRandomscramble(SecureRandom secureRandom, String inputstring){
        char a[] = inputstring.toCharArray();
        for(int i=0; i<a.length; i++){
            int j = secureRandom.nextInt(a.length);
            char temp = a[i];
            a[i] = a[j];
            a[j] = temp;
        }
        return new String(a);
    }
    public String seurerandom(String seed) {
        try {
            String outstr = "name:admin";
            String username = "admin";
            //Random random = new Random();
            SecureRandom secRandom = new SecureRandom();
            if(!seed.equals("")) {
                Long aLong = Long.valueOf(seed).longValue();
                secRandom.setSeed(aLong);
            }
            MessageDigest md = MessageDigest.getInstance("MD5");// 生成一个MD5加密计算摘要
            md.update(username.getBytes());// 计算md5函数
            String hashedPwd = new BigInteger(1, md.digest()).toString(16);// 16是表示转换为16进制数
            outstr = outstr + "</h1><h1>MD5hash:" +hashedPwd;
            String string1 = SecurRandomscramble(secRandom, hashedPwd);
            outstr = outstr + "</h1><h1>encode1:" + string1;
            String string2 = SecurRandomscramble(secRandom, string1);
            outstr = outstr + "</h1><h1>encode2:" + string2;
            return outstr;
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
            return null;
        }
    }
    
    public static String Randomscramble(Random random, String inputstring){
        char a[] = inputstring.toCharArray();
        for(int i=0; i<a.length; i++){
            int j = random.nextInt(a.length);
            char temp = a[i];
            a[i] = a[j];
            a[j] = temp;
        }
        return new String(a);
    }
    
    public String random(String seed) {
        try {
            String outstr = "name:admin";
            String username = "admin";
            Random random = new Random();
            if(!seed.equals("")) {
                Long aLong = Long.valueOf(seed).longValue();
                random.setSeed(aLong);
            }
            MessageDigest md = MessageDigest.getInstance("MD5");// 生成一个MD5加密计算摘要
            md.update(username.getBytes());// 计算md5函数
            String hashedPwd = new BigInteger(1, md.digest()).toString(16);// 16是表示转换为16进制数
            outstr = outstr + "</h1><h1>MD5hash:" +hashedPwd;
            String string1 = Randomscramble(random, hashedPwd);
            outstr = outstr + "</h1><h1>encode1:" + string1;
            String string2 = Randomscramble(random, string1);
            outstr = outstr + "</h1><h1>encode2:" + string2;
            return outstr;
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
            return null;
        }
    }

}

        然后在controller中创建ChangePasswordServlet,代码如下:

package org.example.controller;

import org.example.service.ChangePasswordImpl;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServlet;

@Controller
public class ChangePasswordServlet extends HttpServlet {
    ChangePasswordImpl changePassword = new ChangePasswordImpl();
    @RequestMapping("/index.do")//设置请求路的径
    public ModelAndView hello(){
        changePassword.hello();
        ModelAndView mv = new ModelAndView();
        mv.addObject("team", "teamindex----hello");//相当于request.setAttribute("team", "teamindex----hello")
        //经过InternalResourceViewResolver对象处理后前缀加上后缀就变为了:    /文件夹/index.jsp
        mv.setViewName("index");//未来要经过Springmvc的视图解析器处理,转换成物理资源路径。/相当于request.getRequestDispatcher("index.jsp").forward();
        return mv;
    }


    //设置请求路的径  规定请求的方式是post
    @RequestMapping(value = "/seurerandom.do",method = RequestMethod.GET)//请求方式设定后,只能用post的提交方式
    public ModelAndView seurerandom(@RequestParam("key") String key, @RequestParam("seed") String seed){
        String checkkey = "False";
        String back = changePassword.seurerandom(seed);
        ModelAndView mv = new ModelAndView();

        mv.addObject("backinfor", back);
        mv.addObject("mykey", key);
        if(back.contains(key)){
            checkkey = "Success";
        }
        mv.addObject("checkkey", checkkey);
        //经过InternalResourceViewResolver对象处理后前缀加上后缀就变为了:    /jsp/team/update.jsp
        mv.setViewName("/jsp/CheckSecureRandom");//要经过Springmvc的视图解析器处理,转换成物理资源路径。
        return mv;
    }

    @RequestMapping(value = "/random.do",method = RequestMethod.GET)//请求方式设定后,只能用post的提交方式
    public ModelAndView random(@RequestParam("key") String key, @RequestParam("seed") String seed){
        String checkkey = "False";
        String back = changePassword.random(seed);
        ModelAndView mv = new ModelAndView();
        mv.addObject("backinfor", back);
        mv.addObject("mykey", key);
        if(back.contains(key)){
            checkkey = "Success";
        }
        mv.addObject("checkkey", checkkey);
        //经过InternalResourceViewResolver对象处理后前缀加上后缀就变为了:    /jsp/team/update.jsp
        mv.setViewName("/jsp/CheckRandom");//要经过Springmvc的视图解析器处理,转换成物理资源路径。
        return mv;
    }
}

        然后就要编写相应的界面,这里采用的是jsp代码,webapp中创建index.jsp,添加代码:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>index</title>
</head>
<body>
<h1>${team}</h1>
<form action="/seurerandom.do" method="get">
    <button type="submit">seurerandom提交方式</button>
</form>
<form action="/random.do" method="post">
    <button type="submit">random提交方式</button>
</form>
</body>
</html>

        然后再webapp下创建jsp文件夹,里面创建两个jsp文件,分别为CheckRandom.jsp和CheckSecureRandom.jsp,内容相同:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
  <head>
    <title>Check Random</title>
    <style type="text/css">
      html, body {
        height: 100%;
        overflow: auto;
      }
      body {
        padding: 0;
        margin: 0;
      }
    </style>
  </head>
  <body>
    <div style="width:100%;text-align:center"></div>
    <center>
      <br>
      <br>
      <br>
      <h1>${backinfor}</h1>
      <h1>user post key:${mykey}</h1>
      <h1>check key:${checkkey}</h1>
      <br>
</center>
</body>
</html>

        然后通过tomcat即可运行项目即可访问:

测试带种子生成随机数:

        我们知道了当未设置种子的时候,random是通过时间生成种子,SecureRandom是通过系统文件和系统信息生成种子,那么设定了种子的情况下,SecureRandom还安全吗:

package org.example;

import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Random;
import java.util.concurrent.atomic.AtomicLong;


public class Main {


    public static String scramble(Random random, String inputstring){
        char a[] = inputstring.toCharArray();
        for(int i=0; i<a.length; i++){
            int j = random.nextInt(a.length);
            char temp = a[i];
            a[i] = a[j];
            a[j] = temp;
        }
        return new String(a);
    }

    public static String seurescramble(SecureRandom random, String inputstring){
        char a[] = inputstring.toCharArray();
        for(int i=0; i<a.length; i++){
            int j = random.nextInt(a.length);
            char temp = a[i];
            a[i] = a[j];
            a[j] = temp;
        }
        return new String(a);
    }

    public static String seurerandom(String seed) {
        try {
            String username = "admin";
            SecureRandom secRandom = new SecureRandom();

            if(!seed.equals("")) {
                Long aLong = Long.valueOf(seed).longValue();
                secRandom.setSeed(aLong);
            }
            MessageDigest md = MessageDigest.getInstance("MD5");// 生成一个MD5加密计算摘要
            md.update(username.getBytes());// 计算md5函数
            String hashedPwd = new BigInteger(1, md.digest()).toString(16);// 16是表示转换为16进制数
            System.out.print("admin MD5:" + hashedPwd+ "\n");
            String string1 = seurescramble(secRandom, hashedPwd);
            System.out.print(string1+ "\n");
            String string2 = seurescramble(secRandom, string1);
            System.out.print(string2+ "\n");

            return string2;
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
            return null;
        }
    }

    public static String random(String seed) {
        try {
            String username = "admin";
            Random random = new Random();
            if(!seed.equals("")) {
                Long aLong = Long.valueOf(seed).longValue();
                random.setSeed(aLong);
            }
            MessageDigest md = MessageDigest.getInstance("MD5");// 生成一个MD5加密计算摘要
            md.update(username.getBytes());// 计算md5函数
            String hashedPwd = new BigInteger(1, md.digest()).toString(16);// 16是表示转换为16进制数
            System.out.print("admin MD5:" + hashedPwd + "\n");
            String string1 = scramble(random, hashedPwd);
            System.out.print(string1+ "\n");
            String string2 = scramble(random, string1);
            System.out.print(string2+ "\n");
            return string2;
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
            return null;
        }
    }

    public static void main(String[] args) {
        for (int i =0; i<5; i++) {
            seurerandom("11");
        }
        System.out.print("========================\n");
        for (int i =0; i<5; i++) {
            random("11");
        }
    }

}

        我们设定种子为11,本地执行后结果为:

         可以看到使用random生成的值均为相同值,但是使用securerandom值每次生成的不同,但是是否这个随机值真的随机,我们将代码放入另一个系统中执行:

javac -encoding utf-8 Main.java

java Main

         比对可以发现如果设定了种子,再不同环境下生成的值也可以被预测,即便使用的是securerandom,如果开发者人为设定了种子来生成密钥等,我们可以进行破解;

        这里我们使用上面搭建的web模拟一个密码重置功能,当密码忘记了会有一个密码重置链接,该链接使用用户名md5后使用随机数两次加密后获得,代码是网上的代码,所以更具代表性,下面我们测试下我们可否预测:

        当使用11作为种子,可以看到第一次加密的结果为 9183780aa702a33744252cf524a91aef,和我们之前生成的对比发现第三个就是9183780aa702a33744252cf524a91aef,那么我们多生成一些值:

        这样我们预判下一个为1caa4138793af7a449058232520fe27a,最后生成的值为8204af23e738a15fac59a32407412a79,下面测试:

         可以看到成功预测,如果使用random生成的值每次都相同,就不测试了。

总结:

        所以对random和securerandom源码的分析可以发现在默认情况下,即未设置种子的情况下random不安全,因为其种子生成是靠系统的毫秒时间作为种子,这样攻击者只要本地按照一个时间作为种子或者按照根据随机数爆破种子均可以攻破系统的随机数安全,只是时间成本高一点,但是理论上是可以破解,但是使用securerandom的情况下如果未设置种子是根据系统信息和文件名生成种子,很难预测,生成的随机数较为安全。

        但是如果开发人员手工设置了种子,安全性将为大为降低,只要种子被获取,攻击者便可以根据种子生成随机数列表,然后预测出下一个随机数是什么,进而攻破随机数认证。

        所以生成随机数使用securerandom,并且不要手动设置种子,如果linux下使用securerandom出现线程等待是由于/dev/random作为熵来源,但是服务器产生的键盘操作等操作较少,可能由于内容太少导致等待,可以手工设置为/dev/urandom防止等待。

猜你喜欢

转载自blog.csdn.net/GalaxySpaceX/article/details/131961017