哈希只能认「一模一样的字节」,改一个字节就认不出来了。 所以靠指纹查毒,天生打不过「加壳」和「多态」。
上一节的 IDS 是给网络看的。 这一节的两个东西都装在你自己电脑上:一个管文件,一个管进程。 Demo 拿一个 1 MB 文件改一个比特,看哈希怎么变,再算一遍哈希「雪崩」得有多彻底。
一、指纹查毒是怎么工作的
最早的反病毒软件只有一张清单:把每个已知病毒文件的哈希列出来,扫的时候逐个比。 这个办法简单、快、误报几乎为零,因为哈希撞车在数学上几乎不可能。 它的死穴也在这句话里:只能认「一模一样的字节」。 攻击者只要在文件末尾加一个无用的字节,哈希就全新,而程序干的事一点没变。 这种手法叫加壳,而「每次感染都自动改一下自己」的叫多态。 于是反病毒只能往三个方向走:一是看行为,比如「这个程序在改系统目录、在写启动项」; 二是丢进沙箱跑一遍,看它到底干了什么; 三是算「模糊哈希」,也就是只看文件里那些不容易变的部分。 三条路都比对哈希贵得多,但没办法——因为对手已经不打算让字节保持一样了。
二、个人防火墙为什么能问「这个程序」
网络防火墙看的是地址和端口,它不知道是哪个程序发的包。 而装在你自己电脑上的那个防火墙能弹窗问你「是否允许某某程序联网」—— 因为它和程序跑在同一台机器上,能看见进程。 这个区别决定了它们的用途完全不同:网络防火墙管「外面能不能进来」, 主机防火墙管「里面的东西能不能出去」。 后者恰好补上了前者的盲区——一台中了木马的机器,它的外连请求在网关上看是「合法的出站流量」。 代价是它只能保护这一台机器:一百台电脑就要配一百次,而且用户随手点「允许」是常态。 所以企业里更常见的做法是「白名单加集中管理」,而不是靠弹窗让人自己判断。
三、三个手段,三种分工
哈希、加密、HMAC 经常被混为一谈,其实它们解决的问题完全不同:
| 手段 | 防什么 | 能不能还原 |
|---|---|---|
| 哈希 | 防篡改,改了就发现 | 不能,单向 |
| 加密 | 防窃听,看不懂 | 能,有钥匙就能 |
| HMAC | 防篡改且防伪造 | 不能,但要有密钥 |
第一行最容易被误解:「哈希一下再发」不等于「加密发送」。 哈希之后原文还在,只是多了一串指纹,谁都能看见内容,只是改了就会被发现。 第三行的区别在「钥匙」:普通哈希谁都能算,所以攻击者改完内容可以自己重算一个哈希贴上去; HMAC 用的是一把共享密钥,攻击者没有钥匙就算不出来。 所以「防篡改」这件事,光有哈希不够,必须有密钥参与——这一点在写接口签名的时候天天用到。
四、动手 Demo:改一个字节,哈希就面目全非
一个 1 MB 的随机文件,翻掉中间那个字节的最低位,看 MD5 和 SHA-256 各变成什么, 再统计翻转了多少位。
import hashlib, os
data = os.urandom(1 << 20)
bad = bytearray(data)
bad[len(data) // 2] ^= 0x01
bad = bytes(bad)
for name in ("md5", "sha256"):
a = hashlib.new(name, data).hexdigest()
b = hashlib.new(name, bad).hexdigest()
print(name, a[:16], b[:16], sum(x != y for x, y in zip(a, b)))
tot = 0
for i in range(20):
d = bytearray(data)
d[len(data) // 2] ^= 1 << (i % 8)
h1 = hashlib.sha256(data).digest()
h2 = hashlib.sha256(bytes(d)).digest()
tot += sum(bin(x ^ y).count("1") for x, y in zip(h1, h2))
print(round(tot / 20.0, 1))
实跑输出:
--------------------------------------------------------------------
改一个字节,哈希就面目全非
--------------------------------------------------------------------
**杀毒软件早期靠「文件指纹」认病毒**,**指纹就是哈希。**
**下面拿一个 1 MB 的随机文件,改中间一个字节,看哈希怎么变。**
**只翻了第 524288 个字节的最低位,变化 1 个比特。**
算法 摘要长度 改之前 改之后
----------------------------------------------------------------------------
**MD5** **16 字节** `621d443df915d7b9`… `03412c2fde44098b`…
**SHA-256** **32 字节** `63fe55d839014c37`… `53ea30d4cfa20b5b`…
**两行都完全变了,没有一个字节还是原来的。**
**这不是巧合,是设计目标:所谓「雪崩效应」,输入变一位,输出大约一半的位要翻转。**
**实测二十次,SHA-256 平均翻转 125.5 位,理论值是一半即 128 位。**
**MD5 的理论值同样是一半,也就是 64 位。**
**「大约一半」这四个字很重要**:**如果只变几位,就能靠「相似度」反推内容,哈希就不成立了。**
**但哈希做指纹有一个致命前提:文件的字节不能变。**
**攻击者只要在文件末尾塞一个无用的字节,指纹就全新**,**而程序行为一点没变。**
**这就是为什么「加壳」「多态」能绕开早期的特征库**:
**每一代病毒的哈希都不一样,但干的事是同一件。**
**顺带说清三件事的分工**:
手段 防什么 能做什么 不能做什么
--------------------------------------------------------------------
**哈希** **防篡改** **改了就发现** **不保密**
**加密** **防窃听** **看不懂** **不防篡改**
**HMAC** **防篡改且防伪造** **要有密钥** **不保密**
**第一行第三列是关键**:**哈希不加密任何东西,文件内容仍然是明文。**
**所以「哈希一下再发」不等于「加密发送」**,**这两个需求必须用两个手段分别满足。**
两行摘要全变了,一个字节都没留下。 这不是巧合,是设计目标:所谓「雪崩效应」,输入改一位,输出大约一半的位要翻转。 实测二十次,SHA-256 平均翻转一百二十多位,理论值是一半即 128 位。 MD5 的理论值同样是它的一半,也就是 64 位。 「大约一半」这四个字很重要:如果只变几位,就能靠「相似度」反推内容,哈希就不成立了。 但这段代码同时也证明了反病毒软件的困境: 它只翻了一个比特,文件还完全能运行,指纹却彻底变了。 攻击者要做的事比这还简单——在末尾追加一个字节就行。 所以「靠哈希查毒」这件事,在对手开始主动变形的那天就失效了。
30 秒实验
- 算一下:MD5 有 128 位、SHA-256 有 256 位,随机撞车要试多少次? (各约 2 的 64 次方和 2 的 128 次方)
- 想一下:为什么「加壳」能绕开指纹查毒?
- 想一下:主机防火墙能看见什么,是网络防火墙看不见的?
常见误区
- 以为「哈希就是加密」。 哈希单向、不可还原,原文还在,只是多了一串指纹。
- 以为「哈希能防篡改所以够用了」。 攻击者可以改完重算,要防伪造必须有密钥,也就是 HMAC。
- 以为「装了杀毒就不会中毒」。 它挡的是已知的,零日和数据加密勒索仍要看行为与备份。
自测三问(点开对答案)
1. 为什么改一个字节哈希就全变? —— **雪崩效应**:设计目标就是让输出大约一半的位翻转。 2. 哈希和 HMAC 的差别? —— **HMAC 有共享密钥**,攻击者没有钥匙就伪造不出新的校验值。 3. 主机防火墙补了网络防火墙的什么盲区? —— **「哪个程序在往外连」**,也就是出站方向的可见性。下一节进正题:加密本身。对称和公钥不是二选一,它们必须合起来用。
- ☐ (本节完,下一节 9.3.1 对称密码体制与公钥密码体制)