卡巴斯基密码管理器:你的所有密码都属于我们
专业靶场建设和运营机构
本文译自安全博客文章《Kaspersky Password Manager: All your passwords are belong to us》,作者Jean-Baptiste Bédrune,译者covision敏杰,阅读时间24分钟。
摘要说明
卡巴斯基密码管理器中的密码生成器存在多个问题。最关键的问题是,它使用的PRNG(Pseudo random number generator,伪随机数生成器)不适合用于加密目的。它的熵的唯一来源是当前时间。它创建的所有密码都可以在几秒钟内被暴力破解。本文解释了如何安全地生成密码,卡巴斯基密码管理器失败的原因以及如何利用此缺陷。还提供了一个概念证明,以测试你的版本是否有漏洞。
该产品已更新,其最新版本不受此问题的影响。
简介
两年前,我们研究了卡巴斯基密码管理器(KPM,Kaspersky Password Manager),这是一个由卡巴斯基开发的密码管理器。卡巴斯基密码管理器是一款安全地将密码和文档存储在一个加密的保险库中的产品,受密码保护。此保险库受主密码保护,因此,与其他密码管理器一样,用户必须记住一个密码才能使用和管理所有密码。该产品可用于各种操作系统(Windows、macOS、Android、iOS、Web……),然后加密数据可以在你所有的设备之间自动同步,始终受你的主密码的保护。
卡巴斯基密码管理器的主要功能是密码管理。关于密码管理器的一个关键点是,与人类相反,这些工具能够很好地产生随机、强大的密码。要生成安全密码,卡巴斯基密码管理器必须依靠安全的密码生成机制。我们将首先看到一个很好的密码生成方法的例子,以解释为什么卡巴斯基使用的方法有缺陷,以及我们如何利用它。正如我们将看到的,此工具生成的密码可以在几秒钟内被破解。
不到两年后,卡巴斯基密码管理器的所有版本都打上了了此漏洞的修复补丁。该漏洞已分配给漏洞编号CVE-2020-27020。
从一个字符集生成强大的密码
为了简单起见,让我们来研究一下在KeePass这个开源项目中是如何生成密码的。密码生成是在“KeePassLib.Cryptography.PasswordGenerator”命名空间中的不同类中来实现的。KeePass提供了生成密码的3种方法:基于字符集、基于模式和自定义生成方法。
更简单的方法是基于字符集的生成器,它从一个给定的字符集创建一个密码。让我们看看它是如何工作的。这里是负责生成密码的主要循环:
for (int i = 0; i < passwordLength; i++)
{
password[i] = GenerateCharacter(characterSet, randomSource);
}
调用GenerateCharacter方法以生成密码中的每一个字符。它需要一个字符集和一个随机源,并从字符集中输出一个随机字符。它的实施是相当直接的:
private static char GenerateCharacter(string characterSet, Random randomSource)
{
int index = randomSource.Next(0, characterSet.Length);
return characterSet[index];
}
最后,GetRandomUInt64是一个统一的随机数生成器,输出值在0和cc - 1之间:
private static ulong GetRandomUInt64(ulong cc, Random randomSource)
{
ulong mask = cc - 1;
ulong value;
do
{
value = (ulong)randomSource.Next() << 32 | (ulong)randomSource.Next();
} while ((value & mask) >= cc);
return value % cc;
}
这里重要的是,每个字符的生成都是独立于其他字符的:每个字符都是随机的,知道哪个字符之前已经生成,并不能给我们提供关于下一个将生成的字符的信息。
最后,让我们假设GetRandomUInt64具有很强的加密能力,并生成一个随机的64位数字。为什么这里有一个循环,为什么这个函数不简单地实现为return GetRandomUInt64() % uMaxExcl;呢?
统一的随机数生成
这个循环对于在一个范围内统一地生成数字是必不可少的。
想象一下,你想从一个有10个可能的字符的字符集中得到一个随机字符,你有一个随机数生成器方法GetRandom32,它输出的数字在0到32之间(32不包括在内)。输出这种字符的直接方法是:
char randomCharacter = characterSet[GetRandom32() % characterSet.Length];
让我们看看这些字符都是如何产生的:
- 如果GetRandom32()返回4、14或24(3个可能的值),则返回 "4"
- 如果GetRandom32()返回5、15或25(3个可能的值),则返回 "5"。
- 但如果GetRandom32()返回1、11、21和31(4个可能的值!),则返回 "1"
下面给出了分布情况:
| 字符 | 分布情况 | | ---- | -------- | | 0 | 0 | | 1 | 4 | | 2 | 0 | | 3 | 0 | | 4 | 3 | | 5 | 3 | | 6 | 0 | | 7 | 0 | | 8 | 0 | | 9 | 0 |
因此,这种方法存在一个偏差:从输出结果可以看出,数字0和1的输出频率要高于其他数字。这通常被称为 "模数偏差"(modulo bias)。你应该查看Kudelski Security编写的关于 "模数偏差 "和如何避免它的优秀权威指南《Definitive guide to “modular bias” and how to avoid it》,以了解更多信息。
为了消除这种 "模数偏差",一种常见的方法是丢弃所有大于或等于30的数字(10的最大倍数,小于32):
char randomCharacter = characterSet[GetRandom32() % 10];
这正是KeePass所做的,尽管KeePass中的偏差将比目前的例子中的要小得多,因为GetRandomUInt64产生的值比密码字符集的大小大得多。
我们看到了如何从一个给定的字符范围内统一地选择一个字符,假设我们的随机源是统一的。现在让我们来看看什么样的源适合生成密码学上的强随机数。
密码学上安全的PRNG
生成的数字必须是随机的。但这究竟是什么意思呢?一个普通的不错的PRNG会通过一系列的测试,主要是统计随机性测试,如Diehard或Dieharder测试。
一个密码学上安全的PRNG(CSPRNG,cryptographically secure PRNG)也将通过这些测试,但它还有另外两个要求:
- 它必须满足下一比特测试。知道CSPRNG已经生成的所有比特,没有任何多项式时间方法能以高于0.5的概率预测下一个比特。
- 如果在任何时候,CSPRNG的整个状态被破坏,就没有办法找回之前由CSPRNG返回的比特。
这些点对密码生成至关重要。例如,如果一个密码由于某种原因被泄露,而如果一个非CSPRNG被用来生成这个密码,那么攻击者就可以检索到使用这个PRNG生成的其他密码。大多数操作系统都提供CSPRNG的实现:Windows上的CryptGenRandom,或类UNIX操作系统的/dev/random。
一些软件喜欢使用他们自己的实现,通常完全或部分地由操作系统的PRNG作为种子。KeePass使用两种PRNG,一种是基于Salsa20和ChaCha20,另一种是传统的基于ARCFour的变种。让我们假设前两个PRNG在密码学上是安全的:我们现在有了所有的元素,可以从一个给定的字符集生成随机、安全的密码。
卡巴斯基的密码生成方法
卡巴斯基密码管理器有一个内置的密码生成器,它根据给定的 "策略 "创建密码。策略设置很简单:密码长度,大写字母,小写字母,数字,以及一组自定义的特殊字符。所有这些设置都可以在密码生成器界面进行配置,如图所示(这是标准设置):
默认情况下,卡巴斯基密码管理器使用扩展字符集生成12个字符的密码。
欺骗出现的频率
生成程序要比Keepass方法复杂得多。卡巴斯基密码管理器首先在0和1之间挑选两个随机浮点数r1和r2,然后将它们与密码字符集的长度相乘,在字符集表中挑选一个值:
int index = (int)(r1 * r2 * characterSet.Length);
char randomCharacter = characterSet[index];
r1×r2的分布是(感谢MathWorld):
此函数的分布不均匀:低位值比接近1的值发生的机会更大。这种方法相当令人费解,但似乎正是KPM想要实现的。
字符集是如何创建的?它是完全有序的,像 "abcdefghij…"?不是……。
- 对于前三个字母,字符集是完全有序的(几乎是……我们将在后面看到)。
- 然后,对于接下来的字母,KPM依赖于字母的频率:它假设最少出现的字母(在某些语言中)应该在生成的密码中出现得更多。在KPM中使用的每个字母的假定出现频率,如下图所示:
然后,根据每个字母出现的反频率对字符集进行排序:q, x, z, w… n, a, e。
由于考虑到分布函数,较低的数值更容易出现,我们可以假设一些像 "q "和 "x "这样的字母更容易出现在由KPM生成的密码中。
如果这些统计的值被独立地用来生成密码中的每一个字符,我们可以看到密码中经常出现几个 "q"、"x "或 "z"。然而,事情要复杂得多:生成的字符在计算出现的频率时被考虑进去了。如果产生了一个 "z",那么 "z "在频率表中出现的概率就会大大增加。一旦字符集按照该表排序,"z "将位于该表的末尾,需要采取的变化将少得多。
这些变化也影响到其他字母:在 "z "被选中后,"a"、"e"、"m"、"q"、"s "和 "x "的概率也有所增加。相反,"h "却减少了。但是,在 "h "被选中后,其出现的概率会增加很多。
我们的假设是,该方法被实现来欺骗标准的密码破解工具。密码破解工具,如Hashcat或John the Ripper,试图破解第一个可能的密码,例如由人类产生的密码。他们的密码破解方法依赖于这样一个事实:在人类创造的密码中,"e "和 "a "可能比 "x "或 "j "更多,或者双字母组 "th "和 "he "会比 "qx "或 "zr "出现得更频繁。
专门的技术,如马尔可夫生成器,假设人类生成密码的方式存在一个隐藏的马尔可夫模型,可以直接破解这种生成方法(详情请见利用时间-空间权衡对密码进行快速字典攻击)。
因此,平均而言,由KPM生成的密码将在这些工具测试的候选密码列表中名列前茅。如果一个攻击者试图破解由KPM生成的密码列表,他可能会等待相当长的时间,直到第一个密码被发现。这是很聪明的做法。
然而,如果一个攻击者知道一个密码是由KPM生成的,他可以调整他的工具以考虑到KPM所遵循的模式。由于这些密码在某种意义上是有偏差的(为了对付密码破解者),这种偏差可以被用来生成这个工具所产生的最有可能的密码,并首先对它们进行测试。一个直接的方法可以是使用马尔可夫生成器,就像John the Ripper提供的那个(这个方法还没有被测试)。
我们可以得出结论,生成算法本身并没有那么糟糕:它可以抵御标准工具。然而,如果攻击者知道一个人使用KPM,他将能够比完全随机的密码更容易破解他的密码。然而,我们的建议是,生成足够长的随机密码,使其强大到无法被工具所破解。
我们前面看到,KPM挑选了两个随机值r1和r2来计算字符集表中的索引。现在让我们看看这些值是如何计算出来的。
KPM的随机数发生器
这两个值直接来自KPM的PRNG。这个PRNG均匀地输出0到1之间的浮点数,1除外。
桌面版和网络版使用的PRNG不同:
- 网络版使用的是.NET技术。这个函数不适合生成加密安全的随机数(其中包括生成密码所需的熵),这一点在。Chrome、Firefox和Safari使用的底层PRNG是xorshift128+。它非常快,但不适合生成加密材料。在KPM中的安全后果尚未研究,但我们建议卡巴斯基将其替换为Math.random()或window.crypto.getRandomValues(),这是之前提到的Mozilla文档页面的建议。
- 桌面版使用的是Boost提供的PRNG:mt19937Mersenne Twister。Mersenne Twister是非常好的、广泛使用的PRNG,MT19937是最流行的Mersenne Twister。它是均匀分布的,有很长的周期,与其他 "好的 "PRNG相比,它的速度很快。
然而,使用Mersenne Twister来创建密码是一个好主意吗?肯定不是。
这个发生器的问题在于它不是一个CSPRNG。知道它的几个输出(在这种情况下是624)就可以检索它的全部状态,并预测它将产生的所有数值,加上它已经产生的所有数值(见Belekamp-Massey或Reeds-Sloane算法)。
像randcrack这样的现成工具可以用来破解Python的模块,它使用了MT 19937非常相似(如果不是相同的话)的实现。要破解Boost的实现,应该只需要非常小的调整。random
在实践中,在卡巴斯基的密码管理器中利用这种缺陷是很难的:
- 密码很短:默认为12个字符。检索624个密码字符需要抓取52个密码。
- 原始输出值是不知道的:输出值是密码的每个字母在字符集中的位置。更多的值可能是必要的。
- 而我们看到,这个位置在字符集中是PRNG产生的两个值的乘积。
在KPM的背景下,我们看不到一个直接的方法来攻击这个PRNG。
给梅森旋转算法”种子“
我们看到PRNG均匀地生成[0, 1]的浮点数。负责其初始化的代码应该是这样的:
Random randomSource = new Random(seed);
种子从哪里来?密码生成函数是这样调用的:
GeneratePassword(characterSet, passwordLength, seed);
种子只有32位。这意味着它可以很容易地被破解。
每次生成密码时都会创建一个PRNG的实例。这意味着卡巴斯基密码管理器最多可以为一个给定的字符集生成232个密码。
GetSystemTimeAsFileTime只有在没有向该方法提供种子的情况下才会被用作种子。当用户要求一个新的密码时,如何调用这个方法?答案是:generatePassword。
因此,用于生成每个密码的种子是当前的系统时间,单位是秒。这意味着世界上每一个卡巴斯基密码管理器的实例都会在某一秒生成完全相同的密码。如果每次点击密码生成器界面上的 "生成 "按钮,都会产生相同的密码,这一点就很明显了。然而,由于某些原因,密码生成是动画的:当真正的密码已经被计算出来时,会显示几十个随机的字符。
这个动画需要1秒钟以上,所以不可能在1秒钟内多次点击 "生成 "按钮。这无疑是这个弱点之前没有被发现的原因。
其后果显然是不好的:每一个密码都可以被破解。例如,在2010年和2021年之间有315619200秒,所以KPM最多可以为一个特定的字符集生成315619200个密码。对它们进行暴力破解需要几分钟的时间。
网站或论坛显示账户的创建时间是很常见的。知道了账户的创建时间,攻击者就可以尝试用一个小范围的密码(大约100个)来破解账户密码,并获得访问权。
此外,如果使用卡巴斯基密码管理器生成的泄漏数据库的密码、加密档案的密码、TrueCrypt/Veracrypt卷的密码等,也可以轻松找回。