2. 哈希函数:SHA-256原理、MD5的缺陷、哈希在完整性校验中的应用
哈希函数,说白了就是一把“数字指纹”生成器。你给它任意长度的数据,它给你一个固定长度的摘要。这个摘要就像人的指纹——数据变了,指纹一定变;指纹相同,数据大概率没变。
我在嵌入式安全领域摸爬滚打这些年,哈希函数几乎天天见。从固件校验到密码存储,从数字签名到证书验证,到处都有它的影子。今天咱们就把它彻底聊透。
2.1 哈希函数的核心性质
一个合格的密码学哈希函数,必须满足三个硬性要求:
- 抗原像性:给定哈希值h,你几乎不可能找到任何消息m使得H(m)=h。说白了就是“不可逆”。
- 抗第二原像性:给定消息m1,你几乎不可能找到另一个m2使得H(m1)=H(m2)。
- 抗碰撞性:你几乎不可能找到任意两个不同的m1和m2使得H(m1)=H(m2)。
重要概念:抗碰撞性比抗第二原像性更强。如果哈希函数能抵抗碰撞攻击,它自然也能抵抗第二原像攻击。反过来不一定成立。
你想想看,如果哈希函数不满足这些性质,那它还有什么用?签名可以被伪造,固件可以被篡改,密码可以被逆向——整个安全体系就崩塌了。
2.2 SHA-256:当今的工业标准
SHA-256属于SHA-2家族,输出256位(32字节)的摘要。我个人习惯把它当作“哈希界的AES”——稳定、高效、经过充分验证。
2.2.1 算法流程概览
SHA-256的处理过程可以分为五步:
- 消息填充:在消息末尾补一个'1'位,然后补'0'位,直到长度模512等于448。最后附加64位的原始消息长度。
- 初始化状态:设置8个32位初始哈希值(H0~H7),这些值来自前8个质数的平方根的小数部分。
- 消息分块:将填充后的消息分成512位(64字节)的块。
- 消息调度:每个块扩展成64个32位字(W0~W63)。
- 压缩函数:对每个块执行64轮迭代,更新8个状态变量。
我的经验:在嵌入式平台上实现SHA-256时,我建议优先使用硬件加速器。如果必须用软件实现,注意查表法的内存对齐问题。我曾经在一个Cortex-M4项目上因为未对齐访问导致性能下降30%。
2.2.2 压缩函数详解
压缩函数是SHA-256的核心。它使用6个逻辑函数:
- Σ0(x) = ROTR^2(x) ⊕ ROTR^13(x) ⊕ ROTR^22(x)
- Σ1(x) = ROTR^6(x) ⊕ ROTR^11(x) ⊕ ROTR^25(x)
- σ0(x) = ROTR^7(x) ⊕ ROTR^18(x) ⊕ SHR^3(x)
- σ1(x) = ROTR^17(x) ⊕ ROTR^19(x) ⊕ SHR^10(x)
- Ch(x,y,z) = (x ∧ y) ⊕ (¬x ∧ z)
- Maj(x,y,z) = (x ∧ y) ⊕ (x ∧ z) ⊕ (y ∧ z)
每一轮的计算过程如下:
T1 = h + Σ1(e) + Ch(e,f,g) + Kt + Wt
T2 = Σ0(a) + Maj(a,b,c)
h = g
g = f
f = e
e = d + T1
d = c
c = b
b = a
a = T1 + T2
嗯,这里要注意:64轮迭代完成后,将当前块的状态变量与上一块的状态变量相加,得到新的状态。这就是所谓的“Davies-Meyer”结构。
2.2.3 代码示例:SHA-256核心轮函数
// SHA-256 单轮计算(C语言简化版)
void sha256_round(uint32_t *state, const uint32_t *w, int t) {
uint32_t a = state[0], b = state[1];
uint32_t c = state[2], d = state[3];
uint32_t e = state[4], f = state[5];
uint32_t g = state[6], h = state[7];
uint32_t S1 = ROTR(e, 6) ^ ROTR(e, 11) ^ ROTR(e, 25);
uint32_t ch = (e & f) ^ ((~e) & g);
uint32_t temp1 = h + S1 + ch + K[t] + w[t];
uint32_t S0 = ROTR(a, 2) ^ ROTR(a, 13) ^ ROTR(a, 22);
uint32_t maj = (a & b) ^ (a & c) ^ (b & c);
uint32_t temp2 = S0 + maj;
state[7] = g; state[6] = f; state[5] = e;
state[4] = d + temp1;
state[3] = c; state[2] = b; state[1] = a;
state[0] = temp1 + temp2;
}
2.3 MD5的缺陷:一个活生生的教训
MD5曾经是哈希函数的明星。它由Rivest在1992年设计,输出128位摘要。但到了2004年,中国密码学家王小云团队宣布了MD5的碰撞攻击——可以在普通PC上几分钟内找到碰撞。
严重警告:MD5已经被彻底攻破。任何还在使用MD5做安全校验的系统,都应该立即升级。这不是“可能不安全”,而是“肯定不安全”。
MD5的主要缺陷包括:
- 输出太短:128位,根据生日悖论,碰撞攻击的复杂度只有2^64。这在今天完全不可接受。
- 结构弱点:MD5的压缩函数对差分攻击的抵抗力不足。王小云团队正是利用了这个弱点。
- 无抗量子性:Grover算法可以将碰撞搜索复杂度降到2^32,量子计算机一旦成熟,MD5瞬间崩溃。
我记得有一次做安全审计,发现一个客户的车载系统还在用MD5校验固件。我当场演示了碰撞攻击——用两个不同的固件生成了相同的MD5值。客户的脸都绿了。从那以后,他们所有新项目都强制使用SHA-256。
2.4 哈希在完整性校验中的应用
完整性校验是哈希函数最直接的应用。说白了就是:计算数据的哈希值,保存起来;下次再计算一次,对比是否一致。
2.4.1 固件完整性校验
在安全启动中,固件完整性校验是第一步。流程如下:
- 固件开发者计算固件的SHA-256哈希值。
- 用私钥对哈希值签名,生成签名数据。
- 设备启动时,用公钥验证签名,得到原始哈希值。
- 设备计算当前固件的SHA-256哈希值。
- 对比两个哈希值。一致则固件未被篡改。
关键点:哈希值本身必须被保护。如果攻击者能同时篡改固件和哈希值,那校验就失去了意义。所以哈希值通常通过数字签名或安全存储来保护。
2.4.2 数据完整性校验表
| 应用场景 | 哈希函数 | 校验方式 | 安全等级 |
|---|---|---|---|
| 固件签名 | SHA-256 | 数字签名验证 | 高 |
| 文件下载 | SHA-256/SHA-512 | 哈希值对比 | 中 |
| 密码存储 | bcrypt/scrypt | 加盐哈希 | 高 |
| 消息认证 | HMAC-SHA256 | 密钥哈希 | 高 |
| 数据去重 | SHA-1(不推荐) | 哈希值索引 | 低 |
2.4.3 嵌入式系统中的实践
在嵌入式系统中做完整性校验,有几个坑需要避开:
- 内存限制:不要一次性加载整个固件到内存。使用流式哈希(如mbedTLS的mbedtls_sha256_update_ret),分块处理。
- 时序攻击:比较哈希值时使用恒定时间比较函数,防止攻击者通过时序差异推断信息。
- 回滚攻击:仅仅校验完整性不够,还要校验版本号。否则攻击者可以用旧版本的合法固件替换新版本。
避坑指南:我曾经在一个IoT项目上踩过回滚攻击的坑。设备只校验了固件的签名,没校验版本号。结果攻击者用一年前的旧固件(有已知漏洞)替换了新固件。从那以后,我所有项目都强制在固件头里加入版本号字段,并在校验时检查版本号是否大于等于当前版本。
2.5 哈希函数的知识体系
下面这张图展示了哈希函数的核心知识结构:
2.6 总结与建议
哈希函数是密码学的基础工具。选对了,事半功倍;选错了,满盘皆输。
我的建议很简单:
- 新项目一律用SHA-256。别想着“够用就行”,安全没有“够用”这一说。
- 尽快淘汰MD5和SHA-1。如果现有系统还在用,制定迁移计划,刻不容缓。
- 注意实现细节。流式处理、恒定时间比较、版本号校验——这些细节决定了你的系统是“看起来安全”还是“真的安全”。