欢迎光临
我们一直在努力

全网首发!!!CTF BUUOJ [DASCTF X GFCTF 2022十月挑战赛!]Recover Secret Writeup

全网首发!!!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 数据位数分析

    观察解密结果的位数变化:

    idxx值最小解密值位数
    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)
    未经允许不得转载:171主机测评 » 全网首发!!!CTF BUUOJ [DASCTF X GFCTF 2022十月挑战赛!]Recover Secret Writeup
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址