我最近注意到一个使用.NETPBKDF软件从密码字符串派生加密密钥。该密码字符串是使用System.Random动态生成的。现在,我知道System.Random并不是真正的密码随机性,不应出于安全目的使用。而且,在.NETSystem.Random的实现中存在一些缺陷。

但是我的问题是:


使用System.Random创建密码字符串并从中派生密钥有什么实际影响?我们是否真的有可能在以后重现密钥?是否存在可行的攻击方式,使我能够以较高的概率推断在此情况下生成的随机字符串?还是仅在特定的“实验室”条件或方案中可以利用的那种漏洞?


评论

System.Random没有缺陷。它不应该是密码随机的。它完全可以达到预期目的。

@sbecker System.Random也存在通用PRNG的缺陷。它没有通过基本的统计一致性测试。例如,Next(1431655765)有66%的机会返回偶数。

安全系统的强大之处仅在于其最薄弱的环节。

#1 楼

您要求实际的影响,所以答案是,花\ 120美元,我可能可以在明天之前完成整个密码数据库。


using System;
using System.Text;
using System.Security.Cryptography;

class Program {
    static void Main(string[] args) {
        byte[] pwd = new byte[128];
        byte[] salt = Encoding.ASCII.GetBytes("saltsalt");

        var rand = new Random();
        rand.NextBytes(pwd);
        Rfc2898DeriveBytes k = new Rfc2898DeriveBytes(pwd, salt, 1000);
        Console.WriteLine(System.Convert.ToBase64String(k.GetBytes(16)));
    }
}


我们应该花一些时间谈论您的食盐。我认为它是固定的。通常,您通常会随机生成它-如果您对盐和密码使用单个System.Random实例,则攻击基本上是相同的。如果要创建两个System.Random实例,一个接一个,那么鉴于默认种子的生成方式,您将不太可能获得任何收益。如果您的盐基于递增的计数器或用户名或其他内容,那么我要说的仅是真正适用于攻击单个密码而不是整个数据库。您仍然应该担心这两种情况。

我编译了此代码,并在自己的计算机上运行它,它给了我IDWrj6Fd9c4kqJ6A2FHDEg==密钥。 System.Random有据可查:https://docs.microsoft.com/zh-cn/dotnet/api/system.random.-ctor?view=netframework-4.8。它使用32位整数密钥,实际上,如果密钥为负,则文档会告诉我们由于某种原因它实际上使用了密钥的绝对值。便利!只有31位!让我们用这个程序来打破它:

using System;
using System.Text;
using System.Linq;
using System.Security.Cryptography;

class Program {
    static void Main(string[] args) {
        byte[] target = System.Convert.FromBase64String("IDWrj6Fd9c4kqJ6A2FHDEg==");
        byte[] pwd = new byte[128];
        byte[] salt = Encoding.ASCII.GetBytes("saltsalt");

        for (int i = 0; i < Int32.MaxValue; i++){
            if (i % 1024 == 0) {
                Console.WriteLine(i);
            }
            var rand = new Random(i);
            rand.NextBytes(pwd);
            Rfc2898DeriveBytes k = new Rfc2898DeriveBytes(pwd, salt, 1000);
            if (k.GetBytes(16).SequenceEqual(target)){
                Console.WriteLine("pwnt");
                Console.WriteLine(i);
                Environment.Exit(0);
            }
        }
    }
}


这要花多长时间?dnSpy是用于反向工程C#应用程序的便捷工具,我们可以使用它来查看System.Random的默认构造函数的作用。您的情况可能有所不同,但在我的种子中,未播种的System.Random会将种子设置为Environment.TickCount,这是自启动计算机以来经过的毫秒数。我不会让计算机一次运行超过几个小时,因此Environment.TickCount可能不会超过36,000,000。看着钥匙过去,我正在尝试每分钟14,336个钥匙。 36000000/14336 = 2511分钟才能尝试所有可行的密钥,因此它应在40个小时内完成。即使您更好地采摘了种子,2147483648/14336 = 149796分钟,大约2400小时。我想我可能会花一些时间重新编写我的垃圾层C#程序,以使其运行更快-它确实确实很慢,并且可以进行大量改进-但我不太关心您的密码来做到这一点。只需运行几千个实例,每个实例以不同的种子开始每个小时一个小时,这会更容易。

