文章目录
1、概述
本篇文章将对安全的随机数与真/伪随机数进行介绍,想必大家在编码过程中,很少注意使用真随机数或者安全的随机数去生产随机数,那么为什么要用安全的随机数呢?(废话,因为安全啊!!!)本篇文章将带你探索未知的随机数领域。
(想直接了解本文结论的同学,请直接跳转 【4.4安全随机数】 的章节)
2、名词解释
- 伪随机数:通过数学算法+种子生成的随机数,存在一定的规律,并非真的随机数
- 弱伪随机数:伪随机数中的一种,易于预测的随机数
- 强伪随机数:伪随机数中的一种,难以预测的随机数
- 真随机数:通过物理系统生成的随机数,例如电压波动、磁盘磁头读/写时的寻道空间,电磁波噪声、掷硬币等
- 安全的随机数:具有一定强壮的随机数算法生成的随机数,包括真随机数和强伪随机数
- 盐值:通常在用户注册时,用来和用户输入的密码进行组合的随机数
- 散列函数(散列化):就是我们通常说的Hash(哈希),把输入的信息通过一定的散列算法生成了一种散列值的过程就是散列化,散列函数就提供了散列算法,例如SHA
- RSA:一种非对称加密算法
- UUID:通用唯一识别码,128位
- DCE:分布式计算环境
- SHA1:安全散列算法
- 彩虹表:用于加密散列函数逆运算而进行预先计算的表,针对于破解密码的散列值而出现的
3、随机数存在的安全风险
3.1 弱伪随机数带来的安全风险
刚当上程序员的小新人,遇到随机数的使用场景,一般都会调用Random这样的函数,通过srand(time())这样的形式给rand函数添加随机种子,产生随机数(有的随机数发生器默认就是用时间作为随机种子,例如JAVA的java.util.Random)。这样的随机数只适合用于游戏、抽奖等场景,而不适用于加密、密钥生成等重要操作场景
举个例子:通过邮箱重置密码的操作是很常见的,但是如果进行了不安全的设计和编码,则容易引发安全风险,例如重置密码时,用户ID和当前的时间戳绑定,而该时间戳作为种子用于加密密码。
URI:https://www.xxx.com/password/find
Method:PUT
Param:mail_address=xxxx@qq.com
seed=5512AADBF06182F1FEC988114385557E
将【seed】字段解密后,时一个时间戳:1607993226。因为一般系统时间跟着标准时间同步,所以时间戳时可预测的,当匹配到了该时间戳,那么就成了一个攻击者的突破口
3.2 真随机数真的安全吗
计算机安全领域没有绝对的安全,只有相对的安全(除非你不编码,不想当个程序猿)
相对比于其他随机数,真随机数的产生条件较为苛刻,通过获取物理条件产生,电压波动、磁盘读写的寻道空间等,难以被利用,当然,存在着撞库的风险,概率极低,破解的成本巨大。如果你使用了真随机数,仍然有攻击者想修改你的密码,获取信息,那就恭喜你了,说明在攻击者眼里,你可是个富翁(没有利益驱动,攻击者将对你毫无兴趣)
4、随机数
4.1 什么情况下才使用随机数
这时候你心中定有疑惑,随机数能干嘛?(一般来自新手的疑问,老司机还有这个疑惑那就unbelievable)
随机数常常与密码算法、ID、区块链联系在一起,再具体一点例如福利彩票、门禁蓝牙随机密码、抽奖
举几个例子:
密码学上使用的盐值、密钥等使用随机的条件,加强密码保存或传输的安全性;
UUID使用随机的方法保障ID唯一性,并可以在UUID的基础上,复用UUID拓展更多的功能,例如截取部分UUID作为虚拟机ID、使用UUID生成具有唯一性的存储ID和用户ID;
区块链中使用私钥加密货币,私钥的产生用的就是随机数;POS随机记账人场景(为了公平的分配记账券)
随机数的使用生活中处处可见,进行高危操作,例如加密场景,要使用安全的随机数
4.2 伪随机数
Q1:为什么会有“伪随机数”这个名词?
A1:随机数中的“随机”体现出不可预测性,没有规律可言,而人为的,通过一定算法生成的随机数,存在一定规律性,并不真正随机,所以称为“伪随机数”
Q2:伪随机数的由来
A2:起源于20世纪早期科学工作,研究人员通过物理方法采集随机数,但是通过物理方法采集的“真随机数”十分低效,实时获取随机数的速度缓慢,保存随机数额外占用存储空间等缺点,便开始寻求“伪随机数”的方法,来解决效率等问题
Q3:伪随机数目前分为几类?
A3:伪随机数根据预测性的难易程度,分为弱伪随机数(易预测)和强伪随机数(难预测)。
4.2.1 弱伪随机数
Q1:什么是弱伪随机数?
A1:顾名思义,这种的类型的随机数只是为了满足随机性,但是可以预测。举个例子,Java编程语言Random函数,就是基于时间作为种子,去构造随机数,假如攻击者获取了该时间,那么就可以预测下一个随机数的结果。
Q2:常见的弱伪随机数有哪些?
A2:以下是容易产生弱伪随机数的函数,取决于随机“种子”、随机范围、随机数的对象的选择
编程语言 | 弱伪随机数 | 备注 |
---|---|---|
C/C++ | srand( time() ) + rand() | 以时间为种子,产生随机数 |
C# | Random() System.Guid() |
|
Perl | srand( time() ) + rand() | 以时间为种子,产生随机数 |
Python | import numpy rng = numpy.random.RandomState( time() ) array_rand_num = rng.uniform() --------------------------- import random random.seed() |
|
Ruby | srand( time() ) + rand() | 以时间为种子,产生随机数 |
JAVA | Random |
|
PHP | srand() + rand() mtstrand() + mtrand() |
安全建议:涉及到加密、CSRF Token等重要操作时
- 不要使用时间函数作为种子或者随机数:time()/microtime()
- 不要使用rand这样的弱伪随机数生成器
- 随机数的长度要足够长
4.2.2 强伪随机数
Q1:什么是强伪随机数?
A1:指难以预测的伪随机数,通常用于密码学中,例如JAVA中的java.security.SecureRandom函数,其用于随机的种子是不可预测的,比如鼠标点击、键盘点击等物理条件
4.3 真随机数
Q1:Linux上可靠的真随机数生成方法有哪些?
A1:比较可靠的方法是读取/dev/random或者读取/dev/urandom(在一定程度上,/dev/urandom是强伪随机数)。
Q2:/dev/random和/dev/urandom有什么区别?
A2:
- 首先,Linux环境下是根据系统的熵值来产生随机数,熵的来源是环境噪音,噪音的来源是键盘输入、鼠标移动、内存的使用、进程数等。
- 其次,/dev/random是真随机数生成器,通过消耗熵值来产生随机数、同时熵耗尽的情况下会造成阻塞,直到有心的熵的生成。而/dev/urandom是强伪随机数生成器,它根据初始的随机种子(熵),来产生随机数,但是不会消耗掉熵,不会再熵消耗完的情况下阻塞。
- 这时候就有个问题了,如果系统启动阶段使用了/dev/urandom,而这时没有任何的熵,那么这时候使用的是内置的种子,产生随机数还是存在可预测性的
- 最后,仍然建议使用 /dev/urandom,在计算机安全领域,没有绝对的安全,只有相对的安全(除非你不编码、不开发、不做秃顶的程序猿)
4.4 安全随机数
随机数涉及到加密等重要操作场景,推荐使用以下安全随机数
编程语言 | 安全随机数 | 备注 |
---|---|---|
C/C++ | CryptGenRandom(Windows) | 原理:依据当前进程ID、当前线程ID、当前时间、用户名、计算机名、CPU计数器等,生成随机数,和/dev/random一样,效率低,消耗资源大 |
.NET(C#) | System.Security.Cryptography.RNGCryptoServiceProvider | 原理:和CryptGenRandom(Windows)类似 |
Perl | Math::Random::Secure | 原理:所使用的种子种类繁多,且是随机使用的,攻击者可能要费劲10多年才能遍历完成 |
Python | os.urandomrandom.SystemRandom() | 原理:函数返回的随机字节,是操作系统所带的随机函数产生,具有特异性这里"urandom"里"u"应该指的是"unexpected"–难以预料 |
Ruby | Sysrandom(取代SecureRandom) | 适用于生成session token原理:使用/dev/urandom |
JAVA | java.security.SecureRandom | 原理:提供加密强度高的随机数生成器,默认条件下(不传其他种子的话,默认种子来源是/dev/random,但是存在熵耗尽导致阻塞的现象),就可以产生安全的随机数;解决熵值耗尽的方法要不就是不断增加熵值,要不就种子来源换成/dev/urandom |
PHP | mcrypt_create_iv, openssl_random_pseudo_bytes,random_bytes,random_int | 原理:random_bytes/random_int – 不同系统上,源头不同,Windows上使用CryptGenRandom,Linux上使用getrandom(2)或/dev/urandom |
GNU/Linux或Unix | 读取/dev/random or /dev/urandom | 4.3讲的很清楚了 |
5、拓展场景
5.1* 密文密码中的盐值(拓展)
上图是Linux常用的密码生成与密码验证的原理图,
- a)生成密文密码:输入明文密码+系统产生的盐值 =>(散列化) 密文密码
- b)验证密码:输入明文密码+用户ID +存储的盐值 =>(散列化)密文密码,生成的密文密码与存储的密文密码比对
很明显,加入盐值的目的如下:
- 1、防止使用的密码存储时以明文形式存储。即使两个不同的用户使用相同的明文密码,但是使用了不同的盐值之后,最后的密文密码便是不同的。
- 2、显著增加密码破解的难度。例如一个长度为128位的盐值,可能产生的密文密码数量会是2的128次方个,加大密码猜测的难度。
- 3、让攻击者难以发现同一个用户在不同的操作系统上使用相同的密码
安全建议如下:
- 存储密码时要以密文密码存储,而不是使用明文密码存储
- 如果只时为了校验密码的目的而去使用密码(无论是校验自身平台还是第三方平台),那么使用密码+盐值再进行散列化后的结果形式进行存储,这样比较安全(因为散列化不可逆),进而进行比较时用的时密文密码比较,而非明文密码比较,减少了解密时候带来的安全风险
- 如果存储的密码是为了解密后使用,那么通常的安全建议做法是,对密码进行非对称加密存储,私钥要进行分段加密存储
- 使用的盐值长度建议为128位,极大提高攻击难度
5.2* UUID的生成(拓展)
UUID是如何保持唯一性的呢?这里将讲述UUID经历的5个版本。
UUID Version1:基于时间的UUID |
UUID Version2:DCE安全的UUID |
UUID Version3:基于名字的UUID(MD5) |
UUID Version4:随机UUID |
UUID Version5:基于名字的UUID(SHA1) |
5.3* 验证码的生成(拓展)
验证码的目的,是为了区分操作者是计算机还是正常的人类,防止计算机暴力破解(计算机很笨,只会按照程序猿设定的方式进行,无法对自动生成的问题进行解答)
验证码形式:数字校验、图片校验、拼图校验、语音校验,这些校验大同小异,无非是对数据进行校验
举个例子:
-
不安全的验证码传输:
为什么说它不安全呢?上图是将验证码后续的操作判断交给了前端,由前端去控制下一步操作,这样做有什么风险呢?
举个例子:保存重要配置操作(验证码的目的就是用于校验某次操作的安全性),在点击保存配置的按钮后,前端弹出验证码输入框,用户输入完验证码后,点击确定,这时候交给服务器识别验证码的正确性,然后返回flag=0或flag=1告知前端验证码校验失败或成功,由前端去决定是否调用这次保存配置的API。如果在第6步流程,当验证失败,flag=0,返回的响应信息被截取,将flag修改成1(数据伪造,验证成功),那么这次保存配置的操作将会成功。
安全建议:不要把一切操作都交给前端,特别时敏感操作;建议同步传输,在确认下发API的同时带上验证码数据,由服务器统一处理。
【参考资料】
1、博客:《聊一聊随机数安全》:https://blog.csdn.net/renwotao2009/article/details/51643799
2、博客:《C#生成随机数的三种方法》:https://www.cnblogs.com/xiaowie/p/8759837.html
3、书籍:《白帽子讲Web安全》
4、书籍:《计算机安全原理与实践》