返回 登录
0

开发者常犯的十大加密错误

原文:Top 10 Developer Crypto Mistakes
作者:crazycontini
翻译:Kenneth


图片来自:英文原文

在经历过成千上百行的代码审计以及在Stack Overflow上阅读了大量有关安全方面的帖子之后,我列出了开发人员常犯的10大加密错误。

不幸地是,有问题的加密无处不在。能正确完成加密的次数远远小于不能正确完成的次数。很多问题是由如下两个原因造成的:在默认情况下,加密API是不安全的,没有完备的文档。虽然Java在这两个方面表现最差,但不仅仅只有Java在这两个方面表现最差。也许,Java应该学习它的主要对手.Net,提供一个易用的和更安全的加密API。

另外一个原因是:需要一位训练有素的专家亲自通过代码分析来查找问题。流行的静态分析工具在寻找加密问题方面还做得不是很好。另外,黑盒渗透测试也很难发现这些加密问题。希望代码审计人员和编程人员能够通过本文这10大加密错误来改善软件中的加密问题的状况。

1.硬编码密钥

硬编码密钥拥有者可以有权访问软件代码,他就能知道加密数据的密钥(讨论混淆的开发人员可以看看第10条加密错误)。理想状态下,我们绝不想密钥被别人获得(例如,6年前,RSA攻击事件)。但是很多的公司远远没有达到这个理想状态,因此接下来最重要的事是做权限控制。要求只有安全操作团队才能够访问密钥。当然,开发人员没有访问权限。更特殊的是这些密钥不能被记录到源代码仓库里面。

硬编码密钥不利于密钥的管理。密钥管理是一个复杂的话题。如果密钥被破解了,需要发布新的软件来替换以前的硬编码的密钥,但是该软件必须在上线之前经过测试。发布新的软件需要一段时间,所以当有类似的事件发生时,时间就变得不是很重要了。

安全人员可以很容易的就能告诉开发人员那些事情不能做,但是不幸的现实是出于某些原因,开发人员将能做的变成了不可行的事了。因此,开发人员需要一些折衷的建议。

在这里做一个声明:我既不是安全操作人员,也不是密钥管理的专家,但是我可以根据我所看见做出相应的评论。尽管将密钥存储到配置文件中是一个不错的选择,但是这种方式还是难于理想态望其项背。虽然一些框架提供了加密配置节(见.Net指南),但是开发人员可以使用测试密钥对测试环境和开发环境进行测试,然后再由安全操作团队提供真正的密钥替换测试密钥,最后将其部署到真实的环境中。

上述的方法在现实中运用得一团糟。其中一个案例是,部署团队提交了一个错误的RSA公钥,但由于没有错误公钥对应的私钥,所以导致他们不能进行解密。我建议软件应该有一种能确定自身能否加密和解密的方法或者操作,或者部署一个能起到同样效果的程序。

2.选择初始向量不当

IV的意思是初始向量。 这个问题常常出现在密码分组链接模式(CBC模式)加密中。CBC模式采用硬编码初始向量的方式,一般初始向量的所有元素是由0填充。在其他的案例中,虽然一些好的加密模式是使用秘钥和“盐值”完成的,但是在这些模式中,每一次加密都使用相同的初始向量。我所见过的最糟糕的是将秘钥当做初始向量来使用——见Crypto 101的7.6节,一共出现3次,这是很危险的。

当你使用CBC工作模式时候,需要随机地和不可预测地选择初始向量。在Java中,可以使用SecureRandom类。在.Net中,仅仅使用GenerateIV方法就可以了。并且,我们不能仅仅通过这个方法选择初始向量,然后在其他的加密中使用完全相同的初始向量。每一次加密需要都产生一个新的初始向量。初始向量不是秘密的,一般是最初的加密数据中一部分。

如果你没有正确选择初始向量,那么安全性质就会遗失。在SSL/TLS中初始向量选择不当就是一个例子,这个影响是巨大的