亚马逊将以每小时0.05美元左右的价格向我出售CPU小时,因此我要花费120美元,另外还有一点花费大量时间和精力编写代码并将其保存在他们的服务器上,以破坏整个密码数据库。可能更少,因为我的笔记本电脑可能比其计算节点要慢。

这里的大部分时间都花在了实际计算PBKDF2函数上。 PBKDF2非常棒,您仍然应该使用它-它的设计速度非常慢,因此这样的攻击效率很低。 \ $ 120的估算值是基于在PBKDF2中使用1,000次迭代得出的,这对我的计算机的每个哈希值都需要花费几毫秒的时间。您可以通过将参数调整为10,000来使这次攻击的成本为1200美元,尽管坚定的攻击者仍然乐意为此付出代价以获取用户数据。如果您希望此攻击使我损失1,200,000美元,则可以使用10,000,000次迭代!每次需要密码时,您还会浪费几个小时。我不知道您的应用程序在做什么,但是如果花所有时间在计算哈希上,那么挖掘比特币或其他东西可能会赚更多的钱。数字生成器不是一个好主意。请改用此网址:https://docs.microsoft.com/zh-cn/dotnet/api/system.security.cryptography.rngcryptoserviceprovider.getbytes?view = netframework-4.8。如果要从CSPRNG生成128个字节,那么这种蛮力将使我花费数万亿美元的计算时间,这是整个地球GDP的数百倍。任何组织都无法达到这种计算能力水平。

评论


$ \ begingroup $
.NET是开放源代码的,因此实际上不需要进行逆向工程。 .NET Framework实现在这里,.NET Core(实际上不使用Environment.TickCount)在这里。
$ \ endgroup $
–ArrowCase
19年8月30日在18:46



$ \ begingroup $
如果您不知道盐,要花多长时间?
$ \ endgroup $
– PhillyNJ
19年8月30日在19:02

$ \ begingroup $
@dandavisdocs.microsoft.com/zh-CN/dotnet/api/…
$ \ endgroup $
–彼得
19年8月30日在21:53

$ \ begingroup $
@PhillyNJ盐被假定为公开的。如果假定它们是秘密的,则它们被称为“胡椒粉”。话虽这么说,但我不能说如果数据库使用胡椒粉而不是盐,情况将会如何变化。
$ \ endgroup $
–马克
19年8月30日在22:08

$ \ begingroup $
因为比较麻烦,每个密码为\ 120美元,而不是120美元才能获得所有密码。
$ \ endgroup $
–user253751
19-09-1在2:28



#2 楼

根据您的描述,听起来您的系统工作方式如下:


咨询系统时钟以找到32位种子$ s $。
使用System.Random生成密码$ p = G(s)$。 (这里$ G $是System.Random内部发生的任何计算的简写。)
将PBKDF(2?)的密码短语散列到输出$ x = H(p,\ sigma)$,其中$ \ sigma $是一个盐对手所知。 (此处$ H $是PBKDF2的简写。)

用$ x $做其他事情。假设您公开了$ x $,以便我们可以专注于对手的工作;如果对手能够找到$ p $或$ s $并预测从他们派生的其他一切,那么对手就会获胜。 \ sigma)$,然后公开$ k $的其他函数$ x = f(k)$,但这并没有实质性地改变分析—我们可以将$ f $折叠成'$ H $'并继续


暂时搁置System.Random算法中的缺陷-最多只有$ 2 ^ {32} $个不同的输出,因此对于防御者来说最好的情况是而对于攻击者来说,最坏的情况是,有$ 2 ^ {32} $个可能的$ p $值,它们是均匀分布的。框并尝试找到$ s $。天真的暴力方法是枚举每个候选的32位种子$ s ^ * $,并检查$ x = H(G(s),\ sigma)$。这大约是$ 2 ^ {32} $(四十亿美元)乘以评估$ G $的成本(可以忽略不计(可能是几百个CPU周期))加上评估$ H $的成本,后者取决于PBKDF2参数和成本占主导地位。

