2026DesCTF -- Crypto -- WriteUp

2026DesCTF -- Crypto -- WriteUp

Check in

task.sage

from sage.all import *
from Crypto.Util.number import getStrongPrime, inverse
from hashlib import *
while True:
    p = getStrongPrime(512)
    q = getStrongPrime(512)
    if p % 4 == 3 and q % 4 == 3:
        print(p, q)
        break
N = p * q
phi_N = (p ** 2 + p + 1) * (q ** 2 + q + 1)
d = Integer(N) ** RR(0.47)
e = inverse(Integer(d), phi_N)
flag='flag{'+md5(str(p + q).encode()).hexdigest()+'}'
print(f"e = {e}")
print(f"N = {N}")
print(flag)
"""
e = ...
N = ...
"""

analysis

  • 类似于RSA框架下的Wiener's Attack,但φ(N)的计算方式并不满足RSA解密使用且私钥d的生成方式已知,可以直接恢复d。赛题条件:\(φ(N)=(p²+p+1)\cdot(q²+q+1)=p²q²+pq(p+q)+p²+q²+pq+p+q+1\) 且 \(N = p\cdot q\) 且 \(p²+q² = (p + q)² - 2N\)。设\(m = p + q\),则\(φ(N) = N² + Nm + m² - N + m + 1\quad\quad...\quad(1)\)且 \(e \cdot d = k \cdot φ(N) + 1\)且 \(φ(N) ≈ N²\)。因此,\(k = \lfloor \frac{e \cdot d - 1}{\phi_N} \rfloor \approx \lfloor \frac{e \cdot d - 1}{N^2} \rfloor\),由于\(φ(N)\)明显比\(N²\)要大一些,因此\(k\)一定小于估算出的值,只需在有限范围寻找到对应的\(k\),最终解取方程(1)的整数解即可。

exp.sage

# sage
import hashlib

e = ...
N = ...
d = Integer(Integer(N) ** RR(0.47))
target = e * d - 1
k = target // (N ** 2)
while (target % k != 0):
    k -= 1
phi = target // k
a = 1
b = N + 1
c = N ** 2 - N + 1 - phi
delta = b ** 2 - 4 * a * c
assert delta > 0
s, rem = delta.sqrtrem()
if rem == 0:
    flag = 'flag{' + hashlib.md5(str(s).encode()).hexdigest() + '}'
    print(flag)
# flag{09abda5461517b85e18976e4347a3de3}

尽人事,听天命

task.py

import random
from secret import seed,yin,yang

signs = ['大吉', '中吉', '小吉', '吉', '末吉', '凶', '大凶']
colors = ['赤', '橙', '黄', '青', '蓝', '紫', '黑', '白']
pool = []
count = 2495
bits = 32
kbits = 6

def next_omen():
    global pool
    if not pool:
        x = random.getrandbits(32)
        pool = [x & 0xff, (x >> 8) & 0xff, (x >> 16) & 0xff, (x >> 24) & 0xff]
    return pool.pop(0)

def make_book(name):
    t = next_omen()
    a = t % 101
    b = (t * 3 + 7) % 101
    c = (t * 5 + 11) % 101
    d = (t * 7 + 13) % 101
    color = colors[t & 7]
    sign = signs[(t >> 1) % len(signs)]
    return '\n'.join([
        '【天命阁命书】',
        f'有缘人:{name}',
        f'命途:{a}',
        f'福缘:{b}',
        f'财势:{c}',
        f'劫数:{d}',
        f'吉色:{color}',
        f'签文:{sign}',
        f'天命:{t}'
    ])

def pull(k):
    x = 0
    off = 0
    while off < k:
        x |= random.getrandbits(32) << off
        off += 32
    return x & ((1 << k) - 1)

def main():
    random.seed(seed)
    names = [f'有缘人{i:04d}' for i in range(count)]
    with open('命书.txt', 'w', encoding='utf-8') as f:
        f.write('天命阁旧录\n')
        f.write('知天命,可窥未来。\n')
        f.write('见未来,方知真形。\n\n')
        for name in names:
            f.write(make_book(name))
            f.write('\n\n')
        bound = yin * yang
        trace = yin ^ (yang >> kbits)
        seal_a = bound ^ pull(bits * 2)
        seal_b = trace ^ pull(bits)
        f.write('【未来残页】\n')
        f.write(f'阴阳交积封印:{seal_a}\n')
        f.write(f'阴阳离合封印:{seal_b}\n')
        f.write('\n')
        f.write('flag = DesCTF{md5(str(min(yin,yang)) + "|" + str(max(yin,yang)))}\n')