不幸的是,一些API也是有问题的。Apple API是一个初始向量可以被忽略的典型的例子——这告诉开发者初始向量是可选的,另外如果它没有提供初始向量,则使用全部是0的向量来替代。当然,这样的Apple API 仍然能加密和解密,但是它不再是安全的Apple API了!

在不同的工作模式中,关于初始向量和nonce的更多信息见这里

3.电码本工作模式

当你使用分组密码加密,例如高级加密标准(AES),你应该选择一个分组密码的工作模式。你能选择的最糟糕的工作模式是EBC模式,EBC模式表示电码本工作模式。

在你不关心分组密码的底层的情况下使用EBC模式,因为它会泄漏明文的信息,所以它是不安全的。特别是,重复的明文将会产生重复的密文。如果你认为没什么关系的话,你有可能看不见加密的企鹅了。(图片的版权属于Larry Ewing, lewing@isc.tamu.edu ,提及一下GNU图像处理程序(GIMP)这款软件)。

不好的API将指定默认行为的工作交给了提供者,例如Java在默认的情况下使用的是EBC模式。令人遗憾的是,OWASP在他们的“Good Practice:Use Strong Algorithms”例子中犯了这个错误(使用了默认的EBC模式),但是他们在这里做对了(使用的是CBC模式),这是在互联网上少数几个没有问题的地方之一。

最重要的一点是不要使用ECB模式,安全使用模式的指南见这里

4.密码存储过程中密码学原语的无用或滥用

当加密人员看见PBKDF2函数中迭代次数的值为1000的时候(对密码进行1000次的迭代),他们可能抱怨1000次的迭代次数太少,并且他们认为使用类似bcrypt这类函数是一个更好的选择。另一方面, 我只乐于开发人员做的事情是对的。

这里部分问题是出现术语的定义上,密码学界没有努力去解决这些问题。哈希函数是一个很棒的和很有魔力的函数。他们具有抗碰撞性、抗原像性、抗第二原像性的性质和他们有类似随机预言机的作用,并且不同的哈希函数在相同时间里的运行速度有快,也有慢。也许,与其过度依赖单一不错的函数,还不如现在定义出不同用途的不同加密函数啦。

在密码处理过程中,主要需要的性质有低速性、抗原像性和抗第二原像性。Troy Hunt完美的解释了为什么需要有低速的要求。

满足这些性质的函数有:pbkdf2、bcrypt、 scrypt 和argon2。Thomas Pornin在帮助开发者和安全工程师理解为什么在密码存储过程中建议使用bcrypt函数上发挥了出色的作用。另外,如果我们在处理密码中不需要关心MD5、SHA1、SHA256和SHA512的实现就好了。

另外,虽然PBKDF1函数已经被摒弃使用了,但是还有些API在使用PBKDF1函数,例如MicrosoftJava中的一些API。

此外,另外一个问题是在处理密码过程中硬编码盐值的问题。盐值的主要的用途之一是使两个完全相同的密码通过不同的盐值得到不同的哈希值。如果你硬编码盐值,就不会有上述性质了。既然这样,一个有权访问你数据库的人能透过对“哈希之后”的密码进行频率分析,然后轻而易举地识别一个易受攻击的目标。这使得攻击者的注意更加的集中在上述方法上,并且这也使得攻击者也更加容易获得成功。

就开发人员而言,我的建议正如Thomas Pornin所说的那样。他经常在关于安全的StackExchange社区上,从不同的角度来评论密码处理。

就个人而言,如果可能的话,我更倾向使用bcrypt的加密方式。不幸的是,很多的库只给了PBKDF2加密方式。如果你坚持要使用PBKDF2加密方式,那你能确信你的迭代次数不少于10000次,这样的密码存储效果会更好一些。

5.MD5不会走下历史舞台和SHA1仍会继续被使用

实际上,MD5远在10年前就被破解了,并且使用MD5还会产生警告,这已经存在20多年了。我仍然能在很多很多的地方发现有人使用MD5。MD5常常会使用到一些让人不可思议的地方,在这些地方,所需的安全性质已经很模糊了。