您没有指定什么哈希函数或PBKDF2迭代次数,但是假设您选择了PBKDF2-HMAC-SHA256的1000次迭代。我的笔记本电脑在单个CPU上每秒计算约5000个SHA-256哈希(由openssl speed sha256用16字节输入测量),每秒约2500个HMAC-SHA256哈希,或每秒约2500个密码猜测。我的笔记本电脑还具有八个CPU,因此实际上它每秒可以进行约20 000次猜测。由于我刚在笔记本电脑上对PBKDF2-HMAC-SHA256进行了幼稚的暴力攻击,因此在笔记本电脑的全部功率消耗下,大约需要两天才能找到$ s $和$ p $。 />这没有任何硬件加速:英特尔的SHA-256 CPU指令大概可以将其加快速度(从而降低成本)约4倍,并在数小时内给出答案。该计算基本上不使用内存或与数据相关的内存访问模式,因此即使没有舒适的个人笔记本电脑,也可以将其有效地转移到GPU上以进一步提高速度。不难想象,如果您有一个适度的GPU集群可以解决问题,就像您可以在Amazon租用一样,那么在几秒钟内就能得到答案。

实际上,对手实际上可能不会t只有一个目标$ x = H(G(s),\ sigma)$进行攻击:他们可能有许多目标$ x_1 = H(G(s_1),\ sigma_1),$ $ \ dots,$ $ x_t = H(G(s_t),\ sigma_t)$发起攻击,如果他们打破了任何$ t $目标中的第一个目标,他们就会获胜。 (一旦您立足于网络,通常很容易从那里跳到网络中的许多其他地方。)如果您没有为每个用户选择不同的salt $ \ sigma_i $,那么对手也可以减少只要它们并行化攻击$ n \ geq t ^ 2 $方式,该(相当低!)成本就降低了$ t $倍,从而以成本$ 2 ^ {32} \!/ t $和时间$ 2 ^ {32} \!/ nt $通过使用并行彩虹表搜索[1]。

当然,此分析与应用程序的其余细节是隔离的。可能还有其他方法可以打破它。例如,如果对手知道您什么时候播种System.Random,他们可以大大缩小搜索范围。如果每位使用者有不同的食盐,则如何选择食盐?如果您是从System.Random逐字记录中选择的,则有可能从盐中回收System.Random种子并找到密码短语,而根本不需要花费相当昂贵的搜索费用。就是如果密码具有足够高的最小熵,那么,如果您使用某种密码,那么您的系统就不会以某些特定方式被破坏。宠物项目,因为找到“可行的”攻击和微不足道的奖励非常麻烦-除非他们实际上是从对手那里试图利用您的用户而从您的系统中获得特定的奖励,否则就不会与您分享他们的发现。 RC4工程师拖了他们尽管RC4在其发布后的48小时内就被破坏了[2],并且密码分析学家不断发现其中的更糟[3]和更糟[4]的问题,但仍需要不断地改变。例如,这就是为什么密码分析人员费心研究RC4在WPA和TLS [5] [6] [7]中的具体用法。 SSH,TLS和PGP中的定制怪异结构也发生了同样的事情[8]。

不要成为负责制定伪劣密码决策的工程师,该决策将激励密码分析师在未来数年内戳破您系统中的漏洞。第一次遵循密码学家的建议,以节省密码分析师的工作量,并让他们专注于像NIST PQC这样广泛使用的密码系统,以提高每个人的安全性。如果不确定使用System.Random是否会破坏您的安全性,请假定它会立即用更好的东西代替它,以使您的用户放心。

#3 楼

System.Random的官方文档明确表示不应将其用于生成密码。这是可以预见的,并且只能从系统时钟中得出。这意味着System.Random对时钟精确到一秒的任何人最多具有20位熵。

实际上,尝试在不同的线程上快速连续地创建两个新实例。他们将产生相同的输出!在审核SaaS应用程序中的真实密码重置代码时,我恰好遇到了此问题。相同的密码已发送给现实世界中的多个用户。如果您猜测/知道使用base64编码的System.Random被用来生成重置密码,则可以轻松预测这些密码。

评论


$ \ begingroup $
实际上,文档告诉我们,如果两个System.Random对象创建时彼此之间有两秒钟的延迟,则可以确保它们具有不同的种子,这意味着种子最多每秒仅更改一次。
$ \ endgroup $
–德米特里·格里戈里耶夫(Dmitry Grigoryev)
19年8月30日在8:57

$ \ begingroup $
@DmitryGrigoryev这意味着种子在最坏的情况下每秒仅改变一次!
$ \ endgroup $
–托德·塞维尔(Todd Sewell)
19年8月30日在10:07

$ \ begingroup $
@ToddSewell最糟糕的情况下,每两秒钟一次。我同意无法确切地确定典型的种子变化率是多少,但是我认为如果他们使用毫秒,那么获得不同的种子就不需要两秒钟的延迟。
$ \ endgroup $
–德米特里·格里戈里耶夫(Dmitry Grigoryev)
19年8月30日在10:28

