服务⽤户账户因为不能被锁定,所以成为暴⼒密码破解攻击的最好⽬标。理想情况下,所有的账户都应该使⽤ 强密码,但服务账户(或者其他不被锁定的账户)还是应该特别注意。
有些读者可能已经在笔者的⽹站上看过那篇关于密码的论⽂。在那篇⽂章⾥,提出了⼀种与简单复杂度规则不同的密码评价⽅法。这篇⽂章收到很多读者的积极反馈,本⼈觉得它是对本章内容的很好补充。笔者也会在本书附上相关的源代码,如果想⾃⼰编写密码强度检查⼯具,可以查考这些源代码。接下来将介绍这种密码评价⽅法,以及这种⽅法是如何⼯作的。
管理员总是告诉⽤户:密钥必须⾜够复杂(⽤户的感觉就是⿇烦),但是他们没有告诉我们如何才能设置出好的或强壮的密码。笔者见过的⼀些密码强度计量⼯具都是尽最⼤努⼒给出评级,糟糕的、好的、更好的或者其他⼀些模棱两可的评语,都没有对密码强度给出定量结果,只是基于复杂度给出假设——越复杂的密码越好。虽然这是事实,但是我们仍然得不到任何信息⽤来判断密码应该多复杂才算好,或者为什么需要这样做。 正因为如此,我想做⼀些不同的事情,所以决定增加可衡量的、并且有价值的属性,通过这⼀属性可以
计算出破解密码的密钥空间(所有可能⽤来组成密码的字符个数)需要多长时间,⽽密码是基于使⽤的基本字符组成的。密码强度关⼼的就是如何使密码更难或不可能被破解,越长和越复杂的密码,破解需要的时间就越长。就像安全组合锁,使⽤的锁越多就越安全。 在笔者看来,要真正选择好的密码,就应该总是假设对攻击者最有利的情况和对你最不利的情况。当攻击者暴⼒破解密码的时候,所⽤的⽅法基本上就是枚举所有可能的字符组合:a–z、A–Z、0–9,以及键盘上第⼀排的那些字符——!、@、#、$、%、^、*、(、)、-、=、_、+,还有其他能打印的可见字符,⽐如_、{、>,等等。当然,攻击者使⽤的⽅法并不总是类似这样教科书⼀般的⽅法,但这是他们考虑问题的典型思路。如果输⼊abcd作为密码,4个⼩写字母,攻击者不知道密码的结构,所以必须假设密码可以是任何情况,这意味着必须选择使⽤哪种⽅法攻击密码(⽅法有很多),以及选择哪些字符进⾏密码组合枚举。如果攻击者已经知道密码的组成结构,密码就更容易破解。攻击者了解的事情越多,对他们减少密钥空间就越有利。 ⼀种新⽅法
所有攻击者要做的事情就是枚举所有可能的字符组合,直到猜到密码为⽌。也就是说,从字符a开始循环,每循环⼀次,增加⼀个字符,也许不该多说这⼀句,因为这看起来简单得微不⾜道。⽆论如何,在整个密钥空间中猜解由4个⼩写英⽂字母组成的密码需要迭代475 254次,在互联⽹上可以看到,
许多暴⼒破解程序或⽩⽪书都介绍说,对由4个⼩写英⽂字母组成的密码进⾏猜解需要迭代456976(也就是26的4次⽅)次,但这是错误的。只有从⼀开始就认定密码必须是4个字母,需要的猜解次数才是26的4次⽅。也就是说,枚举密码是从aaaa开始,⽽不是从a开始。实际上,在得到aaaa之前必须先经过a、aa和aaa,所以正确的计算⽅法是使⽤26^4+26^3+26^2+26^1作为计算公式。475254和456976这两个数字看起来差不多,但是对于密码是10个字母的情况,这两个数字的差别就
是56亿。所以从⼀开始,系统使⽤的公式就是错误的,笔者希望计算从a开始的完整的暴⼒破解所需要的密码猜测次数,攻击者通常也是这样做的。
为了确定密码枚举使⽤的基础字符,需要观察密码并确定密码使⽤的最⼩基础密钥空间。如果使⽤abcd作为密码,只需要使⽤a-z作为基础字符。也就是说,只需要使⽤从a到z的⼩写英⽂字母尝试破解密码。如果使⽤abcd1作为密码,就需要使⽤a-z的字母和0到9的数字(基础字符增加到36个)。如果使⽤abcD作为密码,就需要使⽤a–z的⼩写英⽂字母和A–Z的⼤写英⽂字母,基础字符是52个。如果使⽤abcD1,那么基础字符就是62个,增加!或&字符可以使基础字符增加到76个。如果还使⽤了[或~字符,那么最终的基础字符就是95个。
显然现在不得不做⼀些假设,除此之外也没有其他⽅法。得到攻击者必须使⽤的基础字符很有意义,再结合得到的输⼊密码的结构信息,将使之更有意义。但因为现在做的事情是衡量密码的强度,不希
望得到有利的任何东西,所以假设所有情况都对破解者有利。假设破解者不得不做⼀些假设,⽐如他知道⼀些本⼈知道的东西(听起来有点古怪)。为此,将基础字符分成⼏个组,分别是a-z、A-Z、0到9、!到=,剩下的其他字符形成⼀个组。如果输⼊A,那么不得不假设你的密码可能也使⽤了⼩写字母。由于考虑了⼩写字母的情况,因此单独的A也增加到52种可能性。如果使⽤a1,那么0到9的数字也要加进来,基础字符就
是36(26+10)个。如果是A1,那么基础字符是62(52+10)个。单个!也有76种可能性,所以A!的基础字符就是76个。单个[有95种可能性,所以A[的基础字符就
是96个。最后,如果使⽤Ab44!作为密码,那么整个密钥空间就是76^5+76^4+76^3+76^2+76^1,总共2 569 332 380种密码组合。作为参考,通常将使⽤的基础字符分成以下⼏组,每个组都有它们对应的密钥空间:
Base 10: 0123456789
Base 26: abcdefghijklmnopqrstuvwxyz
Base 36: abcdefghijklmnopqrstuvwxyz 0123456789
Base 52: abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUV
–– WXYZ
Base 62: abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUV
–– WXYZ 0123456789
Base 76: abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUV
–– WXYZ 0123456789 !@#$%^&*()-=_+
Base 95: abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUV
–– WXYZ 0123456789 !@#$%^&*()-=_+ []\"{}j;':,./<>?'_
以上分组只是组织字符集合的⽅式,分组的根据是笔者见过的不同攻击者的实现⽅法,以及⼈们使⽤特殊字符的习惯。⼈们总是趋向于将键盘上最上⾯⼀排的特殊字符连在⼀起使⽤,就像T$i*D3这样,作为对⽐的T$i]D3就很少⽤。⿊客⼿册通常关注以ASCII顺序枚举密码,从不提针对特殊字符的顺序检查,但此处以普通⼈创建密码的⽅式考虑如何分组基础字符,尽管是由计算机进⾏破解。没有绝对正确的⽅法,也不存在某种⿊客⼀定会照着做的规则。但是当考虑如何应⽤密码策略时,就需要知道这些事情。对此必须重点关注,笔者没有忘记A-Z和0-9的组合,只是没有发现所有⼤写字母和数字的
价值,因为⼀般很少看到此类密码(译者注:全部由⼤写字母和数字组成的密码)。笔者看到的密码⼀般都是⼩写字母和数字,这是因为默认情况下很多⼈都使⽤这样的密码。这并不是说密码不可能是⼤写字母和数字的组合,这⾥选择的是包含了62个基础字符的集合,⼤写字母和数字组合的情况与⼩写字母和数字组合的情况在数学处理上⼀样。所以,只有当真的确定密钥空间就是⼤写字母加数字时,才可以选择对应的基础字符集合。
现在可以看看图1显⽰的密码强度结果(这个应⽤程序是本⼈编写的,包含在本书的下载资料中)。
图1裁剪过的密码强度计算程序的运⾏截图
根据选择的基础字符集合的信息,决定使⽤最⾼级别的⼯业破解标准(Class F),也就是每秒钟进⾏100万次破解尝试。这是很⼤的数字,再次强调⼀下,总是选择最糟糕的情况,这样才能知道密码到底多强壮。以外还会计算对整个密钥空间进⾏密码测试所需要的时间,并显⽰这个时间。在前⾯的例⼦中,使⽤
过Ab44!作为密码,为破解这个密码,对整个密钥空间进⾏的测试耗时为2.56933238秒!这个结果提醒我们,尽管这个密码被认为是相对复杂的密码,有⼤写字母、⼩写字母、数字和特殊字符,但是2.5秒真的不算多。当然,⼤多数破解者的业余设备每秒只能发起⼏百上千次尝试,但是必须确定,这只是此类分析⽅法展⽰出来的⼒量的冰⼭⼀⾓。
不要再告诉⽤户使⽤强壮的密码(不管这意味着什么)了,相反,要求他们的密码必须能够经受起⼀定时间长度(由你决定)的Class F攻击。这种⽅法使我们有了衡量的基础。再看看图1,使⽤aaaaaaNotGood作为密码,它由13个⼤⼩写混合的字符组成。使⽤Class F强度的攻击破解这个密码的密钥空间,需要耗时650 000年,看起来很强壮,对吧?
好吧,这个想法让⼈开始思考,从破解者的⾓度看,这个问题限制了所能看到的事情。现在,650 000年对⼤家来说是最好的情况(对破解者来说是最糟糕的情况),因为这代表了对整个密钥空间的枚举。所以这时候,笔者决定编写算法来计算破解实际密码需要的枚举次数,⽽不是整个密钥空间。笔者编写的算法能正常⼯作,但速度不是特别快,现在使⽤Will Fischer想出来的公式,这个公式⽐较好,在这个问题上,Will给了我很⼤的帮助,再次感谢。
因为我们已经知道密码,基本上逐列计算每⼀列从a开始枚举到这个字母所需要的迭代次数,计算的基础是定义在内存⾥的带索引字符串,索引字符串从a开始。使⽤字符串可以很容易确定当前是什么字符,对于密码中的每个字符,⼀列⼀列地处理。最后按列计算出总的枚举次数,再与Class F进⾏对⽐,计算出时间。这样的话,对整个密钥空间枚举需要650 000年,对aaaaaaNotGood枚举就减少到12 637.66年。减少这么多时间的原因是直接从6个⼩写字母a开始枚举。如果将密码的第⼀个字母换成H,需要计算的时间就从12 637.66年(398 541 262 291 912 000 000次枚举组合)变成421 660.40年(13 297 482 476 338 200 000 000次枚举组合)。这就是从a变成H的巨⼤差异,对整个密钥空间的枚
举也增加到20 724 145 598 800 400 000 000次,使⽤Class F的破解需要657 158.35年时间。
从实际应⽤的⾓度看,应该能够选择⾃⼰的破解级别(听起来就像选学校),如果选择从最糟糕的情况构建密码强度,选择每秒钟10亿次枚举,那就真的不⽤担⼼那些来⾃慢平台的攻击。当然,可以看到因选择⾃⼰的破解级别⽽带来的价值。
使⽤这个例⼦是为了让你能够看到来⾃真实世界的攻击是什么样⼦,毕竟美国国家安全局不会试图获取每个⼈的数据。当没有选择密码强度,并且还想知道通常每天要⾯对多少⾃攻击者的风险时,以上⽰例还是有价值的。举个例⼦,可以看看图2中显⽰的来⾃Cisco产品的截图。
图2开发⼈员提供的⽤户界⾯可以显式地限制密码的长度和复杂度
在上述产品中,必须创建管理⽤户,但是注意提⽰信息:“你的密码必须⼩于8个字符,并且不能包含空格和特殊字符。”在这种情况下,⽤户被迫使⽤蹩脚的密码,即便选择对破解者最不利的情况,也就是每秒10 000次(打印机都有可能使⽤空闲时间做到这⼀点)的Class A攻击,最多需要61个⼩时就可以破解62个基础字符(a-z、A-Z、0-9)的密钥空间。对于类似AaZzaa99这样的密码,只需26个⼩时即可破解。当考虑这种情况时,使⽤⽤户定义的破解级别就更显得意义重⼤。
这也给了你深⼊了解安全明确的其他限制和默认密码的机会。举个例⼦,我们曾经对邮件列表的成员
关系提醒做过快速搜索,这个查询将提供对存档电⼦邮件的各种链接,邮件列表的管理员没有考虑他们的提醒程序如何通知⽤户,包含有完整⽤户名和密码的信息也被存放到这个列表中并且通过你最喜欢的搜索引擎建⽴索引。因为这些都是明⽂,所以也不需要破解。可以看到⼤多数都是简单的由8个⼩写字母组成的随机密码。如果对整个密钥空间进⾏暴⼒破解,即使采⽤最慢的破解级别,也只需要217秒,⼤约是听完AC/DC乐队的“Have a Drink on Me”所需要的时间。
字典攻击和其他攻击⽅法
精英类型的⿊客⾸先要说的是,“如果从字母Z开始,并且倒退着来呢?”好吧,如果这是你认为合理,并且关注的事情,那就倒着枚举。就算按照字母位置倒着枚举字符,甚⾄可以⼀切都从M开始,这个公式依然能以相同的⽅式⼯作。当然,攻击者也可能决定从中间字母开始,他们抱着万分之⼀的希望,期望你也是这么做的,还有许多诸如此类的⽅法,总之,这是导致收益递减的参数。
笔者认为,同样的道理也适⽤于字典攻击,我和Will把这称为“⾹蕉狗综合征”。可以使⽤包含10000个单词的字典攻击那些对安全⼀⽆所知的⼈,他们通常使⽤banana或dog等单词作为密码。假设把这些词两两组合在⼀起(产⽣含有1亿个单词的词典),即便这些⼈聪明地使⽤bananadog作为密码,攻击者也只需要毫秒级的时间就可以破解。暴⼒破解bananadog的密钥空间需要⼤约94.11分钟,在这段时间⾥,攻击者可以枚举⼤约5 646 683 826 134个最多由9个⼩写字母组成的密码。对于任何⼈,banana
dog攻击总是被限制在总数为1亿个的可能密码中,并且会错过类似bannaadig这样的密码。攻击者想要得到密码,他们不达⽬的誓不罢休。正因为这样,笔者并不担⼼以bananadog作为密码的⼈,如果你是这种类型的⼈,我怀疑你根本不可能读本书。
Will提出⼀种很好的观点,就是如果作为攻击者只需要花费⼏毫秒的时间就可以从字典攻击中获得那个未知的密码,有什么理由不这么做吗?笔者完全赞同这种观点,作为这个逻辑的延伸,需要在密码强度检查⼯具中增加⼀些合理的检查,去除类似bananadog这样的“杂草”。如果准备这么做,就必须先做⼏个不同级别的假设,这意味着实际应⽤的字典攻击的逻辑是⾮常主观的,但是当前的实现⽅法围绕着暴⼒破解的顺序(关于从a到z、从A到Z、从0到9,等等)所做的假设是公平的。需要将这些词连接起来,给类似BananaDog这样的密码评分吗?需要做三次连接,给类似DogBananaDog这样的密码评分吗?如果是BananaDog1,⼜怎么处理?或是B4n4n4D0g呢?笔者发现字典攻击的逻辑太随机了。理论上做这个检查是可以的,但是并不知道如何以⼯具的形式实现;所以把这个问题放在了次要位置上,任何⼈如果有好的建议,请直接联系,我都会回复的。
笔者认为这种⽅法⽐其他衡量密码强度的⽅法更有价值,图3给出了⼀个未公开的测试⽹站作为演⽰⽰例。
图3密码强度计量器显⽰由20个z组成的密码得分是0,⾮常脆弱
这个系统根据⼀系列满⾜最低要求的规则对强度进⾏评级,显然20个z是⾮常脆弱的密码。但是如果计算⼀下,就会发现要破解这个密钥空间需要20 725 274 851 017 800 000 000 000 000次枚举,需要657 194 154 332.12年的时间。在图4中,给出了另⼀个不同的密码。
图4密码强度计量器显⽰:由有⼤写字母、⼩写字母和数字组成的5字符密码获得了50%
的评分,并被评价为Good
重申⼀下,这个假设就是因为Aa!1_⽐20个z更复杂,是个强壮的密码,但是⽤数学来衡量,只需要2 569 332 380次枚举就可以破解这个密码的密钥空间,需要的时间是2.56933238秒。这就是为什么⼀直提醒⼤家使⽤密语⽽不是密码,因为可以很容易地记住这个密码。但是每次输⼊Aa!1_的时候,不得不反复检查,确认没有输错。所以现在有了量化的标准来衡量密码的复杂性,问题是密码可以安全多少年?
查看代码
下⾯就是密码检查⼯具的C#源代码:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Windows.Forms;
namespace PasswordChecker
{
public partial class frmPassword : Form
{
/
/ Assumptions:
// lower is only lower
// upper is only upper
// numbers are only numbers
// Bangs assumes upper and lower and numbers
// Spec assumes upper lower numbers and Bang
public static int iLength;
public static double uCombinations;
public static double uPerSecond;
public static string sPassword;
public static int iBase = 0;
public static int iaz = 0;
public static int iAZ = 0;
public static int iNum = 0;
public static int iBang = 0;
public static int iSpec = 0;
public static string brute = "abcdefghijklmnopqrstuvwxyz" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
"0123456789!@#$%^&*()-=+" +
" []\""+"{}j;': " +",./<>?' ”;
public static double dSeconds;
public static double dMinutes;
public static double dHours;
public static double dDays;
public static double dYears;
public frmPassword()
{
InitializeComponent();
uPerSecond = 1000000000;
uCombinations = 0;
txtPerSecond.Text = "1,000,000,000";
txtPassword.Focus();
}
private void frmPassword_Load(object sender, EventArgs e)
{
}
private void txtPassword_TextChanged(object sender,EventArgs e) {
GetBase();
}
public void GetBase()
{
sPassword = txtPassword.Text;
iLength = txtPassword.TextLength;
iaz = 0; iAZ = 0; iNum = 0; iBang = 0; iSpec = 0;
dSeconds = 0; dHours = 0; dDays = 0; dYears = 0; uCombinations = 0;
MatchCollection az = Regex.Matches(sPassword, @"[a-z]"); MatchCollection AZ = Regex.Matches(sPassword, @"[A-Z]"); MatchCollection Num = Regex.Matches(sPassword, @"[0-9]"); MatchCollection Bang = Regex.Matches(sPassword, @"[!@#$%
–– ^&*\(\)\-_=+]");
MatchCollection Spec = Regex.Matches(sPassword, @"[\[\]
–– \{\}\;\:\'\,\.\<\>\/\j\\\'\ \?\ ]\""");
if (az.Count > 0) {iaz = 26;}
if (AZ.Count > 0) { iAZ = 26; }
if (Num.Count > 0) { iNum = 10; }
if (Bang.Count > 0)
{
iBang = (26 + 26 + 10 + 14);
iaz = 0;
iAZ = 0;
iNum = 0;
}
if (Spec.Count > 0)
{
iSpec = (26 + 26 + 10 + 14 + 20);
iBang = 0;
iaz = 0;
iAZ = 0;
iNum = 0;
}
iBase = iaz + iAZ + iNum + iBang + iSpec;
txtBase.Text = "Dervies " + Convert.ToString(iBase) + "
–– of 96";
txtLength.Text = Convert.ToString(sPassword.Length);
for (int i = 1; i <= sPassword.Length; i++)
{
uCombinations = uCombinations + System.Math.Pow(iBase, i); }
txtCombinations.Text = Convert.ToString(uCombinations);
dSeconds = uCombinations / uPerSecond;
dMinutes = dSeconds / 60;
dHours = dSeconds / 60;
dDays = dHours / 24;
dYears = dDays / 365;
txtMinutes.Text = string.Format("{0:n}", dMinutes); txtHours.Text = string.Format("{0:n}", dHours);
txtDays.Text = string.Format("{0:n}", dDays);
txtYears.Text = string.Format("{0:n}", dYears);
}
}
}