全网首发!!!CTF BUUOJ [DASCTF X GFCTF 2022十月挑战赛!]Recover Secret Writeup
摘要 本文分析了DASCTF X GFCTF 2022十月挑战赛中的Crypto题目"Recover Secret"。题目基于Shamir秘密共享和Paillier加密系统,但存在两个关键漏洞:私钥直接泄露和加密数据被噪声混淆。解题过程包括:1) 解析输出文件获取私钥;2) 使用Paillier解密所有数据;3) 通过位数差异筛选真实数据点;4) 利用拉格朗日插值恢复秘密值。最
·
全网首发!!!CTF BUUOJ [DASCTF X GFCTF 2022十月挑战赛!]Recover Secret Writeup
一、题目信息
- 题目名称:Recover Secret
- 来源:DASCTF X GFCTF 2022 十月挑战赛
- 分类:Crypto
- 题目描述:Little AT told K people the most precious secret, now K people gathered together, please help me to recover the secret.
二、题目分析
题目提供了加密脚本 recover secret.py 和输出文件 output。让我们先分析加密逻辑。
2.1 加密脚本核心逻辑
def main():
k = 6
sk = []
flag = os.environ["flag for GFCTF2022-Crypto Recover Secret"]
secret = libnum.s2n(flag)
assert len(bin(secret)) < 514
enc = Encryptor()
sk = [enc.__key_gen__() for i in range(k)]
print(sk) # 关键点1:私钥直接输出!
s = [_rand_int(_N - 1) for i in range(k)]
s[0] = secret # 秘密是s[0]
# ... Commit部分用于生成X值,非关键 ...
X = []
Y = []
for i in range(1, k + 1):
# X_i = E(∑ i^j * r_j),利用Paillier加法同态性
# Y_i = E(∑ i^j * s_j),即E(S(i))
# S(x) = s_0 + s_1*x + s_2*x^2 + ... + s_5*x^5
X_Y = shuffle(X, Y, 51) # 关键点2:数据混淆
print(X_Y)
2.2 关键发现
发现1:私钥泄露
print(sk)
私钥 sk 直接被打印到 output 文件中!这意味着我们可以直接解密所有Paillier加密的数据。
发现2:秘密共享结构
s[0] = secret
Y_i = E(S(i))
这是一个 Shamir秘密共享 的变体。秘密 secret 是多项式 S(x)S(x)S(x) 的常数项 s0s_0s0:
S(x)=s0+s1x+s2x2+s3x3+s4x4+s5x5S(x) = s_0 + s_1 x + s_2 x^2 + s_3 x^3 + s_4 x^4 + s_5 x^5S(x)=s0+s1x+s2x2+s3x3+s4x4+s5x5
发现3:噪声混淆
def shuffle(X, Y, x):
res = []
for i in range(len(X)):
res.append([i, X[i], Y[i]]) # 真实数据
for j in range(x):
# 添加噪声数据
res.append([i, X[i] + random.randint(-X[i], X[i]),
Y[i] + random.randint(-Y[i], Y[i])
random.shuffle(res)
return res
每个真实数据点 (Xi,Yi)(X_i, Y_i)(Xi,Yi) 对应 51 个噪声点,总共 6+6×51=3126 + 6 \times 51 = 3126+6×51=312 个数据点。
三、解题思路
- 解析output文件:提取私钥
sk和混淆数据X_Y - Paillier解密:使用泄露的私钥解密所有
Y值 - 数据筛选:
- 真实数据解密后约 514-525位(多项式值)
- 噪声数据解密后约 1018-1024位(接近模数n)
- 选择每个索引对应的最小解密值
- 拉格朗日插值:使用筛选出的6个点计算 S(0)=s0S(0) = s_0S(0)=s0
四、详细解题过程
4.1 完整解题脚本
import gmpy2
from gmpy2 import mpz, powmod
import libnum
import re
import ast
from fractions import Fraction
def paillier_dec(c, n, lmd, mu):
"""Paillier解密"""
n2 = n * n
x = powmod(c, lmd, n2)
Lx = (x - 1) // n
m = (Lx * mu) % n
return m
def clean_mpz(s):
"""清理mpz标记"""
return re.sub(r'mpz\((.*?)\)', r'\1', s)
def safe_eval(s):
"""安全的eval"""
try:
return ast.literal_eval(s)
except:
return ast.literal_eval(clean_mpz(s))
def bit_length(n):
"""获取位数"""
return gmpy2.bit_length(n)
# 读取文件
with open('output', 'r', encoding='utf-8') as f:
lines = [l.strip() for l in f.read().strip().split('\n') if l.strip()]
all_data = [safe_eval(line) for line in lines]
sk = all_data[0] # 第1行:私钥
X_Y = all_data[3] # 第4行:混淆数据
print(f"数据: sk(6组密钥), X_Y({len(X_Y)}个点)\n")
# 关键步骤:找到每个索引对应的最小解密值
print("="*60)
print("筛选真实数据点(选择最小解密值)...")
print("="*60 + "\n")
# 对每个索引,收集所有解密结果
idx_values = {} # idx -> [(y_dec, y_val), ...]
for item in X_Y:
idx = item[0]
y_val = item[2]
# 获取对应密钥
n_i = mpz(sk[idx][0][0])
lmd = mpz(sk[idx][1][0])
mu = mpz(sk[idx][1][1])
try:
y_dec = paillier_dec(mpz(y_val), n_i, lmd, mu)
if idx not in idx_values:
idx_values[idx] = []
idx_values[idx].append((int(y_dec), int(y_val)))
except:
pass
# 选择最小的解密值(真实数据约528位,噪声接近1024位)
points = {}
for idx in range(6):
if idx in idx_values:
min_dec, _ = min(idx_values[idx], key=lambda x: x[0])
points[idx + 1] = min_dec
bits = bit_length(min_dec)
print(f"idx={idx}, x={idx+1}: 最小解密值 {bits} 位")
print(f"\n收集到 {len(points)} 个数据点\n")
# 拉格朗日插值求S(0)
print("="*60)
print("拉格朗日插值")
print("="*60 + "\n")
x_vals = sorted(points.keys())
secret_frac = Fraction(0, 1)
for i in x_vals:
xi, yi = i, points[i]
numerator = Fraction(1, 1)
denominator = Fraction(1, 1)
for j in x_vals:
if i != j:
numerator *= Fraction(0 - j, 1)
denominator *= Fraction(xi - j, 1)
l_i_0 = numerator / denominator
secret_frac += Fraction(yi, 1) * l_i_0
secret = secret_frac.numerator // secret_frac.denominator
print(f"恢复的秘密: {secret}")
print(f"位数: {bit_length(abs(secret)) if secret != 0 else 0}")
try:
flag = libnum.n2s(abs(int(secret)))
print(f"\nFlag: {flag}")
try:
print(f"Flag decoded: {flag.decode()}")
except:
pass
except Exception as e:
print(f"\n转换失败: {e}")
print(f"Hex: {hex(abs(int(secret)))}")
4.2 运行结果
数据: sk(6组密钥), X_Y(312个点)
============================================================
筛选真实数据点(选择最小解密值)...
============================================================
idx=0, x=1: 最小解密值 514 位
idx=1, x=2: 最小解密值 518 位
idx=2, x=3: 最小解密值 521 位
idx=3, x=4: 最小解密值 523 位
idx=4, x=5: 最小解密值 524 位
idx=5, x=6: 最小解密值 525 位
收集到 6 个数据点
============================================================
拉格朗日插值
============================================================
恢复的秘密: 2445986771771386901982964970052402491236020951408388483652278064206253525614398139039401896841700904887165
位数: 351
Flag: b'DASCTF{fe22ca94-c002-d10b-7104-1c5eff3f335c}'
Flag decoded: DASCTF{fe22ca94-c002-d10b-7104-1c5eff3f335c}
4.3 数据位数分析
观察解密结果的位数变化:
| idx | x值 | 最小解密值位数 |
|---|---|---|
| 0 | 1 | 514 位 |
| 1 | 2 | 518 位 |
| 2 | 3 | 521 位 |
| 3 | 4 | 523 位 |
| 4 | 5 | 524 位 |
| 5 | 6 | 525 位 |
| 位数随x值增大而增大,这完全符合多项式值的增长规律! | ||
| 而噪声数据解密后位数都在 1018-1024位,接近Paillier模数n的位数(1022位),因此可以通过选择最小解密值来过滤噪声。 |
五、总结
5.1 知识点总结
| 知识点 | 说明 |
|---|---|
| Paillier加密 | 加法同态加密,E(m1)⋅E(m2)=E(m1+m2)E(m_1) \cdot E(m_2) = E(m_1 + m_2)E(m1)⋅E(m2)=E(m1+m2) |
| Shamir秘密共享 | 秘密作为多项式常数项,通过拉格朗日插值恢复 |
| 数据混淆与过滤 | 利用数值大小的显著差异区分真实数据和噪声 |
| 私钥泄露 | 出题人的"失误"(或故意设计)大大降低了题目难度 |
5.2 解题关键
- 识别私钥泄露:发现
print(sk)直接暴露了私钥 - 理解秘密共享结构:意识到 S(0)S(0)S(0) 就是flag
- 噪声过滤策略:利用位长差异筛选真实数据(这是本题的核心难点)
- 精确计算:使用
Fraction进行有理数运算,避免精度损失
5.3 Flag
DASCTF{fe22ca94-c002-d10b-7104-1c5eff3f335c}
更多推荐



所有评论(0)