$ \ begingroup $
@DmitryGrigoryev也许他们正在提供宽容的容忍度,以便为将来的改进留出空间。
$ \ endgroup $
– John Dvorak
19年8月30日在17:36

$ \ begingroup $
@DmitryGrigoryev:我创建了一个简单的C#桌面应用程序来测试各种延迟。延迟1毫秒,运行101个不同的种子,我得到6-7%的连续种子有所不同。延迟为10毫秒,为63-64%。在16ms时,它几乎总是改变的(10000的9999)。 32ms在1万次运行中具有100%的变化。不想坐在那里等待中间跑步。我正在使用VS2017的i5 6600K,W8.1上,以防有人想开始计算用例。 :)
$ \ endgroup $
– MichaelS
19年8月31日在11:20

#4 楼

前一段时间(我认为超过5年),在一个本地论坛上,一个人向社区提出了“密码学挑战”。他给出了一个加密的字符串和一小段产生它的代码。目的是找到加密的字符串是什么。加密密钥基于System.Random(),它是可以被攻击的地方。

我的工作计算机当时有一个4核超线程CPU(因此有8个逻辑CPU)。只需在它们之间划分搜索空间,我就可以在几分钟内探究所有可能的32位种子。我认为还不到10分钟。因此,即使是提出挑战的人也感到震惊。从System.Random()开始,只需尝试所有种子,他们就能在不到一个小时的时间内做到这一点。

评论


$ \ begingroup $
实际上,System.Random将种子值截断为31位。
$ \ endgroup $
–德米特里·格里戈里耶夫(Dmitry Grigoryev)
19年8月30日在15:05

$ \ begingroup $
更好。 :)
$ \ endgroup $
– Vilx-
19年8月30日在15:11

#5 楼

实际的影响是,如果攻击者可以确定密码的创建时间,则他们可以将该时间作为种子输入System.Random,并将获得完全相同的密码。

System.Random使用时间值1秒的精度。假设密码创建时间已知为1秒钟(例如从数据库中的用户注册日期/时间),那么将立即找到密码。如果知道创建时间只有一个小时,则只有3600个种子可以尝试以查找密码。 )或密码生成算法中的缺陷(为多个日期/时间种子生成相同的密码)会进一步减少搜索空间。

评论


$ \ begingroup $
巴西的投票机就是一个很好的(也许不是那么好:s)的例子。通过分析其中的代码,一些研究人员发现使用srand(time(NULL))初始化了一个种子。但是我们确切知道选举在什么时候开始,这意味着我们可以很好地估计机器启动的时间,因此可以估计用于初始化种子的值...
$ \ endgroup $
–Hilder Vitor利马·佩雷拉(Lima Pereira)
19年8月30日在8:43

#6 楼

为了帮助理解,请考虑我的新密码生成方案“在人们检查详细信息之前似乎值得信赖”或“愚蠢”。愚蠢的工作方式是您丢人并选择密码。如果抛出1,则密码为“ dhjousacbjlfswsgolFHfhjQPC”。如果抛出2,则为“ vmcseykogcsKcsNTLhczdg”。依此类推。

这些密码对我来说似乎是非常不错的密码!它们是强度检查承诺可以保留很长时间的密码。如果攻击者以“ aaaaaaaaaaaaaaaaaaaaaaaaaaaa”开头并继续前进,他们会怎么做。但是,假设攻击者知道我在使用愚蠢的东西。他们不会以大量的开头,而是以dhjousacbjlfswsgolFHfhjQPC开头,并且会在6个猜测之内得到正确的答案。本质上,发生的事情是我从一个弱的随机数生成器开始:一个单模。在六个可用骰子结果中,我可能无法使用的任何功能都不会获得超过六个可用选项。

对于任何弱随机发生器,问题都是相同的。也许有几百万个开始值而不是6。但是从根本上说,一百万对计算机来说并不是一个很大的数字。攻击者所做的只是用可能的数百万个输入中的每一个模拟System.Random,他们获得了数百万个可能的输出。与您的密码看起来很长且随机的事实相比,这是一个要隐藏的森林要小得多。

评论


$ \ begingroup $
我觉得您的方案名称可以在软件行业中找到许多用途!
$ \ endgroup $
–德米特里·格里戈里耶夫(Dmitry Grigoryev)
19年8月30日在9:04