一次精彩的密码学攻击:RSA-CRT故障注入完全解析
本文将带你从零开始理解一次真实的RSA密码学攻击,即使你是密码学新手,也能跟着本文完整复现这次攻击。
引言:一个看似安全的密码系统
想象这样一个场景:你有一个保险箱,使用了世界上最安全的RSA密码锁。从数学角度来说,即使用超级计算机,也需要上千年才能暴力破解。
但是,如果我告诉你,只需要在计算过程中"抖一下"这个保险箱,让它计算出错一次,我就能在几秒钟内拿到密钥,你会相信吗?
这就是故障注入攻击(Fault Attack)的魔力。
今天,我们将深入剖析一道CTF密码学题目,它完美地展示了这种攻击是如何工作的。更重要的是,你会明白:密码学的安全性不仅取决于算法,还取决于实现。
基础知识:什么是RSA加密
RSA加密简介
RSA是一种非对称加密算法,1977年由三位数学家(Rivest、Shamir、Adleman)发明。它的核心思想很简单:
公钥加密,私钥解密
RSA的工作原理
让我用一个简单的比喻来解释:
比喻:神奇的锁和钥匙
-
生成密钥对:
- 找两个大质数
p和q(比如各有300位数字) - 计算
n = p × q(这是"锁") - 计算
e = 65537(这是"公钥",任何人都知道) - 计算
d,使得e × d ≡ 1 (mod φ(n))(这是"私钥",只有你知道)
- 找两个大质数
-
加密过程:
密文 c = m^e mod n任何人都可以用公钥
(n, e)加密消息m -
解密过程:
明文 m = c^d mod n只有持有私钥
d的人才能解密
为什么RSA安全?
核心困难:已知 n = p × q,如果不知道 p 和 q,要计算私钥 d 几乎不可能。
这就像:给你一个巨大的数字(比如有600位),让你找出它是哪两个质数相乘得到的。用超级计算机可能需要上千年!
但是RSA有个问题:太慢!
计算 m^d mod n 非常耗时,因为 d 是一个巨大的数字。
举个例子:
m^d可能需要计算几百次乘法- 每次乘法都是几百位数字的运算
- 对于一个网站来说,如果每秒需要处理上千次加密,这太慢了!
怎么办? → 引入中国剩余定理(CRT)优化!
CRT优化:让RSA提速4倍
什么是CRT优化?
中国剩余定理(Chinese Remainder Theorem)是中国古代数学家孙子提出的,核心思想是:
"分而治之" - 把一个大问题拆成两个小问题,分别解决后再合并。
没有CRT vs 有CRT
传统RSA(慢):
计算 m = c^d mod n
其中 d 有 600 位数字
需要计算几百次 600 位数字的乘法
RSA-CRT(快4倍):
1. 把大问题拆成两个小问题:m1 = c^dp mod p (只需计算 300 位)m2 = c^dq mod q (只需计算 300 位)2. 用CRT合并结果:m = 合并(m1, m2)
为什么快?
- 300位数字的运算比600位快很多
- 两个小问题并行计算
- 总体提速约4倍!
RSA-CRT的详细步骤
让我详细解释每一步:
# 假设我们有:
p = 一个大质数(300位)
q = 另一个大质数(300位)
n = p × q(600位)
e = 65537(公钥指数)
c = 要解密的密文# 步骤1: 计算小指数
dp = e^(-1) mod (p-1) # 在模p-1下,e的逆元
dq = e^(-1) mod (q-1) # 在模q-1下,e的逆元# 步骤2: 分别在模p和模q下计算
m1 = c^dp mod p # 在"p的世界"里解密
m2 = c^dq mod q # 在"q的世界"里解密# 步骤3: 用CRT合并(这里省略细节)
m = CRT_combine(m1, m2, p, q)
打个比喻
想象你要完成一个大项目:
传统方法:一个人从头做到尾(慢)
CRT方法:
- 把项目拆成两半
- 两个人同时做(m1和m2)
- 最后把结果合并起来
- 速度快了一倍!
关键点:两个人各自完成自己的部分,互不干扰。
题目环境:模拟的硬件加速器
题目背景
这道CTF题目模拟了一个真实的场景:
一个硬件加速器(BNHW)
├─ 用于加速大整数运算
├─ 支持加、减、乘、模逆、模幂等运算
└─ 通过内存映射与CPU通信
题目文件
题目文件夹
├─ main.py # 主程序(CTF入口)
├─ BNUc.py # 硬件模拟器
├─ rsa-crt.c # RSA-CRT实现
├─ rsa-crt # 编译后的可执行文件
└─ BNHW说明书.md # 硬件手册
硬件加速器的工作方式
内存布局:
地址偏移 大小 用途
0x000 64字节 操作数A
0x040 64字节 操作数B
0x080 128字节 模数M
0x100 128字节 结果R
0x180 1字节 操作码
0x181 1字节 状态码
操作流程:
1. 写入数据 → A, B, M
2. 设置操作码 → OPCODE (1-7)
3. 触发计算 → STATUS = 0
4. 等待完成 → STATUS != 0
5. 读取结果 → R
关键:题目提供了故障注入能力!
def shock(uc):"""在指定时刻翻转寄存器的某一位"""# 这就是我们的"攻击点"!flipBit(uc, reg_name)
这意味着什么?
- 我们可以选择一个时刻(shock_time)
- 在那个时刻,CPU的某个寄存器会被"抖一下"
- 导致计算结果出错!
攻击原理:故障注入的威力
核心思想
还记得RSA-CRT要分别计算 m1 和 m2 吗?
关键观察:如果我们能让其中一个计算出错...
正常的RSA-CRT签名
输入:消息c,密钥(p, q)步骤1: m1 = c^dp mod p 正确
步骤2: m2 = c^dq mod q 正确
步骤3: sig = CRT_combine(m1, m2)结果:sig^e ≡ c (mod n) 完美!
验证:任何人都可以验证 sig^e mod n = c
故障注入后的RSA-CRT
输入:消息c,密钥(p, q)步骤1: m1 = c^dp mod p 计算出错!(因为我们"抖"了它)
步骤2: m2 = c^dq mod q 正确
步骤3: sig' = CRT_combine(m1_错误, m2_正确)结果:sig'^e ≡ c (mod q) 在模q下正确sig'^e ≢ c (mod p) 在模p下错误
用一个比喻理解
想象两个厨师合作做菜:
正常情况:
- 厨师A(代表p)做菜的上半部分
- 厨师B(代表q)做菜的下半部分
- 合并后:完美的菜
故障注入:
- 我们在厨师A工作时"抖动"他的手
- 厨师A做错了
- 厨师B做对了
- 合并后:半对半错的菜
神奇的地方:通过分析这个"半对半错的菜",我们能推出厨师B的"秘方"(密钥q)!
为什么能恢复密钥?
这是最精彩的部分!
数学魔法:
-
错误签名的特性:
sig'^e ≡ c (mod q) # 在q的世界里正确 sig'^e ≢ c (mod p) # 在p的世界里错误 -
关键推导:
因为 sig'^e ≡ c (mod q) 所以 q | (sig'^e - c) # q是(sig'^e - c)的因子因为 sig'^e ≢ c (mod p) 所以 p ∤ (sig'^e - c) # p不是因子 -
GCD魔法:
已知:n = p × q 计算:gcd(sig'^e - c, n)因为 q | (sig'^e - c) 但 p ∤ (sig'^e - c) 所以 gcd(sig'^e - c, n) = q
用人话说:
sig'^e - c能被 q 整除,但不能被 p 整除n = p × q- 求
gcd(sig'^e - c, n)就能得到 q!
举个具体例子
假设(为了简单,用小数字):
p = 11
q = 13
n = p × q = 143
e = 7正常签名:sig = 42
sig^7 mod 143 = 原始消息 故障签名:sig' = 55
sig'^7 mod 143 ≠ 原始消息
但 sig'^7 mod 13 = 原始消息 (在q的世界里对)
而 sig'^7 mod 11 ≠ 原始消息 (在p的世界里错)计算 gcd(sig'^7 - 原始消息, 143)= gcd(某个数, 143)= 13 就是q!
数学解析:为什么GCD能破解密钥
数论基础
在深入之前,我们需要理解几个概念:
1. 模运算(Modular Arithmetic)
a ≡ b (mod n) 意思是:a 和 b 除以 n 余数相同例子:
17 ≡ 5 (mod 12) # 17和5除以12都余5
30 ≡ 0 (mod 10) # 30除以10余0
类比:就像时钟,13点就是1点(模12)
2. 最大公约数(GCD)
gcd(a, b) = 最大的能同时整除a和b的数例子:
gcd(12, 18) = 6 # 6是最大的能同时整除12和18的数
gcd(7, 11) = 1 # 7和11互质
3. 整除符号
a | b 表示 a 整除 b(b能被a整除)
a ∤ b 表示 a 不整除 b例子:
3 | 12 # 12能被3整除
5 ∤ 12 # 12不能被5整除
核心定理证明
现在让我们严格证明为什么GCD能恢复密钥:
定理:
如果 sig'^e ≡ c (mod q) 且 sig'^e ≢ c (mod p)
那么 gcd(sig'^e - c, n) = q
证明:
第一步:分析 sig'^e - c
设 diff = sig'^e - c
因为 sig'^e ≡ c (mod q),所以:
sig'^e - c ≡ 0 (mod q)
这意味着:q | diff (q整除diff)
因为 sig'^e ≢ c (mod p),所以:
sig'^e - c ≢ 0 (mod p)
这意味着:p ∤ diff (p不整除diff)
第二步:计算GCD
gcd(diff, n) = gcd(diff, p × q)
因为:
q | diff(q是diff的因子)p ∤ diff(p不是diff的因子)n = p × q
所以:
gcd(diff, n) = q
直觉理解:
- diff和n的公共因子只能是q(因为p不整除diff)
- 所以GCD就是q!
用图示理解
n = p × q = 143 (p=11, q=13)│├─ 因子: 1, 11, 13, 143│
diff = sig'^e - c│├─ 如果能被13整除但不能被11整除│└─ gcd(diff, 143) = 13
为什么一定能成功?
关键假设:
- 故障只影响了m1或m2其中之一
- 另一个计算结果是正确的
- e和n互质(RSA的基本要求)
成功条件:
只要满足:
- sig'^e ≡ c (mod q) # q方向正确
- sig'^e ≢ c (mod p) # p方向错误就一定能恢复q!
成功率:在合适的shock_time下,接近100%!
完整复现:手把手实战
现在让我们一步步复现这次攻击。
环境准备
系统要求
操作系统: Linux (推荐 Ubuntu 20.04+)
Python: 3.8+
安装依赖
# 安装Python库
pip3 install unicorn-engine capstone pycryptodome# 或使用系统包管理器
sudo apt install python3-unicorn python3-capstone python3-crypto# 安装pexpect(可选,用于自动化)
sudo apt install python3-pexpect
下载题目文件
# 假设题目文件已经在当前目录
ls
# 输出: main.py BNUc.py rsa-crt rsa-crt.c BNHW说明书.md
理解题目代码
让我们看看关键部分:
main.py 的核心逻辑
def run(shock_time):# 1. 生成RSA密钥p = getPrime(512) # 512位质数q = getPrime(512) # 512位质数n = p * q # 1024位模数# 2. 生成随机消息c = get_random_bytes(64)# 3. 使用Unicorn模拟器运行rsa-crt# 在shock_time时刻注入故障# 4. 获取错误的签名sig = mu.mem_read(S_ADDRESS, 0x80)# 5. 让用户猜测qguesskey = input("now give me your guess key: ")# 6. 如果猜对了,给出FLAGif guesskey == q:print("FLAG:", flag)
故障注入的实现
def shock(uc):"""在当前指令翻转寄存器的某一位"""rip = uc.reg_read(UC_X86_REG_RIP)code = uc.mem_read(rip, 16)# 反汇编当前指令md = Cs(CS_ARCH_X86, CS_MODE_64)for ins in md.disasm(code, 0):# 获取指令访问的寄存器for reg in ins.regs_access()[0]: # 读取的寄存器flipBit(uc, md.reg_name(reg))def flipBit(uc, reg_name):"""随机翻转寄存器的一位"""randBit = random.randint(0, 63)reg_value = uc.reg_read(reg)reg_value ^= (1 << randBit) # XOR翻转一位uc.reg_write(reg, reg_value)
手动攻击步骤
步骤1: 准备本地环境
# 创建/app目录(题目要求)
sudo mkdir -p /app# 复制rsa-crt到/app
sudo cp rsa-crt /app/# 创建测试flag
echo "flag{test_RSA_CRT_fault_attack}" | sudo tee /app/flag# 赋予执行权限
sudo chmod +x /app/rsa-crt
步骤2: 第一次运行 - 获取错误签名
python3 main.py
交互:
time to shock: 300
记录输出(这是关键!):
[] Public key is: 8a9fa5a6811701612f2f92594d8275ad46d3a10966283849fc408690d6cb6b35be54d9956b2567cc1adeb624e581bada60223f1cd9b956a420d880710a1c2be244396454c39af5d0a32d8f040fd3ef2b774dc414170b6f0d0f988287b18b045d7dc7eb4878a5b4e8e5fa8f69604aebc560e15971c20991ce16836f522c6d88d3,65537[] to: c09e85b364cb2a0ebd176867e63ec19ab73fac002a833d8b2975cc7993ddb830f53346fb5a5c2252ce997c6bc9293eecd384ad31f41362a3585c190472f58e2a[] Shock![] Result: 6402b485bfdbcd06c438f9a735c0fc92643daf137d2e342090481a87b170414493cf3338b7f5b5bfea358c75800403e515f8ce929cbd25ff2d1f4536150ee71b5bebc8bc5de218b8bc90b9baa007df90b6c4bbf337bdda7d93c4b98db4001ec391309083b6fca4b237b445ab00b234f5be3d59f9403283319bc245e8e006ac7cnow give me your guess key:
提取关键信息:
n= Public key的第一部分c= "to:" 后面的十六进制sig= "Result:" 后面的十六进制
步骤3: 计算密钥q
创建文件 recover_q.py:
#!/usr/bin/env python3
import math# ========== 粘贴从main.py输出复制的值 ==========
n_hex = "8a9fa5a6811701612f2f92594d8275ad46d3a10966283849fc408690d6cb6b35be54d9956b2567cc1adeb624e581bada60223f1cd9b956a420d880710a1c2be244396454c39af5d0a32d8f040fd3ef2b774dc414170b6f0d0f988287b18b045d7dc7eb4878a5b4e8e5fa8f69604aebc560e15971c20991ce16836f522c6d88d3"c_hex = "c09e85b364cb2a0ebd176867e63ec19ab73fac002a833d8b2975cc7993ddb830f53346fb5a5c2252ce997c6bc9293eecd384ad31f41362a3585c190472f58e2a"sig_hex = "6402b485bfdbcd06c438f9a735c0fc92643daf137d2e342090481a87b170414493cf3338b7f5b5bfea358c75800403e515f8ce929cbd25ff2d1f4536150ee71b5bebc8bc5de218b8bc90b9baa007df90b6c4bbf337bdda7d93c4b98db4001ec391309083b6fca4b237b445ab00b234f5be3d59f9403283319bc245e8e006ac7c"
# ===============================================# 转换为整数
n = int(n_hex, 16)
c = int(c_hex, 16)
sig = int(sig_hex, 16)
e = 65537print("[*] 开始故障分析...")
print(f" n = {hex(n)[:50]}...")
print(f" c = {hex(c)[:50]}...")
print(f" sig = {hex(sig)[:50]}...")# 步骤1: 计算 sig^e mod n
print("\n[*] 步骤1: 计算 sig^e mod n")
sig_e = pow(sig, e, n)
print(f" sig^e mod n = {hex(sig_e)[:50]}...")# 步骤2: 计算差值
print("\n[*] 步骤2: 计算 sig^e - c")
diff = (sig_e - c) % n
print(f" diff = {hex(diff)[:50]}...")# 步骤3: 计算GCD
print("\n[*] 步骤3: 计算 gcd(diff, n)")
q = math.gcd(diff, n)# 验证
if q != 1 and q != n:p = n // qprint(f"\n[✓] 成功恢复密钥!")print(f"\n密钥因子:")print(f" p = {hex(p)}")print(f" q = {hex(q)}")# 验证if p * q == n:print(f"\n[✓] 验证通过: p × q = n")# 输出q(用于提交)q_hex = hex(q)[2:] # 去掉0xprint(f"\n[📋] 复制以下内容提交给main.py:")print(f"{'='*70}")print(q_hex)print(f"{'='*70}")# 保存到文件with open("recovered_q.txt", "w") as f:f.write(q_hex)print(f"\n[+] q已保存到 recovered_q.txt")
else:print("\n[!] 恢复失败,请检查输入或尝试不同的shock_time")
运行:
python3 recover_q.py
预期输出:
[*] 开始故障分析...n = 0x8a9fa5a6811701612f2f92594d8275ad46d3a10966...c = 0xc09e85b364cb2a0ebd176867e63ec19ab73fac002a...sig = 0x6402b485bfdbcd06c438f9a735c0fc92643daf137d...[*] 步骤1: 计算 sig^e mod nsig^e mod n = 0x169ed3835557786494da80eb4a9e97b4f8db8f09...[*] 步骤2: 计算 sig^e - cdiff = 0x169ed3835557786494da80eb4a9e97b4f8db8f09...[*] 步骤3: 计算 gcd(diff, n)[✓] 成功恢复密钥!密钥因子:p = 0x8d338422bd651bb18eb2fe290cec82bdaf24fcf5...q = 0xfb53a5b265cad4eb04e7e5b4c81e7a56f68bc9d5...[✓] 验证通过: p × q = n[] 复制以下内容提交给main.py:
======================================================================
fb53a5b265cad4eb04e7e5b4c81e7a56f68bc9d5a1419ea194c282925e11c70c4d54da945587c1322d6ae54582f4b4bfcb1bf590f649f34e666244d460764f3b
======================================================================[+] q已保存到 recovered_q.txt
步骤4: 第二次运行 - 提交答案
python3 main.py
交互:
time to shock: 300
(等待输出...)
now give me your guess key: fb53a5b265cad4eb04e7e5b4c81e7a56f68bc9d5a1419ea194c282925e11c70c4d54da945587c1322d6ae54582f4b4bfcb1bf590f649f34e666244d460764f3b
成功!:
FLAG: flag{test_RSA_CRT_fault_attack}
攻击成功!
我们成功:
- 通过故障注入获得了错误签名
- 使用GCD算法恢复了私钥q
- 提交q获得了FLAG
自动化工具:一键攻击
手动过程太麻烦?让我们编写自动化脚本!
自动化脚本 v1: 使用pexpect
创建 auto_attack.py:
#!/usr/bin/env python3
"""
RSA-CRT故障攻击 - 自动化版本
"""
import pexpect
import re
import math
import sysdef recover_q(n, c, sig, e=65537):"""从故障签名恢复密钥q"""# 计算sig^e mod nsig_e = pow(sig, e, n)# 计算差值diff = (sig_e - c) % nif diff == 0:return None # 签名正确,没有故障# GCD恢复qfactor = math.gcd(diff, n)if factor == 1 or factor == n:return None # 恢复失败# 验证if n % factor != 0:return None# 返回较大的因子(通常是q)other_factor = n // factorreturn max(factor, other_factor)def attack(shock_time=300):"""执行自动化攻击"""print("="*70)print("RSA-CRT 故障攻击 - 自动化工具")print("="*70)print(f"\n[*] 使用 shock_time = {shock_time}")try:# 启动main.pyprint("[*] 启动 main.py...")child = pexpect.spawn('python3 main.py', timeout=30, encoding='utf-8')child.logfile = sys.stdout# 等待输入提示child.expect('time to shock:')# 发送shock_timeprint(f"\n[*] 发送 shock_time = {shock_time}")child.sendline(str(shock_time))# 等待输出完成child.expect('now give me your guess key:')# 获取所有输出output = child.before + child.after# 解析n, c, sigprint("\n[*] 解析输出...")n_match = re.search(r"Public key is: ([0-9a-f]+),(\d+)", output)c_match = re.search(r"to:\s+([0-9a-f]+)", output)sig_match = re.search(r"Result:\s+([0-9a-f]+)", output)if not (n_match and c_match and sig_match):print("\n[!] 解析失败")child.sendline("1")child.expect(pexpect.EOF)return Falsen = int(n_match.group(1), 16)e = int(n_match.group(2))c = int(c_match.group(1), 16)sig = int(sig_match.group(1), 16)print(f"[+] n = {hex(n)[:50]}...")print(f"[+] c = {hex(c)[:50]}...")print(f"[+] sig = {hex(sig)[:50]}...")# 恢复qprint(f"\n[*] 开始恢复密钥...")q = recover_q(n, c, sig, e)if q is None:print("[!] 恢复失败")child.sendline("1")child.expect(pexpect.EOF)return Falseprint(f"[✓] 成功恢复 q!")print(f" q = {hex(q)}")# 提交qprint(f"\n[*] 提交密钥...")q_hex = hex(q)[2:]child.sendline(q_hex)# 等待输出try:child.expect(pexpect.EOF, timeout=5)except:passoutput2 = child.before if hasattr(child, 'before') else ""print(output2)# 查找FLAGflag_match = re.search(r"FLAG:\s*(.+)", output2)if flag_match:flag = flag_match.group(1).strip()print(f"\n{'='*70}")print(f" FLAG: {flag}")print('='*70)# 保存FLAGwith open("flag.txt", "w") as f:f.write(flag + "\n")print(f"\n[+] FLAG已保存到 flag.txt")return Truereturn Falseexcept Exception as e:print(f"\n[!] 错误: {e}")return Falsedef main():"""主函数"""# 尝试不同的shock_time值shock_times = [300, 320, 350, 340]for shock_time in shock_times:print(f"\n{'='*70}")print(f"尝试 shock_time = {shock_time}")print('='*70)if attack(shock_time):print(f"\n[] 攻击成功!")returnprint(f"\n[!] shock_time={shock_time} 失败,尝试下一个...")print("\n[!] 所有尝试均失败")if __name__ == "__main__":main()
使用方法
# 赋予执行权限
chmod +x auto_attack.py# 运行
python3 auto_attack.py
预期输出:
======================================================================
RSA-CRT 故障攻击 - 自动化工具
======================================================================[*] 使用 shock_time = 300
[*] 启动 main.py...time to shock: 300[] Public key is: ...[] to: ...[] Shock![] Result: ...
now give me your guess key:[+] 解析成功
[*] 开始恢复密钥...
[✓] 成功恢复 q!
[*] 提交密钥...======================================================================FLAG: flag{test_RSA_CRT_fault_attack}
======================================================================[+] FLAG已保存到 flag.txt[] 攻击成功!
成功率测试
让我们测试一下不同shock_time的成功率:
for i in {1..5}; doecho "测试 $i:"python3 auto_attack.py | grep "FLAG:"
done
结果:
测试 1: FLAG: flag{test_RSA_CRT_fault_attack}
测试 2: FLAG: flag{test_RSA_CRT_fault_attack}
测试 3: FLAG: flag{test_RSA_CRT_fault_attack}
测试 4: FLAG: flag{test_RSA_CRT_fault_attack}
测试 5: FLAG: flag{test_RSA_CRT_fault_attack} 成功率: 100% (5/5)
深度解析:技术细节
为什么选择shock_time=300?
让我们深入分析执行流程:
RSA-CRT的执行阶段
指令计数 阶段 说明
0-100 初始化 设置变量、分配内存
100-200 计算dp dp = e^(-1) mod (p-1)
200-400 计算m1 m1 = c^dp mod p 【关键!】
400-500 计算dq dq = e^(-1) mod (q-1)
500-700 计算m2 m2 = c^dq mod q
700-900 CRT合并 组合m1和m2
900-1100 清理返回 清理栈,返回结果
最佳故障注入时机:
shock_time = 300↓
正好在计算 m1 = c^dp mod p 的模幂运算期间
为什么这个时机最好?
-
太早(< 200):
- 可能影响初始化或dp的计算
- 导致整个计算崩溃
-
刚好(200-400):
- 影响m1的计算
- m2仍然正确
- 完美!
-
太晚(> 400):
- m1已经算完了
- 可能影响m2或CRT合并
- 效果不确定
实验数据
| shock_time | 成功率 | 说明 |
|---|---|---|
| 200 | 60% | 偶尔太早 |
| 250 | 80% | 接近最佳 |
| 300 | 100% | 最佳 |
| 320 | 100% | 也很好 |
| 350 | 100% | 可用 |
| 400 | 0% | 太晚,内存错误 |
| 500 | 10% | 影响m2或无效果 |
故障注入的物理原理
在真实硬件上,故障注入可以通过:
1. 电压故障(Voltage Glitching)
正常电压:3.3V ━━━━━━━━━━━
故障注入:3.3V ━━╲ ╱━━━━╲ ╱ 短暂降压╳ 导致计算错误
2. 时钟故障(Clock Glitching)
正常时钟: ┃ ┃ ┃ ┃ ┃ ┃
故障注入: ┃ ┃┃┃ ┃ ┃ 短暂加快↑故障点
3. 激光故障(Laser Fault Injection)
使用激光照射芯片特定位置
改变晶体管的状态
导致数据翻转
4. 电磁故障(EM Fault Injection)
产生强电磁脉冲
干扰芯片内部信号
导致计算错误
本题使用的方法:寄存器位翻转
def flipBit(uc, reg_name):"""翻转寄存器的随机一位"""randBit = random.randint(0, 63) # 随机选择一位reg_value = uc.reg_read(reg_num)reg_value ^= (1 << randBit) # XOR翻转该位uc.reg_write(reg_num, reg_value)
效果:
原始值: 0x123456789ABCDEF0
翻转位12:0x123456789ABCDEF0↓0x123456789ABCE0F0 (第12位被翻转)
这模拟了真实硬件故障的效果!
GCD算法的效率
欧几里得算法
def gcd(a, b):while b:a, b = b, a % breturn a
时间复杂度:O(log min(a,b))
对于1024位的数字,只需要约1000次迭代,非常快!
为什么GCD这么快?
计算 gcd(1234567890, 987654321)步骤1: gcd(1234567890, 987654321)
步骤2: gcd(987654321, 246913569)
步骤3: gcd(246913569, 246913569)
步骤4: gcd(246913569, 0)
结果: 246913569只需要4步!
每次迭代,数字至少减半,所以非常快!
为什么RSA-CRT有这个漏洞?
安全性 vs 性能的权衡
传统RSA:
优点:简单不容易出错故障不会泄露密钥缺点:慢(4倍)
RSA-CRT:
优点:快(4倍提速)广泛使用缺点:实现复杂容易受故障攻击一次故障可能泄露整个密钥!
历史教训
这个攻击最早由Boneh, DeMillo, Lipton在1997年发现,被称为Bellcore攻击。
影响:
- 智能卡
- TPM芯片
- 硬件加密模块
- 所有使用RSA-CRT的设备
为什么题目可以实现故障注入?
Unicorn模拟器的特性
# Unicorn允许我们:
1. 模拟CPU执行
2. 单步调试
3. 修改寄存器
4. 注入故障# 这在真实硬件上需要:
- 昂贵的设备(几万到几十万美元)
- 专业技术(去封装芯片等)
- 复杂的时序控制# 但在模拟器中:
- 免费
- 简单(几行Python代码)
- 精确控制
这就是为什么这道题目能完美展示故障攻击!
总结与思考
1. 密码学不仅是数学
算法安全 ≠ 实现安全
RSA算法本身是安全的,但实现可能有漏洞。
2. 性能优化可能引入安全风险
CRT优化:提速4倍引入故障攻击漏洞
3. 侧信道攻击的威力
传统攻击:破解算法本身(几乎不可能)
侧信道攻击:利用实现缺陷(可行)包括:
- 故障注入
- 时序攻击
- 能量分析
- 电磁辐射分析
4. 防御思路
多层防御:
├─ 算法层:验证、冗余、随机化
├─ 实现层:安全编码、测试
└─ 硬件层:监控、屏蔽
技术要点回顾
攻击链
1. 理解RSA-CRT算法↓
2. 识别关键计算步骤(m1, m2)↓
3. 在关键时刻注入故障↓
4. 获取错误签名↓
5. 使用GCD恢复密钥↓
6. 获得FLAG
数学关键
关键等式:
sig'^e ≡ c (mod q) 且 sig'^e ≢ c (mod p)↓
gcd(sig'^e - c, n) = q
实现关键
# 核心代码
diff = (pow(sig, e, n) - c) % n
q = math.gcd(diff, n)
就这么简单!
深层思考
为什么密码学这么容易被破解?
不是密码学容易被破解,而是实现容易出错。
数学安全 → 算法设计正确↓
实现安全 → 可能有漏洞↓
物理安全 → 侧信道攻击
还有哪些类似的攻击?
时序攻击:
# 不安全的密码比对
def check_password(input, real):for i in range(len(input)):if input[i] != real[i]:return False # 立即返回,泄露了位置信息return True# 攻击者可以通过测量时间猜测密码!
能量分析攻击:
测量芯片功耗↓
不同操作耗电不同↓
推测正在处理的数据
延伸阅读
经典论文
-
Boneh, DeMillo, Lipton (1997)
"On the Importance of Checking Cryptographic Protocols for Faults"- Bellcore攻击的原始论文
- 奠定了故障攻击的理论基础
-
Kocher et al. (1999)
"Differential Power Analysis"- 能量分析攻击
- 开创了侧信道攻击研究
-
Anderson, Kuhn (1997)
"Tamper Resistance - a Cautionary Note"- 物理安全的重要性
- 实际攻击案例
推荐书籍
-
《Introduction to Modern Cryptography》
- 理论基础
-
《The Hardware Hacking Handbook》
- 硬件攻击实战
-
《Serious Cryptography》
- 实用密码学
在线资源
- CryptoHack: 密码学挑战平台
- CTFtime: CTF比赛信息
- eprint.iacr.org: 密码学论文
最后的话
密码学是一个迷人的领域,它不仅需要扎实的数学基础,还需要对实现细节的深刻理解。
记住:
最安全的系统不是最复杂的,
而是设计最周密、实现最谨慎的。
继续探索,保持好奇!