📘 第十一章 Web的攻击技术
📄 一句话概括

字典攻击不是「算出来」,是「试试看」。Demo 里 20 条常见密码、46 次尝试就撞开 2 个账号; 第三个 Zq7#vL2!xR 猜不中。加盐改不了弱密码,它改的是「同密码不同哈希」。

📝 本节和前面几节的关系

前面十几节都在「骗过程序」。这一节最直白:一个漏洞都不用,直接猜。

一、三种猜法,都不是破解算法

字典攻击:拿一份常见密码表逐个试。Demo 的 20 条字典里就有 pa55word 和 sunshine。 暴力破解:按字符集穷举,空间等于字符集大小的位数次方。它慢,但一定能跑完——只是可能要跑几百亿年。 撞库:拿别的站泄露的账号密码来试。 因为人习惯在多处用同一个密码,这一招命中率最高、算力最省,也是现实里最常见的一种。 三种都不是「破解算法」——哈希不可逆这件事没被推翻。它们都是「试」。 所以防御的重点也不在算法,在限制试的次数和让每个密码够长。

二、实测:46 次尝试,撞开 2 个

三个账号,密码分别是 pa55word、sunshine、Zq7#vL2!xR。前两个在字典里,第三个不在。 结果:bozai 猜中、guest 猜中、root 没猜中。 字典 20 条,总尝试 46 次(每个账号逐条试,命中即停),命中 2 个。 46 次是什么概念:一次登录请求的往返时间里能试几十个。 所以「连错三次锁定五分钟」这条规则,比换更长的哈希算法管用得多。 注意 pa55word 这种写法:把 o 换成 0、s 换成 5,人觉得自己很聪明, 但这类变形在字典里早就是标配——字典不只收原词,还收常见变形。

三、加盐改的是什么

加盐是在密码后面拼一段随机值再哈希。 Demo 里 bozai 的哈希从 4c1b9bb3405f53cf 变成 f6cc58f0eca9fd1f,盐是 0f9d5d39。 它挡不住字典攻击:攻击者拿到盐,对每条候选密码把盐拼上去再算一遍就行, 多花的成本只是一次字符串拼接。Demo 里加盐之后,那 20 条照样能撞中。 它真正挡住的是两件事。一是彩虹表:预先算好的海量哈希表全部作废。 二是「同密码同哈希」这个信息泄露——三个账号密码都是 123456 的话,加盐后三串哈希互不相同, 攻击者就看不出它们同密码。 所以盐要每用户独立、随机、和哈希一起存。全站共用一个固定盐,等于没加。 顺带说一句:Demo 里那个盐只有 4 字节,是为了好打印,真实系统至少 16 字节。

四、穷举要多久

本机 sha256 速度 55 万次/秒(Python 单线程)。 8 位小写字母的空间是 26 的 8 次方,等于 208827064576 种;照这个速度跑完要 105 小时。 这个数只代表这台机器用 Python 单线程。真实攻击用 GPU,一秒能算几十亿次,8 位纯小写字母是分钟级的。 所以它不能当「够安全」的依据。 由此得出一条实用的结论:长度比复杂度值钱。多一位,空间乘 26; 把 a 换成 @,空间只多一点点,而且这种替换早在字典的变形表里。 实际能落地的三条:长度 12 以上、不用词典词、不重复使用。 剩下的交给限速、锁定、二次验证——这些是服务端能控制的。

五、动手 Demo:20 条字典,46 次尝试,撞开 2 个

造三个账号,密码一个用字典词加常见变形、一个是字典词、一个随机。 然后拿一份 20 条的小字典逐条算 sha256 去比对,数一数总共试了几次。 最后对本机的哈希速度做一次实测换算。

def h(pw, salt=u''):
    return hashlib.sha256((salt + pw).encode()).hexdigest()

for user, real in USERS.items():
    for cand in DICT:
        tries += 1
        if h(cand) == h(real):
            hit.append((user, cand))
            break

实跑输出:

--------------------------------------------------------------------
密码破解:字典、加盐、穷举要多久
--------------------------------------------------------------------
  三个账号,密码分别是:一个在字典里、一个也在字典里、一个不在。

  账号       密码(只有我们知道)     sha256 前 16 位
  bozai    pa55word       4c1b9bb3405f53cf
  guest    sunshine       a941a4c4fd0c01cd
  root     Zq7#vL2!xR     37f02984c2f8a3ff

  字典攻击(20 条常见密码,逐个算 sha256 去比对):
    bozai    猜中:pa55word
    guest    猜中:sunshine
    root     没猜中

  字典大小                   20 条
  尝试次数                   46 次
  命中                       2 个账号

  `root` 那个没猜中——因为它的密码不在字典里。这就是字典攻击的全部原理:
  不是「算出来」,是「试试看」;管用的唯一原因是人爱用常见密码。

  加盐之后 bozai 的哈希      f6cc58f0eca9fd1f5acbc9a2e8f9e817
  它的盐                     0f9d5d39
  和加盐前一样吗             不一样

  加盐之后,字典攻击还是能猜中弱密码——因为攻击者对每条候选密码把盐拼上去再算一遍就行。
  它真正防住的是另一件事:**同密码不同哈希**,所以彩虹表和「一次算完全网通用」失效。
  三个账号的密码如果都是 `123456`,加盐后三串哈希互不相同,也就不暴露「这三个号同密码」。

  本机 sha256 速度           55 万次/秒
  8 位小写字母的空间         26 的 8 次方 = 208827064576 种
  照这个速度跑完             105 小时

  这个数只代表**这台机器用 Python 单线程**的速度。
  真实攻击用 GPU,一秒钟能算几十亿次,所以纯字母数字的 8 位密码早就守不住了。

输出里三个账号的哈希前 16 位都打了出来,字典 20 条、尝试 46 次、命中 2 个。 46 是怎么来的:逐个账号扫字典,命中就停,没命中的那个要扫满 20 条。 root 没猜中不是因为它多强——Zq7#vL2!xR 只有 10 位——而是因为它不在那 20 条里。 字典攻击的全部原理就在这一句:它不知道答案,它在试。 加盐那一段:哈希变了,盐是 8 个十六进制字符。 这里要说明白——盐短只是为了好打印,短盐不会让加盐失效,但真实系统不该这么短。 55 万次/秒和 105 小时都是这台机器 + Python 单线程的实测换算。 换 GPU 会小几个数量级,所以这两个数的用途是「说明长度空间怎么算」,不是「说明这个密码很安全」。

30 秒实验

  1. 用 hashlib 加一份 10 条的字典,试着「破解」你自己设过的一个弱密码。
  2. 算一算:12 位纯小写字母的空间,是 8 位的多少倍?
  3. 想一想:为什么「连错三次锁定」比「哈希换得更强」更管用?

常见误区

  1. 「加了大写和符号就很难猜」——P@ssw0rd 在字典里排得很靠前。
  2. 「加了盐就不怕字典攻击」——盐挡的是彩虹表,弱密码照样一猜就中。
  3. 「哈希算法够强就安全」——攻击者不破解哈希,他直接试密码。
自测三问(点开对答案) 1. 字典攻击为什么管用? —— 人爱用常见密码,把常见密码表逐个试一遍就能撞上。 2. 加盐防住什么、防不住什么? —— 防彩虹表和「同密码同哈希」,防不住弱密码被猜中。 3. 为什么长度比复杂度重要? —— 多一位空间乘字符集大小;换个符号只增几个候选,还容易被变形表覆盖。

猜密码还在「替用户试」。下一种连服务端都不碰,只骗用户的眼睛。

相关笔记