HITBCTF2017:HackInTheCardI—侧信道功耗攻击完全解析

admin 2026-03-04 10:39:05 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 该文档解析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)):
&nbsp; &nbsp;&nbsp;if&nbsp;v ==&nbsp;'h'&nbsp;and&nbsp;data[i] < limit: &nbsp; &nbsp; &nbsp;# 高->低 转变
&nbsp; &nbsp; &nbsp; &nbsp; output.append((1, i - _min +&nbsp;1))
&nbsp; &nbsp; &nbsp; &nbsp; _min = i
&nbsp; &nbsp; &nbsp; &nbsp; v =&nbsp;'l'
&nbsp; &nbsp;&nbsp;elif&nbsp;v ==&nbsp;'l'&nbsp;and&nbsp;data[i] >= limit: &nbsp;&nbsp;# 低->高 转变
&nbsp; &nbsp; &nbsp; &nbsp; output.append((0, i - _min +&nbsp;1))
&nbsp; &nbsp; &nbsp; &nbsp; _min = i
&nbsp; &nbsp; &nbsp; &nbsp; v =&nbsp;'h'

# 处理最后一段
if&nbsp;v ==&nbsp;'h':
&nbsp; &nbsp; output.append((1, i - _min +&nbsp;1))
else:
&nbsp; &nbsp; output.append((0, i - _min +&nbsp;1))

其中 (1, n) 表示高电压段持续 n 个采样点,(0, n) 表示低电压段持续 n 个采样点。

切分结果统计

运行后得到总共 4103 个电压段,统计各段的持续长度:

低电压段(level=0)持续长度分布:
&nbsp; 长度 &nbsp;2:2 次 &nbsp; (异常短段,位于波形开头)
&nbsp; 长度 25:1 次 &nbsp; (异常段)
&nbsp; 长度 31:1 次 &nbsp; (异常段)
&nbsp; 长度 50:1 次 &nbsp; (异常段)
&nbsp; 长度 51:2047 次(主要模式)

高电压段(level=1)持续长度分布:
&nbsp; 长度 29:1 次 &nbsp; (异常段,位于波形开头)
&nbsp; 长度 50:1 次 &nbsp; (异常段)
&nbsp; 长度 51:1011 次(短脉冲)
&nbsp; 长度 68:1 次 &nbsp; (异常段)
&nbsp; 长度 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)&nbsp;for&nbsp;l, dur&nbsp;in&nbsp;output&nbsp;if&nbsp;l ==&nbsp;1&nbsp;and&nbsp;(dur ==&nbsp;51&nbsp;or&nbsp;dur ==&nbsp;101)]
# 结果:2048 个有效段

第五步:从波形中提取私钥 d

构建密钥比特序列

对每个有效高电压段,按其出现顺序(即 RSA 算法处理密钥的顺序)生成比特:

d_bits_in_trace_order =&nbsp;''
for&nbsp;lev, dur&nbsp;in&nbsp;high_segs:
&nbsp; &nbsp;&nbsp;if&nbsp;dur ==&nbsp;101:
&nbsp; &nbsp; &nbsp; &nbsp; d_bits_in_trace_order +=&nbsp;'1'&nbsp; &nbsp;&nbsp;# 长脉冲 = 密钥比特 1
&nbsp; &nbsp;&nbsp;else:
&nbsp; &nbsp; &nbsp; &nbsp; d_bits_in_trace_order +=&nbsp;'0'&nbsp; &nbsp;&nbsp;# 短脉冲 = 密钥比特 0

关键细节:比特序与翻转

波形中最先出现的是密钥 d 的最低有效位(LSB),最后出现的是最高有效位(MSB)

这是因为本题智能卡使用的是 LSB-first 的平方-乘法算法,处理顺序从 d 的第 0 位(最低位)到第 2047 位(最高位)。

因此,需要将提取出的比特序列逆序,才能得到以 MSB 优先表示的私钥整数:

d_bits_msb_first = d_bits_in_trace_order[::-1] &nbsp;&nbsp;# 逆序
d = int(d_bits_msb_first,&nbsp;2) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;# 转换为整数

验证私钥正确性

利用 RSA 的数学性质验证:对任意消息 m,应有 (m^e mod n)^d mod n == m

