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.pyimport 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}\) 的权重,使得格基在约化过程中能更准确地捕捉短向量。项目中的矩阵建立方式很棒。

2026DesCTF -- Crypto -- WriteUp
浙公网安备 33010602011771号