第二十二讲:侧信道攻击与防护——计时攻击、功耗分析、常数时间比较
各位同学,今天我们来聊一个很有意思的话题——侧信道攻击。
你可能会想:“我写的代码逻辑没问题啊,加密算法也是标准的,怎么会有漏洞?”
嗯,我当年也是这么想的。直到有一次,我在做一个嵌入式安全模块,代码审查了好几轮,自认为万无一失。结果测试团队用一台示波器,愣是把我的AES密钥给“看”出来了。那一刻我才真正意识到——安全不只是算法的事,物理世界也会泄密。
什么是侧信道攻击?
传统攻击方式,是盯着你的数据流、协议、算法漏洞。侧信道攻击不一样——它盯的是你的物理实现。
说白了,就是你的设备在执行加密运算时,会泄露一些“额外信息”:
- 运算花了多长时间(计时攻击)
- 芯片消耗了多少电流(功耗分析)
- 电磁辐射的波形(电磁攻击)
- 甚至CPU缓存命中率(缓存侧信道)
这些信息,攻击者收集起来,就能反推出你的密钥。
核心观点: 加密算法本身可能是安全的,但它的物理实现不一定安全。侧信道攻击利用的是“实现漏洞”,而非“算法漏洞”。
计时攻击(Timing Attack)
计时攻击是最直观的侧信道攻击方式。攻击者测量你的加密操作花了多少时间,然后根据时间差异推断出密钥的每一位。
为什么会这样?因为很多加密操作中,条件分支的执行时间不同。
举个最简单的例子——字符串比较。你写一个密码校验函数:
// 不安全的字符串比较
int insecure_compare(const char *a, const char *b, size_t len) {
for (size_t i = 0; i < len; i++) {
if (a[i] != b[i]) {
return 0; // 一旦发现不匹配,立即返回
}
}
return 1;
}
这个函数有什么问题?
问题大了。攻击者可以逐位猜测密码:
- 如果第一位猜对了,函数会继续比较第二位,耗时稍长
- 如果第一位猜错了,函数立即返回,耗时较短
攻击者只需要测量几千次比较的时间,就能把密码一位一位地“试”出来。我在项目中见过有人用这种方式破解了一个简单的PIN码验证,前后不到两分钟。
注意: 计时攻击不仅限于字符串比较。RSA、DSA、ECDSA等签名算法中,如果密钥位上的运算时间不同,同样会被攻击。OpenSSL历史上就出过这类漏洞。
功耗分析(Power Analysis)
功耗分析比计时攻击更隐蔽,也更强大。它不看你花了多少时间,而是看你消耗了多少电流。
芯片在执行不同指令时,电流波形是不一样的。乘法运算和加法运算,电流波形明显不同。甚至处理“1”和“0”时,电流也有微小差异。
功耗分析分为两种:
| 类型 | 方法 | 难度 | 需要样本数 |
|---|---|---|---|
| 简单功耗分析(SPA) | 直接观察单次功耗波形 | 低 | 1次 |
| 差分功耗分析(DPA) | 统计多组功耗波形,找出相关性 | 高 | 数千到数万次 |
SPA就像直接看心电图——如果操作序列是固定的,攻击者一眼就能看出你在做什么。DPA则更狠,它通过统计手段,把噪声中的信号提取出来,连密钥位都能还原。
我记得有一次做智能卡项目,客户要求通过CC EAL5+认证。功耗分析是必测项。我们团队花了整整两个月,才把DPA的防护做到位。那段时间我天天盯着示波器上的波形,眼睛都快瞎了。
常数时间比较(Constant-Time Comparison)
好了,问题摆在这了。怎么防?
最核心的思路就一句话:让所有操作的执行时间与输入数据无关。
这就是“常数时间”编程。不管密钥是什么,不管比较到哪一位,执行时间必须完全一样。
拿刚才的字符串比较来说,正确的写法是:
// 安全的常数时间比较
int constant_time_compare(const char *a, const char *b, size_t len) {
int result = 0;
for (size_t i = 0; i < len; i++) {
result |= a[i] ^ b[i]; // 异或后累积,不提前返回
}
return result == 0;
}
看到区别了吗?
- 原来的版本:发现不匹配就
return 0,提前退出 - 常数时间版本:不管匹配不匹配,都遍历完所有字节,最后才返回结果
这样攻击者就没办法通过时间差异来猜测密钥了。
小技巧: 写常数时间代码时,记住一个原则——不要用if/else分支来处理秘密数据。用位运算、逻辑运算代替条件分支。比如用result |= a[i] ^ b[i]代替if (a[i] != b[i]) return 0。
常数时间编程的常见陷阱
嗯,这里要注意。常数时间编程没那么简单。你以为写对了,编译器可能给你“优化”掉了。
我曾经踩过一个坑:写了一个常数时间比较函数,用了volatile关键字防止编译器优化。结果换了一个编译器版本,它把volatile的语义理解变了,又给我优化了。密钥直接暴露。
常见的陷阱包括:
- 编译器优化:编译器可能把“无用”的循环优化掉,或者把位运算替换成条件分支
- CPU分支预测:即使代码没有if,CPU内部的分支预测单元也可能泄露时间信息
- 内存访问模式:访问数组时,索引如果是秘密数据,缓存命中率会泄露信息
- 乘法与移位:某些CPU上,乘法指令的执行时间与操作数有关
怎么应对?我个人的习惯是:
- 用专门的常数时间库(如libsodium、BearSSL)
- 关键函数用汇编手写,避免编译器干扰
- 写完后做计时测试,确认时间确实恒定
防护功耗分析的方法
功耗分析比计时攻击更难防。因为电流波形是物理层面的泄露,光靠改代码不够。
常用的防护手段有:
| 方法 | 原理 | 代价 |
|---|---|---|
| 掩码(Masking) | 将秘密数据拆分成多个随机分片,运算时每个分片独立处理 | 运算量增加2-5倍 |
| 隐藏(Hiding) | 在运算中插入随机延迟或随机指令,打乱功耗波形 | 时间增加,且不能完全消除 |
| 双轨逻辑(Dual-Rail) | 用互补电路实现,使总功耗恒定 | 硬件面积翻倍,功耗翻倍 |
| 去耦电容 | 在芯片电源引脚加电容,平滑电流波动 | 增加硬件成本 |
说实话,功耗分析防护是硬件和软件协同的事。纯软件方案(掩码+隐藏)能挡住大部分攻击,但面对专业的DPA攻击,还是得上硬件防护。
知识体系总览
下面这张图,我把本章的核心逻辑梳理了一下:
实战建议
最后,给各位一些实战中的建议:
- 别自己造轮子:加密库里的常数时间函数,都是经过专家审查和测试的。自己手写很容易出问题。
- 测试要到位:写完后用大量随机输入跑计时测试,看时间分布是否均匀。我一般跑100万次,标准差超过1微秒就报警。
- 关注编译器行为:不同编译器、不同优化级别,生成的代码可能完全不同。建议在-O2和-O3下都测试一遍。
- 硬件防护是最后一道防线:如果产品要过安全认证,别指望纯软件方案能搞定DPA。该加硬件防护就加。
一句话总结: 侧信道攻击利用的是“实现”的漏洞,不是“算法”的漏洞。防护的核心是让所有操作的执行时间和功耗与秘密数据无关。常数时间编程是基础,但面对功耗分析,还需要硬件层面的配合。
好了,这一讲就到这里。侧信道攻击是个大话题,今天我们只讲了最核心的计时攻击和功耗分析。实际项目中,你还会遇到电磁攻击、缓存攻击、故障注入等等。但万变不离其宗——物理世界会泄密,写代码时心里要装着物理世界。