什么是时序攻击(Timing Attack)?
Posted on Thu 12 February 2026 in Journal
| Abstract | 什么是时序攻击(Timing Attack)? |
|---|---|
| Authors | Walter Fan |
| Category | Journal |
| Version | v1.0 |
| Updated | 2026-02-12 |
| License | CC-BY-NC-ND 4.0 |
短大纲
- 用「每次请求多几微秒就被利用」的小场景开场,点出:比较秘密时,时间会泄密
- 时序攻击是什么:攻击者通过测量比较耗时,推断秘密内容
- 为什么简单比较容易泄密:
==/strings.Compare等会「第一个字节不对就返回」,时间差可被统计区分 - 举例:密码校验、HMAC、API Token 比较
- 防御:常数时间比较(constant-time comparison),Go/其他语言怎么用
- 总结 + 思维导图 + 明天就能做 + 适用边界 + 开放式问题
2026-02-12
有一次安全审计,发现某接口用「用户提交的 token」和「数据库里的 token」直接做 == 比较。功能上没问题,但攻击者不需要拿到内存——只要反复请求、统计响应时间,就能按「猜对多少」把 token 一个字节一个字节猜出来。 这类利用「比较耗时」来推断秘密的攻击,就叫 时序攻击(Timing Attack)。
时序攻击是什么?
人话版:程序在比较两个值(比如密码、HMAC、API Key)时,若「发现第一个不一样就立刻返回」,那么比较得越久,说明前面猜对的字节越多。攻击者通过大量请求测响应时间,用统计方法把「哪一字节对了」区分出来,从而逐字节还原秘密。
所以:比较秘密时,不能「提前返回」——必须把整段比完,且耗时与内容无关,也就是要做「常数时间比较」(constant-time comparison)。
为什么简单比较容易泄密?
很多语言里的「相等比较」是短路式的:
- 字符串:
==、strings.Compare、strcmp等,通常是逐字节比,第一个不同就返回。 - 结果:猜对前 N 个字节时,比较会多执行 N 次循环,响应时间略长;猜错第一个字节就返回,响应时间更短。
攻击者不需要单次精确到纳秒;只要在同一网络环境下发大量请求、对每组猜测做统计(例如中位数、方差),就能把「这一字节是否猜对」从噪声里区分出来。再对下一个字节重复,就能逐字节还原整个秘密。
一个小实验:Python 里时间差真的可测
下面这段代码模拟「校验 token」:朴素比较一发现字节不同就返回,常数时间比较则始终比完整个长度。用同一批猜测各测很多次,就能看到「猜对第一个字节」时总耗时明显更长——说明单次几微秒的差异,在统计下是可以被利用的。
import time
import hmac
SECRET = b"admin_token_42" # 模拟服务端存的 token
def naive_compare(guess: bytes, secret: bytes) -> bool:
"""朴素比较:第一个不同就返回,耗时随「猜对多少」变化。"""
if len(guess) != len(secret):
return False
for i in range(len(secret)):
if guess[i] != secret[i]:
return False # 提前返回 → 攻击者可通过时间推断这里就错了
return True
def constant_time_compare(guess: bytes, secret: bytes) -> bool:
"""常数时间比较:始终比完,耗时与内容无关。"""
return hmac.compare_digest(guess, secret)
def measure_many(compare_fn, guess: bytes, rounds: int = 50000) -> float:
"""多次比较取总时间(秒),便于放大差异。"""
start = time.perf_counter()
for _ in range(rounds):
compare_fn(guess, SECRET)
return time.perf_counter() - start
if __name__ == "__main__":
# 猜错第一个字节:b"x..." vs b"a..."
wrong_first = b"x" + SECRET[1:]
# 猜对第一个字节:b"a..." vs b"admin..."
right_first = b"a" + SECRET[1:]
print("朴素比较(50000 次):")
t_wrong = measure_many(naive_compare, wrong_first)
t_right = measure_many(naive_compare, right_first)
print(f" 猜错首字节: {t_wrong:.4f}s")
print(f" 猜对首字节: {t_right:.4f}s ← 明显更慢")
print(f" 差异: {(t_right - t_wrong) * 1000:.2f} ms")
print("常数时间比较(50000 次):")
t_wrong_safe = measure_many(constant_time_compare, wrong_first)
t_right_safe = measure_many(constant_time_compare, right_first)
print(f" 猜错首字节: {t_wrong_safe:.4f}s")
print(f" 猜对首字节: {t_right_safe:.4f}s ← 几乎一样")
print(f" 差异: {(t_right_safe - t_wrong_safe) * 1000:.2f} ms")
本地跑一遍(python timing_demo.py),你会看到:朴素比较下「猜对首字节」比「猜错首字节」多出一截时间(通常几毫秒到几十毫秒,视机器而定),而 hmac.compare_digest 两种猜测的耗时几乎一致。这说明时间差真实存在,攻击者用大量请求做统计就能逐字节推断;换成真实 API 和网络延迟,思路相同,只是需要更多请求来压过噪声。
哪些场景要防?
只要「比较结果」依赖秘密数据、且比较方式依赖内容提前返回,就存在被时序攻击放大的风险。典型场景:
- 密码 / 口令校验:用户提交的 hash 与库中存储的 hash 比较。
- HMAC / 签名校验:例如 Webhook 签名、Cookie 签名,用
secret算出的期望值与请求体比较。 - API Token / 会话 ID:服务端用「请求里的 token」和「库里或缓存里的 token」做字符串相等比较。
这些地方一旦用 ==、strings.Compare、strcmp 等,就属于「可被时序攻击」的实现。
怎么防:常数时间比较
思路就一条:比较耗时与「是否相等」无关,与秘密内容无关——不提前 return,且每次都对整段做固定次数的操作。
- Go:
crypto/subtle里的subtle.ConstantTimeCompare(a, b []byte) int,只有 0/1 两种结果,且比较过程常数时间。对字符串先转[]byte再比,并保证长度一致(长度不一致时也应用常数时间逻辑:例如先比长度,再比内容,或统一先ConstantTimeCompare定长部分)。 - 其他语言:多数密码学库都提供类似 API(如 Python 的
hmac.compare_digest、OpenSSL 的CRYPTO_memcmp)。用这些替代手写==或strcmp。
注意:长度本身也不能泄露。若「正确 token 长度」和「错误 token 长度」返回时间明显不同,攻击者可以先通过时间推断长度,再逐字节猜内容。所以常见做法是:无论对错,都先做一次定长、常数时间的比较(例如和固定长度的占位比,或统一按「较长者」的长度比),再根据结果返回业务上的成功/失败。
一个跨领域的类比
可以把「朴素比较」想成考试判卷:看到第一道错题就拍桌子「不及格」。考生不用看到卷子,只要观察你判卷的时间长短,就能推断「第一道题对没对、第二道题对没对」……直到把答案反推出来。常数时间比较相当于:不管对错,都把整张卷子从头到尾看满固定时间,再统一宣布结果——从时间上就推不出具体哪一题对、哪一题错。
总结
- 时序攻击:通过测量「比较秘密」的耗时,用统计方法逐字节推断秘密内容。
- 根因:用
==、strings.Compare等会「第一个不同就返回」的比较方式处理密码、HMAC、Token。 - 防御:用常数时间比较(如 Go 的
subtle.ConstantTimeCompare),且不泄露长度信息;敏感比较全部走 crypto 库提供的接口。
@startmindmap
<style>
mindmapDiagram {
node { BackgroundColor #F5F5F5; RoundCorner 8; Padding 8; FontSize 14 }
:depth(0) { BackgroundColor #2C3E50; FontColor white; FontSize 16; FontStyle bold }
:depth(1) { FontSize 14; FontStyle bold }
:depth(2) { FontSize 13 }
}
</style>
* 时序攻击(Timing Attack)
** 是什么
*** 通过测量比较耗时推断秘密
*** 逐字节猜测(统计区分时间差)
** 为何简单比较容易泄密
*** == / strings.Compare 提前返回
*** 猜对越多,比较越久
** 典型场景
*** 密码/Hash 校验
*** HMAC/签名校验
*** API Token 比较
** 防御
*** 常数时间比较(constant-time)
*** Go: crypto/subtle ConstantTimeCompare
*** 不泄露长度
@endmindmap

明天就能做的 4 件事
- 搜一遍项目里所有「和密码/HMAC/Token 比较」的代码(15 分钟)
-
怎么算做得好:列出每一处用的是
==/Compare还是ConstantTimeCompare(或等价 API),敏感比较全部走常数时间。 -
把至少一处敏感比较改成常数时间(10 分钟)
-
怎么算做得好:Go 用
subtle.ConstantTimeCompare,且长度一致或按「不泄露长度」的方式处理;其他语言用官方推荐的compare_digest/CRYPTO_memcmp等。 -
在 code review 清单里加一条:「涉及密钥/密码/Token 的相等判断是否用了常数时间比较?」
-
怎么算做得好:新人也能在 MR 里看到这条,且有一两个示例链接。
-
确认「错误时」不因长度不同而明显变快/变慢(10 分钟)
- 怎么算做得好:对「正确长度错误内容」和「错误长度」的请求,响应时间在统计上无明显差异(或已用定长比较掩盖长度差异)。
什么时候必须防、什么时候可以放宽
必须防的:对外暴露的登录、API Key 校验、Webhook 签名、会话 Token 校验等,只要比较的是秘密,就应用常数时间比较。
可以后置的:纯内网、且已假设攻击者无法大量测时(例如无法发大量请求)的环境,可以列为「后续加固」;但新代码仍建议一开始就用常数时间,成本很低。
代价与权衡:常数时间比较会多算几次循环,通常可忽略;换来的是不把「比较时间」变成信息泄露通道。
最后一个问题留给你:你们当前代码库里,还有没有用 == 或 strings.Compare 比较密码、HMAC 或 Token 的地方?如果有,你打算先改哪一处、用哪一门语言的哪种 API?
扩展阅读
- Go: crypto/subtle — ConstantTimeCompare
- Timing attacks on key confirmation (CWE-208)
- Python: hmac.compare_digest
本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。