if __name__ == '__main__':
    main()

analysis

  • \(MT19937random\)预测,\(R0 = t0 | (t1 << 8) | (t2 << 16) | (t3 << 24)\)。
  • 输入 \(624\) 个连续的 \(32\) 位输出,逆向得到内部状态数组。有了内部状态,可以 \(clone\) 这个随机数生成器,预测后续的输出(即 pull(64) 和 pull(32) 所用的随机数)。
  • 第 \(624\) 个 \(R623\) 只有 \(3\) 个字节已知。对缺失的第 \(4\) 个字节从 \(0\) 到 \(255\) 枚举,补全 \(R623\)。
  • 已知 \(B = yin \cdot yang\) 和 \(T = yin \oplus (yang \gg 6)\)。通过分解 \(B\) 的因数,或者利用 \(T\) 的性质快速搜索。由于 \(B\) 只有 \(64\) 位,直接对其进行大数分解(Factorization)并遍历因数即可。

exp.py

import re
import hashlib
from randcrack import RandCrack
from sympy import divisors

def getdata(filename):
    with open(filename, 'r', encoding = 'utf-8') as f:
        content = f.read()
    t = [int(x) for x in re.findall(r'天命:(\d+)', content)]
    a = int(re.search(r'阴阳交积封印:(\d+)', content).group(1))
    b = int(re.search(r'阴阳离合封印:(\d+)', content).group(1))
    
    return t, a, b

def pull(k, rc):
    x = 0
    off = 0
    while off < k:
        x |= rc.predict_getrandbits(32) << off
        off += 32
    return x & ((1 << k) - 1)

t, a, b = getdata('命书.txt')

rc_inputs = []
for i in range(0, 2492, 4):
    val = t[i] | (t[i+1] << 8) | (t[i+2] << 16) | (t[i+3] << 24)
    rc_inputs.append(val)
    
last_three_bytes = t[2492:] # t2492, t2493, t2494
    

for miss in range(256):
    r623 = last_three_bytes[0] | (last_three_bytes[1] << 8) | (last_three_bytes[2] << 16) | (miss << 24)
        
    rc = RandCrack()
    for v in rc_inputs:
        rc.submit(v)
    rc.submit(r623)
        
    p64 = pull(64, rc)
    p32 = pull(32, rc)
        
    A = a ^ p64
    B = b ^ p32
    if A == 0: continue
        
    try:
        for d in divisors(A):
            yin = d
            yang = A // d
            if (yin ^ (yang >> 6)) == B:
                v1, v2 = min(yin, yang), max(yin, yang)
                res = f"{v1}|{v2}"
                flag = hashlib.md5(res.encode()).hexdigest()
                print(f"DesCTF{{{flag}}}")
                exit(0)
    except:
        continue
            
# DesCTF{28dc9b687456ef5897c4477a98a8bdbf}

Low Bits, High Risk

task.py

from hashlib import *
from secrets import randbelow
from ecdsa import SigningKey, SECP160r1, util

leak = 3
total = 61
mask = (1 << leak) - 1

def main():
    sk = SigningKey.generate(curve=SECP160r1)
    vk = sk.verifying_key
    q = SECP160r1.order
    # print(f"q = {q}")
    # q = 1461501637330902918203687197606826779884643492439
    # print(q.bit_length())
    # 161
    print(hex(vk.pubkey.point.x()))
    print(hex(vk.pubkey.point.y()))
    d = sk.privkey.secret_multiplier
    # print(f"d = {d}")
    flag = "flag{" + md5(str(d).encode()).hexdigest() + "}"
    for i in range(total):
        msg = f"msg-{i}".encode()
        digest = sha1(msg).digest()
        h = int.from_bytes(digest, "big") % q
        k = randbelow(q - 1) + 1
        sig = sk.sign_digest(digest, sigencode=util.sigencode_string, k=k)
        r, s = util.sigdecode_string(sig, q)
        print(f"({hex(h)}, {hex(int(r))}, {hex(int(s))}, {hex(k & mask)}),")


if __name__ == "__main__":
    main()
"""
0x8e0a0071e8cf437efec4233ff8444a4ff8adba2f
0xa869fce702b70799c443b450a4d41cc4f65b3eee
..
"""