在理论上,只要MD5被破解了,SHA1也已经被破解了,但是现实中第一个对SHA1的攻击最近才发生。Google在SHA1实际被破解之前,已经不在使用SHA1进行证书签证了,所以Google在这方面做得不错,但是SHA1仍然还是在很多开发者的代码中存在。

无论何时看见开发人员使用加密的哈希函数,我总是忧心忡忡。他们经常不知道他们正在做的事情。哈希函数本身也是一种不错的原语,并且密码学家使用哈希函数用来建立了一系列有用的密码学原语,例如建立消息认证码、数字签名算法和不同的伪随机数产生器,但是这让乐于使用这些原语的开发人员开发的代码陷入危险的处境,就如同给一位8岁的小孩一把机关枪一样危险。开发人员你们确信需要这些函数吗?

6.密码不是加密密钥

我经常看见这样的问题:密码和加密秘钥之间有什么区别?一方面,密码需要人们记住,长度也是任意的。另一方面,密钥是可以包含不可打印的字符,并且其长度是固定的。

这里的安全问题是:就其本质而言,密钥应该是全熵,而密码是低熵。有时候,你需要将密码转化为一个密钥。正确的方法是使用基于口令的密钥导出函数(pbkdf2、bcrypt、scrypt 或者 argon2),该方法首先是将密码进行一个很耗时的处理,然后导出密钥,这个函数是通过用耗时密码的处理来弥补低熵的输入。我发现很少有这样使用的。

一些库将密钥的概念和密码的概念混淆在一起,就像crypto-js。这让使用crypto-js的人想知道:为什么不能在JavaScript中将数据进行加密之后,然后在Java、.Net、其他语言或者其他框架中使用密码对数据进行解密? 更糟糕的是,有些库使用的将密码转换为密钥的算法是一些基于MD5的不好的的算法

我的建议是:开发人员无论在什么时候都要避免使用那些提供了对密码或者密码短语的加密功能的API,除非你明确知道如何将密码转换成秘钥。希望这种转换能通过例如PBKDF2、bcrypt、scrypt或者argon2这类算法完成。

对于将密钥作为输入的API,可以用加密的伪随机数生成器生成密钥,例如SecureRandom。

7.假设加密提供消息完整性

加密的目的是隐藏数据,但是攻击者很有可能修改加密之后的数据。如果你不做消息完整性检查,你的软件就很有可能接受修改后的数据。虽然在开发人员眼中加密后的数据就如同垃圾数据,但是在一位优秀的安全工程师眼中,首先可以分析这些垃圾数据在软件中产生错误的行为频率,然后将这种分析转换为实际的攻击。在我看过众多的案例中,它们仅仅对消息进行了加密,却没有检查消息的完整性。你要明白你的需求是什么。

一些加密的工作模式既提供了保密性,也提供了消息的完整性,其中最有名的是GCM。如果开发人员要重复使用初始向量,GCM就变成非常糟糕的模式啦。如果已知初始向量的复用频率,我建议不要使用GCM模式,但是可以使用.Net带有计算器的CBC-MAC和Java BouncyCastle包中的CCMBlockCipher、EAXBlockCipher和OCBBlockCipher类。

如果只要求消息的完整性,HMAC是一个不错的选择。HMAC内部使用的是一个哈希函数,但是使用的是哪一个哈希函数并不重要。我建议在底层使用类似SHA256这类哈希函数。尽管SHA1缺乏抗碰撞性,但是HAMC-SHA1已经相当的安全了。

值得注意的是:将加密和HMAC结合就可以同时提供保密性和消息的完整性了。另外,HMAC是运用在密文和初始向量上,而不是运用在明文上的。在这里要感谢在/r/crypto上对本节早期版本的修正的人士们

8.非对称秘钥长度太短

