导语:在对小型初创企业和大型银行和电信公司进行了数百次安全代码审查,并阅读了数百个在安全社区发布的堆栈溢出的帖子之后,我们列出了开发人员最容易犯的十大加密错误。
在对小型初创企业和大型银行和电信公司进行了数百次安全代码审查,并阅读了数百个在安全社区发布的堆栈溢出的帖子之后,我们列出了开发人员最容易犯的十大加密错误。
不幸的现实是,错误的加密方式随处可见。正确加密的次数远远低于我们发现的错误加密的次数,许多问题是由于复杂的加密API在默认的情况下是不安全的。另一个原因是,我们需要训练有素的专家进行手动代码分析才能发现问题。根据我的经验,流行的静态分析工具在寻找加密问题方面并不出色,此外,黑盒渗透测试也几乎无法发现这些问题。我希望代码审阅员和程序员可以根据此次发布的10大错误列表来改进软件加密的状态:
Top 10 加密错误列表:
硬编码密钥;
没有选择一个合适的IV(初始化向量);
ECB(电子密码本模式)运作模式;
密码存储误用或滥用密码学原语;
MD5不会消亡以及SHA1也需要继续使用;
密码不是加密密钥;
假设加密提供消息完整性;
非对称密钥长度太短;
不安全的随机数;
加密汤(Crypto soup);
详细分析
1. 硬编码密钥
这一问题是我们经常可以看到了。硬编码密钥意味着谁有访问软件代码的权限,谁就可以知道解密数据的密钥。理想情况下,我们从来不希望加密密钥可以被别人访问(例如,6年前的RSA事件)。但是很多的公司远远无法实现这一理想状态,所以我们只能要求仅限安全操作团队才能访问加密密钥,开发人员不能进行访问,更重要的是这些密钥不应该被记录到源代码存储库中。
硬编码密钥也代表对密钥管理的思考不足。密钥管理是一个复杂的主题,远远超出了本次文章所能阐述的范围。但是必须说的是,如果一个密钥受到威胁,那么就需要发布新的软件来进行替换,这些新发布软件上线前必须进行测试。发布新软件需要时间,此时发生类似的安全事件就不是什么新鲜事了。
对于安全人员而言,可以很容易的告诉开发人员哪些事情不能做,但是不幸的现实是,出于各种原因,我们真正希望他们做的事往往是不可行的。因此,开发人员需要一些折衷的建议。
在此特别声明,我既不是安全操作人员,也不是密钥管理专家,但是我可以根据我所看见做出评论。即使无法实现理想状态,但是配置文件对于密钥存储而言是比硬编码更为理想的选择。虽然一些框架支持加密配置部分(另请参阅.Net指南),但是真正需要的是,开发人员拥有测试密钥对测试和开发环境进行测试,随后安全操作团队再用真正的密钥替换测试密钥,并将其部署到真实环境中。
上述方法在真实实践中运用得并不理想。有这样一种情况,部署团队提交了一个错误的RSA公钥,且由于没有错误公钥所对应的私钥,因此无法解密。我的建议是,软件需要一种方法来测试自己,以此确保自身能够加密/解密,或者为部署过程增加一个程序,以确保事情按预期实现。
2. 没有选择一个合适的IV(初始化向量)
IV表示初始化向量。这个问题通常出现在密码分组链接模式(CBC模式)加密中。很多时候它是硬编码的IV,一般所有元素都是0。在其他的情况下,一些加密模式是用秘钥和“盐值(salt)”完成的,但最终的结果是,每次加密都使用相同的IV。我遇到过最糟糕的情况是将密钥当做IV来使用,这种情况还出现了3次。
当你使用CBC操作模式时,要求是你需要随机地和不可预测地选择IV。在Java中,使用SecureRandom。在.Net中,只需使用GenerateIV就可以了。此外,你不能只通过这个方法选择IV,然后在其他的加密中使用完全相同的IV。对于每次加密,需要生成一个新的IV。IV不是秘密的,且通常包含在最初的加密数据中。
如果你没有正确选择IV,那么安全属性就会丢失。在SSL/TLS中没有正确选择IV就是一个例子,其影响是巨大的。
不幸的是,API通常也存在问题。其中Apple API就是一个IV可以被忽略的完美案例,它告诉开发者IV是可选的,另外如果它没有提供IV,则使用全部是0的向量来替代。当然,它仍然会加密和解密,但它不再是安全的Apple API了!
3. ECB(电子密码本模式)操作模式
当你使用分组密码(block cipher),例如高级加密标准(AES)进行加密时,你应该选择一种操作模式。你能选择的最差的操作模式是EBC模式,也就是所谓的电子密码本操作模式。
当你认为分组密码(block cipher)的底层情况无关紧要:如果你使用EBC模式,那么它是不安全的,因为它会泄漏明文的信息。特别是,重复的明文将会产生重复的密文。如果你认为这并不重要,那么你有可能看不见加密的企鹅(encrypted penguin)了。(图片版权属于Larry Ewing, [email protected],我需要提一下GIMP这款软件)。
糟糕的API(像Java)会将指定默认行为的工作交给提供者。通常,在默认的情况下使用的是EBC模式。令人遗憾的是,OWASP在他们的“Good Practice:Use Strong Algorithms”例子中犯了这个错误(使用了默认的EBC模式),但是他们在这里做对了(使用的是CBC模式),这是在互联网上少数几个几乎没有问题的地方之一。
重要提醒:不要使用ECB模式。点击参阅安全使用模式的指南。
4. 密码存储误用或滥用密码学原语
当加密人员看见PBKDF2函数使用1000迭代进行密码存储时,他们可能会抱怨1000次的迭代次数太少,而且使用像bcrypt这样的函数会是更好的选择。
这里存在的部分问题是术语(terminology)问题,密码学界并没有努力去修复这一问题。哈希函数是很棒、很神奇的函数。它们具有抗冲击、抗原像(preimage resistant)、抗第二原像等性质,且相同时间里同时存在快速和缓慢的运行速度。也许,现在是时候为不同目的而定义出不同的加密函数,而不是依赖于某一个神奇的函数(如哈希函数)。
对于密码处理,我们需要的主要属性是速度慢、抗原像以及抗第二原像等。能够实现这些目标的函数有:pbkdf2、bcrypt、 scrypt 和argon2。此外,我发现有些API还在使用PBKDF1函数(已被放弃使用),例如Microsoft和Java中的一些API。
另一个常见的问题是在处理密码过程中硬编码“盐值”问题。盐值的主要的目的之一是使两个完全相同的密码得到不同的哈希值。如果你硬编码盐值,那么就会失去这个属性。在这种情况下,一个获取你数据库访问权限的人,就可以通过对“hashed”的密码进行频率分析来轻松识别简单的目标。
就个人而言,如果可能的话我会选择bcrypt加密方式。不幸的是,很多的库只给了你PBKDF2加密方式。如果是这样的话,一定要确保你的迭代次数不少于10000次,这样你的密码存储效果会比大多数人更好些。
5. MD5不会消亡以及SHA1也需要继续使用
早在10多年前MD5就已经被破解了,且早在20多年前使用MD5就会发出警告,但是,目前仍然能在很多地方发现有人使用MD5。通常情况下,MD5会被用于一些非常疯狂的方式中,在那里,人们不清楚他们需要什么样的安全属性。
在理论上,MD5被破解了,SHA1也已经被破解了,只是最近才发生了第一个对SHA1的攻击事件。在SHA1被破解之前,Google已经不再使用SHA1进行证书签证了,但是SHA1依然存在于很多开发者的代码中。
每当看见开发人员使用加密的哈希函数,我都会很担心。他们通常不知道他们在做什么。哈希函数是密码学家用来构建有用的密码学原语(如消息认证码、数字签名算法和各种prngs)的一种不错的原语形式,但是这也让喜欢使用这些原语的开发人员所开发的代码陷入了危险的处境。开发者们,你确定这些函数是你需要的吗?
6. 密码不是加密密钥
我经常看到很多人不了解密码和加密秘钥之间的区别。密码是需要人们记住的东西,可以是任意长度。另一方面,密钥不局限于可打印的字符,且具有固定的长度。
这里的安全问题是密钥应该是全熵,而密码本身是低熵的。有时候,你需要将密码更改为密钥。这样做的正确方法是:使用基于密码的密钥导出函数(pbkdf2、bcrypt、scrypt 或argon2),其通过将密码进行一个耗时的处理,然后导出密钥来弥补低熵的输入。我很少看到有这样做的。
像crypto-js这样的库将密钥的概念和密码的概念混淆在一起,这让使用它的人不可避免地想知道,为什么不能在JavaScript中将数据进行加密后,再在Java、.Net、其他语言或者其他框架中使用密码对数据进行解密?更糟糕的是,该库使用一些糟糕的算法(如基于MD5的算法)将密码转换为密钥。
我对开发人员的建议是,无论何时只要发现那些提供了对密码或者密码短语的加密功能的API,都要尽力去避免,除非你明确知道如何将密码转换成密钥。希望通过诸如PBKDF2、bcrypt、scrypt或argon2这类算法完成转换。
对于将密钥作为输入的API,可以用加密的prng(伪随机数生成器,例如SecureRandom)生成密钥。
7. 假设加密提供消息完整性
加密可以隐藏数据,但是攻击者很有可能会修改加密数据,如果不检查消息完整性,你的软件就很有可能接受修改的加密数据。虽然开发人员会说“修改后的数据在解密后就如同垃圾数据”,但优秀的安全工程师能够分析这些垃圾数据在软件中造成不利行为的可能性,然后将其转换为真正的攻击。我看过的许多加密案例中都只是对消息进行了加密,但是要知道检查消息的完整性比加密更重要。你需要了解自己的需求是什么。
一些加密的操作模式既提供了保密性,也提供了消息的完整性,其中最有名的是GCM。但是如果开发人员重复使用IV,GCM就会变成非常糟糕的模式。鉴于IV的复用频率,我建议不要使用GCM模式。可选择的选项包括.Net带有计算器的CBC-MAC和Java BouncyCastle包中的CCMBlockCipher、EAXBlockCipher和OCBBlockCipher。
仅对于消息的完整性而言,HMAC是一个很好的选择。HMAC内部使用哈希函数,但是具体是哪个哈希函数并不重要。我建议大家在底层使用诸如SHA256这类哈希函数,即使SHA1缺乏抗冲击性,但是HAMC-SHA1事实上已经是相对安全的了。
请注意,加密和HMAC可以结合起来提供保密性和消息的完整性。但是,需要提醒的是,HMAC应该运用在与IV结合的密文上,而不是运用在明文上。
8. 非对称密钥长度太短
开发人员在选择对称加密的密钥长度方面做的很好,通常都会选择比他们要求的长度更强的(128 bit已经足够了)。但是对于非对称加密的密钥长度,他们在选择却往往犯错。
对于RSA、DAS、DH和相似的算法而言,1024bit是像NSA这种大型的机构所需要达到的密钥长度,而根据摩尔定律(Moore’s law),一些小型的机构很快也会达到这个长度。届时,那些大型机构至少需要达到2048bit。
对于基于椭圆曲线(elliptic curve based)的系统,人们可以选择更短的密钥。目前,我还没看到开发人员使用基于椭圆曲线的算法,所以我也没有发现有关密钥长短的相关问题。
点击查阅密钥长度指南。
9. 不安全的随机数
我很惊讶,虽然这种问题不会频繁地发生,但我还是会不时的发现了这类问题。一般情况是,典型的(伪)随机数生成器产生的随机数,在没有经过专业训练的人的眼中是随机的,但在训练有素的专家面前,这种随机数生成器并没有达到不可预测的要求。
例如,假设你正在使用java.util.Random生成一个web应用程序的会话标记。当我作为合法用户获取会话标记时,我(利用我的密码专长)可以预测下一个用户和上一个用户的会话标记。然后我就可以劫持他们的会话。
如果使用SecureRandom产生会话标记,则不可能上述情况。伪随机数生成器是密码安全性的基本需求。在.net中,好的源代码是System.Security.Cryptography.RandomNumberGenerator。
还有一点值得一提的是,即使你使用了很好的随机数生成源,但并不代表你不会犯错。例如,我见过这样一个实现,从SecureRandom中生成一个32 bit的整数,并将其进行哈希生成会话标记。开发人员从来没有想过,在最多2^32个可能的会话标记中,攻击者可以通过枚举出所有可能的会话标记来劫持刚才实现的会话。
10. 加密汤(Crypto soup)
我使用“加密汤”这个术语来表示开发人员将一堆密码学原语,没有明确目的地混合在一起的现象。我不喜欢称它为自己发明加密算法(roll-your-own-crypto),因为我认为这个术语是在尝试建立一个有明确目标的加密算法,例如分组密码(block cipher)。
“加密汤”经常使用哈希函数,对于这一点,你可能需要回头看看观点5的最后一段内容。说到这,我想警告开发人员,“请远离哈希函数,你并不知道你在做什么!”
总结
开发人员可以遵循一下建议,来提高自身进行代码加密的状态:
1. 我们需要更多的教育工作者!
我正在和一些了解加密和开发人员代码的人士进行讨论。我很高兴可以在Stack Overflow上寻找到一些优秀的人士,但是在互联网上仍然存在很多错误的指导。我们需要更多优秀的人士来帮忙纠正这些问题。
2. 加密API需要变得更好
首先,API能够使加密功能更简单,其次,API在默认的情况下需要是安全的,此外,文档需要对发生的问题非常的清楚。目前,Microsoft正在朝这个正确的方向努力,而Java却不是。
3. 静态分析工具需要改进
上面所说的这些工具可能找不出加密问题,但是其他的一些工具应该能。我知道有一款叫“Cryptosense”的工具,但不幸的是我试过觉得并不是很好。我也用过很多大牌的工具,但是它们缺乏发现加密问题的现状都让我很失望。
4. 代码审计人员需要手动搜索加密问题
这其实并没有那么难,首先构建一个grep -Rli crypt来获取包含“crypto”一词的文件列表。也可以查找MD5等等。加密研究人员需要更多的重视现实世界的安全问题。如果Dan Boneh和他的同事能够做这样的研究,那么其他研究员一样也可以做到。我们需要人的参与来帮助解决世界上的加密混乱问题。