# 用简单测试值 42 验证
test =&nbsp;42
assert&nbsp;pow(pow(test, e, n), d, n) == test &nbsp;&nbsp;# 验证通过

验证通过,私钥 d 提取正确。

提取到的私钥 d(2048 位):

d = 16390828876181053318339100750675858805085075693347294634845140223155300471343339735725090534836292007632963907771855159582051536368599877409046147073794159158745099750588781353203665310135991045663088765613847315146223892540146656146285947287434457737619192355597891759277954557284874079842576095414413634632218060041204741585014292743457235750761832960116264601238658366251628950826877369616205949504632422319118366206008416080422671942922507275909660907589263322598929672262664264325308446859589845055502830104300952306424775575141740987190395212323715203016486280360859837271948470735247258851655355384807804629939

第六步:解密密文,获取 Flag

有了私钥 d、模数 n,以及题目给出的密文,直接执行 RSA 解密:

from&nbsp;Crypto.Util.number&nbsp;import&nbsp;long_to_bytes

ciphertext_hex = (
&nbsp; &nbsp;&nbsp;"014b05e1a09668c83e13fda8be28d148568a2342aed833e0ad646bd45461da2de"
&nbsp; &nbsp;&nbsp;"cf9d538c2d3ab245b272873beb112586bb7b17dc4b30f0c5408d8b03cfbc8388b"
&nbsp; &nbsp;&nbsp;"2bd579fb419a1cac38798da1c3da75dc9a74a90d98c8f986fd8ab8b2dc539768b"
&nbsp; &nbsp;&nbsp;"eb339cadc13383c62b5223a50e050cb9c6b759072962c2b2cf21b4421ca73394d"
&nbsp; &nbsp;&nbsp;"9e12cfbc958fc5f6b596da368923121e55a3c6a7b12fdca127ecc0e8470463f6e"
&nbsp; &nbsp;&nbsp;"04f27cd4bb3de30555b6c701f524c8c032fa51d719901e7c75cc72764ac00976a"
&nbsp; &nbsp;&nbsp;"c6427a1f483779f61cee455ed319ee9071abefae4473e7c637760b4b3131f25e5"
&nbsp; &nbsp;&nbsp;"eb9950dd9d37666e129640c82a4b01b8bdc1a78b007f8ec71e7bad48046"
)

c = int(ciphertext_hex,&nbsp;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&nbsp;re
from&nbsp;Crypto.PublicKey&nbsp;import&nbsp;RSA
from&nbsp;Crypto.Util.number&nbsp;import&nbsp;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()}&nbsp;bits):&nbsp;{n}")
print(f"e ({e.bit_length()}&nbsp;bits):&nbsp;{e}")

# --- 第二步:提取电压数据 ---
with&nbsp;open("index.html",&nbsp;"r")&nbsp;as&nbsp;f:
&nbsp; &nbsp; content = f.read()

match = re.search(r'var data =(\[.*?\]);', content, re.DOTALL)
data = eval(match.group(1))
print(f"\n电压采样点总数:&nbsp;{len(data)}")

# --- 第三步:按阈值 225 mV 切分波形段 ---
output = []
_min =&nbsp;0
limit =&nbsp;225

v =&nbsp;'h'&nbsp;if&nbsp;data[0] >= limit&nbsp;else&nbsp;'l'

for&nbsp;i&nbsp;in&nbsp;range(len(data)):
&nbsp; &nbsp;&nbsp;if&nbsp;v ==&nbsp;'h'&nbsp;and&nbsp;data[i] < limit:
&nbsp; &nbsp; &nbsp; &nbsp; output.append((1, i - _min +&nbsp;1))
&nbsp; &nbsp; &nbsp; &nbsp; _min = i
&nbsp; &nbsp; &nbsp; &nbsp; v =&nbsp;'l'
&nbsp; &nbsp;&nbsp;elif&nbsp;v ==&nbsp;'l'&nbsp;and&nbsp;data[i] >= limit:
&nbsp; &nbsp; &nbsp; &nbsp; output.append((0, i - _min +&nbsp;1))
&nbsp; &nbsp; &nbsp; &nbsp; _min = i
&nbsp; &nbsp; &nbsp; &nbsp; v =&nbsp;'h'