开发人员在选择对称加密的密钥长度上的表现不错,一般都大于他们要求的长度(128比特就足够了)。但是他们在选择非对称加密的密钥长度上往往犯错。

在RSA、DAS、DH和相似的算法中,像NSA这种大型的机构所选的密钥长度达到1024比特,而根据摩尔定律,一些小型的机构在不久的将来与会达到这个长度,与此同时,那些大型机构至少是2048比特了。

对于椭圆曲线的系统,可以选择更短的密钥。到目前为止,还没看过开发人员使用基于椭圆曲线的算法。

关于密钥长度的指南见这里

9.不安全的随机数

虽然关于不安全的随机数不常出现,但是我还是一次又一次的发现了这个问题。在没有经过专业训练的人的眼中,典型的(伪)随机数生成器产生的随机数看似随机,但是在训练有素的专家面前,这种随机数生成器并没有达到不可预测的要求。
例如,你可以使用java.util.Random类生成一个web应用程序的会话标记。我作为一个合法的获取会话标记的用户, 我可以利用我密码学方面的专业知识来预测下一个用户和前一个用户的会话标记。然后我就可以劫持他们的会话了。

如果使用SecureRandom类产生会话标记,就不可能发生会话劫持了。伪随机数生成器是密码安全性的基本需求。在.net中System.Security.Cryptography.RandomNumberGenerator是一个不错的生成器。

值得提醒的是:虽然使用了不错的随机数的生成源,但是这并不代表你不会犯错。例如,我见过一个这样的实现:首先使用SecureRandom类生成一个32比特的整数;然后将这个整数进行哈希生成会话标记。开发人员认为有最多2^32中可能的会话标记(有40多亿),就不会发生会话劫持,但是攻击者可以通过枚举出所有可能的会话标记,然后劫持刚才实现的会话。

10.“加密汤”

“加密汤”这个术语表示开发人员盲目的将一些密码学原语混合使用(生成自己的加密算法)。但我不喜欢称它为自己发明加密算法,是因为我认为这个术语是在尝试建立一个有明确目标的加密算法,例如分组密码。

“加密汤”常常使用的是哈希函数。此时,你可以再次看看观点5的最后一段内容。当我在看观点5的时候,我想对开发人员大声的尖叫:“请远离哈希函数,你并不知道你在干什么!”

关于“加密汤”的一个案例是开发人员使用硬编码的密钥。开发者在没有搞清楚做什么之前,不能使用硬编码的密钥。这会使你很烦恼,是因为除了硬编码密钥这个方式之外,没有其他的途径获取密钥了。最后,他解释道:我所做的事并不需要安全,但是这使得他更加的困惑了。哈哈!这就是一个关于“加密汤”的实际对话。

结束语

为了改善开发人员代码中加密问题的状况,下面给出如下建议:

  • 需要更多的有相关背景的教育者。我正在和了解加密和熟悉开发人员的代码的人士讨论。我也很乐于在Stack Overflow上寻找一些优秀的人士,但是互联网上仍然存在很多错误的指导。需要更多优秀的人士来修正它们。

  • 需要有更好的加密API。一是这个API能够加密功能更简单;二是这些API在默认的情况下是安全的;最后文档应该非常清晰的记录可能会发生的问题。Microsoft正在朝着这个方向努力,但是Java却不是

  • 需要改进静态工具。前面所说的工具可能找不出加密问题,但是其他的工具也许能。我知道有一款加Cryptosense的工具,不幸的是,这款工具并不是很好。我也使用过一些大牌的工具,但是都缺少寻找加密问题的能力。

  • 代码审计人员需要手动的寻找加密问题。这其实并不是很难。通过grep -Rli crypt(见PowerShell 等价的命令)命令可以得到包含“crypto”单词的文件列表。也可以查找MD5等等。

  • 密码学研究员需要更加重视现实中的安全问题。如果能像Dan Boneh和他的同事这样做研究话,我相信其他研究员同样也能做到,来解决世界上的加密问题。

评论