返回列表 发帖

保护口令:第一部分

口令私密性 在任何口令系统中第一个要克服的障碍就是私密性问题。许多人都不希望其他人读取(或使用)他们的口令。如果代码以明文方式存储了口令,您就可以随意读取它。更糟糕的是,如果系统的安全性不那么完善,其他人也能读取口令!真讨厌。 那我们怎么解决这些问题呢?有些人会说,“相信我们吧...我们不会读取你的口令,除非你需要我们这么做。”但没理由相信这些话。就算提供服务的人说的是实话,他们也可能会依法强制公开出您的口令。但假设这对您来说不是问题;您信任运行身份验证代理的机构。这样问题就变成他们通过什么方式来保护您的口令不被攻击者查看。 别遗忘密钥 一个解决办法是为口令加密。但这个解决方案不那么简单。需要有密钥来为口令加密和解密。这就将问题从口令存储转换成口令密钥存储(和使用)问题。当然,身份验证软件在您登录时需要口令密钥来确认您。如果有办法,我们希望身份验证软件无需人为的介入,这样密钥就需要位于软件可以访问到的地方。问题是如果软件可以访问到它,一个成功的攻击者也可以。我们的目的就变成寻找一种使加密密钥难以找到的方法。这就是一种“晦涩的安全性”,我们应该尽全力避免使用这种手段。它的结果就是无论使用什么方式,隐藏密钥都不是非常容易的问题。(我们很快就会在讲述许可证管理时讨论到密钥隐藏的问题。) 存储散列 幸亏有一种更好的办法。实际上,这种更好的办法不仅处理了密钥存储问题,还解决了口令存储问题。一句话说,就是在我们的解决方案中,身份验证实体存储和使用口令的密码散列。当用户输入口令时,口令被发送到身份验证代理,它对口令进行散列,并将散列与它存储的散列比较。如果两个散列相同,身份验证就成功了! 现在身份验证实体不再处理明文口令了。但这并不意味着机器也不能继续使用它!我们说过,身份验证代理执行口令检查。暗示着代理随后会“忘记”口令。一个有敌意的管理员可能会继续在用户口令的秘密文件中继续保留它。攻击者就有可能获得对这个文件的访问。或者可能管理员本身就是攻击者。显然我们不应该给予管理员我们想象他们应该具有的访问权,来向有问题的系统妥协。问题是在于许多人都倾向于在不同的机器上使用相同的口令。任何聪明的攻击者都会在尝试其它方法之前先会在新的帐户上尝试您的旧口令。然后,攻击者就将尝试使用您的旧口令的各种变化。有一些针对这种问题的复杂的解决办法(例如,我们可以使用公钥密码术 -- 请参见 developerWorks 上的 历经考验的真正加密 (Tried and true encryption) 一文来了解有关公钥密码术的详细信息),但就目前来说,我们只能回过来信任管理人员和身份验证系统。 crypt 方法的详细信息 基于散列的口令存储机制的一个常见实现是对 Posix crypt() 的调用。crypt() 调用不从技术上执行单向散列。它使用修订过的 DES 算法来加密由零组成的字符串,这种 DES 算法从实质上而言是使用您的口令作为加密密钥。实际上,它的功能和单向散列的功能是相同的。当要对所有的零字符加密时,只有知道口令的人才能够生成将产生相同密码文本 (ciphertext) 的密钥。 crypt() 解决方案优于传统密码单向散列的良好特性就是,它能保证没有两个口令可以加密出相同的密码文本。在使用密码散列情况下,通常一个攻击者碰上冲突的可能性非常小,在有冲突的情况下,第二个口令(散列后)与第一个(散列后)能产生完全相同的密码文本。通常,冲突的机会非常小,它并不构成很大的风险。 与例如 MD5 和 SHA-1 这样的密码散列函数能够对任意长度的字符串进行操作不同,crypt() 调用有很大的限制。它只能处理 8 个或少于 8 个字符的口令。如果您的口令是 12 个字符,则只有前 8 个字符才真正有用。 crypt() 调用将忽略 8 个以后的字符。有 96 个不同的字符可以用于口令。这意味着有只有少于 253 个唯一的口令。听上去这是个非常大的数字(实际数字大概是 7,213 万亿)。它确实很大,但还不是加密学中“大”的定义。让我们假设一下每个口令都唯一地映射为一段密码文本的情况。在使用几百万亿字节的存储器情况下,可以存储每一对可能的口令和密码文本,并能快速查找它们。我们需要预先计算出所有数据,但只需要执行一次。在拥有有足够多的资源情况下,这只需花费一年。一个足够大的组织最差情况下几星期内也就可以完成。 要“撒点盐 (salt)”吗? 在设计 crypt() 时,就预见到了这种攻击(称为字典式攻击)。但人们那时没有想到能创建出一个完整可用的字典,最近我们却有了这种可能。他们预料到的只是有一些特定的口令会非常普遍 -- 例如字典中的字、正确的名词等等。要防止创建口令的公共数据库,向 crypt() 添加添加了一个步骤。不使用口令作为密钥来加密由零直接组成的字符串。而是选择两字节的“盐”(salt)。 salt 基本是随机产生的,但却是完全公开的数据,任何人都可以查看。salt 与口令拼接起来构成密钥。使用这种方法,当相同的口令通过 crypt() 过程执行多次后,就会加密出不同的字符串(假设您使用的是不同的“盐”)。 salt 中只能使用 64 个唯一的字符,并且它只能有两个字节。因此只有 4,096 个不同的 salt。这使得对复杂的字典式攻击的难度增加了 4,000 倍(同时需要增加 4.000 倍的空间密度)。使用 salt 方法后,可能的密钥空间从 253 增长到 266。最初编写 crypt() 时,这可是巨大的密钥空间。现在它一点也不大了,特别是考虑到 salt 应该是能泄露出的。salt 通常是和密码文本一起保存,我们通常假定攻击者不论通过什么方式都能获得明文。除非存放口令的数据库对除了有特殊权限以外的人都不可读,否则通常是有可能获得明文。 破译密码文本 获得密码文本的意义是很重要的。一旦某人有了密码文本后,使用蛮力攻击 (brute-force attack) 将它进行匹配 -- 尝试所有 253 个口令,直到使用相应 salt 加密出给定文本的口令 -- 只是时间的问题。许多人相信政府具有在几分钟内,甚至在几秒钟内执行这样的破译的计算资源。使用蛮力的攻击绝对是一个具有相当资源的组织可以做到的事情。使用每个可能的 salt 为每个可能的口令存储完整的字典将会耗费几百万万亿的磁盘空间,当前这是可以做到的,但要花费极高的成本。在几年内,它可能会成为更实用的方法。如果没有预先计算好的字典,要获得每秒能尝试 218 个不同口令顺序的机器仍然也不太困难。分布在具有这种性能的 8,000 台左右的机器上所进行的大规模工作绝对可以在 48 天内破译出任何口令。当然,大多数口令都划分成若干个几百万数据的分组,这使得它们非常容易被破译。(下一次,我们将花费更多时间在口令如何选取问题上。)请注意,上面的数字只是针对于给定一个 salt 后的预先计算出每个口令和其密码文本的任务。因此将需要大量的计算能力来使用所有 salt 构建出预先计算好的字典,这将能在下个十年的范围内实现。 在讨论对称密钥密码术时,我们讨论了相当大的密钥空间大小。我们的喜好是 128 位或更多位的密钥空间。这种级别的安全性会很好。问题是您需要的安全性位越多,口令就会越长。人们都希望使用短的而不是长的口令。您可能需要有 20 个字符的口令,每个字符都是随机选择的,才能获得 128 位的安全性。对于大多数人来说,要记住这些字符太难了。再加上这些字符中的每一个都需要是完全随机的。由于人们往往会选取拙劣的口令,所以攻击者在找到口令之前只需要搜索密钥空间中的一小部分。 向口令数据库添加用户 让我们考虑向一个向基于文件的口令数据库中添加用户的 C 程序问题(我们也会简略地介绍一下验证用户身份的问题)。假设讨论中的用户正坐在计算机前,我们可以从标准输入中读取用户的输入。首先,我们看一下包含几个问题的本机实现。我们将讨论这些问题,然后改进程序来解决它们。 有一个样本程序,用于向 UNIX 样式的口令数据库添加用户。应该将 PW_FILE 的值做相应的更改,并在使用该程序之前对该文件运行 "touch"。并要记住,这个示例代码是有一些问题,所以不要在任何生产系统中使用它。 代码解析 现在我们有了要讨论的对象,可以看看上面样本代码中存在哪些问题。答案呢?有很多。最明显的问题就是口令被回显到我们运行程序的屏幕。真糟糕。不过我们可以通过向 ReadLine 函数添加一个 "echo" 参数来改正这个问题,然后,如果该参数被设置为零,使用 ioctl 系统调用来使与 stdin 连接的终端将被抑制回显。我们还应该通过检查来确保存在与 stdin 连接的终端(如果程序的输入是通过重定向而来的,则没有)。 第二个问题是对 crypt() 库的不正确使用。在代码中出现 crypt(pw, pw) 是相当普遍的现象。不幸的是,这是非常不好的应用。问题在于 salt 是口令的前两个字符,而salt 又必须以明文方式存储。因此,任何能看到口令数据库的人只需要猜 6 个字符而不是 8 个。破译程序能更快地找到破绽。而且,我们已经放弃了 salt 的所有好处。所有具有口令 "blah" 的用户都有相同的加密口令 "blk1x.w.IslDw"。在这种情况下,一个现有的字典式攻击对大多数口令来说都是非常有效的攻击。存储几百万个可能的口令只需要几十 GB 的磁盘空间。 我们确实需要选择一个随机的 salt。因为 salt 是公开的,所以如果我们使用“安全的”随机数字生成方法来选取一个 salt,与按时间取得的伪随机数字生成器都可以用于这个目的。风险则是攻击者可以对随机数的产生有影响。攻击者能让所选择的 salt 是他们已进行对其进行过大规模预先计算的一个,来提高破解的可能性。这种攻击更具有理论性而不具有实际性,因为大多数现有的破译程序都是非常有效的,并不需要进行任何的预先计算。但我们仍然需要安全性,要从合理的随机数源中获得我们的数。对于我们的示例,将使用在上一篇文章中介绍过的基于 Linux 的随机数库,但使用“强”随机数的泄露并不一定是 100% 安全的。 上述样本代码的第三个问题是我们应该更多地注意将口令超出必要的事件以明文形式存放的现象,即使是在我们所控制的内存中。攻击者有时能在程序运行时访问它的内存,或者能够强制的核心转储(core dump)在稍后检查找出口令。我们应当通过最小化口令以明文格式存储的时间,来减少这些易受攻击的“窗口”。一旦读取了口令,就应该为它加密,然后对包含口令的内存空间填零。即使我们是在通过比较来确保两个口令是等同的来确认用户输入的是正确的口令时,我们也应该根据产生的密码文本而不是根据明文来比较它们。 第四个问题是数据库的可访问性。运行该程序的用户必须本身能够对数据库进行读写。它在许多情况下都不是个好的想法。如果可能,我们甚至要防止不是该程序的任何其它程序读取数据库。如果登录是来自网络而不是来自本地机器的,人们就不太可能读取和修改数据库。但如果有可利用的堆栈溢出,或者有一些其它问题,使得攻击者可以利用来运行代码,扰乱数据库当然就不是不可能了。在 UNIX 世界中,解决这个问题的答案是使用 setuid 编程,它也有自己一系列的风险,这是另一个复杂的主题了。我们将在结束身份验证后马上讨论这个主题。现在我们暂时使代码保留在这方面有破绽的状况,我们将在以后的文章中回过来解决这个问题。 更干净的代码 根据我们最近的深入了解,这里是一个该示例程序的修正本(请注意,它需要我们在上一专栏中所构建的安全随机数库): 当然,我们仍然不喜欢 crypt(),因为它将口令限制在 8 个字符,并限制了我们可以拥有的 salt 数。我们可以提供以下使用 MD5 加密的替代版本: md5_crypt.h: #include #define DIGEST_LEN_BYTES 16 char *md5_crypt(char *pw, char *salt); md5_crypt.c: #include "md5_crypt.h" char *md5_crypt(char *pw, char *salt) { MD5_CTX context; char digest[DIGEST_LEN_BYTES]; char *result = malloc(strlen(salt) + DIGEST_LEN_BYTES + 1); if(!strlen(salt)) abort(); MD5Init(&context); MD5Update(&context, salt, strlen(salt)); MD5Update(&context, pw, strlen(pw)); MD5Final(digest, &context); strcpy(result, digest); strcat(result, salt); } 这个函数的行为与标准加密稍有不同,因为 salt 是变长的。特别是调用 md5_crypt(typedpw, storedpw) 是无效的。使用传统加密的情况下,这种类型的代码还可以,因为只考虑第二个参数的前两个字节。不幸的是,由于 salt 不是定长的,md5_crypt 永远不能单从一个 salt 来辨别出其后附加有的 MD5 散列的 salt -- 即使 salt 是放在散列前。与传统的加密不同,我们将 salt 放在散列之后,这样我们就可以在检查口令时方便地使用以下方法调用 md5_crypt了: md5_crypt(typedpw, storedpw[DIGEST_LEN_BYTES]); 在下一部分中,我们将讨论如何创建基于口令的简单身份验证系统。

保护口令:第一部分

TOP

保护口令:第一部分

又是支持吗?

TOP

保护口令:第一部分

游学到知识了
高兴

TOP

返回列表 回复 发帖