文章总结: 该文档解析HITBCTF侧信道题目,利用RSA平方乘法算法功耗差异实施SPA攻击。原理是密钥比特0对应短脉冲,1对应长脉冲。通过分析电压数据,设定阈值切分波形,按时长映射比特还原私钥,最终解密Flag。文章提供完整理论与Python脚本,实战价值高。 综合评分: 95 文章分类: CTF,逆向分析,实战经验
前置知识:理解攻击场景
电路图说明
电路图展示了如下连接关系:
+5V --- [50 Ohm 电阻] --- 智能卡 VCC
|
示波器探头(测量电阻两端电压)
|
计算机(I/O 通信)
智能卡在工作时,其芯片内部的数字逻辑门频繁翻转,导致消耗的瞬态电流随计算内容的不同而变化。串联的 50 欧姆电阻将这种电流变化转化为可测量的电压变化,示波器则实时记录这一信号。
这正是**简单功耗分析(Simple Power Analysis,SPA)**攻击的经典实验装置。
什么是 SPA 攻击
SPA(Simple Power Analysis)是侧信道攻击(Side-Channel Attack)的一种。所谓侧信道,指的是密码设备在运行时泄露的与密钥相关的物理信息,例如:
- 功耗(电流/电压变化)
- 电磁辐射
- 执行时间
- 声音
SPA 的核心思想是:直接从单次功耗波形中读出密钥的比特信息,而不需要统计大量样本。这与需要大量样本取平均的差分功耗分析(DPA)不同。
SPA 对 RSA 有效,根本原因在于 RSA 解密使用了”平方-乘法”(Square-and-Multiply)算法,而该算法对密钥 d 的 0 比特和 1 比特执行的操作数量不同,直接导致功耗波形的差异。
RSA 基础知识
RSA 密钥关系
RSA 加密体系由三个核心参数组成:
n:模数,两个大素数 p 和 q 的乘积e:公钥指数,对外公开d:私钥指数,必须保密
它们满足如下关系:
e * d ≡ 1 (mod lambda(n))
其中 lambda(n) = lcm(p-1, q-1)。
RSA 加解密
加密:c = m^e mod n
解密:m = c^d mod n
解密操作的安全性完全依赖于私钥 d 的保密性。如果攻击者能够恢复 d,就能解密任何密文。
平方-乘法算法(Square-and-Multiply)
由于 d 通常是一个几千比特的大整数,直接计算 c^d mod n 需要把 c 自乘 d 次,这在计算上完全不可行。实际实现中使用快速幂算法(Binary Exponentiation),也称平方-乘法算法。
从最低位到最高位(LSB-first)的版本如下:
Result = 1
Base = c
for i from 0 to len(d)-1: # 从 d 的最低有效位到最高有效位
if bit_i(d) == 1:
Result = Result * Base mod n # 乘法:仅当该比特为 1 时执行
Base = Base^2 mod n # 平方:每一步都执行
关键点:
- 平方操作:每个比特都会执行,无论该比特是 0 还是 1
- 乘法操作:只有当该比特为 1 时才执行
这意味着:
- 处理
d的 0 比特:只做 1 次平方 - 处理
d的 1 比特:做 1 次平方 + 1 次乘法
两种情况的计算量不同,对应的功耗波形也不同。SPA 攻击正是利用这一差异来逐位恢复密钥 d。
第一步:读取公钥
首先解析 publickey.pem 文件,获取 RSA 公钥中的模数 n 和公钥指数 e:
from Crypto.PublicKey import RSA
f = open("publickey.pem").read()
rsa_key = RSA.import_key(f)
n = rsa_key.n
e = rsa_key.e
得到结果:
n = 47830991326662824495508745118568461312057067619428659356734566601099157328561772266786369732207157614012208882509135468382927222492226812161023306187476368556666259072593987275776240126026405046740493511011769813311265911446182284249358661325491593325128567903122300947825374952396598082333020873697072902730284981746738140514942865793051404492036843072310907030493213000256408177304906033076004715917828055018951199826324370737967524900853959347077446211016971403718483175032252836768235567275508492706437525428812021725384559939724866313936809864983670005723367448319464418489922501808521834700244861150919309179587
e = 43457214399853106709365518016104583245597682563258997442852833557875824462848972074458890178842612202837273300092599906320145501787876067274540439747471714632459629837789201065247110607915518396101921915152021497825731566161955942985363115369201280506947037645379672658506866933540812391567835004807653166726519027887641890536890078107300074210640682400667202092348666903506251859460633558343547831022251337805499704499516967419532137037318140567400958376222119904648572117307678724771761426815722612585741949304251832071591103792001813116224387497530356953058973407238164403155834703898398361662735994028269881647963
n 的比特长度:2049
e 的比特长度:2049
这是一个 2048 位的 RSA 密钥(n 略超 2048 位是因为最高位进位)。注意:e 也很大(约 2049 位),这表明智能卡使用了一个非标准的大公钥指数,这在嵌入式实现中并不罕见。
第二步:理解电压数据
打开 index.html,其内部嵌入了一个名为 data 的 JavaScript 数组,包含 256,850 个电压采样值(单位:mV):
var data = [207.594..., 158.463..., 202.069..., ...];
数据特征:
- 总采样点数:256,850
- 电压范围:约 97 mV ~ 386 mV
- 采样点密集,每个 RSA 运算步骤对应数十个采样点
用 Python 提取这个数组:
import re
with open("index.html", "r") as f:
content = f.read()
match = re.search(r'var data =(\[.*?\]);', content, re.DOTALL)
data = eval(match.group(1))
print("采样点总数:", len(data)) # 256850
print("电压范围:", min(data), "~", max(data)) # 96.98 ~ 386.25
第三步:分析功耗波形结构
选取阈值
观察电压数据,其分布呈现两个区间:
- 低电压区(约 97~220 mV):芯片处于空闲状态,消耗电流小
- 高电压区(约 220~386 mV):芯片正在执行模幂运算,消耗电流大
选取阈值 225 mV 作为高低状态的分界线,将连续数据切分为交替出现的”高电压段”和”低电压段”。
切分算法
output = [] # 存储 (电平, 持续长度) 的列表
_min = 0
limit = 225
if data[0] >= limit:
v = 'h'
else:
v = 'l'
for i in range(len(data)):
if v == 'h' and data[i] < limit: # 高->低 转变
output.append((1, i - _min + 1))
_min = i
v = 'l'
elif v == 'l' and data[i] >= limit: # 低->高 转变
output.append((0, i - _min + 1))
_min = i
v = 'h'
# 处理最后一段
if v == 'h':
output.append((1, i - _min + 1))
else:
output.append((0, i - _min + 1))
其中 (1, n) 表示高电压段持续 n 个采样点,(0, n) 表示低电压段持续 n 个采样点。
切分结果统计
运行后得到总共 4103 个电压段,统计各段的持续长度:
低电压段(level=0)持续长度分布:
长度 2:2 次 (异常短段,位于波形开头)
长度 25:1 次 (异常段)
长度 31:1 次 (异常段)
长度 50:1 次 (异常段)
长度 51:2047 次(主要模式)
高电压段(level=1)持续长度分布:
长度 29:1 次 (异常段,位于波形开头)
长度 50:1 次 (异常段)
长度 51:1011 次(短脉冲)
长度 68:1 次 (异常段)
长度 101:1037 次(长脉冲)
结论非常清晰:
| 段类型 | 持续长度 | 物理含义 | | — | — | — | | 低电压段 | 51(正常值) | 两次运算之间的间隔 | | 高电压段 | 51 | 单次运算(仅平方)= 密钥比特为 0 | | 高电压段 | 101 | 双次运算(平方+乘法)= 密钥比特为 1 |
高电压段共 1011 + 1037 = 2048 次,恰好等于 RSA 密钥 d 的比特位数,完美对应!
第四步:理解波形与密钥比特的对应关系
为什么高电压段长度 101 = 两倍于 51
在平方-乘法算法中:
- 处理密钥
d的 0 比特:执行 1 次模平方,芯片高功耗时间 = 1 个操作周期,对应高电压段长度 51 - 处理密钥
d的 1 比特:执行 1 次模平方 + 1 次模乘法,芯片连续高功耗,两次操作之间几乎没有间隔,波形”融合”为 1 个持续时间约为 2 倍的高电压段,对应长度 101(约 2 × 51)
这就是 SPA 攻击奏效的根本原因:密钥比特的值(0 或 1)直接决定了芯片在对应时刻执行的操作次数,从而留下不同持续时长的功耗特征。
波形中的初始异常段
波形开头的几个异常段(长度为 29、68 等非标准值)是智能卡上电初始化阶段产生的,不属于 RSA 模幂计算过程,需要跳过。筛选方法:只保留高电压段中长度为 51 或 101 的段:
high_segs = [(l, dur) for l, dur in output if l == 1 and (dur == 51 or dur == 101)]
# 结果:2048 个有效段
第五步:从波形中提取私钥 d
构建密钥比特序列
对每个有效高电压段,按其出现顺序(即 RSA 算法处理密钥的顺序)生成比特:
d_bits_in_trace_order = ''
for lev, dur in high_segs:
if dur == 101:
d_bits_in_trace_order += '1' # 长脉冲 = 密钥比特 1
else:
d_bits_in_trace_order += '0' # 短脉冲 = 密钥比特 0
关键细节:比特序与翻转
波形中最先出现的是密钥 d 的最低有效位(LSB),最后出现的是最高有效位(MSB)。
这是因为本题智能卡使用的是 LSB-first 的平方-乘法算法,处理顺序从 d 的第 0 位(最低位)到第 2047 位(最高位)。
因此,需要将提取出的比特序列逆序,才能得到以 MSB 优先表示的私钥整数:
d_bits_msb_first = d_bits_in_trace_order[::-1] # 逆序
d = int(d_bits_msb_first, 2) # 转换为整数
验证私钥正确性
利用 RSA 的数学性质验证:对任意消息 m,应有 (m^e mod n)^d mod n == m。
# 用简单测试值 42 验证
test = 42
assert pow(pow(test, e, n), d, n) == test # 验证通过
验证通过,私钥 d 提取正确。
提取到的私钥 d(2048 位):
d = 16390828876181053318339100750675858805085075693347294634845140223155300471343339735725090534836292007632963907771855159582051536368599877409046147073794159158745099750588781353203665310135991045663088765613847315146223892540146656146285947287434457737619192355597891759277954557284874079842576095414413634632218060041204741585014292743457235750761832960116264601238658366251628950826877369616205949504632422319118366206008416080422671942922507275909660907589263322598929672262664264325308446859589845055502830104300952306424775575141740987190395212323715203016486280360859837271948470735247258851655355384807804629939
第六步:解密密文,获取 Flag
有了私钥 d、模数 n,以及题目给出的密文,直接执行 RSA 解密:
from Crypto.Util.number import long_to_bytes
ciphertext_hex = (
"014b05e1a09668c83e13fda8be28d148568a2342aed833e0ad646bd45461da2de"
"cf9d538c2d3ab245b272873beb112586bb7b17dc4b30f0c5408d8b03cfbc8388b"
"2bd579fb419a1cac38798da1c3da75dc9a74a90d98c8f986fd8ab8b2dc539768b"
"eb339cadc13383c62b5223a50e050cb9c6b759072962c2b2cf21b4421ca73394d"
"9e12cfbc958fc5f6b596da368923121e55a3c6a7b12fdca127ecc0e8470463f6e"
"04f27cd4bb3de30555b6c701f524c8c032fa51d719901e7c75cc72764ac00976a"
"c6427a1f483779f61cee455ed319ee9071abefae4473e7c637760b4b3131f25e5"
"eb9950dd9d37666e129640c82a4b01b8bdc1a78b007f8ec71e7bad48046"
)
c = int(ciphertext_hex, 16)
m = pow(c, d, n)
flag = long_to_bytes(m)
print(flag.decode('utf-8'))
输出:
HITB{My name is Alice, and this is my story, the end of my story}
完整 Python 解题脚本
import re
from Crypto.PublicKey import RSA
from Crypto.Util.number import long_to_bytes
# --- 第一步:读取公钥 ---
rsa_key = RSA.import_key(open("publickey.pem").read())
n = rsa_key.n
e = rsa_key.e
print(f"n ({n.bit_length()} bits): {n}")
print(f"e ({e.bit_length()} bits): {e}")
# --- 第二步:提取电压数据 ---
with open("index.html", "r") as f:
content = f.read()
match = re.search(r'var data =(\[.*?\]);', content, re.DOTALL)
data = eval(match.group(1))
print(f"\n电压采样点总数: {len(data)}")
# --- 第三步:按阈值 225 mV 切分波形段 ---
output = []
_min = 0
limit = 225
v = 'h' if data[0] >= limit else 'l'
for i in range(len(data)):
if v == 'h' and data[i] < limit:
output.append((1, i - _min + 1))
_min = i
v = 'l'
elif v == 'l' and data[i] >= limit:
output.append((0, i - _min + 1))
_min = i
v = 'h'
if v == 'h':
output.append((1, i - _min + 1))
else:
output.append((0, i - _min + 1))
print(f"波形段总数: {len(output)}")
# --- 第四步:筛选有效的 RSA 运算脉冲 ---
# 有效高电压段:长度为 51(0 比特)或 101(1 比特)
high_segs = [(l, dur) for l, dur in output if l == 1 and (dur == 51 or dur == 101)]
print(f"有效 RSA 运算脉冲数: {len(high_segs)}")
print(f" 短脉冲 (d比特=0): {sum(1 for _,d in high_segs if d==51)} 个")
print(f" 长脉冲 (d比特=1): {sum(1 for _,d in high_segs if d==101)} 个")
# --- 第五步:提取私钥比特,逆序还原 d ---
# 波形顺序为 LSB-first,需逆序得到 MSB-first 的整数
d_bits_lsb_first = ''.join('1' if dur == 101 else '0' for _, dur in high_segs)
d_bits_msb_first = d_bits_lsb_first[::-1]
d = int(d_bits_msb_first, 2)
print(f"\n私钥 d ({d.bit_length()} bits): {d}")
# 验证:RSA 性质检验
assert pow(pow(42, e, n), d, n) == 42, "私钥验证失败!"
print("私钥验证通过!")
# --- 第六步:解密密文 ---
ciphertext_hex = (
"014b05e1a09668c83e13fda8be28d148568a2342aed833e0ad646bd45461da2de"
"cf9d538c2d3ab245b272873beb112586bb7b17dc4b30f0c5408d8b03cfbc8388b"
"2bd579fb419a1cac38798da1c3da75dc9a74a90d98c8f986fd8ab8b2dc539768b"
"eb339cadc13383c62b5223a50e050cb9c6b759072962c2b2cf21b4421ca73394d"
"9e12cfbc958fc5f6b596da368923121e55a3c6a7b12fdca127ecc0e8470463f6e"
"04f27cd4bb3de30555b6c701f524c8c032fa51d719901e7c75cc72764ac00976a"
"c6427a1f483779f61cee455ed319ee9071abefae4473e7c637760b4b3131f25e5"
"eb9950dd9d37666e129640c82a4b01b8bdc1a78b007f8ec71e7bad48046"
)
c = int(ciphertext_hex, 16)
m = pow(c, d, n)
flag = long_to_bytes(m)
print(f"\nFlag: {flag.decode('utf-8')}")
技术要点总结
为什么 SPA 能攻击 RSA
- 算法层面的信息泄露:标准的平方-乘法算法在处理
d的0比特和1比特时,执行的操作次数不同(1 次 vs 2 次),这种差异直接反映在功耗波形上。 - 短脉冲(长度 51)= 密钥比特 0:芯片仅执行一次模平方,高功耗持续约 51 个采样周期后结束。
- 长脉冲(长度 101)= 密钥比特 1:芯片执行模平方后紧接着执行模乘法,两次运算的高功耗波形融合在一起,持续约 101 个采样周期(约 2 × 51)。
- 2048 次脉冲对应 2048 位密钥:统计得到 1011 个短脉冲和 1037 个长脉冲,总计 2048 个脉冲,与 RSA-2048 私钥长度完全吻合。
防御方法
了解 SPA 攻击的防御方法同样重要:
- 基于加法链的幂运算(Montgomery Ladder):无论密钥比特为 0 或 1,每步都执行相同的运算序列(一次平方 + 一次乘法),消除了功耗差异。
- 随机化密钥:在执行前对密钥
d进行随机化变换(如d' = d + k * phi(n)),使每次运算的波形不同。 - 消息盲化(Message Blinding):在计算前将密文
c乘以一个随机因子,使攻击者无法关联波形与输入。 - 硬件级防护:在芯片设计中引入功耗噪声注入、固定功耗运算单元等措施。
本题的密钥比特顺序问题
本题智能卡采用的是 LSB-first 的平方-乘法算法(从密钥的最低位开始处理)。波形中第一个脉冲对应密钥 d 的最低有效位(第 0 位),最后一个脉冲对应最高有效位(第 2047 位)。
因此在将脉冲序列转换为整数时,必须将比特串逆序,否则得到的数值是错误的(验证方法:用提取到的 d 计算 pow(pow(42, e, n), d, n),若结果等于 42 则正确,否则尝试逆序)。
Flag
HITB{My name is Alice, and this is my story, the end of my story}
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:破镜安全 破镜安全 破镜安全《HITB CTF 2017: Hack In The Card I — 侧信道功耗攻击完全解析》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。









评论