SecureRandom漏洞描述
2013年比特币开发商在一篇博客中透露,由于Android系统存在一处关键漏洞,该平台上的比特币电子钱包很容易失窃。比特币开发商称,该漏洞影响到Android平台上的每一个比特币电子钱包应用程序,包括流行的比特币钱包(Bitcoin Wallet)、blockchain.info钱包(blockchain.info wallet)、BitcoinSpinner钱包(BitcoinSpinner Wallet)和Mycelium钱包(Mycelium Wallet)等。
该漏洞存在于Android系统随机生成数字串安全密钥的环节中。该漏洞的生成原因是对SecureRandom类的不正确使用方式导致的。 翻看Android的官方文档会发现。对于SecureRandom类的构造函数SecureRandom(byte[] seed)和SecureRandom#setSeed方法有一段安全性提醒:
“Seeds this SecureRandom instance with the specified Seeding SecureRandom may be insecure”
遗憾的是Android官网并未对此做过多的解释。setSeed方法为什么会引起安全风险?应该怎样使用SecureRandom类?
SecureRandom#SecureRandom(byte[] seed)
SecureRandom#setSeed(long seed)
SecureRandom#setSeed(byte[] seed)
在调用SecureRandom类的构造函数SecureRandom(byte[] seed)。 或者在生成随机数之前调用setSeed(long seed)或者setSeed(byte[] seed)方法设置随机种子
SecureRandom随机性是通过它的seed来保证的。如果输入相同的seed会导致生成重复的随机数。SecureRandom内部维护一个internal random state,它生成随机数的方式具有确定性。(如果输入相同的seed那么生成的随机数也相同)具体过程如下图:
在生成一个随机数时internal random state会从seed源中取出一个seed。通过内部运算生成随机数。internal random state具有确定性,不能保证SecureRandom的随机性, 所以SecureRandom依靠输入的seed的随机性保证自己能够生成出不相同的随机数。
在SecureRandom生成随机数时,如果我们不调用setSeed方法,SecureRandom会从系统的中找到一个默认随机源。每次生成随机数时都会从这个随机源中取seed。在linux和Android中这个随机源位于/dev/urandom文件。 如果我们在终端可以运行cat /dev/urandom命令,会观察到随机值会不断的打印到屏幕上。
在Android 4.2以下,SecureRandom是基于老版的Bouncy Castle实现的。如果生成SecureRandom对象后马上调用setSeed方法。SecureRandom会用用户设置的seed代替默认的随机源。使得每次生成随机数时都是会使用相同的seed作为输入。从而导致生成的随机数是相同的。下面是一段存在安全风险的使用方法:
SecureRandom secureRandom = new SecureRandom(); byte[] b = new byte[] { (byte) 1 }; secureRandom.setSeed(b); // Prior to Android 4.2, the next line would always return the same number! System.out.println(secureRandom.nextInt());
在Android4.2 之前使用以上代码, Android会用种子byte b[]代替了系统默认的随机数生成源。导致nextInt()方法总是范围同一个随机数。
4.2以上的SecureRadom类为什么没有这个问题呢?因为经过比特币钱包漏洞之后Google修改了SecureRandom的内部实现,用基于OpenSSL的算法替代了老版的Bouncy Castle。用户调用setSeed时会将用户设置的seed添加到随机源(/dev/urandom)中而不是简单的替换。
SecureRandom#SecureRandom(byte[] seed) SecureRandom#setSeed(long seed) SecureRandom#setSeed(byte[] seed)
2. 在调用setSeed方法前先调用任意nextXXX方法。 原理如下:
我们可以通过SecureRandom#nextBytes(byte[] bytes)避免这个问题。具体做法是调用setSeed方法前先调用一次SecureRandom#nextBytes(byte[] bytes)方法。为什么这样就可以避免默认随机源被替代呢? 我们可以从源码中找到答案。(本文所引用代码全部基于Android API 16)
在SecureRandom初始化时, 会生成一个SecureRandomSpi对象。SecureRandom的核心方法都由SecureRandomSpi对象代理。 下面是SecureRandomSpi类的子类SHA1PRNG_SecureRandomImpl的初始化源码:
在初始化时会将内部状态机设置为state = UNDEFINED。如果使用者接着调用 SecureRandom#nextBytes方法,
SecureRandom会调用SecureRandomSpi#engineNextBytes方法
如果state的状态为UNDEFINED,那么nextBytes会使用默认的随机源。并将state设置为NEXT_BYTES之后如果调用setSeed方法,最终会调用到SecureRandomSpi#engineSetSeed方法.源码如下:
当state == NEXT_BYTES时会恢复原有的hash再更新seed,因为nextBytes时已经设置了系统的seed所以setSeed中传入的seed将添加到系统seed的尾部。 这样生成随机数时也会在系统的随机源中找seed,从而保证其随机性。
最近网上流传一种利用SecureRandom输出固定随机值并用这个随机值当作加密秘钥的用法。这种模式利用前一节中提到的用特定seed代替系统随机源的方法,故意让SecureRandom每次都输出固定的随机值。通过这个固定值作为秘钥加密本地文本。其使用方式和流程如下:
使用例子:
这种方式确实可以对原始秘钥做一定的隐藏。起到混淆的作用。但google官方博客否定了该方式的使用。 原因如下:
1. 对资深的攻击者而言这种方式的加密太过简单。他们可以轻易的看懂这里在干什么,并构造有效的攻击代码。
2. 整个加密的过程依赖SecureRandom的实现细节,这种依赖使得程序健壮性和可扩展性都非常脆弱。例如在Android API 17以后SecureRandom的默认实现方式从Cipher.RSA 换到了 OpenSSL。SecureRandom新的实现方式不能将自己的seed替换掉系统的seed。造成这段代码在API 17以上不能工作。APP必须强制升级才能继续运作。对某个类内部细节的依赖是软件设计中的大忌。
3. 从seed到生成key的过程非常的廉价,时间成本和资源要求的很低。如果攻击者采用暴力破解这种加密方式将显的很脆弱。
标准的秘钥生成方式应该使用PKCS#5算法。该算法主要有两个优点。
1. 利用随机盐加强秘钥的强度。随机盐可以有效的防止暴力破解。同一个password可以生成多个秘钥。攻击者不得不针对每个salt构造不同的秘钥字典。
2. 通过迭代方式增加秘钥生成的时间成本。使得攻击者破解秘钥的时间大大增加。
Android的JCE provider 现在能支持PBKDF2WithHmacSHA1。下面的代码将展示如何通过PBK算法将password生成为一个秘钥。
参考文章:
http://android-developers.blogspot.co.uk/2013/02/using-cryptography-to-store-credentials.html
http://crypto.stackexchange.com/questions/11260/why-is-sharing-the-seed-and-using-securerandom-deterministically-so-bad
http://stackoverflow.com/questions/13433529/android-4-2-broke-my-encrypt-decrypt-code-and-the-provided-solutions-dont-work
http://nelenkov.blogspot.com/2012/04/using-password-based-encryption-on.html
http://android-developers.blogspot.com/2013/08/some-securerandom-thoughts.html
http://emboss.github.io/blog/2013/08/21/openssl-prng-is-not-really-fork-safe/
https://bitcoin.org/en/alert/2013-08-11-android