if&nbsp;v ==&nbsp;'h':
&nbsp; &nbsp; output.append((1, i - _min +&nbsp;1))
else:
&nbsp; &nbsp; output.append((0, i - _min +&nbsp;1))

print(f"波形段总数:&nbsp;{len(output)}")

# --- 第四步:筛选有效的 RSA 运算脉冲 ---
# 有效高电压段:长度为 51(0 比特)或 101(1 比特)
high_segs = [(l, dur)&nbsp;for&nbsp;l, dur&nbsp;in&nbsp;output&nbsp;if&nbsp;l ==&nbsp;1&nbsp;and&nbsp;(dur ==&nbsp;51&nbsp;or&nbsp;dur ==&nbsp;101)]
print(f"有效 RSA 运算脉冲数:&nbsp;{len(high_segs)}")
print(f" &nbsp;短脉冲 (d比特=0):&nbsp;{sum(1&nbsp;for&nbsp;_,d&nbsp;in&nbsp;high_segs&nbsp;if&nbsp;d==51)}&nbsp;个")
print(f" &nbsp;长脉冲 (d比特=1):&nbsp;{sum(1&nbsp;for&nbsp;_,d&nbsp;in&nbsp;high_segs&nbsp;if&nbsp;d==101)}&nbsp;个")

# --- 第五步:提取私钥比特,逆序还原 d ---
# 波形顺序为 LSB-first,需逆序得到 MSB-first 的整数
d_bits_lsb_first =&nbsp;''.join('1'&nbsp;if&nbsp;dur ==&nbsp;101&nbsp;else&nbsp;'0'&nbsp;for&nbsp;_, dur&nbsp;in&nbsp;high_segs)
d_bits_msb_first = d_bits_lsb_first[::-1]
d = int(d_bits_msb_first,&nbsp;2)
print(f"\n私钥 d ({d.bit_length()}&nbsp;bits):&nbsp;{d}")

# 验证:RSA 性质检验
assert&nbsp;pow(pow(42, e, n), d, n) ==&nbsp;42,&nbsp;"私钥验证失败!"
print("私钥验证通过!")

# --- 第六步:解密密文 ---
ciphertext_hex = (
&nbsp; &nbsp;&nbsp;"014b05e1a09668c83e13fda8be28d148568a2342aed833e0ad646bd45461da2de"
&nbsp; &nbsp;&nbsp;"cf9d538c2d3ab245b272873beb112586bb7b17dc4b30f0c5408d8b03cfbc8388b"
&nbsp; &nbsp;&nbsp;"2bd579fb419a1cac38798da1c3da75dc9a74a90d98c8f986fd8ab8b2dc539768b"
&nbsp; &nbsp;&nbsp;"eb339cadc13383c62b5223a50e050cb9c6b759072962c2b2cf21b4421ca73394d"
&nbsp; &nbsp;&nbsp;"9e12cfbc958fc5f6b596da368923121e55a3c6a7b12fdca127ecc0e8470463f6e"
&nbsp; &nbsp;&nbsp;"04f27cd4bb3de30555b6c701f524c8c032fa51d719901e7c75cc72764ac00976a"
&nbsp; &nbsp;&nbsp;"c6427a1f483779f61cee455ed319ee9071abefae4473e7c637760b4b3131f25e5"
&nbsp; &nbsp;&nbsp;"eb9950dd9d37666e129640c82a4b01b8bdc1a78b007f8ec71e7bad48046"
)

c = int(ciphertext_hex,&nbsp;16)
m = pow(c, d, n)
flag = long_to_bytes(m)

print(f"\nFlag:&nbsp;{flag.decode('utf-8')}")

技术要点总结

为什么 SPA 能攻击 RSA

  1. 算法层面的信息泄露:标准的平方-乘法算法在处理 d 的 0 比特和 1 比特时,执行的操作次数不同(1 次 vs 2 次),这种差异直接反映在功耗波形上。
  2. 短脉冲(长度 51)= 密钥比特 0:芯片仅执行一次模平方,高功耗持续约 51 个采样周期后结束。
  3. 长脉冲(长度 101)= 密钥比特 1:芯片执行模平方后紧接着执行模乘法,两次运算的高功耗波形融合在一起,持续约 101 个采样周期(约 2 × 51)。
  4. 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 — 侧信道功耗攻击完全解析》

评论:0   参与:  0