文章总结: 本文档为第四届熵密杯初始谜题题解。谜题一剖析了自实现SM2签名的逻辑缺陷,其验证函数在取模前校验参数,攻击者设r为n减1且s为1使取模后为零,触发错误倍点逻辑,成功构造万能签名。谜题二涉及AES加密但分析中断。核心建议是密码学开发务必在取模后执行参数校验以防范逻辑绕过。 综合评分: 89 文章分类: CTF,漏洞分析,安全开发,代码审计
第四届熵密杯——初始谜题
原创
Van1sh Van1sh
Van1sh
2026年7月29日 10:45 中国香港
在小说阅读器读本章
去阅读
第三次参加熵密杯了,在同事的助力下,这次侥幸拿下一等奖。美中不足的是最后一道题只差临门一脚,赛后检查发现是代码中有一些数据处理的小bug,可惜。 由于篇幅原因,这次题解会分成三篇文章,分别是初始谜题、工程师站路线和日志管理服务器路线。
初始谜题一
题目代码
import secrets
default_table = {
'n': 'FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFF7203DF6B21C6052B53BBF40939D54123',
'p': 'FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFF',
'g': '32c4ae2c1f1981195f9904466a39c9948fe30bbff2660be1715a4589334c74c7'
'bc3736a2f4f6779c59bdcee36b692153d0a9877cc62a474002df32e52139f0a0',
'a': 'FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFC',
'b': '28E9FA9E9D9F5E344D5A9E4BCF6509A7F39789F515AB8F92DDBCBD414D940E93',
}
class Crypt():
def __init__(self, private_key, public_key, mode=0):
self.private_key = private_key
if public_key.startswith("04"):
self.public_key = public_key[2:]
else:
self.public_key = public_key
self.para_len = len(default_table['n'])
self.ecc_a3 = (
int(default_table['a'], base=16) + 3) % int(default_table['p'], base=16)
assert mode in (0, 1), 'mode must be one of (0, 1)'
self.mode = mode
def _kg(self, k, Point): # kP运算
if k == 0:
return None #无穷远点
Point = '%s%s' % (Point, '1')
mask_str = '8'
for i in range(self.para_len - 1):
mask_str += '0'
mask = int(mask_str, 16)
Temp = Point
flag = False
for n in range(self.para_len * 4):
if (flag):
Temp = self._double_point(Temp)
if (k & mask) != 0:
if (flag):
Temp = self._add_point(Temp, Point)
else:
flag = True
Temp = Point
k = k << 1
return self._convert_jacb_to_nor(Temp)
def _double_point(self, Point): # 倍点
if Point is None:
return None
l = len(Point)
len_2 = 2 * self.para_len
if l < self.para_len * 2:
return None
else:
x1 = int(Point[0:self.para_len], 16)
y1 = int(Point[self.para_len:len_2], 16)
if l == len_2:
z1 = 1
else:
z1 = int(Point[len_2:], 16)
T6 = (z1 * z1) % int(default_table['p'], base=16)
T2 = (y1 * y1) % int(default_table['p'], base=16)
T3 = (x1 + T6) % int(default_table['p'], base=16)
T4 = (x1 - T6) % int(default_table['p'], base=16)
T1 = (T3 * T4) % int(default_table['p'], base=16)
T3 = (y1 * z1) % int(default_table['p'], base=16)
T4 = (T2 * 8) % int(default_table['p'], base=16)
T5 = (x1 * T4) % int(default_table['p'], base=16)
T1 = (T1 * 3) % int(default_table['p'], base=16)
T6 = (T6 * T6) % int(default_table['p'], base=16)
T6 = (self.ecc_a3 * T6) % int(default_table['p'], base=16)
T1 = (T1 + T6) % int(default_table['p'], base=16)
z3 = (T3 + T3) % int(default_table['p'], base=16)
T3 = (T1 * T1) % int(default_table['p'], base=16)
T2 = (T2 * T4) % int(default_table['p'], base=16)
x3 = (T3 - T5) % int(default_table['p'], base=16)
if (T5 % 2) == 1:
T4 = (T5 + ((T5 + int(default_table['p'], base=16)) >> 1) - T3) % int(
default_table['p'], base=16)
else:
T4 = (T5 + (T5 >> 1) - T3) % int(default_table['p'], base=16)
T1 = (T1 * T4) % int(default_table['p'], base=16)
y3 = (T1 - T2) % int(default_table['p'], base=16)
form = '%%0%dx' % self.para_len
form = form * 3
return form % (x3, y3, z3)
def _add_point(self, P1, P2): # 点加函数,P2点为仿射坐标即z=1,P1为Jacobian加重射影坐标
if P1 is None:
return P2
if P2 is None:
return P1
len_2 = 2 * self.para_len
l1 = len(P1)
l2 = len(P2)
if (l1 < len_2) or (l2 < len_2):
return None
else:
X1 = int(P1[0:self.para_len], 16)
Y1 = int(P1[self.para_len:len_2], 16)
if (l1 == len_2):
Z1 = 1
else:
Z1 = int(P1[len_2:], 16)
x2 = int(P2[0:self.para_len], 16)
y2 = int(P2[self.para_len:len_2], 16)
T1 = (Z1 * Z1) % int(default_table['p'], base=16)
T2 = (y2 * Z1) % int(default_table['p'], base=16)
T3 = (x2 * T1) % int(default_table['p'], base=16)
T1 = (T1 * T2) % int(default_table['p'], base=16)
T2 = (T3 - X1) % int(default_table['p'], base=16)
T3 = (T3 + X1) % int(default_table['p'], base=16)
T4 = (T2 * T2) % int(default_table['p'], base=16)
T1 = (T1 - Y1) % int(default_table['p'], base=16)
Z3 = (Z1 * T2) % int(default_table['p'], base=16)
T2 = (T2 * T4) % int(default_table['p'], base=16)
T3 = (T3 * T4) % int(default_table['p'], base=16)
T5 = (T1 * T1) % int(default_table['p'], base=16)
T4 = (X1 * T4) % int(default_table['p'], base=16)
X3 = (T5 - T3) % int(default_table['p'], base=16)
T2 = (Y1 * T2) % int(default_table['p'], base=16)
T3 = (T4 - X3) % int(default_table['p'], base=16)
T1 = (T1 * T3) % int(default_table['p'], base=16)
Y3 = (T1 - T2) % int(default_table['p'], base=16)
form = '%%0%dx' % self.para_len
form = form * 3
return form % (X3, Y3, Z3)
def _convert_jacb_to_nor(self, Point): # Jacobian加重射影坐标转换成仿射坐标
if Point is None:
return None
len_2 = 2 * self.para_len
x = int(Point[0:self.para_len], 16)
y = int(Point[self.para_len:len_2], 16)
z = int(Point[len_2:], 16)
z_inv = pow(
z, int(default_table['p'], base=16) - 2, int(default_table['p'], base=16))
z_invSquar = (z_inv * z_inv) % int(default_table['p'], base=16)
z_invQube = (z_invSquar * z_inv) % int(default_table['p'], base=16)
x_new = (x * z_invSquar) % int(default_table['p'], base=16)
y_new = (y * z_invQube) % int(default_table['p'], base=16)
z_new = (z * z_inv) % int(default_table['p'], base=16)
if z_new == 1:
form = '%%0%dx' % self.para_len
form = form * 2
return form % (x_new, y_new)
else:
return None
def verify(self, Sign, data):
if Sign is None or Sign == '':
return None
r = int(Sign[0:self.para_len], 16)
s = int(Sign[self.para_len:2*self.para_len], 16)
e = int(data.hex(), 16)
# 参数合法性
if not (1 <= r < int(default_table['n'], base=16) and 1 <= s < int(default_table['n'], base=16)):
return False
t = r + s
if t == 0:
return False
else:
t = t % int(default_table['n'], base=16)
if self.public_key is None or self.public_key == '':
return None
P1 = self._kg(s, default_table['g'])
if P1 is None:
return False
P2 = self._kg(t, self.public_key)
if P1 == P2:
P1 = '%s%s' % (P1, 1)
P1 = self._double_point(P1)
else:
P1 = '%s%s' % (P1, 1)
P1 = self._add_point(P1, P2)
P1 = self._convert_jacb_to_nor(P1)
x = int(P1[0:self.para_len], 16)
return r == ((e + x) % int(default_table['n'], base=16))
def sign(self, data):
k = secrets.randbelow(int(default_table['n'], 16) - 1) + 1
if not (1 <= k <= int(default_table['n'], base=16)- 1):
return None
E = data.hex()
e = int(E, 16)
if self.private_key is None or self.private_key == '':
return None
d = int(self.private_key, 16)
P1 = self._kg(k, default_table['g'])
x = int(P1[0:self.para_len], 16)
A = ((e + x) % int(default_table['n'], base=16))
if A == 0 or A + k == int(default_table['n'], base=16):
return None
d_1 = pow(
d+1, int(default_table['n'], base=16) - 2, int(default_table['n'], base=16))
B = (d_1*(k + A) - A) % int(default_table['n'], base=16)
if B == 0:
return None
else:
return '%064x%064x' % (A, B)
if __name__ == '__main__':
crypt = Crypt(
# 公钥格式为64字节的16进制字符串
public_key='',
# 私钥格式为32字节的16进制字符串
private_key=''
)
data = '919535c3ef53d3fa359196b5229c4bbb386ce209f5905d33fc7bcdff46ae27c2'
sign = crypt.sign(bytes.fromhex(data))
print(sign)
verify = crypt.verify(sign,bytes.fromhex(data))
print(verify)
题目要求
构造一组可以通过验签的摘要值以及对应的签名值,并提交答案。
题目考点
SM2签名自实现缺陷
解题思路
初始谜题一自实现了一个 SM2 的签名,并要求我们构造一组可以通过验签的摘要值以及对应的签名值,并提交答案。
由于题目并没有给出公钥,因此可以断定我们需要构造一组能够绕过Verify的消息签名,即对于任意公钥均能验签成功,因此我们关注 Verify 函数的实现。
这段代码虽然检查了 不能等于0,但是取余运算却放在了最后,因此我们可以构造
,使得 值先通过判断,然后再取余后为0。方便起见,我们可以设 。
于是根据验签公式,我们有
最后验签是要求
而正好,这里的消息哈希 e 我们也是能够直接控制的,于是我们设 即可
(当然也可以设,,此时摘要就是,答案不唯一)
参考答案
摘要值:r-G.x % n
cd3b51d2e0e67ee6a066fbb995c6366ae220d3ab2f5ff949e261ae800688cc5b
签名值:n-1||1
FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFF7203DF6B21C6052B53BBF40939D541220000000000000000000000000000000000000000000000000000000000000001
初始谜题二
题目代码
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
import struct
def encrypt_card_data(item) -> str:
result = f"{item.card},{item.pin},{item.id}"
plaintext = result.encode('utf-8')
key = get_key()
# item.index 数据序号
nonce = get_iv_byte(item.index)
tmp = encrypt(key,nonce,plaintext).hex()
return tmp
def get_iv_byte(index: int, prefix=b'', length=16) -> bytes:
if not (0 <= index < 2**32):
raise ValueError("Index 必须在 0 到 2^32-1 之间")
index_bytes = struct.pack('>I', index)
pad_len = length - len(prefix) - len(index_bytes)
if pad_len < 0:
prefix=b''
pad_len = length - len(index_bytes)
return prefix + (b'\x00' * pad_len) + index_bytes
def encrypt(key,nonce,plaintext) -> bytes:
algorithm = algorithms.SM4(key)
mode = modes.CTR(nonce)
cipher = Cipher(algorithm, mode)
encryptor = cipher.encryptor()
ciphertext = encryptor.update(plaintext) + encryptor.finalize()
return ciphertext
相关数据
序号 密文(十六进制)
1000 7d2a3dc99109ff913cc52877cf3578621eaf51d256b043550eac8c377c98357de1b0d616039e6f821850386d86d96057f195539c28b8ed31430f27b16ca55f
1001 1cae58cb5cb947500aa4963f7d93607eeeb4d717069f678b0053226887cd6754fdc24cd32bebb8395f0a72b86ee90c278c4323c288be33e5d9282884acf9fb
1002 eeb6d40e049f6f8003523e6f8fc33854f0ce46d229e3bf3d45083cb96efd0b25d4453c8e8db930ecc67c2ed7a8edff97a668a0c45d36b0c2b36257a2039117
序号 银行卡号 PIN码 用户ID
1000 6205310997298382439 984631 019f1942-4000-7650-a25c-b7ce812783a2
1001 6205310379680232603 118954 019f1942-4000-7b11-a3a2-a17392cc649e
1002
题目要求
提交序号为1003用户的银行卡号、PIN码
题目考点
SM4-CTR 模式 nonce 选取不正确导致密钥流复用
解题思路
题目利用SM4-CTR 模式对明文数据进行加密,但是注意到加密时nonce的取值来自item.index,而明文数据中的index是连续递增的。因此加密第一份明文的密钥流的第二组,就是加密第二份明文密钥流的第一组,如下图所示
于是想要恢复第三组明文,我们可以取第一组的明文和密文,异或得到密钥流;从第三组开始,即截取32字节后的部分,用于解密第三组密文,即可得到题目所需银行卡号与PIN码(用户ID我们无法恢复完全)
(当然也可以取第一组的明文和密文,异或得到密钥流;从第二组开始,即截取16字节后的部分,用于解密第三组密文)
解题代码
from pwn import xor
from Crypto.Util.number import *
a = b"6205310997298382439,984631,019f1942-4000-7650-a25c-b7ce812783a2"
b = b"6205310379680232603,118954,019f1942-4000-7b11-a3a2-a17392cc649e"
c = bytes.fromhex('7d2a3dc99109ff913cc52877cf3578621eaf51d256b043550eac8c377c98357de1b0d616039e6f821850386d86d96057f195539c28b8ed31430f27b16ca55f')
d = bytes.fromhex('eeb6d40e049f6f8003523e6f8fc33854f0ce46d229e3bf3d45083cb96efd0b25d4453c8e8db930ecc67c2ed7a8edff97a668a0c45d36b0c2b36257a2039117')
print(xor(xor(a,c)[32:],d))
初始谜题三
题目代码
"""
参数:
n = 17669 (环维度,要求为素数(保证 X^n-1 在 GF(2) 上仅有两个不可约因子))
k = 16 (消息长度,字节)
n1 = 46 (外码 RS[46, 16, 31] 的码长)
n2 = 384 (内码重复 RM 的码长(RM(1,7)×3 = [384, 8, 192]))
delta = 15 (RS纠错能力)
w = 66 (私钥向量 x, y 的 Hamming 重量)
w_r = 75 (随机向量 r1, r2 的 Hamming 重量)
w_e = 75 (误差向量 e 的 Hamming 重量)
级联码结构:RS[46,16,31] over GF(2^8) ⊗ RM(1,7)×3
"""
import os, hashlib, random, reedsolo
from typing import Tuple, List
# ============================================================
# 参数
# ============================================================
PARAM_N = 17669 # 环 R = GF(2)[X]/(X^17669 - 1) 的维度
PARAM_K = 16 # 消息长度 (字节)
PARAM_N1 = 46 # 外码 RS[46, 16, 31] 的码长
PARAM_N2 = 384 # 内码重复 RM 的码长(RM(1,7)×3 = [384, 8, 192])
PARAM_DELTA = 15 # RS可纠错数
PARAM_W = 66 # 私钥向量 x, y 的 Hamming 重量
PARAM_W_R = 75 # 随机向量 r1, r2 的 Hamming 重量
PARAM_W_E = 75 # 误差向量 e 的 Hamming 重量
PARAM_RM_M = 7 # RM(1,m)
PARAM_RM_LEN = 128 # RM(1,7) 码长 = 2^7
PARAM_REP = 3 # RM 码重复次数
# GF(2^8) 不可约多项式: x^8 + x^4 + x^3 + x^2 + 1 = 0x11D
GF256_MOD = 0x11D
# ============================================================
# Reed-Muller RM(1,7) 编码与解码
# ============================================================
# RM(1,7) 是一阶 Reed-Muller 码,参数为 [128, 8, 64]:
# 码长 n = 2^7 = 128
# 维度 k = 1 + 7 = 8(常数项 + 7 个坐标函数)
# 最小距离 d = 2^(7-1) = 64
def rm17_encode(symbol: int) -> int:
"""
RM(1,7) 编码:8 位符号 → 128 位码字。
符号格式:bit7 = 常数项,bits[6:0] = 线性项系数。
"""
codeword = 0
const_bit = (symbol >> 7) & 1 # 最高位 = 常数项
for pt in range(128): # 在所有点评估仿射函数
val = const_bit
for bit in range(7):
if (symbol >> bit) & 1:
val ^= (pt >> bit) & 1
if val:
codeword |= (1 << pt)
return codeword
def rm17_decode(codeword: int) -> int:
"""
RM(1,7) 解码:128 位码字 → 8 位符号(Walsh-Hadamard 变换)。
"""
# Map {0,1} -> {+1,-1}
signal = [1 - 2 * ((codeword >> i) & 1) for i in range(128)]
# 快速Walsh-Hadamard变换
wht = list(signal)
h_step = 1
while h_step < 128:
for i in range(0, 128, h_step * 2):
for j in range(i, i + h_step):
x, y = wht[j], wht[j + h_step]
wht[j], wht[j + h_step] = x + y, x - y
h_step *= 2
# 找到绝对值最大值
best_idx = max(range(128), key=lambda i: abs(wht[i]))
sign_bit = 1 if wht[best_idx] < 0 else 0
return (sign_bit << 7) | best_idx
# ============================================================
# 重复 Reed-Muller 码 [384, 8, 192] = RM(1,7) × 3
# ============================================================
# 将 RM(1,7) 码重复 3 次:
# 编码:128 位码字复制 3 份,拼接为 384 位
# 解码:将 3 份 128 位段逐位取多数表决(bitwise majority vote),
# 再调用 RM(1,7) 解码
# 重复设计将最小距离从 64 提升至 192,代价是码率降低为 1/3
def dup_rm_encode(symbol: int) -> int:
"""重复 RM(1,7)×3 编码:8 位符号 → 384 位码字。"""
base = rm17_encode(symbol)
return base | (base << 128) | (base << 256)
def dup_rm_decode(bits: int) -> int:
"""重复 RM(1,7)×3 解码:384 位码字 → 8 位符号。"""
mask = (1 << 128) - 1
seg0 = bits & mask
seg1 = (bits >> 128) & mask
seg2 = (bits >> 256) & mask
# 按位多数表决
majority = (seg0 & seg1) | (seg1 & seg2) | (seg0 & seg2)
return rm17_decode(majority)
# ============================================================
# Reed-Solomon RS[46, 16, 31] over GF(2^8)
# ============================================================
# RS 码参数:
# 码长 n1 = 46,信息位 k = 16,最小距离 d = 31
# 纠错能力 δ = (d-1)/2 = 15 个 GF(2^8) 符号
# 系统码形式:码字 = [消息符号 | 校验符号],前 16 个符号即为原始消息
_rs_codec = reedsolo.RSCodec(nsym=PARAM_N1 - PARAM_K, fcr=1, c_exp=8) # 默认 GF(2^8)
def rs_encode(msg: bytes) -> List[int]:
"""编码:消息 -> 系统码码字(消息 + 校验)"""
if len(msg) != PARAM_K:
raise ValueError(f"消息长度必须为 {PARAM_K} 字节")
encoded_bytes = _rs_codec.encode(msg)
return list(encoded_bytes)
def rs_decode(symbols: List[int]) -> bytes:
if len(symbols) != PARAM_N1:
raise ValueError(f"接收码字长度必须为 {PARAM_N1} 个符号")
received_bytes = bytes(symbols)
try:
# 兼容不同版本 reedsolo 的返回值
decoded_bytes = _rs_codec.decode(received_bytes)[0]
except reedsolo.ReedSolomonError as e:
raise ValueError(f"RS解码失败:{e}") from e
return decoded_bytes[:PARAM_K]
# ============================================================
# 级联码 Concatenated Code 编码与解码
# ============================================================
# 编码流程(外码先行):
# 16 字节消息
# → RS[46,16,31] 外码编码 → 46 个 GF(2^8) 符号
# → 每个符号经 dup_rm_encode → 46 × 384 = 17664 位
# → 嵌入 n=17669 维环 R 中
#
# 解码流程(内码先行):
# 17664+ 位码字
# → 按 384 位分组为 46 组,每组经 dup_rm_decode → 46 个 GF(2^8) 符号
# → RS 解码(综合征检验 + 可选 BM 纠错)→ 16 字节消息
def concat_encode(msg: bytes) -> int:
"""级联码编码:16 字节消息 → 17664 位码字(Python 整数形式)。"""
rs_symbols = rs_encode(msg)
codeword = 0
for i, sym in enumerate(rs_symbols):
inner = dup_rm_encode(sym)
codeword |= (inner << (i * PARAM_N2))
return codeword
def concat_decode(codeword: int) -> bytes:
"""级联码解码:17664+ 位码字(Python 整数)→ 16 字节消息。"""
symbols = []
for i in range(PARAM_N1):
chunk = (codeword >> (i * PARAM_N2)) & ((1 << PARAM_N2) - 1)
symbols.append(dup_rm_decode(chunk))
return rs_decode(symbols)
# ============================================================
# 多项式环 R = GF(2)[X]/(X^n - 1) 上的运算
# ============================================================
# 全部公钥/密文运算均在此环上进行。
# 元素以 Python 整数表示:第 i 位对应 X^i 的系数。
# 重要性质:
# 加法 = XOR(GF(2) 特征为 2,加减等价)
# 乘法 = 循环卷积(模 X^n - 1 的多项式乘法)
# 公钥 s = x + h·y 的安全性依赖于求解 QCSD 问题的困难性
def poly_add(a: int, b: int) -> int:
return a ^ b
def poly_mul_mod(a: int, b: int, n: int = PARAM_N) -> int:
"""
循环多项式乘法:(a · b) mod (X^n - 1)。
实现策略:枚举 a 的非零位(稀疏乘法),对 b 施加相应的循环移位后 XOR 累加。
时间复杂度 O(wt(a) · n),适合 Hamming 重量较小的向量(如私钥和随机向量)。
"""
result = 0
mask = (1 << n) - 1
# 优化:遍历a的置位
temp = a
i = 0
while temp:
if temp & 1:
shifted = b << i
result ^= (shifted & mask) ^ (shifted >> n)
temp >>= 1
i += 1
return result & mask
def poly_weight(a: int) -> int:
return bin(a).count("1")
def poly_to_bytes(p: int) -> bytes:
return p.to_bytes((PARAM_N + 7) // 8, "little")
def poly_from_bytes(data: bytes) -> int:
return int.from_bytes(data, "little") & ((1 << PARAM_N) - 1)
# ============================================================
# 随机向量采样
# ============================================================
# 安全性要求密钥和随机向量的 Hamming 重量精确固定。
# 采样算法:从 {0,...,n-1} 中均匀无重复地抽取 w 个位置,置为 1。
def sample_fixed_weight(n: int, w: int, rng=None) -> int:
"""采样 Hamming 重量恰好为 w 的随机二进制向量。"""
if rng is None: rng = random.SystemRandom()
positions = rng.sample(range(n), w)
result = 0
for p in positions: result |= (1 << p)
return result
def sample_random_poly(n: int, rng=None) -> int:
"""采样均匀随机的 n 位多项式(用于公钥 h 的生成)。"""
nbytes = (n + 7) // 8
if rng is None:
raw = os.urandom(nbytes)
else:
raw = bytes(rng.getrandbits(8) for _ in range(nbytes))
return int.from_bytes(raw, "little") & ((1 << n) - 1)
# ============================================================
# 密钥生成/封装
# ============================================================
def keygen(seed: bytes = None) -> Tuple[dict, dict]:
"""
密钥生成。
公钥 pk = (h, s):h 为均匀随机多项式,s = x + h·y
私钥 sk = (x, y):两个 Hamming 重量为 w 的低权重多项式
安全性基于求解 2-QCSD 问题的困难性。
"""
if seed:
rng = random.Random(seed)
else:
rng = random.SystemRandom()
h = sample_random_poly(PARAM_N, rng)
x = sample_fixed_weight(PARAM_N, PARAM_W, rng)
y = sample_fixed_weight(PARAM_N, PARAM_W, rng)
s = poly_add(x, poly_mul_mod(h, y))
return {"h": h, "s": s}, {"x": x, "y": y}
def encapsulate(pk: dict, m: bytes, r1: int, r2: int, e: int):
h, s = pk["h"], pk["s"]
u = poly_add(r1, poly_mul_mod(h, r2))
v = poly_add(poly_add(concat_encode(m), poly_mul_mod(s, r2)), e)
shared_key = hashlib.sha512(m).digest()[:32]
return u, v, shared_key
相关数据
密钥封装过程中参数(数字很长,篇幅原因就不全展示了,不影响解题思路,可利用代码本地自行生成一组数据)
h
bef1c1d67d3e1e8039dcd...
s
75b9007fbaf0946a16d19...
u
14506bf660b1098c4cd35...
v
1be57933bc5febab3dc71...
题目要求
计算上述密钥封装过程中所使用的协商密钥。
题目考点
HQC 密钥封装随机向量重用
解题思路
初始谜题三 实现了HQC(Hamming Quasi-Cyclic)码基的密钥封装(赛后查的,其实我并没有见过)
但是我们能根据 keygen 和 encapsulate 函数的代码写出下面的公式
然后我们尝试调用题目提供的密钥封装服务,会发现密钥封装过程参量中的 是不变的
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
u
14506bf660b109...
v
be40236919fab1...
根据 ,也就意味着每次加密, 的值都是不变的 ( 是随机向量和误差向量)
w_r = 75 (随机向量 r1, r2 的 Hamming 重量)
w_e = 75 (误差向量 e 的 Hamming 重量)
甚至有理由怀疑 中的 e 也是不变的
于是我们设题目的共享密钥为 ,我们自己设定 ,分别进行密钥封装,则有
两式相减
注意上述的减法运算其实是GF(2) 下多项式减法,等价于加法,等价于异或(所以多项式下的 ,在转为整数后就是 )代码中的注释倒是也说的很明白了
通过本地生成密钥、加解密测试数据,我们会发现,对 做一个解码,能得到 ,不过需要注意题目中 的存储是小端序的,在将 从十六进制转为十进制时,不能直接用 int(x,16)去转。不然解码会报错
解题代码
v1 = ''
v2 = ''
v1 = int.from_bytes(bytes.fromhex(v1),'little')
v2 = int.from_bytes(bytes.fromhex(v2),'little')
import sys
sys.set_int_max_str_digits(100000000)
from pwn import xor
data = bytes.fromhex('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')
m = xor(concat_decode(v1^v2),data)
shared_key = hashlib.sha512(m).digest()[:32]
print(shared_key.hex())
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:Van1sh Van1sh Van1sh《第四届熵密杯——初始谜题》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。






![[BSidesCF2019]Kookie](/images/random/titlepic/14.jpg)





评论