analysis

  • \(HNP\)问题求解\(ECDSA\)框架下的私钥\(d\),具体来讲的话属于线性位泄露问题中的\(LSB\)泄露问题:https://hasegawaazusa.github.io/hidden-number-problem.html#lsb-1

  • 尝试了\(LLL\)与\(BKZ\)规约之后一直没有完成\(HNP\)问题的求解,规约的结果有很多,但是没有能够通过公钥验证的私钥\(d\):\(s\equiv k^{-1}(h+r\cdot d)\pmod q\rightarrow k\equiv s^{-1}h+s^{-1}r\cdot d\pmod q\),由于泄露了\(k\)的低\(L = 3\)位,设\(k=2^L\cdot k_{high}+k_{low}\),代入且两边乘\(2^{-L}\pmod q\),得\(k_{high}\equiv(s^{-1}r\cdot 2^{-L})\cdot d+ (s^{-1}h-k_{low})\cdot 2^{-L}\pmod q\),设\(t_i\equiv s_i^{-1}r_i\cdot2^{-L}\pmod q\),\(u_i\equiv(s_i^{-1}h_i-k_{low,i})\cdot2^{-L}\pmod q\),则有\(k_{high,i}\equiv t_i\cdot d+u_i\pmod q\),由于\(k_{high,i}<q/2^L\),转化为格基约化解决的CVP问题。

  • 后面一直尝试修改平衡和缩放因子,还是没有成功。后面就找到了下面这个Github项目:https://github.com/bitlogik/lattice-attack/tree/master 这个项目很有趣,针对于此类LLBP问题的求解是专门完成的一个库(可以积累一下),但是该项目与本道题目存在不适用性: (i). 项目是基于\(Python\)的\(fpylll\)库完成的,没有依赖于原生的\(sagemath\)环境,因此在进行椭圆曲线的相关计算的时候,其曲线采用的\(cryptophydom\)的相关曲线,而题目涉及的\(SECP160r1\)并不在其中。 (ii). 项目中提及其使用限制在k的泄露bit数最少为\(4bit\)。 基于其对于本道题目的不适用性,但是主要借鉴的就是项目中针对于此类RCDSA求解的格的矩阵建立方法即可,改写项目中lattice_attack.py中的函数,很多函数都是可以直接抄过来的,将数据写成\(json\)文件内即可,这次成功求解了私钥\(d\):
    exp.py

    import json
    import random
    # import time
    from hashlib import md5
    from fpylll import LLL, BKZ, IntegerMatrix
    from ecdsa import SigningKey, SECP160r1, util
    
    q = int(SECP160r1.order)
    
    def check(d):
        x = 0x8e0a0071e8cf437efec4233ff8444a4ff8adba2f
        y = 0xa869fce702b70799c443b450a4d41cc4f65b3eee
        
        sk = SigningKey.from_secret_exponent(d, curve=SECP160r1)
        xx = sk.verifying_key.pubkey.point.x()
        yy = sk.verifying_key.pubkey.point.y()
        
        return xx == x and yy == y
    
    def inverse_mod(a, m):
        if a < 0 or m <= a:
            a = a % m
        i, j = a, m
        x_a, x_b = 1, 0
        while i != 1:
            quot, rem = divmod(j, i)
            x_rem = x_b - quot * x_a
            j, i, x_b, x_a = i, rem, x_a, x_rem
        return x_a % m
    
    def build_matrix(sigs, num_bits):
        n_sigs = len(sigs)
        lattice = IntegerMatrix(n_sigs + 2, n_sigs + 2)
        kbi = 2 ** num_bits
        
        for i in range(n_sigs):
            lattice[i, i] = 2 * kbi * q
            hash_i = int(sigs[i]["hash"])
            r = int(sigs[i]["r"])
            s = int(sigs[i]["s"])
            kp = int(sigs[i]["kp"])
            
            inv_s = inverse_mod(s, q)
            inv_kbi = inverse_mod(kbi, q)
            
            t1 = (r * inv_s) % q
            t2 = (kp - hash_i * inv_s) % q
            
            lattice[n_sigs, i] = 2 * kbi * ((inv_kbi * t1) % q)
            lattice[n_sigs + 1, i] = (2 * kbi * ((inv_kbi * t2) % q) + q) % (2*kbi*q)
        
        lattice[n_sigs, n_sigs] = 1
        lattice[n_sigs + 1, n_sigs + 1] = q
        return lattice
    
    def reduce_lattice(lattice, block_size=None):
        if block_size is None:
            return LLL.reduction(lattice)
        return BKZ.reduction(lattice, BKZ.Param(block_size=block_size))
    
    # begin_time = time.time()
    with open("data.json", "r") as f:
        data = json.load(f)
    signatures = data["signatures"]
    n_sigs = 61
    lattice = build_matrix(signatures, 3)
    for block in [None, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60]:
        # print(f"block = {block}")
        reduced = reduce_lattice(lattice, block)
        for row in reduced:
            val = row[-2]
            if isinstance(val, (list, tuple)):
                val = val[0] if val else 0
            try:
                cand = int(str(val)) % q
                if check(cand):
                    # end_time = time.time()
                    # print(f"runtime: {end_time - begin_time}")
                    flag = "DesCTF{" + md5(str(cand).encode()).hexdigest() + "}"
                    print(flag)
                    exit(0)
            except:
                continue
    # runtime: 13.418259620666504
    # DesCTF{9669bfa7126ffabbc99cd92f1165938a}
    
  • 后续想着借着项目中的矩阵构造,利用\(sagemath\)中的原生函数完成这道题目的求解,不再借用\(fpylll\)库与\(ecdsa\)库,判断一下两者的效率问题:

    exp.sage

    # sage
    import hashlib
    # import time
    
    p = 0xffffffffffffffffffffffffffffffff7fffffff
    a = 0xffffffffffffffffffffffffffffffff7ffffffc
    b = 0x1c97befc54bd7a8b65acf89f81d4d4adc565fa45
    E = EllipticCurve(GF(p), [a, b])
    q = 0x100000000000000000001f4c8f927aed3ca752257
    G = E(0x4a96b5688ef573284664698968c38bb913cbfc82, 0x23a628553168947d59dcc912042351377ac5fb32)
    
    x = 0x8e0a0071e8cf437efec4233ff8444a4ff8adba2f
    y = 0xa869fce702b70799c443b450a4d41cc4f65b3eee
    Q = E(x, y)
    
    data = [...]
    
    n = len(data)
    leak = 3
    kbi = 1 << leak
    
    # begin_time = time.time()
    M = Matrix(ZZ, n + 2, n + 2)
    inv_kbi = inverse_mod(kbi, q)
    
    for i, (h, r, s, kp) in enumerate(data):
        inv_s = inverse_mod(s, q)
        M[i, i] = 2 * kbi * q
        t = (2 * kbi * (inv_kbi * (r * inv_s) % q))
        M[n, i] = t
        u = (2 * kbi * (inv_kbi * (kp - h * inv_s) % q) + q)
        M[n+1, i] = u
    
    M[n, n] = 1
    M[n+1, n+1] = q
        
    curr_M = M
    for effort in [None, 15, 25, 30, 35, 40]:
        if effort is None:
            curr_M = curr_M.LLL()
        else:
            curr_M = curr_M.BKZ(block_size=effort)
            
        for row in curr_M:
            cand = abs(int(row[n])) % q
            if cand == 0: continue
                
            for d in [cand, (q - cand) % q]:
                if d == 0: continue
                try:
                    if d * G == Q:
                        # end_time = time.time()
                        # print(f"runtime: {end_time - begin_time}")
                        flag = hashlib.md5(str(d).encode()).hexdigest()
                        print(f"DesCTF{{{flag}}}")
                        exit()
                except:
                    pass
    # runtime: 1.7806310653686523
    # DesCTF{9669bfa7126ffabbc99cd92f1165938a}
    
  • 这里可以看到\(sagemath\)原生的脚本运行时间是远低于依赖于\(python\)库中的相关函数的脚本,目前认为是借用的项目中的函数并没有最优化,而\(sagemath\)底层中的椭圆曲线计算优化的很好。另外就是\(sagemath\)中的\(BKZ\)算法的\(block\)在真正规约出正确的d的时候使用的\(block\)更小。这道题目的求解关键在于相比于正常的\(HNP\)问题,如何构造正确的矩阵,为了提高约化效率,我们采用了 \(BitLogi\) 方案 中的放大因子构造。通过将同余式整体乘以 \(2 \cdot 2^{leak}\),平衡了私钥 \(d\) 与未知分量 \(k_{high}\) 的权重,使得格基在约化过程中能更准确地捕捉短向量。项目中的矩阵建立方式很棒。

posted @ 2026-03-08 22:32  chen_xing  阅读(227)  评论(0)    收藏  举报