第二十二讲:侧信道攻击与防护——计时攻击、功耗分析、常数时间比较

各位同学,今天我们来聊一个很有意思的话题——侧信道攻击。

你可能会想:“我写的代码逻辑没问题啊,加密算法也是标准的,怎么会有漏洞?”

嗯,我当年也是这么想的。直到有一次,我在做一个嵌入式安全模块,代码审查了好几轮,自认为万无一失。结果测试团队用一台示波器,愣是把我的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上,乘法指令的执行时间与操作数有关

怎么应对?我个人的习惯是:

  1. 用专门的常数时间库(如libsodium、BearSSL)
  2. 关键函数用汇编手写,避免编译器干扰
  3. 写完后做计时测试,确认时间确实恒定

防护功耗分析的方法

功耗分析比计时攻击更难防。因为电流波形是物理层面的泄露,光靠改代码不够。

常用的防护手段有:

方法 原理 代价
掩码(Masking) 将秘密数据拆分成多个随机分片,运算时每个分片独立处理 运算量增加2-5倍
隐藏(Hiding) 在运算中插入随机延迟或随机指令,打乱功耗波形 时间增加,且不能完全消除
双轨逻辑(Dual-Rail) 用互补电路实现,使总功耗恒定 硬件面积翻倍,功耗翻倍
去耦电容 在芯片电源引脚加电容,平滑电流波动 增加硬件成本

说实话,功耗分析防护是硬件和软件协同的事。纯软件方案(掩码+隐藏)能挡住大部分攻击,但面对专业的DPA攻击,还是得上硬件防护。

知识体系总览

下面这张图,我把本章的核心逻辑梳理了一下:

侧信道攻击与防护知识体系 攻击方式 计时攻击 功耗分析 (SPA/DPA) 电磁/缓存侧信道 核心问题 执行时间与数据相关 功耗波形泄露密钥位 物理实现泄露信息 防护方案 常数时间编程 掩码与隐藏 硬件防护(双轨等) 核心原则:所有操作的执行时间/功耗与秘密数据无关 用位运算代替条件分支,用恒定循环代替提前返回

实战建议

最后,给各位一些实战中的建议:

  • 别自己造轮子:加密库里的常数时间函数,都是经过专家审查和测试的。自己手写很容易出问题。
  • 测试要到位:写完后用大量随机输入跑计时测试,看时间分布是否均匀。我一般跑100万次,标准差超过1微秒就报警。
  • 关注编译器行为:不同编译器、不同优化级别,生成的代码可能完全不同。建议在-O2和-O3下都测试一遍。
  • 硬件防护是最后一道防线:如果产品要过安全认证,别指望纯软件方案能搞定DPA。该加硬件防护就加。

一句话总结: 侧信道攻击利用的是“实现”的漏洞,不是“算法”的漏洞。防护的核心是让所有操作的执行时间和功耗与秘密数据无关。常数时间编程是基础,但面对功耗分析,还需要硬件层面的配合。

好了,这一讲就到这里。侧信道攻击是个大话题,今天我们只讲了最核心的计时攻击和功耗分析。实际项目中,你还会遇到电磁攻击、缓存攻击、故障注入等等。但万变不离其宗——物理世界会泄密,写代码时心里要装着物理世界

公众号:蓝海资料掘金营,微信deep3321